mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2025-11-05 10:52:49 +00:00
Compare commits
58 Commits
2025-06-24
...
2025-06-27
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
692ac62add | ||
|
|
216cc7e5c3 | ||
|
|
bcc113406a | ||
|
|
0067075ed1 | ||
|
|
d60911a063 | ||
|
|
abad754f61 | ||
|
|
a632d315ab | ||
|
|
520bae01d6 | ||
|
|
7057fba151 | ||
|
|
e24ca6472c | ||
|
|
028feb363f | ||
|
|
491b341fdf | ||
|
|
db77e42a50 | ||
|
|
cf3f790f03 | ||
|
|
a0da56997c | ||
|
|
c000235d81 | ||
|
|
578d8067dc | ||
|
|
03d2a76ff1 | ||
|
|
650a5f5df5 | ||
|
|
5130cc6bc9 | ||
|
|
7ebe0139c2 | ||
|
|
08da826302 | ||
|
|
d94c7b846c | ||
|
|
97a1c64fad | ||
|
|
4b8e1e9015 | ||
|
|
c2b5747718 | ||
|
|
d31fd08d69 | ||
|
|
e6230de022 | ||
|
|
db7aaa3158 | ||
|
|
af1f22a4d6 | ||
|
|
4cc3a87b0e | ||
|
|
db2671ed95 | ||
|
|
0a72c81ea5 | ||
|
|
dfd612480c | ||
|
|
64397b16c5 | ||
|
|
bd49471ebc | ||
|
|
7289c68399 | ||
|
|
4a5ddc8410 | ||
|
|
93808fbd75 | ||
|
|
24394a0947 | ||
|
|
4676eb616c | ||
|
|
e9ae558c25 | ||
|
|
afee37794b | ||
|
|
72e7bda418 | ||
|
|
69e14c8fca | ||
|
|
6394c0cf17 | ||
|
|
d1deffb235 | ||
|
|
ac885f8adb | ||
|
|
8d91a5df5f | ||
|
|
5ad9323944 | ||
|
|
559bf61c31 | ||
|
|
3a391c34fc | ||
|
|
332a96ea03 | ||
|
|
454c574d38 | ||
|
|
2512c828e7 | ||
|
|
a99ecb60ef | ||
|
|
24f22dfecc | ||
|
|
8521e2389b |
3
.github/workflows/frontend-cicd.yml
generated
vendored
3
.github/workflows/frontend-cicd.yml
generated
vendored
@@ -44,9 +44,6 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci --prefer-offline --legacy-peer-deps
|
run: npm ci --prefer-offline --legacy-peer-deps
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: npm run test
|
|
||||||
|
|
||||||
- name: Configure Next.js for pages
|
- name: Configure Next.js for pages
|
||||||
uses: actions/configure-pages@v5
|
uses: actions/configure-pages@v5
|
||||||
with:
|
with:
|
||||||
|
|||||||
48
.github/workflows/push-to-gitea.yaml
generated
vendored
Normal file
48
.github/workflows/push-to-gitea.yaml
generated
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
name: Sync to Gitea
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
if: github.repository == 'community-scripts/ProxmoxVE'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout source repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Change all links to git.community-scripts.org
|
||||||
|
run: |
|
||||||
|
echo "Searching for files containing raw.githubusercontent.com URLs..."
|
||||||
|
|
||||||
|
# Find all files containing GitHub raw URLs, excluding certain directories
|
||||||
|
files_with_github_urls=$(grep -r "https://raw.githubusercontent.com/community-scripts/ProxmoxVE" . --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=.github/workflows --files-with-matches || true)
|
||||||
|
|
||||||
|
if [ -n "$files_with_github_urls" ]; then
|
||||||
|
echo "$files_with_github_urls" | while read file; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
sed -i 's|https://raw\.githubusercontent\.com/community-scripts/ProxmoxVE/|https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/|g' "$file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "No files found containing GitHub raw URLs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- name: Push to Gitea
|
||||||
|
run: |
|
||||||
|
git config --global user.name "Push From Github"
|
||||||
|
git config --global user.email "actions@github.com"
|
||||||
|
git remote add gitea https://$GITEA_USER:$GITEA_TOKEN@git.community-scripts.org/community-scripts/ProxmoxVE.git
|
||||||
|
git add .
|
||||||
|
git commit -m "Sync to Gitea"
|
||||||
|
git push gitea --all --force
|
||||||
|
env:
|
||||||
|
GITEA_USER: ${{ secrets.GITEA_USERNAME }}
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
71
CHANGELOG.md
71
CHANGELOG.md
@@ -14,8 +14,79 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
|||||||
All LXC instances created using this repository come pre-installed with Midnight Commander, which is a command-line tool (`mc`) that offers a user-friendly file and directory management interface for the terminal environment.
|
All LXC instances created using this repository come pre-installed with Midnight Commander, which is a command-line tool (`mc`) that offers a user-friendly file and directory management interface for the terminal environment.
|
||||||
|
|
||||||
|
|
||||||
|
## 2025-06-28
|
||||||
|
|
||||||
|
## 2025-06-27
|
||||||
|
|
||||||
|
### 🆕 New Scripts
|
||||||
|
|
||||||
|
- BookLore ([#5524](https://github.com/community-scripts/ProxmoxVE/pull/5524))
|
||||||
|
|
||||||
|
### 🚀 Updated Scripts
|
||||||
|
|
||||||
|
- #### 🐞 Bug Fixes
|
||||||
|
|
||||||
|
- wizarr: remove unneeded tmp file [@MickLesk](https://github.com/MickLesk) ([#5517](https://github.com/community-scripts/ProxmoxVE/pull/5517))
|
||||||
|
|
||||||
|
### 🧰 Maintenance
|
||||||
|
|
||||||
|
- #### 🐞 Bug Fixes
|
||||||
|
|
||||||
|
- Remove npm legacy errors, created single source of truth for ESlint. updated analytics url. updated script background [@BramSuurdje](https://github.com/BramSuurdje) ([#5498](https://github.com/community-scripts/ProxmoxVE/pull/5498))
|
||||||
|
|
||||||
|
- #### 📂 Github
|
||||||
|
|
||||||
|
- New workflow to push to gitea and change links to gitea [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#5510](https://github.com/community-scripts/ProxmoxVE/pull/5510))
|
||||||
|
|
||||||
|
### 🌐 Website
|
||||||
|
|
||||||
|
- #### 📝 Script Information
|
||||||
|
|
||||||
|
- Wireguard, Update Link to Documentation. [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#5514](https://github.com/community-scripts/ProxmoxVE/pull/5514))
|
||||||
|
|
||||||
|
## 2025-06-26
|
||||||
|
|
||||||
|
### 🆕 New Scripts
|
||||||
|
|
||||||
|
- ConvertX ([#5484](https://github.com/community-scripts/ProxmoxVE/pull/5484))
|
||||||
|
|
||||||
|
### 🚀 Updated Scripts
|
||||||
|
|
||||||
|
- [tools] Update setup_nodejs function [@tremor021](https://github.com/tremor021) ([#5488](https://github.com/community-scripts/ProxmoxVE/pull/5488))
|
||||||
|
- [tools] Fix setup_mongodb function [@tremor021](https://github.com/tremor021) ([#5486](https://github.com/community-scripts/ProxmoxVE/pull/5486))
|
||||||
|
|
||||||
## 2025-06-25
|
## 2025-06-25
|
||||||
|
|
||||||
|
### 🚀 Updated Scripts
|
||||||
|
|
||||||
|
- #### 🐞 Bug Fixes
|
||||||
|
|
||||||
|
- Docmost: Increase resources [@tremor021](https://github.com/tremor021) ([#5458](https://github.com/community-scripts/ProxmoxVE/pull/5458))
|
||||||
|
|
||||||
|
- #### ✨ New Features
|
||||||
|
|
||||||
|
- tools.func: new helper for imagemagick [@MickLesk](https://github.com/MickLesk) ([#5452](https://github.com/community-scripts/ProxmoxVE/pull/5452))
|
||||||
|
- YunoHost: add Update-Function [@MickLesk](https://github.com/MickLesk) ([#5450](https://github.com/community-scripts/ProxmoxVE/pull/5450))
|
||||||
|
|
||||||
|
- #### 🔧 Refactor
|
||||||
|
|
||||||
|
- Refactor: Tailscale [@MickLesk](https://github.com/MickLesk) ([#5454](https://github.com/community-scripts/ProxmoxVE/pull/5454))
|
||||||
|
|
||||||
|
### 🌐 Website
|
||||||
|
|
||||||
|
- #### 🐞 Bug Fixes
|
||||||
|
|
||||||
|
- Update Tooltips component to conditionally display updateable status based on item type [@BramSuurdje](https://github.com/BramSuurdje) ([#5461](https://github.com/community-scripts/ProxmoxVE/pull/5461))
|
||||||
|
- Refactor CommandMenu to prevent duplicate scripts across categories [@BramSuurdje](https://github.com/BramSuurdje) ([#5463](https://github.com/community-scripts/ProxmoxVE/pull/5463))
|
||||||
|
|
||||||
|
- #### ✨ New Features
|
||||||
|
|
||||||
|
- Enhance InstallCommand component to support Gitea as an alternative source for installation scripts. [@BramSuurdje](https://github.com/BramSuurdje) ([#5464](https://github.com/community-scripts/ProxmoxVE/pull/5464))
|
||||||
|
|
||||||
|
- #### 📝 Script Information
|
||||||
|
|
||||||
|
- Website: mark VM's and "OS"-LXC's as updatable [@MickLesk](https://github.com/MickLesk) ([#5453](https://github.com/community-scripts/ProxmoxVE/pull/5453))
|
||||||
|
|
||||||
## 2025-06-24
|
## 2025-06-24
|
||||||
|
|
||||||
### 🆕 New Scripts
|
### 🆕 New Scripts
|
||||||
|
|||||||
79
ct/booklore.sh
Normal file
79
ct/booklore.sh
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
|
||||||
|
# Copyright (c) 2021-2025 community-scripts ORG
|
||||||
|
# Author: MickLesk (CanbiZ)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
# Source: https://github.com/adityachandelgit/BookLore
|
||||||
|
|
||||||
|
APP="BookLore"
|
||||||
|
var_tags="${var_tags:-books;library}"
|
||||||
|
var_cpu="${var_cpu:-3}"
|
||||||
|
var_ram="${var_ram:-2048}"
|
||||||
|
var_disk="${var_disk:-7}"
|
||||||
|
var_os="${var_os:-debian}"
|
||||||
|
var_version="${var_version:-12}"
|
||||||
|
var_unprivileged="${var_unprivileged:-1}"
|
||||||
|
|
||||||
|
header_info "$APP"
|
||||||
|
variables
|
||||||
|
color
|
||||||
|
catch_errors
|
||||||
|
|
||||||
|
function update_script() {
|
||||||
|
header_info
|
||||||
|
check_container_storage
|
||||||
|
check_container_resources
|
||||||
|
|
||||||
|
if [[ ! -d /opt/booklore ]]; then
|
||||||
|
msg_error "No ${APP} Installation Found!"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
RELEASE=$(curl -fsSL https://api.github.com/repos/adityachandelgit/BookLore/releases/latest | yq '.tag_name' | sed 's/^v//')
|
||||||
|
if [[ "${RELEASE}" != "$(cat ~/.booklore 2>/dev/null)" ]] || [[ ! -f ~/.booklore ]]; then
|
||||||
|
msg_info "Stopping $APP"
|
||||||
|
systemctl stop booklore
|
||||||
|
msg_ok "Stopped $APP"
|
||||||
|
|
||||||
|
fetch_and_deploy_gh_release "booklore" "adityachandelgit/BookLore"
|
||||||
|
|
||||||
|
msg_info "Building Frontend"
|
||||||
|
cd /opt/booklore/booklore-ui
|
||||||
|
$STD npm install --force
|
||||||
|
$STD npm run build --configuration=production
|
||||||
|
msg_ok "Built Frontend"
|
||||||
|
|
||||||
|
msg_info "Building Backend"
|
||||||
|
cd /opt/booklore/booklore-api
|
||||||
|
APP_VERSION=$(curl -fsSL https://api.github.com/repos/adityachandelgit/BookLore/releases/latest | yq '.tag_name' | sed 's/^v//')
|
||||||
|
yq eval ".app.version = \"${APP_VERSION}\"" -i src/main/resources/application.yaml
|
||||||
|
$STD ./gradlew clean build --no-daemon
|
||||||
|
mkdir -p /opt/booklore/dist
|
||||||
|
JAR_PATH=$(find /opt/booklore/booklore-api/build/libs -maxdepth 1 -type f -name "booklore-api-*.jar" ! -name "*plain*" | head -n1)
|
||||||
|
if [[ -z "$JAR_PATH" ]]; then
|
||||||
|
msg_error "Backend JAR not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cp "$JAR_PATH" /opt/booklore/dist/app.jar
|
||||||
|
msg_ok "Built Backend"
|
||||||
|
|
||||||
|
msg_info "Starting $APP"
|
||||||
|
systemctl start booklore
|
||||||
|
systemctl reload nginx
|
||||||
|
msg_ok "Started $APP"
|
||||||
|
|
||||||
|
msg_ok "Update Successful"
|
||||||
|
else
|
||||||
|
msg_ok "No update required. ${APP} is already at v${RELEASE}"
|
||||||
|
fi
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
start
|
||||||
|
build_container
|
||||||
|
description
|
||||||
|
|
||||||
|
msg_ok "Completed Successfully!\n"
|
||||||
|
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||||
|
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
|
||||||
|
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:6060${CL}"
|
||||||
70
ct/convertx.sh
Normal file
70
ct/convertx.sh
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
|
||||||
|
# Copyright (c) 2021-2025 community-scripts ORG
|
||||||
|
# Author: Omar Minaya | MickLesk (CanbiZ)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
# Source: https://github.com/C4illin/ConvertX
|
||||||
|
|
||||||
|
APP="ConvertX"
|
||||||
|
var_tags="${var_tags:-converter}"
|
||||||
|
var_cpu="${var_cpu:-2}"
|
||||||
|
var_ram="${var_ram:-4096}"
|
||||||
|
var_disk="${var_disk:-20}"
|
||||||
|
var_os="${var_os:-debian}"
|
||||||
|
var_version="${var_version:-12}"
|
||||||
|
var_unprivileged="${var_unprivileged:-1}"
|
||||||
|
|
||||||
|
header_info "$APP"
|
||||||
|
variables
|
||||||
|
color
|
||||||
|
catch_errors
|
||||||
|
|
||||||
|
function update_script() {
|
||||||
|
header_info
|
||||||
|
check_container_storage
|
||||||
|
check_container_resources
|
||||||
|
if [[ ! -d /var ]]; then
|
||||||
|
msg_error "No ${APP} Installation Found!"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
RELEASE=$(curl -fsSL https://api.github.com/repos/C4illin/ConvertX/releases/latest | grep "tag_name" | awk '{print substr($2, 2, length($2)-3) }')
|
||||||
|
if [[ "${RELEASE}" != "$(cat ~/.convertx 2>/dev/null)" ]] || [[ ! -f ~/.convertx ]]; then
|
||||||
|
msg_info "Stopping $APP"
|
||||||
|
systemctl stop convertx
|
||||||
|
msg_ok "Stopped $APP"
|
||||||
|
|
||||||
|
msg_info "Move data-Folder"
|
||||||
|
if [[ -d /opt/convertx/data ]]; then
|
||||||
|
mv /opt/convertx/data /opt/data
|
||||||
|
fi
|
||||||
|
msg_ok "Moved data-Folder"
|
||||||
|
|
||||||
|
fetch_and_deploy_gh_release "ConvertX" "C4illin/ConvertX" "tarball" "latest" "/opt/convertx"
|
||||||
|
|
||||||
|
msg_info "Updating $APP to v${RELEASE}"
|
||||||
|
if [[ -d /opt/data ]]; then
|
||||||
|
mv /opt/data /opt/convertx/data
|
||||||
|
fi
|
||||||
|
cd /opt/convertx
|
||||||
|
$STD bun install
|
||||||
|
msg_ok "Updated $APP to v${RELEASE}"
|
||||||
|
|
||||||
|
msg_info "Starting $APP"
|
||||||
|
systemctl start convertx
|
||||||
|
msg_ok "Started $APP"
|
||||||
|
|
||||||
|
msg_ok "Update Successful"
|
||||||
|
else
|
||||||
|
msg_ok "No update required. ${APP} is already at v${RELEASE}"
|
||||||
|
fi
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
start
|
||||||
|
build_container
|
||||||
|
description
|
||||||
|
|
||||||
|
msg_ok "Completed Successfully!\n"
|
||||||
|
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||||
|
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
|
||||||
|
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:3000${CL}"
|
||||||
@@ -8,8 +8,8 @@ source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxV
|
|||||||
APP="Docmost"
|
APP="Docmost"
|
||||||
var_tags="${var_tags:-documents}"
|
var_tags="${var_tags:-documents}"
|
||||||
var_cpu="${var_cpu:-3}"
|
var_cpu="${var_cpu:-3}"
|
||||||
var_ram="${var_ram:-3072}"
|
var_ram="${var_ram:-4096}"
|
||||||
var_disk="${var_disk:-7}"
|
var_disk="${var_disk:-8}"
|
||||||
var_os="${var_os:-debian}"
|
var_os="${var_os:-debian}"
|
||||||
var_version="${var_version:-12}"
|
var_version="${var_version:-12}"
|
||||||
|
|
||||||
|
|||||||
6
ct/headers/booklore
Normal file
6
ct/headers/booklore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
____ __ __
|
||||||
|
/ __ )____ ____ / /__/ / ____ ________
|
||||||
|
/ __ / __ \/ __ \/ //_/ / / __ \/ ___/ _ \
|
||||||
|
/ /_/ / /_/ / /_/ / ,< / /___/ /_/ / / / __/
|
||||||
|
/_____/\____/\____/_/|_/_____/\____/_/ \___/
|
||||||
|
|
||||||
6
ct/headers/convertx
Normal file
6
ct/headers/convertx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
______ __ _ __
|
||||||
|
/ ____/___ ____ _ _____ _____/ /| |/ /
|
||||||
|
/ / / __ \/ __ \ | / / _ \/ ___/ __/ /
|
||||||
|
/ /___/ /_/ / / / / |/ / __/ / / /_/ |
|
||||||
|
\____/\____/_/ /_/|___/\___/_/ \__/_/|_|
|
||||||
|
|
||||||
@@ -70,6 +70,7 @@ function update_script() {
|
|||||||
systemctl stop jellyseerr
|
systemctl stop jellyseerr
|
||||||
rm -rf dist .next node_modules
|
rm -rf dist .next node_modules
|
||||||
export CYPRESS_INSTALL_BINARY=0
|
export CYPRESS_INSTALL_BINARY=0
|
||||||
|
cd /opt/jellyseerr
|
||||||
$STD pnpm install --frozen-lockfile
|
$STD pnpm install --frozen-lockfile
|
||||||
export NODE_OPTIONS="--max-old-space-size=3072"
|
export NODE_OPTIONS="--max-old-space-size=3072"
|
||||||
$STD pnpm build
|
$STD pnpm build
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ function update_script() {
|
|||||||
|
|
||||||
msg_info "Cleaning Up"
|
msg_info "Cleaning Up"
|
||||||
rm -rf "$BACKUP_FILE"
|
rm -rf "$BACKUP_FILE"
|
||||||
rm /tmp/"$RELEASE".zip
|
|
||||||
msg_ok "Cleanup Completed"
|
msg_ok "Cleanup Completed"
|
||||||
msg_ok "Update Successful"
|
msg_ok "Update Successful"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -27,9 +27,15 @@ function update_script() {
|
|||||||
msg_error "No ${APP} Installation Found!"
|
msg_error "No ${APP} Installation Found!"
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
msg_info "Updating $APP LXC"
|
msg_info "Updating OS"
|
||||||
$STD apt-get update
|
$STD apt-get update
|
||||||
$STD apt-get -y upgrade
|
$STD apt-get -y upgrade
|
||||||
|
msg_ok "Updated OS"
|
||||||
|
|
||||||
|
msg_info "Updating $APP LXC"
|
||||||
|
$STD yunohost tools update
|
||||||
|
$STD yunohost tools upgrade system
|
||||||
|
$STD yunohost tools upgrade apps
|
||||||
msg_ok "Updated $APP LXC"
|
msg_ok "Updated $APP LXC"
|
||||||
exit
|
exit
|
||||||
}
|
}
|
||||||
|
|||||||
5
frontend/.eslintrc.json
generated
5
frontend/.eslintrc.json
generated
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["next/core-web-vitals"],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"plugins": ["@typescript-eslint"]
|
|
||||||
}
|
|
||||||
51
frontend/.vscode/settings.json
generated
vendored
Normal file
51
frontend/.vscode/settings.json
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
// Disable the default formatter, use eslint instead
|
||||||
|
"prettier.enable": false,
|
||||||
|
"editor.formatOnSave": false,
|
||||||
|
|
||||||
|
// Auto fix
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.organizeImports": "never"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||||
|
"eslint.rules.customizations": [
|
||||||
|
{ "rule": "style/*", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "format/*", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-indent", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-spacing", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-spaces", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-order", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-dangle", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-newline", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*quotes", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*semi", "severity": "off", "fixable": true }
|
||||||
|
],
|
||||||
|
|
||||||
|
// Enable eslint for all supported languages
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact",
|
||||||
|
"vue",
|
||||||
|
"html",
|
||||||
|
"markdown",
|
||||||
|
"json",
|
||||||
|
"json5",
|
||||||
|
"jsonc",
|
||||||
|
"yaml",
|
||||||
|
"toml",
|
||||||
|
"xml",
|
||||||
|
"gql",
|
||||||
|
"graphql",
|
||||||
|
"astro",
|
||||||
|
"svelte",
|
||||||
|
"css",
|
||||||
|
"less",
|
||||||
|
"scss",
|
||||||
|
"pcss",
|
||||||
|
"postcss"
|
||||||
|
]
|
||||||
|
}
|
||||||
281
frontend/README.md
Normal file
281
frontend/README.md
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
# Proxmox VE Helper-Scripts Frontend
|
||||||
|
|
||||||
|
> 🚀 **Modern frontend for the Community-Scripts Proxmox VE Helper-Scripts repository**
|
||||||
|
|
||||||
|
A comprehensive, user-friendly interface built with Next.js that provides access to 300+ automation scripts for Proxmox Virtual Environment management. This frontend serves as the official website for the Community-Scripts organization's Proxmox VE Helper-Scripts repository.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## 🌟 Features
|
||||||
|
|
||||||
|
### Core Functionality
|
||||||
|
|
||||||
|
- **📜 Script Management**: Browse, search, and filter 300+ Proxmox VE scripts
|
||||||
|
- **📱 Responsive Design**: Mobile-first approach with modern UI/UX
|
||||||
|
- **🔍 Advanced Search**: Fuzzy search with category filtering
|
||||||
|
- **📊 Analytics Integration**: Built-in analytics for usage tracking
|
||||||
|
- **🌙 Dark/Light Mode**: Theme switching with system preference detection
|
||||||
|
- **⚡ Performance Optimized**: Static site generation for lightning-fast loading
|
||||||
|
|
||||||
|
### Technical Features
|
||||||
|
|
||||||
|
- **🎨 Modern UI Components**: Built with Radix UI and shadcn/ui
|
||||||
|
- **📈 Data Visualization**: Charts and metrics using Chart.js
|
||||||
|
- **🔄 State Management**: React Query for efficient data fetching
|
||||||
|
- **📝 Type Safety**: Full TypeScript implementation
|
||||||
|
- **🚀 Static Export**: Optimized for GitHub Pages deployment
|
||||||
|
|
||||||
|
## 🛠️ Tech Stack
|
||||||
|
|
||||||
|
### Frontend Framework
|
||||||
|
|
||||||
|
- **[Next.js 15.2.4](https://nextjs.org/)** - React framework with App Router
|
||||||
|
- **[React 19.0.0](https://react.dev/)** - Latest React with concurrent features
|
||||||
|
- **[TypeScript 5.8.2](https://www.typescriptlang.org/)** - Type-safe JavaScript
|
||||||
|
|
||||||
|
### Styling & UI
|
||||||
|
|
||||||
|
- **[Tailwind CSS 3.4.17](https://tailwindcss.com/)** - Utility-first CSS framework
|
||||||
|
- **[Radix UI](https://www.radix-ui.com/)** - Unstyled, accessible UI components
|
||||||
|
- **[shadcn/ui](https://ui.shadcn.com/)** - Re-usable components built on Radix UI
|
||||||
|
- **[Framer Motion](https://www.framer.com/motion/)** - Animation library
|
||||||
|
- **[Lucide React](https://lucide.dev/)** - Icon library
|
||||||
|
|
||||||
|
### Data & State Management
|
||||||
|
|
||||||
|
- **[TanStack Query 5.71.1](https://tanstack.com/query)** - Powerful data synchronization
|
||||||
|
- **[Zod 3.24.2](https://zod.dev/)** - TypeScript-first schema validation
|
||||||
|
- **[nuqs 2.4.1](https://nuqs.47ng.com/)** - Type-safe search params state manager
|
||||||
|
|
||||||
|
### Development Tools
|
||||||
|
|
||||||
|
- **[Vitest 3.1.1](https://vitest.dev/)** - Fast unit testing framework
|
||||||
|
- **[React Testing Library](https://testing-library.com/react)** - Simple testing utilities
|
||||||
|
- **[ESLint](https://eslint.org/)** - Code linting and formatting
|
||||||
|
- **[Prettier](https://prettier.io/)** - Code formatting
|
||||||
|
|
||||||
|
### Additional Libraries
|
||||||
|
|
||||||
|
- **[Chart.js](https://www.chartjs.org/)** - Data visualization
|
||||||
|
- **[Fuse.js](https://fusejs.io/)** - Fuzzy search
|
||||||
|
- **[date-fns](https://date-fns.org/)** - Date utility library
|
||||||
|
- **[Next Themes](https://github.com/pacocoursey/next-themes)** - Theme management
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- **Node.js 18+** (recommend using the latest LTS version)
|
||||||
|
- **npm**, **yarn**, **pnpm**, or **bun** package manager
|
||||||
|
- **Git** for version control
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. **Clone the repository**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/community-scripts/ProxmoxVE.git
|
||||||
|
cd ProxmoxVE/frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install dependencies**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using npm
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Using yarn
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# Using pnpm
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Using bun
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Start the development server**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Open your browser**
|
||||||
|
|
||||||
|
Navigate to [http://localhost:3000](http://localhost:3000) to see the application running.
|
||||||
|
|
||||||
|
### Environment Configuration
|
||||||
|
|
||||||
|
The application uses the following environment variables:
|
||||||
|
|
||||||
|
- `BASE_PATH`: Set to "ProxmoxVE" for GitHub Pages deployment
|
||||||
|
- Analytics configuration is handled in `src/config/siteConfig.tsx`
|
||||||
|
|
||||||
|
## 🧪 Development
|
||||||
|
|
||||||
|
### Available Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
npm run dev # Start development server with Turbopack
|
||||||
|
npm run build # Build for production
|
||||||
|
npm run start # Start production server (after build)
|
||||||
|
|
||||||
|
# Code Quality
|
||||||
|
npm run lint # Run ESLint
|
||||||
|
npm run typecheck # Run TypeScript type checking
|
||||||
|
npm run format:write # Format code with Prettier
|
||||||
|
npm run format:check # Check code formatting
|
||||||
|
|
||||||
|
# Deployment
|
||||||
|
npm run deploy # Build and deploy to GitHub Pages
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
|
||||||
|
1. **Feature Development**
|
||||||
|
|
||||||
|
- Create a new branch for your feature
|
||||||
|
- Follow the established TypeScript and React patterns
|
||||||
|
- Use the existing component library (shadcn/ui)
|
||||||
|
- Ensure responsive design principles
|
||||||
|
|
||||||
|
2. **Code Standards**
|
||||||
|
|
||||||
|
- Follow TypeScript strict mode
|
||||||
|
- Use functional components with hooks
|
||||||
|
- Implement proper error boundaries
|
||||||
|
- Write descriptive variable and function names
|
||||||
|
- Use early returns for better readability
|
||||||
|
|
||||||
|
3. **Styling Guidelines**
|
||||||
|
|
||||||
|
- Use Tailwind CSS utility classes
|
||||||
|
- Follow mobile-first responsive design
|
||||||
|
- Implement dark/light mode considerations
|
||||||
|
- Use CSS variables from the design system
|
||||||
|
|
||||||
|
4. **Testing**
|
||||||
|
- Write unit tests for utility functions
|
||||||
|
- Test React components with React Testing Library
|
||||||
|
- Ensure accessibility standards are met
|
||||||
|
- Run tests before committing
|
||||||
|
|
||||||
|
### Component Development
|
||||||
|
|
||||||
|
The project uses a component-driven development approach:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Example component structure
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
interface ComponentProps {
|
||||||
|
title: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Component = ({ title, className }: ComponentProps) => {
|
||||||
|
return (
|
||||||
|
<div className={cn("default-classes", className)}>
|
||||||
|
<Button>{title}</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration for Static Export
|
||||||
|
|
||||||
|
The application is configured for static export in `next.config.mjs`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const nextConfig = {
|
||||||
|
output: "export",
|
||||||
|
basePath: `/ProxmoxVE`,
|
||||||
|
images: {
|
||||||
|
unoptimized: true // Required for static export
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
We welcome contributions from the community! Here's how you can help:
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
|
||||||
|
1. **Fork the repository** on GitHub
|
||||||
|
2. **Clone your fork** locally
|
||||||
|
3. **Create a new branch** for your feature or bugfix
|
||||||
|
4. **Make your changes** following our coding standards
|
||||||
|
5. **Submit a pull request** with a clear description
|
||||||
|
|
||||||
|
### Contribution Guidelines
|
||||||
|
|
||||||
|
#### Code Style
|
||||||
|
|
||||||
|
- Follow the existing TypeScript and React patterns
|
||||||
|
- Use descriptive variable and function names
|
||||||
|
- Implement proper error handling
|
||||||
|
- Write self-documenting code with appropriate comments
|
||||||
|
|
||||||
|
#### Component Guidelines
|
||||||
|
|
||||||
|
- Use functional components with hooks
|
||||||
|
- Implement proper TypeScript types
|
||||||
|
- Follow accessibility best practices
|
||||||
|
- Ensure responsive design
|
||||||
|
- Use the existing design system components
|
||||||
|
|
||||||
|
#### Pull Request Process
|
||||||
|
|
||||||
|
1. Update documentation if needed
|
||||||
|
2. Update the README if you've added new features
|
||||||
|
3. Request review from maintainers
|
||||||
|
|
||||||
|
### Areas for Contribution
|
||||||
|
|
||||||
|
- **🐛 Bug fixes**: Report and fix issues
|
||||||
|
- **✨ New features**: Enhance functionality
|
||||||
|
- **📚 Documentation**: Improve guides and examples
|
||||||
|
- **🎨 UI/UX**: Improve design and user experience
|
||||||
|
- **♿ Accessibility**: Enhance accessibility features
|
||||||
|
- **🚀 Performance**: Optimize loading and runtime performance
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
- **[tteck](https://github.com/tteck)** - Original creator of the Proxmox VE Helper-Scripts
|
||||||
|
- **[Community-Scripts Organization](https://github.com/community-scripts)** - Maintaining and expanding the project
|
||||||
|
- **[Proxmox Community](https://forum.proxmox.com/)** - For continuous feedback and support
|
||||||
|
- **All Contributors** - Thank you for your valuable contributions!
|
||||||
|
|
||||||
|
## 📚 Additional Resources
|
||||||
|
|
||||||
|
- **[Proxmox VE Documentation](https://pve.proxmox.com/pve-docs/)**
|
||||||
|
- **[Community Scripts Repository](https://github.com/community-scripts/ProxmoxVE)**
|
||||||
|
- **[Discord Community](https://discord.gg/2wvnMDgdnU)**
|
||||||
|
- **[GitHub Discussions](https://github.com/community-scripts/ProxmoxVE/discussions)**
|
||||||
|
|
||||||
|
## 🔗 Links
|
||||||
|
|
||||||
|
- **🌐 Live Website**: [https://community-scripts.github.io/ProxmoxVE/](https://community-scripts.github.io/ProxmoxVE/)
|
||||||
|
- **💬 Discord Server**: [https://discord.gg/2wvnMDgdnU](https://discord.gg/2wvnMDgdnU)
|
||||||
|
- **📝 Change Log**: [https://github.com/community-scripts/ProxmoxVE/blob/main/CHANGELOG.md](https://github.com/community-scripts/ProxmoxVE/blob/main/CHANGELOG.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Made with ❤️ by the Community-Scripts team and contributors**
|
||||||
41
frontend/eslint.config.mjs
Normal file
41
frontend/eslint.config.mjs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import antfu from "@antfu/eslint-config";
|
||||||
|
|
||||||
|
export default antfu(
|
||||||
|
{
|
||||||
|
type: "app",
|
||||||
|
typescript: true,
|
||||||
|
formatters: true,
|
||||||
|
next: true,
|
||||||
|
stylistic: {
|
||||||
|
indent: 2,
|
||||||
|
semi: true,
|
||||||
|
quotes: "double",
|
||||||
|
},
|
||||||
|
ignores: ["src/components/ui/**", "README.md", "public/json/**"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
"ts/no-redeclare": "off",
|
||||||
|
"ts/consistent-type-definitions": ["error", "type"],
|
||||||
|
"no-console": ["warn"],
|
||||||
|
"antfu/no-top-level-await": ["off"],
|
||||||
|
"node/prefer-global/process": ["off"],
|
||||||
|
"node/no-process-env": ["error"],
|
||||||
|
"perfectionist/sort-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
type: "line-length",
|
||||||
|
order: "desc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
"unicorn/filename-case": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
case: "kebabCase",
|
||||||
|
ignore: ["README.md"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
8372
frontend/package-lock.json
generated
8372
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
frontend/package.json
generated
30
frontend/package.json
generated
@@ -1,22 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "proxmox-helper-scripts-website",
|
"name": "proxmox-helper-scripts-website",
|
||||||
|
"type": "module",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Bram Suurd",
|
"name": "Bram Suurd",
|
||||||
"url": "https://github.com/community-scripts"
|
"url": "https://github.com/community-scripts"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "eslint . --fix",
|
||||||
"test": "vitest",
|
|
||||||
"deploy": "next build && touch out/.nojekyll && git add out/ && git commit -m \"Deploy\" && git subtree push --prefix out origin gh-pages",
|
|
||||||
"format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache",
|
|
||||||
"format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache",
|
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -45,7 +41,7 @@
|
|||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
"mini-svg-data-uri": "^1.4.4",
|
"mini-svg-data-uri": "^1.4.4",
|
||||||
"next": "15.2.4",
|
"next": "15.2.4",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.4.4",
|
||||||
"nuqs": "^2.4.1",
|
"nuqs": "^2.4.1",
|
||||||
"pocketbase": "^0.21.5",
|
"pocketbase": "^0.21.5",
|
||||||
"prettier-plugin-organize-imports": "^4.1.0",
|
"prettier-plugin-organize-imports": "^4.1.0",
|
||||||
@@ -53,7 +49,7 @@
|
|||||||
"react-chartjs-2": "^5.3.0",
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-code-blocks": "^0.1.6",
|
"react-code-blocks": "^0.1.6",
|
||||||
"react-datepicker": "^7.6.0",
|
"react-datepicker": "^7.6.0",
|
||||||
"react-day-picker": "8.10.1",
|
"react-day-picker": "^9.4.3",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-simple-typewriter": "^5.0.1",
|
"react-simple-typewriter": "^5.0.1",
|
||||||
@@ -64,9 +60,10 @@
|
|||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@antfu/eslint-config": "^4.16.1",
|
||||||
|
"@eslint-react/eslint-plugin": "^1.52.2",
|
||||||
|
"@next/eslint-plugin-next": "^15.3.4",
|
||||||
"@tanstack/eslint-plugin-query": "^5.68.0",
|
"@tanstack/eslint-plugin-query": "^5.68.0",
|
||||||
"@testing-library/dom": "^10.4.0",
|
|
||||||
"@testing-library/react": "^16.2.0",
|
|
||||||
"@types/node": "^22.13.16",
|
"@types/node": "^22.13.16",
|
||||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||||
@@ -75,6 +72,9 @@
|
|||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.23.0",
|
||||||
"eslint-config-next": "15.0.2",
|
"eslint-config-next": "15.0.2",
|
||||||
|
"eslint-plugin-format": "^1.0.1",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
"jsdom": "^25.0.1",
|
"jsdom": "^25.0.1",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
@@ -83,11 +83,13 @@
|
|||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"tailwindcss-animated": "^1.1.2",
|
"tailwindcss-animated": "^1.1.2",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
"vitest": "^3.1.1"
|
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
|
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"react": "19.0.0",
|
||||||
|
"react-dom": "19.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
frontend/public/json/add-tailscale-lxc.json
generated
4
frontend/public/json/add-tailscale-lxc.json
generated
@@ -32,10 +32,6 @@
|
|||||||
"password": null
|
"password": null
|
||||||
},
|
},
|
||||||
"notes": [
|
"notes": [
|
||||||
{
|
|
||||||
"text": "Only supported on Debian 12 LXCs",
|
|
||||||
"type": "warning"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"text": "After the script finishes, reboot the LXC then run `tailscale up` in the LXC console",
|
"text": "After the script finishes, reboot the LXC then run `tailscale up` in the LXC console",
|
||||||
"type": "info"
|
"type": "info"
|
||||||
|
|||||||
2
frontend/public/json/alpine.json
generated
2
frontend/public/json/alpine.json
generated
@@ -6,7 +6,7 @@
|
|||||||
],
|
],
|
||||||
"date_created": "2024-05-02",
|
"date_created": "2024-05-02",
|
||||||
"type": "ct",
|
"type": "ct",
|
||||||
"updateable": false,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": null,
|
"interface_port": null,
|
||||||
"documentation": null,
|
"documentation": null,
|
||||||
|
|||||||
2
frontend/public/json/archlinux-vm.json
generated
2
frontend/public/json/archlinux-vm.json
generated
@@ -6,7 +6,7 @@
|
|||||||
],
|
],
|
||||||
"date_created": "2025-01-27",
|
"date_created": "2025-01-27",
|
||||||
"type": "vm",
|
"type": "vm",
|
||||||
"updateable": false,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": null,
|
"interface_port": null,
|
||||||
"documentation": null,
|
"documentation": null,
|
||||||
|
|||||||
35
frontend/public/json/booklore.json
generated
Normal file
35
frontend/public/json/booklore.json
generated
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "BookLore",
|
||||||
|
"slug": "booklore",
|
||||||
|
"categories": [
|
||||||
|
13
|
||||||
|
],
|
||||||
|
"date_created": "2025-06-27",
|
||||||
|
"type": "ct",
|
||||||
|
"updateable": true,
|
||||||
|
"privileged": false,
|
||||||
|
"interface_port": 6060,
|
||||||
|
"documentation": "https://github.com/ejmg/booklore",
|
||||||
|
"website": "https://booklore.app/",
|
||||||
|
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/booklore.webp",
|
||||||
|
"config_path": "/opt/booklore_storage/.env",
|
||||||
|
"description": "BookLore is a self-hosted digital library for managing and reading books, offering a beautiful interface and support for metadata management. Built with a modern tech stack, it provides support for importing, organizing, and reading EPUBs and PDFs, while also managing cover images and book metadata.",
|
||||||
|
"install_methods": [
|
||||||
|
{
|
||||||
|
"type": "default",
|
||||||
|
"script": "ct/booklore.sh",
|
||||||
|
"resources": {
|
||||||
|
"cpu": 3,
|
||||||
|
"ram": 2048,
|
||||||
|
"hdd": 7,
|
||||||
|
"os": "debian",
|
||||||
|
"version": "12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default_credentials": {
|
||||||
|
"username": null,
|
||||||
|
"password": null
|
||||||
|
},
|
||||||
|
"notes": []
|
||||||
|
}
|
||||||
40
frontend/public/json/convertx.json
generated
Normal file
40
frontend/public/json/convertx.json
generated
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "ConvertX",
|
||||||
|
"slug": "convertx",
|
||||||
|
"categories": [
|
||||||
|
9
|
||||||
|
],
|
||||||
|
"date_created": "2025-06-26",
|
||||||
|
"type": "ct",
|
||||||
|
"updateable": true,
|
||||||
|
"privileged": false,
|
||||||
|
"config_path": "/opt/convertx/.env",
|
||||||
|
"interface_port": 3000,
|
||||||
|
"documentation": "https://github.com/C4illin/ConvertX",
|
||||||
|
"website": "https://github.com/C4illin/ConvertX",
|
||||||
|
"logo": "https://raw.githubusercontent.com/selfhst/icons/refs/heads/main/svg/convertx.svg",
|
||||||
|
"description": "ConvertX is a self-hosted online file converter supporting over 1000 formats, including images, audio, video, documents, and more, powered by FFmpeg, GraphicsMagick, and other libraries.",
|
||||||
|
"install_methods": [
|
||||||
|
{
|
||||||
|
"type": "default",
|
||||||
|
"script": "ct/convertx.sh",
|
||||||
|
"resources": {
|
||||||
|
"cpu": 2,
|
||||||
|
"ram": 4096,
|
||||||
|
"hdd": 20,
|
||||||
|
"os": "Debian",
|
||||||
|
"version": "12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default_credentials": {
|
||||||
|
"username": null,
|
||||||
|
"password": null
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
{
|
||||||
|
"text": "Complete setup via the web interface at http://<container-ip>:3000. Create and secure the admin account immediately.",
|
||||||
|
"type": "info"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
2
frontend/public/json/debian-vm.json
generated
2
frontend/public/json/debian-vm.json
generated
@@ -6,7 +6,7 @@
|
|||||||
],
|
],
|
||||||
"date_created": "2024-05-02",
|
"date_created": "2024-05-02",
|
||||||
"type": "vm",
|
"type": "vm",
|
||||||
"updateable": false,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": null,
|
"interface_port": null,
|
||||||
"documentation": null,
|
"documentation": null,
|
||||||
|
|||||||
2
frontend/public/json/debian.json
generated
2
frontend/public/json/debian.json
generated
@@ -6,7 +6,7 @@
|
|||||||
],
|
],
|
||||||
"date_created": "2024-05-02",
|
"date_created": "2024-05-02",
|
||||||
"type": "ct",
|
"type": "ct",
|
||||||
"updateable": false,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": null,
|
"interface_port": null,
|
||||||
"documentation": null,
|
"documentation": null,
|
||||||
|
|||||||
2
frontend/public/json/docker-vm.json
generated
2
frontend/public/json/docker-vm.json
generated
@@ -7,7 +7,7 @@
|
|||||||
],
|
],
|
||||||
"date_created": "2025-01-20",
|
"date_created": "2025-01-20",
|
||||||
"type": "vm",
|
"type": "vm",
|
||||||
"updateable": false,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": null,
|
"interface_port": null,
|
||||||
"documentation": null,
|
"documentation": null,
|
||||||
|
|||||||
4
frontend/public/json/docmost.json
generated
4
frontend/public/json/docmost.json
generated
@@ -20,8 +20,8 @@
|
|||||||
"script": "ct/docmost.sh",
|
"script": "ct/docmost.sh",
|
||||||
"resources": {
|
"resources": {
|
||||||
"cpu": 3,
|
"cpu": 3,
|
||||||
"ram": 3072,
|
"ram": 4096,
|
||||||
"hdd": 7,
|
"hdd": 8,
|
||||||
"os": "debian",
|
"os": "debian",
|
||||||
"version": "12"
|
"version": "12"
|
||||||
}
|
}
|
||||||
|
|||||||
2
frontend/public/json/haos-vm.json
generated
2
frontend/public/json/haos-vm.json
generated
@@ -6,7 +6,7 @@
|
|||||||
],
|
],
|
||||||
"date_created": "2024-04-29",
|
"date_created": "2024-04-29",
|
||||||
"type": "vm",
|
"type": "vm",
|
||||||
"updateable": false,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": 8123,
|
"interface_port": 8123,
|
||||||
"documentation": "https://www.home-assistant.io/docs/",
|
"documentation": "https://www.home-assistant.io/docs/",
|
||||||
|
|||||||
2
frontend/public/json/mikrotik-routeros.json
generated
2
frontend/public/json/mikrotik-routeros.json
generated
@@ -7,7 +7,7 @@
|
|||||||
],
|
],
|
||||||
"date_created": "2024-05-02",
|
"date_created": "2024-05-02",
|
||||||
"type": "vm",
|
"type": "vm",
|
||||||
"updateable": false,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": null,
|
"interface_port": null,
|
||||||
"documentation": null,
|
"documentation": null,
|
||||||
|
|||||||
2
frontend/public/json/nextcloud-vm.json
generated
2
frontend/public/json/nextcloud-vm.json
generated
@@ -6,7 +6,7 @@
|
|||||||
],
|
],
|
||||||
"date_created": "2023-11-14",
|
"date_created": "2023-11-14",
|
||||||
"type": "vm",
|
"type": "vm",
|
||||||
"updateable": false,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": 80,
|
"interface_port": 80,
|
||||||
"documentation": null,
|
"documentation": null,
|
||||||
|
|||||||
2
frontend/public/json/openwrt.json
generated
2
frontend/public/json/openwrt.json
generated
@@ -7,7 +7,7 @@
|
|||||||
],
|
],
|
||||||
"date_created": "2024-05-02",
|
"date_created": "2024-05-02",
|
||||||
"type": "vm",
|
"type": "vm",
|
||||||
"updateable": false,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": null,
|
"interface_port": null,
|
||||||
"documentation": null,
|
"documentation": null,
|
||||||
|
|||||||
2
frontend/public/json/opnsense-vm.json
generated
2
frontend/public/json/opnsense-vm.json
generated
@@ -7,7 +7,7 @@
|
|||||||
],
|
],
|
||||||
"date_created": "2025-02-11",
|
"date_created": "2025-02-11",
|
||||||
"type": "vm",
|
"type": "vm",
|
||||||
"updateable": false,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": 443,
|
"interface_port": 443,
|
||||||
"documentation": "https://docs.opnsense.org/",
|
"documentation": "https://docs.opnsense.org/",
|
||||||
|
|||||||
2
frontend/public/json/owncloud-vm.json
generated
2
frontend/public/json/owncloud-vm.json
generated
@@ -6,7 +6,7 @@
|
|||||||
],
|
],
|
||||||
"date_created": "2024-05-02",
|
"date_created": "2024-05-02",
|
||||||
"type": "vm",
|
"type": "vm",
|
||||||
"updateable": false,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": 80,
|
"interface_port": 80,
|
||||||
"documentation": null,
|
"documentation": null,
|
||||||
|
|||||||
2
frontend/public/json/pimox-haos-vm.json
generated
2
frontend/public/json/pimox-haos-vm.json
generated
@@ -6,7 +6,7 @@
|
|||||||
],
|
],
|
||||||
"date_created": "2024-04-29",
|
"date_created": "2024-04-29",
|
||||||
"type": "vm",
|
"type": "vm",
|
||||||
"updateable": false,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": 8123,
|
"interface_port": 8123,
|
||||||
"documentation": null,
|
"documentation": null,
|
||||||
|
|||||||
2
frontend/public/json/postgresql.json
generated
2
frontend/public/json/postgresql.json
generated
@@ -6,7 +6,7 @@
|
|||||||
],
|
],
|
||||||
"date_created": "2024-05-02",
|
"date_created": "2024-05-02",
|
||||||
"type": "ct",
|
"type": "ct",
|
||||||
"updateable": false,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": 5432,
|
"interface_port": 5432,
|
||||||
"documentation": null,
|
"documentation": null,
|
||||||
|
|||||||
2
frontend/public/json/ubuntu.json
generated
2
frontend/public/json/ubuntu.json
generated
@@ -6,7 +6,7 @@
|
|||||||
],
|
],
|
||||||
"date_created": "2024-05-02",
|
"date_created": "2024-05-02",
|
||||||
"type": "ct",
|
"type": "ct",
|
||||||
"updateable": false,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": null,
|
"interface_port": null,
|
||||||
"documentation": null,
|
"documentation": null,
|
||||||
|
|||||||
2
frontend/public/json/ubuntu2204-vm.json
generated
2
frontend/public/json/ubuntu2204-vm.json
generated
@@ -6,7 +6,7 @@
|
|||||||
],
|
],
|
||||||
"date_created": "2024-05-02",
|
"date_created": "2024-05-02",
|
||||||
"type": "vm",
|
"type": "vm",
|
||||||
"updateable": false,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": null,
|
"interface_port": null,
|
||||||
"documentation": null,
|
"documentation": null,
|
||||||
|
|||||||
2
frontend/public/json/ubuntu2404-vm.json
generated
2
frontend/public/json/ubuntu2404-vm.json
generated
@@ -6,7 +6,7 @@
|
|||||||
],
|
],
|
||||||
"date_created": "2024-05-02",
|
"date_created": "2024-05-02",
|
||||||
"type": "vm",
|
"type": "vm",
|
||||||
"updateable": false,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": null,
|
"interface_port": null,
|
||||||
"documentation": null,
|
"documentation": null,
|
||||||
|
|||||||
2
frontend/public/json/ubuntu2410-vm.json
generated
2
frontend/public/json/ubuntu2410-vm.json
generated
@@ -6,7 +6,7 @@
|
|||||||
],
|
],
|
||||||
"date_created": "2025-01-24",
|
"date_created": "2025-01-24",
|
||||||
"type": "vm",
|
"type": "vm",
|
||||||
"updateable": false,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": null,
|
"interface_port": null,
|
||||||
"documentation": null,
|
"documentation": null,
|
||||||
|
|||||||
2
frontend/public/json/ubuntu2504-vm.json
generated
2
frontend/public/json/ubuntu2504-vm.json
generated
@@ -6,7 +6,7 @@
|
|||||||
],
|
],
|
||||||
"date_created": "2025-06-19",
|
"date_created": "2025-06-19",
|
||||||
"type": "vm",
|
"type": "vm",
|
||||||
"updateable": false,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": null,
|
"interface_port": null,
|
||||||
"documentation": null,
|
"documentation": null,
|
||||||
|
|||||||
414
frontend/public/json/versions.json
generated
414
frontend/public/json/versions.json
generated
@@ -1,19 +1,214 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"name": "plexguide/Huntarr.io",
|
||||||
|
"version": "8.1.10",
|
||||||
|
"date": "2025-06-27T23:24:52Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pocket-id/pocket-id",
|
||||||
|
"version": "v1.5.0",
|
||||||
|
"date": "2025-06-27T22:04:32Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "linkwarden/linkwarden",
|
||||||
|
"version": "v2.11.1",
|
||||||
|
"date": "2025-06-27T21:21:59Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "homarr-labs/homarr",
|
||||||
|
"version": "v1.26.0",
|
||||||
|
"date": "2025-06-27T19:15:24Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ollama/ollama",
|
||||||
|
"version": "v0.9.4-rc2",
|
||||||
|
"date": "2025-06-27T18:45:33Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "home-assistant/core",
|
||||||
|
"version": "2025.6.3",
|
||||||
|
"date": "2025-06-24T13:00:12Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mattermost/mattermost",
|
||||||
|
"version": "preview-v0.1",
|
||||||
|
"date": "2025-06-27T14:35:47Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "goauthentik/authentik",
|
||||||
|
"version": "version/2025.6.3",
|
||||||
|
"date": "2025-06-27T14:01:06Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rclone/rclone",
|
||||||
|
"version": "v1.70.2",
|
||||||
|
"date": "2025-06-27T13:21:17Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "documenso/documenso",
|
||||||
|
"version": "v1.12.0-rc.7",
|
||||||
|
"date": "2025-06-27T12:17:45Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sabnzbd/sabnzbd",
|
||||||
|
"version": "4.5.1",
|
||||||
|
"date": "2025-04-11T09:57:47Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FlowiseAI/Flowise",
|
||||||
|
"version": "flowise@3.0.3",
|
||||||
|
"date": "2025-06-27T09:53:57Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nzbgetcom/nzbget",
|
||||||
|
"version": "v25.1",
|
||||||
|
"date": "2025-06-27T09:14:14Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cockpit-project/cockpit",
|
||||||
|
"version": "341.1",
|
||||||
|
"date": "2025-06-27T08:50:16Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "zabbix/zabbix",
|
||||||
|
"version": "7.2.10",
|
||||||
|
"date": "2025-06-27T06:40:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jackett/Jackett",
|
||||||
|
"version": "v0.22.2064",
|
||||||
|
"date": "2025-06-27T05:54:03Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MediaBrowser/Emby.Releases",
|
||||||
|
"version": "4.9.1.2",
|
||||||
|
"date": "2025-06-26T22:08:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "prometheus/prometheus",
|
||||||
|
"version": "v3.4.2",
|
||||||
|
"date": "2025-06-26T21:45:21Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "home-assistant/operating-system",
|
||||||
|
"version": "15.2",
|
||||||
|
"date": "2025-04-14T15:37:12Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keycloak/keycloak",
|
||||||
|
"version": "26.2.5",
|
||||||
|
"date": "2025-05-28T06:49:43Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "netbox-community/netbox",
|
||||||
|
"version": "v4.3.3",
|
||||||
|
"date": "2025-06-26T18:42:56Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "apache/tika",
|
||||||
|
"version": "3.2.1-rc2",
|
||||||
|
"date": "2025-06-26T17:10:25Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tailscale/tailscale",
|
||||||
|
"version": "v1.84.3",
|
||||||
|
"date": "2025-06-26T16:31:57Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fuma-nama/fumadocs",
|
||||||
|
"version": "fumadocs-ui@15.5.5",
|
||||||
|
"date": "2025-06-26T15:54:17Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "traefik/traefik",
|
||||||
|
"version": "v3.5.0-rc1",
|
||||||
|
"date": "2025-06-26T15:08:43Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "meilisearch/meilisearch",
|
||||||
|
"version": "prototype-no-simd-x86-arroy-0",
|
||||||
|
"date": "2025-06-26T14:54:18Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AdguardTeam/AdGuardHome",
|
||||||
|
"version": "v0.107.63",
|
||||||
|
"date": "2025-06-26T14:34:19Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "node-red/node-red",
|
||||||
|
"version": "4.1.0-beta.2",
|
||||||
|
"date": "2025-06-26T14:23:26Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dolibarr/dolibarr",
|
||||||
|
"version": "18.0.7",
|
||||||
|
"date": "2025-06-26T09:16:33Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "firefly-iii/firefly-iii",
|
||||||
|
"version": "v6.2.18",
|
||||||
|
"date": "2025-06-20T04:45:37Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mongodb/mongo",
|
||||||
|
"version": "r8.1.2-rc1",
|
||||||
|
"date": "2025-06-25T22:42:04Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rcourtman/Pulse",
|
||||||
|
"version": "v3.32.0",
|
||||||
|
"date": "2025-06-25T22:27:01Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "gristlabs/grist-core",
|
||||||
|
"version": "v1.6.1",
|
||||||
|
"date": "2025-06-25T21:19:25Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "coder/code-server",
|
||||||
|
"version": "v4.101.2",
|
||||||
|
"date": "2025-06-25T21:18:52Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "msgbyte/tianji",
|
||||||
|
"version": "v1.22.4",
|
||||||
|
"date": "2025-06-25T20:46:20Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "influxdata/influxdb",
|
||||||
|
"version": "v3.2.0",
|
||||||
|
"date": "2025-06-25T17:31:48Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "wavelog/wavelog",
|
||||||
|
"version": "2.0.5",
|
||||||
|
"date": "2025-06-25T14:53:31Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "jenkinsci/jenkins",
|
||||||
|
"version": "jenkins-2.504.3",
|
||||||
|
"date": "2025-06-25T14:43:01Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bunkerity/bunkerweb",
|
||||||
|
"version": "testing",
|
||||||
|
"date": "2025-06-16T18:10:42Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "n8n-io/n8n",
|
||||||
|
"version": "n8n@1.100.0",
|
||||||
|
"date": "2025-06-23T12:48:35Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "moghtech/komodo",
|
||||||
|
"version": "v1.18.4",
|
||||||
|
"date": "2025-06-25T00:06:56Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "duplicati/duplicati",
|
"name": "duplicati/duplicati",
|
||||||
"version": "v2.1.0.120-2.1.0.120_canary_2025-06-24",
|
"version": "v2.1.0.120-2.1.0.120_canary_2025-06-24",
|
||||||
"date": "2025-06-24T22:39:50Z"
|
"date": "2025-06-24T22:39:50Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "jenkinsci/jenkins",
|
|
||||||
"version": "jenkins-2.516",
|
|
||||||
"date": "2025-06-24T21:06:15Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "ollama/ollama",
|
|
||||||
"version": "v0.9.3-rc1",
|
|
||||||
"date": "2025-06-24T20:26:55Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "evcc-io/evcc",
|
"name": "evcc-io/evcc",
|
||||||
"version": "0.204.5",
|
"version": "0.204.5",
|
||||||
@@ -39,56 +234,26 @@
|
|||||||
"version": "v2.37.3",
|
"version": "v2.37.3",
|
||||||
"date": "2025-06-24T14:05:33Z"
|
"date": "2025-06-24T14:05:33Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "influxdata/influxdb",
|
|
||||||
"version": "v3.2.0",
|
|
||||||
"date": "2025-06-24T13:20:53Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Checkmk/checkmk",
|
"name": "Checkmk/checkmk",
|
||||||
"version": "v2.4.0p5",
|
"version": "v2.4.0p5",
|
||||||
"date": "2025-06-24T13:06:53Z"
|
"date": "2025-06-24T13:06:53Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "home-assistant/core",
|
|
||||||
"version": "2025.6.3",
|
|
||||||
"date": "2025-06-24T13:00:12Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "rcourtman/Pulse",
|
|
||||||
"version": "v3.31.2",
|
|
||||||
"date": "2025-06-24T09:45:34Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "fallenbagel/jellyseerr",
|
"name": "fallenbagel/jellyseerr",
|
||||||
"version": "preview-fix-proxy-axios",
|
"version": "preview-fix-proxy-axios",
|
||||||
"date": "2025-06-24T08:50:22Z"
|
"date": "2025-06-24T08:50:22Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Jackett/Jackett",
|
|
||||||
"version": "v0.22.2052",
|
|
||||||
"date": "2025-06-24T05:59:30Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "wazuh/wazuh",
|
"name": "wazuh/wazuh",
|
||||||
"version": "coverity-w26-4.13.0",
|
"version": "coverity-w26-4.13.0",
|
||||||
"date": "2025-06-24T02:02:34Z"
|
"date": "2025-06-24T02:02:34Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "meilisearch/meilisearch",
|
|
||||||
"version": "prototype-incremental-vector-store-1",
|
|
||||||
"date": "2025-06-23T21:37:47Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "minio/minio",
|
"name": "minio/minio",
|
||||||
"version": "RELEASE.2025-06-13T11-33-47Z",
|
"version": "RELEASE.2025-06-13T11-33-47Z",
|
||||||
"date": "2025-06-23T20:58:42Z"
|
"date": "2025-06-23T20:58:42Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "keycloak/keycloak",
|
|
||||||
"version": "26.2.5",
|
|
||||||
"date": "2025-05-28T06:49:43Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "esphome/esphome",
|
"name": "esphome/esphome",
|
||||||
"version": "2025.6.1",
|
"version": "2025.6.1",
|
||||||
@@ -96,44 +261,24 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "runtipi/runtipi",
|
"name": "runtipi/runtipi",
|
||||||
"version": "v4.2.1",
|
"version": "nightly",
|
||||||
"date": "2025-06-03T20:04:28Z"
|
"date": "2025-06-23T19:10:33Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "VictoriaMetrics/VictoriaMetrics",
|
"name": "VictoriaMetrics/VictoriaMetrics",
|
||||||
"version": "pmm-6401-v1.120.0",
|
"version": "pmm-6401-v1.120.0",
|
||||||
"date": "2025-06-23T15:12:12Z"
|
"date": "2025-06-23T15:12:12Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "n8n-io/n8n",
|
|
||||||
"version": "n8n@1.98.2",
|
|
||||||
"date": "2025-06-18T18:20:16Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Graylog2/graylog2-server",
|
"name": "Graylog2/graylog2-server",
|
||||||
"version": "6.3.0-rc.2",
|
"version": "6.3.0-rc.2",
|
||||||
"date": "2025-06-23T11:31:38Z"
|
"date": "2025-06-23T11:31:38Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "mattermost/mattermost",
|
|
||||||
"version": "v9.11.17",
|
|
||||||
"date": "2025-06-18T08:12:05Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "firefly-iii/firefly-iii",
|
|
||||||
"version": "v6.2.18",
|
|
||||||
"date": "2025-06-20T04:45:37Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "gotson/komga",
|
"name": "gotson/komga",
|
||||||
"version": "1.22.0",
|
"version": "1.22.0",
|
||||||
"date": "2025-06-23T03:11:37Z"
|
"date": "2025-06-23T03:11:37Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "plexguide/Huntarr.io",
|
|
||||||
"version": "8.1.8",
|
|
||||||
"date": "2025-06-23T00:21:30Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "OliveTin/OliveTin",
|
"name": "OliveTin/OliveTin",
|
||||||
"version": "2025.6.22",
|
"version": "2025.6.22",
|
||||||
@@ -144,26 +289,11 @@
|
|||||||
"version": "release-5.1.1",
|
"version": "release-5.1.1",
|
||||||
"date": "2025-06-22T21:41:17Z"
|
"date": "2025-06-22T21:41:17Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "pocket-id/pocket-id",
|
|
||||||
"version": "v1.4.1",
|
|
||||||
"date": "2025-06-22T19:38:08Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "msgbyte/tianji",
|
|
||||||
"version": "v1.22.3",
|
|
||||||
"date": "2025-06-22T18:29:00Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "clusterzx/paperless-ai",
|
"name": "clusterzx/paperless-ai",
|
||||||
"version": "v3.0.7",
|
"version": "v3.0.7",
|
||||||
"date": "2025-06-22T17:49:29Z"
|
"date": "2025-06-22T17:49:29Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "fuma-nama/fumadocs",
|
|
||||||
"version": "create-fumadocs-app@15.5.4",
|
|
||||||
"date": "2025-06-22T13:12:24Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "TandoorRecipes/recipes",
|
"name": "TandoorRecipes/recipes",
|
||||||
"version": "1.5.35",
|
"version": "1.5.35",
|
||||||
@@ -204,11 +334,6 @@
|
|||||||
"version": "0.50.4",
|
"version": "0.50.4",
|
||||||
"date": "2025-06-21T07:47:02Z"
|
"date": "2025-06-21T07:47:02Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "coder/code-server",
|
|
||||||
"version": "v4.101.1",
|
|
||||||
"date": "2025-06-21T02:47:08Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "go-gitea/gitea",
|
"name": "go-gitea/gitea",
|
||||||
"version": "v1.24.2",
|
"version": "v1.24.2",
|
||||||
@@ -219,41 +344,11 @@
|
|||||||
"version": "v1.135.3",
|
"version": "v1.135.3",
|
||||||
"date": "2025-06-20T20:19:20Z"
|
"date": "2025-06-20T20:19:20Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "apache/tika",
|
|
||||||
"version": "3.2.1-rc1",
|
|
||||||
"date": "2025-06-20T19:41:10Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "homarr-labs/homarr",
|
|
||||||
"version": "v1.25.0",
|
|
||||||
"date": "2025-06-20T19:15:43Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "mongodb/mongo",
|
|
||||||
"version": "r8.1.2-rc0",
|
|
||||||
"date": "2025-06-20T17:35:38Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "nzbgetcom/nzbget",
|
|
||||||
"version": "v25.0",
|
|
||||||
"date": "2025-05-12T09:12:04Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Sonarr/Sonarr",
|
"name": "Sonarr/Sonarr",
|
||||||
"version": "v4.0.15.2941",
|
"version": "v4.0.15.2941",
|
||||||
"date": "2025-06-20T17:20:54Z"
|
"date": "2025-06-20T17:20:54Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "bunkerity/bunkerweb",
|
|
||||||
"version": "testing",
|
|
||||||
"date": "2025-06-16T18:10:42Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "zabbix/zabbix",
|
|
||||||
"version": "7.2.9",
|
|
||||||
"date": "2025-06-20T10:58:45Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "syncthing/syncthing",
|
"name": "syncthing/syncthing",
|
||||||
"version": "2.0.0-rc.19",
|
"version": "2.0.0-rc.19",
|
||||||
@@ -269,11 +364,6 @@
|
|||||||
"version": "v2.17.1",
|
"version": "v2.17.1",
|
||||||
"date": "2025-06-19T19:35:01Z"
|
"date": "2025-06-19T19:35:01Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "rclone/rclone",
|
|
||||||
"version": "v1.70.1",
|
|
||||||
"date": "2025-06-19T13:19:02Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "icereed/paperless-gpt",
|
"name": "icereed/paperless-gpt",
|
||||||
"version": "v0.21.0",
|
"version": "v0.21.0",
|
||||||
@@ -364,11 +454,6 @@
|
|||||||
"version": "2025.6.1",
|
"version": "2025.6.1",
|
||||||
"date": "2025-06-17T12:45:39Z"
|
"date": "2025-06-17T12:45:39Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "sabnzbd/sabnzbd",
|
|
||||||
"version": "4.5.1",
|
|
||||||
"date": "2025-04-11T09:57:47Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "crowdsecurity/crowdsec",
|
"name": "crowdsecurity/crowdsec",
|
||||||
"version": "v1.6.9",
|
"version": "v1.6.9",
|
||||||
@@ -399,11 +484,6 @@
|
|||||||
"version": "2.36.1",
|
"version": "2.36.1",
|
||||||
"date": "2025-06-16T19:20:54Z"
|
"date": "2025-06-16T19:20:54Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "goauthentik/authentik",
|
|
||||||
"version": "version/2025.6.2",
|
|
||||||
"date": "2025-06-16T17:54:39Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "emqx/emqx",
|
"name": "emqx/emqx",
|
||||||
"version": "e5.9.1-alpha.1",
|
"version": "e5.9.1-alpha.1",
|
||||||
@@ -419,16 +499,6 @@
|
|||||||
"version": "v8.1.16",
|
"version": "v8.1.16",
|
||||||
"date": "2025-06-16T13:49:37Z"
|
"date": "2025-06-16T13:49:37Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "home-assistant/operating-system",
|
|
||||||
"version": "15.2",
|
|
||||||
"date": "2025-04-14T15:37:12Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "moghtech/komodo",
|
|
||||||
"version": "v1.18.3",
|
|
||||||
"date": "2025-06-16T07:03:46Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "jellyfin/jellyfin",
|
"name": "jellyfin/jellyfin",
|
||||||
"version": "v10.10.7",
|
"version": "v10.10.7",
|
||||||
@@ -499,11 +569,6 @@
|
|||||||
"version": "v3.3.25",
|
"version": "v3.3.25",
|
||||||
"date": "2025-06-14T02:52:44Z"
|
"date": "2025-06-14T02:52:44Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "FlowiseAI/Flowise",
|
|
||||||
"version": "flowise@3.0.2",
|
|
||||||
"date": "2025-06-12T22:48:11Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "leiweibau/Pi.Alert",
|
"name": "leiweibau/Pi.Alert",
|
||||||
"version": "v2025-06-12",
|
"version": "v2025-06-12",
|
||||||
@@ -514,16 +579,6 @@
|
|||||||
"version": "v3.3.0",
|
"version": "v3.3.0",
|
||||||
"date": "2025-06-12T06:54:48Z"
|
"date": "2025-06-12T06:54:48Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "documenso/documenso",
|
|
||||||
"version": "v1.12.0-rc.4",
|
|
||||||
"date": "2025-06-12T00:27:41Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "MediaBrowser/Emby.Releases",
|
|
||||||
"version": "4.8.11.0",
|
|
||||||
"date": "2025-03-10T06:39:11Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "autobrr/autobrr",
|
"name": "autobrr/autobrr",
|
||||||
"version": "v1.63.1",
|
"version": "v1.63.1",
|
||||||
@@ -539,16 +594,6 @@
|
|||||||
"version": "v0.15.0-rc2",
|
"version": "v0.15.0-rc2",
|
||||||
"date": "2025-06-11T04:29:22Z"
|
"date": "2025-06-11T04:29:22Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "node-red/node-red",
|
|
||||||
"version": "4.1.0-beta.1",
|
|
||||||
"date": "2025-06-10T15:47:59Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "AdguardTeam/AdGuardHome",
|
|
||||||
"version": "v0.107.62",
|
|
||||||
"date": "2025-05-27T12:10:19Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "OctoPrint/OctoPrint",
|
"name": "OctoPrint/OctoPrint",
|
||||||
"version": "1.11.2",
|
"version": "1.11.2",
|
||||||
@@ -559,11 +604,6 @@
|
|||||||
"version": "v0.8.4",
|
"version": "v0.8.4",
|
||||||
"date": "2025-06-10T07:57:14Z"
|
"date": "2025-06-10T07:57:14Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "tailscale/tailscale",
|
|
||||||
"version": "v1.84.2",
|
|
||||||
"date": "2025-06-09T23:43:27Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Brandawg93/PeaNUT",
|
"name": "Brandawg93/PeaNUT",
|
||||||
"version": "v5.8.0",
|
"version": "v5.8.0",
|
||||||
@@ -619,11 +659,6 @@
|
|||||||
"version": "10.1.42",
|
"version": "10.1.42",
|
||||||
"date": "2025-06-05T22:39:40Z"
|
"date": "2025-06-05T22:39:40Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "netbox-community/netbox",
|
|
||||||
"version": "v4.3.2",
|
|
||||||
"date": "2025-06-05T19:57:01Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "benjaminjonard/koillection",
|
"name": "benjaminjonard/koillection",
|
||||||
"version": "1.6.14",
|
"version": "1.6.14",
|
||||||
@@ -649,11 +684,6 @@
|
|||||||
"version": "v4.1.1",
|
"version": "v4.1.1",
|
||||||
"date": "2025-06-04T19:10:05Z"
|
"date": "2025-06-04T19:10:05Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "cockpit-project/cockpit",
|
|
||||||
"version": "340",
|
|
||||||
"date": "2025-06-04T16:41:44Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "intri-in/manage-my-damn-life-nextjs",
|
"name": "intri-in/manage-my-damn-life-nextjs",
|
||||||
"version": "v0.7.1",
|
"version": "v0.7.1",
|
||||||
@@ -694,11 +724,6 @@
|
|||||||
"version": "v5.18.1",
|
"version": "v5.18.1",
|
||||||
"date": "2025-05-31T23:06:08Z"
|
"date": "2025-05-31T23:06:08Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "prometheus/prometheus",
|
|
||||||
"version": "v3.4.1",
|
|
||||||
"date": "2025-05-31T13:45:40Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "blakeblackshear/frigate",
|
"name": "blakeblackshear/frigate",
|
||||||
"version": "v0.14.1",
|
"version": "v0.14.1",
|
||||||
@@ -714,11 +739,6 @@
|
|||||||
"version": "0.26.3",
|
"version": "0.26.3",
|
||||||
"date": "2025-05-29T21:18:15Z"
|
"date": "2025-05-29T21:18:15Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "gristlabs/grist-core",
|
|
||||||
"version": "v1.6.0",
|
|
||||||
"date": "2025-05-29T19:11:21Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "navidrome/navidrome",
|
"name": "navidrome/navidrome",
|
||||||
"version": "v0.56.1",
|
"version": "v0.56.1",
|
||||||
@@ -749,11 +769,6 @@
|
|||||||
"version": "1.2.34",
|
"version": "1.2.34",
|
||||||
"date": "2025-05-27T18:18:00Z"
|
"date": "2025-05-27T18:18:00Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "traefik/traefik",
|
|
||||||
"version": "v3.4.1",
|
|
||||||
"date": "2025-05-27T12:53:58Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "dani-garcia/vaultwarden",
|
"name": "dani-garcia/vaultwarden",
|
||||||
"version": "1.34.1",
|
"version": "1.34.1",
|
||||||
@@ -824,16 +839,6 @@
|
|||||||
"version": "v1.1.1",
|
"version": "v1.1.1",
|
||||||
"date": "2025-05-17T10:10:36Z"
|
"date": "2025-05-17T10:10:36Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "wavelog/wavelog",
|
|
||||||
"version": "2.0.4",
|
|
||||||
"date": "2025-05-16T15:09:53Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Dolibarr/dolibarr",
|
|
||||||
"version": "18.0.7",
|
|
||||||
"date": "2025-05-15T08:24:30Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Ombi-app/Ombi",
|
"name": "Ombi-app/Ombi",
|
||||||
"version": "v4.47.1",
|
"version": "v4.47.1",
|
||||||
@@ -894,11 +899,6 @@
|
|||||||
"version": "v1.8.1",
|
"version": "v1.8.1",
|
||||||
"date": "2025-05-06T04:27:00Z"
|
"date": "2025-05-06T04:27:00Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "linkwarden/linkwarden",
|
|
||||||
"version": "v2.10.2",
|
|
||||||
"date": "2025-05-06T03:12:53Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "postgres/postgres",
|
"name": "postgres/postgres",
|
||||||
"version": "REL_13_21",
|
"version": "REL_13_21",
|
||||||
|
|||||||
2
frontend/public/json/wireguard.json
generated
2
frontend/public/json/wireguard.json
generated
@@ -44,7 +44,7 @@
|
|||||||
},
|
},
|
||||||
"notes": [
|
"notes": [
|
||||||
{
|
{
|
||||||
"text": "Wireguard and WGDashboard are not the same. More info: `https://donaldzou.github.io/WGDashboard-Documentation/what-is-wireguard-what-is-wgdashboard.html`",
|
"text": "Wireguard and WGDashboard are not the same. More info: `https://docs.wgdashboard.dev/what-is-wireguard-what-is-wgdashboard.html`",
|
||||||
"type": "info"
|
"type": "info"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
2
frontend/public/json/yunohost.json
generated
2
frontend/public/json/yunohost.json
generated
@@ -6,7 +6,7 @@
|
|||||||
],
|
],
|
||||||
"date_created": "2024-05-02",
|
"date_created": "2024-05-02",
|
||||||
"type": "ct",
|
"type": "ct",
|
||||||
"updateable": false,
|
"updateable": true,
|
||||||
"privileged": false,
|
"privileged": false,
|
||||||
"interface_port": 80,
|
"interface_port": 80,
|
||||||
"documentation": null,
|
"documentation": null,
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { screen } from "@testing-library/dom";
|
|
||||||
import { render } from "@testing-library/react";
|
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import Page from "@/app/page";
|
|
||||||
|
|
||||||
describe("Page", () => {
|
|
||||||
it("should show button to view scripts", () => {
|
|
||||||
render(<Page />);
|
|
||||||
expect(screen.getByRole("button", { name: "View Scripts" })).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { describe, it, assert, beforeAll } from "vitest";
|
|
||||||
import { promises as fs } from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import { ScriptSchema, type Script } from "@/app/json-editor/_schemas/schemas";
|
|
||||||
import { Metadata } from "@/lib/types";
|
|
||||||
console.log('Current directory: ' + process.cwd());
|
|
||||||
const jsonDir = "public/json";
|
|
||||||
const metadataFileName = "metadata.json";
|
|
||||||
const versionsFileName = "versions.json";
|
|
||||||
const encoding = "utf-8";
|
|
||||||
|
|
||||||
const fileNames = (await fs.readdir(jsonDir))
|
|
||||||
.filter((fileName) => fileName !== metadataFileName && fileName !== versionsFileName);
|
|
||||||
|
|
||||||
describe.each(fileNames)("%s", async (fileName) => {
|
|
||||||
let script: Script;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const filePath = path.resolve(jsonDir, fileName);
|
|
||||||
const fileContent = await fs.readFile(filePath, encoding)
|
|
||||||
script = JSON.parse(fileContent);
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
it("should have valid json according to script schema", () => {
|
|
||||||
ScriptSchema.parse(script);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should have a corresponding script file", () => {
|
|
||||||
script.install_methods.forEach((method) => {
|
|
||||||
const scriptPath = path.resolve("..", method.script)
|
|
||||||
//FIXME: Dose note account for new dir structure and files in /script/tools
|
|
||||||
|
|
||||||
assert(fs.stat(scriptPath), `Script file not found: ${scriptPath}`)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
describe(`${metadataFileName}`, async () => {
|
|
||||||
let metadata: Metadata;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const filePath = path.resolve(jsonDir, metadataFileName);
|
|
||||||
const fileContent = await fs.readFile(filePath, encoding)
|
|
||||||
metadata = JSON.parse(fileContent);
|
|
||||||
})
|
|
||||||
it("should have valid json according to metadata schema", () => {
|
|
||||||
// TODO: create zod schema for metadata. Move zod schemas to /lib/types.ts
|
|
||||||
assert(metadata.categories.length > 0);
|
|
||||||
metadata.categories.forEach((category) => {
|
|
||||||
assert.isString(category.name)
|
|
||||||
assert.isNumber(category.id)
|
|
||||||
assert.isNumber(category.sort_order)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { vi } from "vitest";
|
|
||||||
|
|
||||||
// Mock canvas getContext
|
|
||||||
HTMLCanvasElement.prototype.getContext = vi.fn();
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Metadata, Script } from "@/lib/types";
|
|
||||||
import { promises as fs } from "fs";
|
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import path from "path";
|
import { promises as fs } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import type { Metadata, Script } from "@/lib/types";
|
||||||
|
|
||||||
export const dynamic = "force-static";
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
@@ -10,21 +11,21 @@ const metadataFileName = "metadata.json";
|
|||||||
const versionFileName = "version.json";
|
const versionFileName = "version.json";
|
||||||
const encoding = "utf-8";
|
const encoding = "utf-8";
|
||||||
|
|
||||||
const getMetadata = async () => {
|
async function getMetadata() {
|
||||||
const filePath = path.resolve(jsonDir, metadataFileName);
|
const filePath = path.resolve(jsonDir, metadataFileName);
|
||||||
const fileContent = await fs.readFile(filePath, encoding);
|
const fileContent = await fs.readFile(filePath, encoding);
|
||||||
const metadata: Metadata = JSON.parse(fileContent);
|
const metadata: Metadata = JSON.parse(fileContent);
|
||||||
return metadata;
|
return metadata;
|
||||||
};
|
}
|
||||||
|
|
||||||
const getScripts = async () => {
|
async function getScripts() {
|
||||||
const filePaths = (await fs.readdir(jsonDir))
|
const filePaths = (await fs.readdir(jsonDir))
|
||||||
.filter((fileName) =>
|
.filter(fileName =>
|
||||||
fileName.endsWith(".json") &&
|
fileName.endsWith(".json")
|
||||||
fileName !== metadataFileName &&
|
&& fileName !== metadataFileName
|
||||||
fileName !== versionFileName
|
&& fileName !== versionFileName,
|
||||||
)
|
)
|
||||||
.map((fileName) => path.resolve(jsonDir, fileName));
|
.map(fileName => path.resolve(jsonDir, fileName));
|
||||||
|
|
||||||
const scripts = await Promise.all(
|
const scripts = await Promise.all(
|
||||||
filePaths.map(async (filePath) => {
|
filePaths.map(async (filePath) => {
|
||||||
@@ -34,7 +35,7 @@ const getScripts = async () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return scripts;
|
return scripts;
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
@@ -43,7 +44,7 @@ export async function GET() {
|
|||||||
|
|
||||||
const categories = metadata.categories
|
const categories = metadata.categories
|
||||||
.map((category) => {
|
.map((category) => {
|
||||||
category.scripts = scripts.filter((script) =>
|
category.scripts = scripts.filter(script =>
|
||||||
script.categories?.includes(category.id),
|
script.categories?.includes(category.id),
|
||||||
);
|
);
|
||||||
return category;
|
return category;
|
||||||
@@ -51,7 +52,8 @@ export async function GET() {
|
|||||||
.sort((a, b) => a.sort_order - b.sort_order);
|
.sort((a, b) => a.sort_order - b.sort_order);
|
||||||
|
|
||||||
return NextResponse.json(categories);
|
return NextResponse.json(categories);
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
console.error(error as Error);
|
console.error(error as Error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to fetch categories" },
|
{ error: "Failed to fetch categories" },
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { AppVersion } from "@/lib/types";
|
|
||||||
import { error } from "console";
|
|
||||||
import { promises as fs } from "fs";
|
|
||||||
// import Error from "next/error";
|
// import Error from "next/error";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import path from "path";
|
import { promises as fs } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import type { AppVersion } from "@/lib/types";
|
||||||
|
|
||||||
export const dynamic = "force-static";
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
@@ -11,33 +11,32 @@ const jsonDir = "public/json";
|
|||||||
const versionsFileName = "versions.json";
|
const versionsFileName = "versions.json";
|
||||||
const encoding = "utf-8";
|
const encoding = "utf-8";
|
||||||
|
|
||||||
const getVersions = async () => {
|
async function getVersions() {
|
||||||
const filePath = path.resolve(jsonDir, versionsFileName);
|
const filePath = path.resolve(jsonDir, versionsFileName);
|
||||||
const fileContent = await fs.readFile(filePath, encoding);
|
const fileContent = await fs.readFile(filePath, encoding);
|
||||||
const versions: AppVersion[] = JSON.parse(fileContent);
|
const versions: AppVersion[] = JSON.parse(fileContent);
|
||||||
|
|
||||||
const modifiedVersions = versions.map(version => {
|
const modifiedVersions = versions.map((version) => {
|
||||||
let newName = version.name;
|
let newName = version.name;
|
||||||
newName = newName.toLowerCase().replace(/[^a-z0-9/]/g, '');
|
newName = newName.toLowerCase().replace(/[^a-z0-9/]/g, "");
|
||||||
return { ...version, name: newName, date: new Date(version.date) };
|
return { ...version, name: newName, date: new Date(version.date) };
|
||||||
});
|
});
|
||||||
|
|
||||||
return modifiedVersions;
|
return modifiedVersions;
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const versions = await getVersions();
|
const versions = await getVersions();
|
||||||
return NextResponse.json(versions);
|
return NextResponse.json(versions);
|
||||||
|
}
|
||||||
} catch (error) {
|
catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
const err = error as globalThis.Error;
|
const err = error as globalThis.Error;
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
name: err.name,
|
name: err.name,
|
||||||
message: err.message || "An unexpected error occurred",
|
message: err.message || "An unexpected error occurred",
|
||||||
version: "No version found - Error"
|
version: "No version found - Error",
|
||||||
}, {
|
}, {
|
||||||
status: 500,
|
status: 500,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import { Category } from "@/lib/types";
|
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
import type { Category } from "@/lib/types";
|
||||||
|
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
const defaultLogo = "/default-logo.png"; // Fallback logo path
|
const defaultLogo = "/default-logo.png"; // Fallback logo path
|
||||||
const MAX_DESCRIPTION_LENGTH = 100; // Set max length for description
|
const MAX_DESCRIPTION_LENGTH = 100; // Set max length for description
|
||||||
const MAX_LOGOS = 5; // Max logos to display at once
|
const MAX_LOGOS = 5; // Max logos to display at once
|
||||||
|
|
||||||
const formattedBadge = (type: string) => {
|
function formattedBadge(type: string) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "vm":
|
case "vm":
|
||||||
return <Badge className="text-blue-500/75 border-blue-500/75 badge">VM</Badge>;
|
return <Badge className="text-blue-500/75 border-blue-500/75 badge">VM</Badge>;
|
||||||
@@ -24,9 +26,9 @@ const formattedBadge = (type: string) => {
|
|||||||
return <Badge className="text-green-500/75 border-green-500/75 badge">ADDON</Badge>;
|
return <Badge className="text-green-500/75 border-green-500/75 badge">ADDON</Badge>;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
}
|
||||||
|
|
||||||
const CategoryView = () => {
|
function CategoryView() {
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
const [selectedCategoryIndex, setSelectedCategoryIndex] = useState<number | null>(null);
|
const [selectedCategoryIndex, setSelectedCategoryIndex] = useState<number | null>(null);
|
||||||
const [currentScripts, setCurrentScripts] = useState<any[]>([]);
|
const [currentScripts, setCurrentScripts] = useState<any[]>([]);
|
||||||
@@ -36,6 +38,7 @@ const CategoryView = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCategories = async () => {
|
const fetchCategories = async () => {
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line node/no-process-env
|
||||||
const basePath = process.env.NODE_ENV === "production" ? "/ProxmoxVE" : "";
|
const basePath = process.env.NODE_ENV === "production" ? "/ProxmoxVE" : "";
|
||||||
const response = await fetch(`${basePath}/api/categories`);
|
const response = await fetch(`${basePath}/api/categories`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -50,7 +53,8 @@ const CategoryView = () => {
|
|||||||
initialLogoIndices[category.name] = 0;
|
initialLogoIndices[category.name] = 0;
|
||||||
});
|
});
|
||||||
setLogoIndices(initialLogoIndices);
|
setLogoIndices(initialLogoIndices);
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
console.error("Error fetching categories:", error);
|
console.error("Error fetching categories:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -74,8 +78,8 @@ const CategoryView = () => {
|
|||||||
|
|
||||||
const navigateCategory = (direction: "prev" | "next") => {
|
const navigateCategory = (direction: "prev" | "next") => {
|
||||||
if (selectedCategoryIndex !== null) {
|
if (selectedCategoryIndex !== null) {
|
||||||
const newIndex =
|
const newIndex
|
||||||
direction === "prev"
|
= direction === "prev"
|
||||||
? (selectedCategoryIndex - 1 + categories.length) % categories.length
|
? (selectedCategoryIndex - 1 + categories.length) % categories.length
|
||||||
: (selectedCategoryIndex + 1) % categories.length;
|
: (selectedCategoryIndex + 1) % categories.length;
|
||||||
setSelectedCategoryIndex(newIndex);
|
setSelectedCategoryIndex(newIndex);
|
||||||
@@ -86,12 +90,13 @@ const CategoryView = () => {
|
|||||||
const switchLogos = (categoryName: string, direction: "prev" | "next") => {
|
const switchLogos = (categoryName: string, direction: "prev" | "next") => {
|
||||||
setLogoIndices((prev) => {
|
setLogoIndices((prev) => {
|
||||||
const currentIndex = prev[categoryName] || 0;
|
const currentIndex = prev[categoryName] || 0;
|
||||||
const category = categories.find((cat) => cat.name === categoryName);
|
const category = categories.find(cat => cat.name === categoryName);
|
||||||
if (!category || !category.scripts) return prev;
|
if (!category || !category.scripts)
|
||||||
|
return prev;
|
||||||
|
|
||||||
const totalLogos = category.scripts.length;
|
const totalLogos = category.scripts.length;
|
||||||
const newIndex =
|
const newIndex
|
||||||
direction === "prev"
|
= direction === "prev"
|
||||||
? (currentIndex - MAX_LOGOS + totalLogos) % totalLogos
|
? (currentIndex - MAX_LOGOS + totalLogos) % totalLogos
|
||||||
: (currentIndex + MAX_LOGOS) % totalLogos;
|
: (currentIndex + MAX_LOGOS) % totalLogos;
|
||||||
|
|
||||||
@@ -109,26 +114,39 @@ const CategoryView = () => {
|
|||||||
const hdd = script.install_methods[0]?.resources.hdd;
|
const hdd = script.install_methods[0]?.resources.hdd;
|
||||||
|
|
||||||
const resourceParts = [];
|
const resourceParts = [];
|
||||||
if (cpu)
|
if (cpu) {
|
||||||
resourceParts.push(
|
resourceParts.push(
|
||||||
<span key="cpu">
|
<span key="cpu">
|
||||||
<b>CPU:</b> {cpu}vCPU
|
<b>CPU:</b>
|
||||||
|
{" "}
|
||||||
|
{cpu}
|
||||||
|
vCPU
|
||||||
</span>,
|
</span>,
|
||||||
);
|
);
|
||||||
if (ram)
|
}
|
||||||
|
if (ram) {
|
||||||
resourceParts.push(
|
resourceParts.push(
|
||||||
<span key="ram">
|
<span key="ram">
|
||||||
<b>RAM:</b> {ram}MB
|
<b>RAM:</b>
|
||||||
|
{" "}
|
||||||
|
{ram}
|
||||||
|
MB
|
||||||
</span>,
|
</span>,
|
||||||
);
|
);
|
||||||
if (hdd)
|
}
|
||||||
|
if (hdd) {
|
||||||
resourceParts.push(
|
resourceParts.push(
|
||||||
<span key="hdd">
|
<span key="hdd">
|
||||||
<b>HDD:</b> {hdd}GB
|
<b>HDD:</b>
|
||||||
|
{" "}
|
||||||
|
{hdd}
|
||||||
|
GB
|
||||||
</span>,
|
</span>,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return resourceParts.length > 0 ? (
|
return resourceParts.length > 0
|
||||||
|
? (
|
||||||
<div className="text-sm text-gray-400">
|
<div className="text-sm text-gray-400">
|
||||||
{resourceParts.map((part, index) => (
|
{resourceParts.map((part, index) => (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
@@ -137,7 +155,8 @@ const CategoryView = () => {
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
)
|
||||||
|
: null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -145,7 +164,8 @@ const CategoryView = () => {
|
|||||||
{categories.length === 0 && (
|
{categories.length === 0 && (
|
||||||
<p className="text-center text-gray-500">No categories available. Please check the API endpoint.</p>
|
<p className="text-center text-gray-500">No categories available. Please check the API endpoint.</p>
|
||||||
)}
|
)}
|
||||||
{selectedCategoryIndex !== null ? (
|
{selectedCategoryIndex !== null
|
||||||
|
? (
|
||||||
<div>
|
<div>
|
||||||
{/* Header with Navigation */}
|
{/* Header with Navigation */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
@@ -172,7 +192,7 @@ const CategoryView = () => {
|
|||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||||
{currentScripts
|
{currentScripts
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.map((script) => (
|
.map(script => (
|
||||||
<Card
|
<Card
|
||||||
key={script.name}
|
key={script.name}
|
||||||
className="p-4 cursor-pointer hover:shadow-md transition-shadow duration-300"
|
className="p-4 cursor-pointer hover:shadow-md transition-shadow duration-300"
|
||||||
@@ -188,7 +208,9 @@ const CategoryView = () => {
|
|||||||
className="h-12 w-12 object-contain mx-auto"
|
className="h-12 w-12 object-contain mx-auto"
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-gray-500 text-center">
|
<p className="text-sm text-gray-500 text-center">
|
||||||
<b>Created at:</b> {script.date_created || "No date available"}
|
<b>Created at:</b>
|
||||||
|
{" "}
|
||||||
|
{script.date_created || "No date available"}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
className="text-sm text-gray-700 hover:text-gray-900 text-center transition-colors duration-300"
|
className="text-sm text-gray-700 hover:text-gray-900 text-center transition-colors duration-300"
|
||||||
@@ -213,13 +235,16 @@ const CategoryView = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
)
|
||||||
|
: (
|
||||||
<div>
|
<div>
|
||||||
{/* Categories Grid */}
|
{/* Categories Grid */}
|
||||||
<div className="flex justify-between items-center mb-8">
|
<div className="flex justify-between items-center mb-8">
|
||||||
<h1 className="text-3xl font-semibold mb-4">Categories</h1>
|
<h1 className="text-3xl font-semibold mb-4">Categories</h1>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
{categories.reduce((total, category) => total + (category.scripts?.length || 0), 0)} Total scripts
|
{categories.reduce((total, category) => total + (category.scripts?.length || 0), 0)}
|
||||||
|
{" "}
|
||||||
|
Total scripts
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8">
|
||||||
@@ -244,8 +269,8 @@ const CategoryView = () => {
|
|||||||
>
|
>
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
{category.scripts &&
|
{category.scripts
|
||||||
category.scripts
|
&& category.scripts
|
||||||
.slice(logoIndices[category.name] || 0, (logoIndices[category.name] || 0) + MAX_LOGOS)
|
.slice(logoIndices[category.name] || 0, (logoIndices[category.name] || 0) + MAX_LOGOS)
|
||||||
.map((script, i) => (
|
.map((script, i) => (
|
||||||
<div key={i} className="flex flex-col items-center">
|
<div key={i} className="flex flex-col items-center">
|
||||||
@@ -284,6 +309,6 @@ const CategoryView = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default CategoryView;
|
export default CategoryView;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { JSX, useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import DatePicker from 'react-datepicker';
|
import "react-datepicker/dist/react-datepicker.css";
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
|
||||||
import ApplicationChart from "../../components/ApplicationChart";
|
|
||||||
|
|
||||||
interface DataModel {
|
import ApplicationChart from "../../components/application-chart";
|
||||||
|
|
||||||
|
type DataModel = {
|
||||||
id: number;
|
id: number;
|
||||||
ct_type: number;
|
ct_type: number;
|
||||||
disk_size: number;
|
disk_size: number;
|
||||||
@@ -22,13 +22,13 @@ interface DataModel {
|
|||||||
error: string;
|
error: string;
|
||||||
type: string;
|
type: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface SummaryData {
|
type SummaryData = {
|
||||||
total_entries: number;
|
total_entries: number;
|
||||||
status_count: Record<string, number>;
|
status_count: Record<string, number>;
|
||||||
nsapp_count: Record<string, number>;
|
nsapp_count: Record<string, number>;
|
||||||
}
|
};
|
||||||
|
|
||||||
const DataFetcher: React.FC = () => {
|
const DataFetcher: React.FC = () => {
|
||||||
const [data, setData] = useState<DataModel[]>([]);
|
const [data, setData] = useState<DataModel[]>([]);
|
||||||
@@ -37,16 +37,18 @@ const DataFetcher: React.FC = () => {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(25);
|
const [itemsPerPage, setItemsPerPage] = useState(25);
|
||||||
const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'ascending' | 'descending' } | null>(null);
|
const [sortConfig, setSortConfig] = useState<{ key: string; direction: "ascending" | "descending" } | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchSummary = async () => {
|
const fetchSummary = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("https://api.htl-braunau.at/data/summary");
|
const response = await fetch("https://api.htl-braunau.at/data/summary");
|
||||||
if (!response.ok) throw new Error(`Failed to fetch summary: ${response.statusText}`);
|
if (!response.ok)
|
||||||
|
throw new Error(`Failed to fetch summary: ${response.statusText}`);
|
||||||
const result: SummaryData = await response.json();
|
const result: SummaryData = await response.json();
|
||||||
setSummary(result);
|
setSummary(result);
|
||||||
} catch (err) {
|
}
|
||||||
|
catch (err) {
|
||||||
setError((err as Error).message);
|
setError((err as Error).message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -58,13 +60,16 @@ const DataFetcher: React.FC = () => {
|
|||||||
const fetchPaginatedData = async () => {
|
const fetchPaginatedData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`https://api.htl-braunau.at/data/paginated?page=${currentPage}&limit=${itemsPerPage === 0 ? '' : itemsPerPage}`);
|
const response = await fetch(`https://api.htl-braunau.at/data/paginated?page=${currentPage}&limit=${itemsPerPage === 0 ? "" : itemsPerPage}`);
|
||||||
if (!response.ok) throw new Error(`Failed to fetch data: ${response.statusText}`);
|
if (!response.ok)
|
||||||
|
throw new Error(`Failed to fetch data: ${response.statusText}`);
|
||||||
const result: DataModel[] = await response.json();
|
const result: DataModel[] = await response.json();
|
||||||
setData(result);
|
setData(result);
|
||||||
} catch (err) {
|
}
|
||||||
|
catch (err) {
|
||||||
setError((err as Error).message);
|
setError((err as Error).message);
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -73,26 +78,35 @@ const DataFetcher: React.FC = () => {
|
|||||||
}, [currentPage, itemsPerPage]);
|
}, [currentPage, itemsPerPage]);
|
||||||
|
|
||||||
const sortedData = React.useMemo(() => {
|
const sortedData = React.useMemo(() => {
|
||||||
if (!sortConfig) return data;
|
if (!sortConfig)
|
||||||
|
return data;
|
||||||
const sorted = [...data].sort((a, b) => {
|
const sorted = [...data].sort((a, b) => {
|
||||||
if (a[sortConfig.key] < b[sortConfig.key]) {
|
if (a[sortConfig.key] < b[sortConfig.key]) {
|
||||||
return sortConfig.direction === 'ascending' ? -1 : 1;
|
return sortConfig.direction === "ascending" ? -1 : 1;
|
||||||
}
|
}
|
||||||
if (a[sortConfig.key] > b[sortConfig.key]) {
|
if (a[sortConfig.key] > b[sortConfig.key]) {
|
||||||
return sortConfig.direction === 'ascending' ? 1 : -1;
|
return sortConfig.direction === "ascending" ? 1 : -1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
return sorted;
|
return sorted;
|
||||||
}, [data, sortConfig]);
|
}, [data, sortConfig]);
|
||||||
|
|
||||||
if (loading) return <p>Loading...</p>;
|
if (loading)
|
||||||
if (error) return <p>Error: {error}</p>;
|
return <p>Loading...</p>;
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
Error:
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const requestSort = (key: string) => {
|
const requestSort = (key: string) => {
|
||||||
let direction: 'ascending' | 'descending' = 'ascending';
|
let direction: "ascending" | "descending" = "ascending";
|
||||||
if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') {
|
if (sortConfig && sortConfig.key === key && sortConfig.direction === "ascending") {
|
||||||
direction = 'descending';
|
direction = "descending";
|
||||||
}
|
}
|
||||||
setSortConfig({ key, direction });
|
setSortConfig({ key, direction });
|
||||||
};
|
};
|
||||||
@@ -102,8 +116,8 @@ const DataFetcher: React.FC = () => {
|
|||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const month = date.getMonth() + 1;
|
const month = date.getMonth() + 1;
|
||||||
const day = date.getDate();
|
const day = date.getDate();
|
||||||
const hours = String(date.getHours()).padStart(2, '0');
|
const hours = String(date.getHours()).padStart(2, "0");
|
||||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||||
const timezoneOffset = dateString.slice(-6);
|
const timezoneOffset = dateString.slice(-6);
|
||||||
return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`;
|
return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`;
|
||||||
};
|
};
|
||||||
@@ -114,49 +128,76 @@ const DataFetcher: React.FC = () => {
|
|||||||
<ApplicationChart data={summary} />
|
<ApplicationChart data={summary} />
|
||||||
<p className="text-lg font-bold mt-4"> </p>
|
<p className="text-lg font-bold mt-4"> </p>
|
||||||
<div className="mb-4 flex justify-between items-center">
|
<div className="mb-4 flex justify-between items-center">
|
||||||
<p className="text-lg font-bold">{summary?.total_entries} results found</p>
|
<p className="text-lg font-bold">
|
||||||
<p className="text-lg font">Status Legend: 🔄 installing {summary?.status_count["installing"] ?? 0} | ✔️ completed {summary?.status_count["done"] ?? 0} | ❌ failed {summary?.status_count["failed"] ?? 0} | ❓ unknown</p>
|
{summary?.total_entries}
|
||||||
|
{" "}
|
||||||
|
results found
|
||||||
|
</p>
|
||||||
|
<p className="text-lg font">
|
||||||
|
Status Legend: 🔄 installing
|
||||||
|
{summary?.status_count.installing ?? 0}
|
||||||
|
{" "}
|
||||||
|
| ✔️ completed
|
||||||
|
{summary?.status_count.done ?? 0}
|
||||||
|
{" "}
|
||||||
|
| ❌ failed
|
||||||
|
{summary?.status_count.failed ?? 0}
|
||||||
|
{" "}
|
||||||
|
| ❓ unknown
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="overflow-y-auto lg:overflow-y-visible">
|
<div className="overflow-y-auto lg:overflow-y-visible">
|
||||||
<table className="min-w-full table-auto border-collapse">
|
<table className="min-w-full table-auto border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('status')}>Status</th>
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("status")}>Status</th>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('type')}>Type</th>
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("type")}>Type</th>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('nsapp')}>Application</th>
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("nsapp")}>Application</th>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_type')}>OS</th>
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("os_type")}>OS</th>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_version')}>OS Version</th>
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("os_version")}>OS Version</th>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('disk_size')}>Disk Size</th>
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("disk_size")}>Disk Size</th>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('core_count')}>Core Count</th>
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("core_count")}>Core Count</th>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('ram_size')}>RAM Size</th>
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("ram_size")}>RAM Size</th>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('method')}>Method</th>
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("method")}>Method</th>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('pve_version')}>PVE Version</th>
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("pve_version")}>PVE Version</th>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('error')}>Error Message</th>
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("error")}>Error Message</th>
|
||||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('created_at')}>Created At</th>
|
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("created_at")}>Created At</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{sortedData.map((item, index) => (
|
{sortedData.map((item, index) => (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td className="px-4 py-2 border-b">
|
<td className="px-4 py-2 border-b">
|
||||||
{item.status === "done" ? (
|
{item.status === "done"
|
||||||
|
? (
|
||||||
"✔️"
|
"✔️"
|
||||||
) : item.status === "failed" ? (
|
)
|
||||||
|
: item.status === "failed"
|
||||||
|
? (
|
||||||
"❌"
|
"❌"
|
||||||
) : item.status === "installing" ? (
|
)
|
||||||
|
: item.status === "installing"
|
||||||
|
? (
|
||||||
"🔄"
|
"🔄"
|
||||||
) : (
|
)
|
||||||
|
: (
|
||||||
item.status
|
item.status
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 border-b">{item.type === "lxc" ? (
|
<td className="px-4 py-2 border-b">
|
||||||
|
{item.type === "lxc"
|
||||||
|
? (
|
||||||
"📦"
|
"📦"
|
||||||
) : item.type === "vm" ? (
|
)
|
||||||
|
: item.type === "vm"
|
||||||
|
? (
|
||||||
"🖥️"
|
"🖥️"
|
||||||
) : (
|
)
|
||||||
|
: (
|
||||||
item.type
|
item.type
|
||||||
)}</td>
|
)}
|
||||||
|
</td>
|
||||||
<td className="px-4 py-2 border-b">{item.nsapp}</td>
|
<td className="px-4 py-2 border-b">{item.nsapp}</td>
|
||||||
<td className="px-4 py-2 border-b">{item.os_type}</td>
|
<td className="px-4 py-2 border-b">{item.os_type}</td>
|
||||||
<td className="px-4 py-2 border-b">{item.os_version}</td>
|
<td className="px-4 py-2 border-b">{item.os_version}</td>
|
||||||
@@ -175,11 +216,14 @@ const DataFetcher: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex justify-between items-center">
|
<div className="mt-4 flex justify-between items-center">
|
||||||
<button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage === 1} className="p-2 border">Previous</button>
|
<button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage === 1} className="p-2 border">Previous</button>
|
||||||
<span>Page {currentPage}</span>
|
<span>
|
||||||
|
Page
|
||||||
|
{currentPage}
|
||||||
|
</span>
|
||||||
<button onClick={() => setCurrentPage(prev => prev + 1)} className="p-2 border">Next</button>
|
<button onClick={() => setCurrentPage(prev => prev + 1)} className="p-2 border">Next</button>
|
||||||
<select
|
<select
|
||||||
value={itemsPerPage}
|
value={itemsPerPage}
|
||||||
onChange={(e) => setItemsPerPage(Number(e.target.value))}
|
onChange={e => setItemsPerPage(Number(e.target.value))}
|
||||||
className="p-2 border"
|
className="p-2 border"
|
||||||
>
|
>
|
||||||
<option value={10}>10</option>
|
<option value={10}>10</option>
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { AlertColors } from "@/config/siteConfig";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { PlusCircle, Trash2 } from "lucide-react";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { ScriptSchema, type Script } from "../_schemas/schemas";
|
|
||||||
import { memo, useCallback, useRef } from "react";
|
|
||||||
|
|
||||||
type NoteProps = {
|
|
||||||
script: Script;
|
|
||||||
setScript: (script: Script) => void;
|
|
||||||
setIsValid: (isValid: boolean) => void;
|
|
||||||
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
function Note({
|
|
||||||
script,
|
|
||||||
setScript,
|
|
||||||
setIsValid,
|
|
||||||
setZodErrors,
|
|
||||||
}: NoteProps) {
|
|
||||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
|
||||||
|
|
||||||
const addNote = useCallback(() => {
|
|
||||||
setScript({
|
|
||||||
...script,
|
|
||||||
notes: [...script.notes, { text: "", type: "" }],
|
|
||||||
});
|
|
||||||
}, [script, setScript]);
|
|
||||||
|
|
||||||
const updateNote = useCallback((
|
|
||||||
index: number,
|
|
||||||
key: keyof Script["notes"][number],
|
|
||||||
value: string,
|
|
||||||
) => {
|
|
||||||
const updated: Script = {
|
|
||||||
...script,
|
|
||||||
notes: script.notes.map((note, i) =>
|
|
||||||
i === index ? { ...note, [key]: value } : note,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
const result = ScriptSchema.safeParse(updated);
|
|
||||||
setIsValid(result.success);
|
|
||||||
setZodErrors(result.success ? null : result.error);
|
|
||||||
setScript(updated);
|
|
||||||
// Restore focus after state update
|
|
||||||
if (key === "text") {
|
|
||||||
setTimeout(() => {
|
|
||||||
inputRefs.current[index]?.focus();
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}, [script, setScript, setIsValid, setZodErrors]);
|
|
||||||
|
|
||||||
const removeNote = useCallback((index: number) => {
|
|
||||||
setScript({
|
|
||||||
...script,
|
|
||||||
notes: script.notes.filter((_, i) => i !== index),
|
|
||||||
});
|
|
||||||
}, [script, setScript]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h3 className="text-xl font-semibold">Notes</h3>
|
|
||||||
{script.notes.map((note, index) => (
|
|
||||||
<NoteItem key={index} note={note} index={index} updateNote={updateNote} removeNote={removeNote} />
|
|
||||||
))}
|
|
||||||
<Button type="button" size="sm" onClick={addNote}>
|
|
||||||
<PlusCircle className="mr-2 h-4 w-4" /> Add Note
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const NoteItem = memo(
|
|
||||||
({
|
|
||||||
note,
|
|
||||||
index,
|
|
||||||
updateNote,
|
|
||||||
removeNote,
|
|
||||||
}: {
|
|
||||||
note: Script["notes"][number];
|
|
||||||
index: number;
|
|
||||||
updateNote: (index: number, key: keyof Script["notes"][number], value: string) => void;
|
|
||||||
removeNote: (index: number) => void;
|
|
||||||
}) => {
|
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
||||||
|
|
||||||
const handleTextChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
updateNote(index, "text", e.target.value);
|
|
||||||
setTimeout(() => {
|
|
||||||
inputRef.current?.focus();
|
|
||||||
}, 0);
|
|
||||||
}, [index, updateNote]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-2 border p-4 rounded">
|
|
||||||
<Input
|
|
||||||
placeholder="Note Text"
|
|
||||||
value={note.text}
|
|
||||||
onChange={handleTextChange}
|
|
||||||
ref={inputRef}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
value={note.type}
|
|
||||||
onValueChange={(value) => updateNote(index, "type", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="flex-1">
|
|
||||||
<SelectValue placeholder="Type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{Object.keys(AlertColors).map((type) => (
|
|
||||||
<SelectItem key={type} value={type}>
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
{type.charAt(0).toUpperCase() + type.slice(1)}{" "}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"size-4 rounded-full border",
|
|
||||||
AlertColors[type as keyof typeof AlertColors],
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="destructive"
|
|
||||||
type="button"
|
|
||||||
onClick={() => removeNote(index)}
|
|
||||||
>
|
|
||||||
<Trash2 className="mr-2 h-4 w-4" /> Remove Note
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
NoteItem.displayName = 'NoteItem';
|
|
||||||
|
|
||||||
|
|
||||||
export default memo(Note);
|
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import { Label } from "@/components/ui/label";
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
import { memo } from "react";
|
||||||
|
|
||||||
|
import type { Category } from "@/lib/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -6,11 +11,10 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Category } from "@/lib/types";
|
import { Label } from "@/components/ui/label";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { z } from "zod";
|
|
||||||
import { type Script } from "../_schemas/schemas";
|
import type { Script } from "../_schemas/schemas";
|
||||||
import { memo } from "react";
|
|
||||||
|
|
||||||
type CategoryProps = {
|
type CategoryProps = {
|
||||||
script: Script;
|
script: Script;
|
||||||
@@ -22,7 +26,7 @@ type CategoryProps = {
|
|||||||
|
|
||||||
const CategoryTag = memo(({
|
const CategoryTag = memo(({
|
||||||
category,
|
category,
|
||||||
onRemove
|
onRemove,
|
||||||
}: {
|
}: {
|
||||||
category: Category;
|
category: Category;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
@@ -53,7 +57,7 @@ const CategoryTag = memo(({
|
|||||||
</span>
|
</span>
|
||||||
));
|
));
|
||||||
|
|
||||||
CategoryTag.displayName = 'CategoryTag';
|
CategoryTag.displayName = "CategoryTag";
|
||||||
|
|
||||||
function Categories({
|
function Categories({
|
||||||
script,
|
script,
|
||||||
@@ -79,14 +83,16 @@ function Categories({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Category <span className="text-red-500">*</span>
|
Category
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Select onValueChange={(value) => addCategory(Number(value))}>
|
<Select onValueChange={value => addCategory(Number(value))}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a category" />
|
<SelectValue placeholder="Select a category" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{categories.map((category) => (
|
{categories.map(category => (
|
||||||
<SelectItem key={category.id} value={category.id.toString()}>
|
<SelectItem key={category.id} value={category.id.toString()}>
|
||||||
{category.name}
|
{category.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -101,13 +107,15 @@ function Categories({
|
|||||||
>
|
>
|
||||||
{script.categories.map((categoryId) => {
|
{script.categories.map((categoryId) => {
|
||||||
const category = categoryMap.get(categoryId);
|
const category = categoryMap.get(categoryId);
|
||||||
return category ? (
|
return category
|
||||||
|
? (
|
||||||
<CategoryTag
|
<CategoryTag
|
||||||
key={categoryId}
|
key={categoryId}
|
||||||
category={category}
|
category={category}
|
||||||
onRemove={() => removeCategory(categoryId)}
|
onRemove={() => removeCategory(categoryId)}
|
||||||
/>
|
/>
|
||||||
) : null;
|
)
|
||||||
|
: null;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import type { z } from "zod";
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { OperatingSystems } from "@/config/siteConfig";
|
|
||||||
import { PlusCircle, Trash2 } from "lucide-react";
|
import { PlusCircle, Trash2 } from "lucide-react";
|
||||||
import { memo, useCallback, useRef } from "react";
|
import { memo, useCallback, useRef } from "react";
|
||||||
import { z } from "zod";
|
|
||||||
import { InstallMethodSchema, ScriptSchema, type Script } from "../_schemas/schemas";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { OperatingSystems } from "@/config/site-config";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
|
import type { Script } from "../_schemas/schemas";
|
||||||
|
|
||||||
|
import { InstallMethodSchema, ScriptSchema } from "../_schemas/schemas";
|
||||||
|
|
||||||
type InstallMethodProps = {
|
type InstallMethodProps = {
|
||||||
script: Script;
|
script: Script;
|
||||||
@@ -28,9 +33,11 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
|
|
||||||
if (type === "pve") {
|
if (type === "pve") {
|
||||||
scriptPath = `tools/pve/${slug}.sh`;
|
scriptPath = `tools/pve/${slug}.sh`;
|
||||||
} else if (type === "addon") {
|
}
|
||||||
|
else if (type === "addon") {
|
||||||
scriptPath = `tools/addon/${slug}.sh`;
|
scriptPath = `tools/addon/${slug}.sh`;
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
scriptPath = `${type}/${slug}.sh`;
|
scriptPath = `${type}/${slug}.sh`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,8 +72,8 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
const updatedMethod = { ...method, [key]: value };
|
const updatedMethod = { ...method, [key]: value };
|
||||||
|
|
||||||
if (key === "type") {
|
if (key === "type") {
|
||||||
updatedMethod.script =
|
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
|
// Set OS to Alpine and reset version if type is alpine
|
||||||
if (value === "alpine") {
|
if (value === "alpine") {
|
||||||
@@ -89,7 +96,8 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
setIsValid(result.success);
|
setIsValid(result.success);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
setZodErrors(result.error);
|
setZodErrors(result.error);
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
setZodErrors(null);
|
setZodErrors(null);
|
||||||
}
|
}
|
||||||
return updated;
|
return updated;
|
||||||
@@ -100,7 +108,7 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
|
|
||||||
const removeInstallMethod = useCallback(
|
const removeInstallMethod = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
setScript((prev) => ({
|
setScript(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
install_methods: prev.install_methods.filter((_, i) => i !== index),
|
install_methods: prev.install_methods.filter((_, i) => i !== index),
|
||||||
}));
|
}));
|
||||||
@@ -113,7 +121,7 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
<h3 className="text-xl font-semibold">Install Methods</h3>
|
<h3 className="text-xl font-semibold">Install Methods</h3>
|
||||||
{script.install_methods.map((method, index) => (
|
{script.install_methods.map((method, index) => (
|
||||||
<div key={index} className="space-y-2 border p-4 rounded">
|
<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>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Type" />
|
<SelectValue placeholder="Type" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -130,12 +138,11 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
placeholder="CPU in Cores"
|
placeholder="CPU in Cores"
|
||||||
type="number"
|
type="number"
|
||||||
value={method.resources.cpu || ""}
|
value={method.resources.cpu || ""}
|
||||||
onChange={(e) =>
|
onChange={e =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
cpu: e.target.value ? Number(e.target.value) : null,
|
cpu: e.target.value ? Number(e.target.value) : null,
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
@@ -144,12 +151,11 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
placeholder="RAM in MB"
|
placeholder="RAM in MB"
|
||||||
type="number"
|
type="number"
|
||||||
value={method.resources.ram || ""}
|
value={method.resources.ram || ""}
|
||||||
onChange={(e) =>
|
onChange={e =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
ram: e.target.value ? Number(e.target.value) : null,
|
ram: e.target.value ? Number(e.target.value) : null,
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
@@ -158,31 +164,29 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
placeholder="HDD in GB"
|
placeholder="HDD in GB"
|
||||||
type="number"
|
type="number"
|
||||||
value={method.resources.hdd || ""}
|
value={method.resources.hdd || ""}
|
||||||
onChange={(e) =>
|
onChange={e =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
hdd: e.target.value ? Number(e.target.value) : null,
|
hdd: e.target.value ? Number(e.target.value) : null,
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Select
|
<Select
|
||||||
value={method.resources.os || undefined}
|
value={method.resources.os || undefined}
|
||||||
onValueChange={(value) =>
|
onValueChange={value =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
os: value || null,
|
os: value || null,
|
||||||
version: null, // Reset version when OS changes
|
version: null, // Reset version when OS changes
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
disabled={method.type === "alpine"}
|
disabled={method.type === "alpine"}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="OS" />
|
<SelectValue placeholder="OS" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{OperatingSystems.map((os) => (
|
{OperatingSystems.map(os => (
|
||||||
<SelectItem key={os.name} value={os.name}>
|
<SelectItem key={os.name} value={os.name}>
|
||||||
{os.name}
|
{os.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -191,19 +195,18 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
</Select>
|
</Select>
|
||||||
<Select
|
<Select
|
||||||
value={method.resources.version || undefined}
|
value={method.resources.version || undefined}
|
||||||
onValueChange={(value) =>
|
onValueChange={value =>
|
||||||
updateInstallMethod(index, "resources", {
|
updateInstallMethod(index, "resources", {
|
||||||
...method.resources,
|
...method.resources,
|
||||||
version: value || null,
|
version: value || null,
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
disabled={method.type === "alpine"}
|
disabled={method.type === "alpine"}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Version" />
|
<SelectValue placeholder="Version" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<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}>
|
<SelectItem key={version.slug} value={version.name}>
|
||||||
{version.name}
|
{version.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -212,12 +215,16 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</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
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
{" "}
|
||||||
|
Remove Install Method
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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
|
<PlusCircle className="mr-2 h-4 w-4" />
|
||||||
|
{" "}
|
||||||
|
Add Install Method
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
159
frontend/src/app/json-editor/_components/note.tsx
Normal file
159
frontend/src/app/json-editor/_components/note.tsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
import { PlusCircle, Trash2 } from "lucide-react";
|
||||||
|
import { memo, useCallback, useRef } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { AlertColors } from "@/config/site-config";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import type { Script } from "../_schemas/schemas";
|
||||||
|
|
||||||
|
import { ScriptSchema } from "../_schemas/schemas";
|
||||||
|
|
||||||
|
const NoteItem = memo(
|
||||||
|
({
|
||||||
|
note,
|
||||||
|
index,
|
||||||
|
updateNote,
|
||||||
|
removeNote,
|
||||||
|
}: {
|
||||||
|
note: Script["notes"][number];
|
||||||
|
index: number;
|
||||||
|
updateNote: (index: number, key: keyof Script["notes"][number], value: string) => void;
|
||||||
|
removeNote: (index: number) => void;
|
||||||
|
}) => {
|
||||||
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const handleTextChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
updateNote(index, "text", e.target.value);
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}, 0);
|
||||||
|
}, [index, updateNote]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2 border p-4 rounded">
|
||||||
|
<Input
|
||||||
|
placeholder="Note Text"
|
||||||
|
value={note.text}
|
||||||
|
onChange={handleTextChange}
|
||||||
|
ref={inputRef}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={note.type}
|
||||||
|
onValueChange={value => updateNote(index, "type", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="flex-1">
|
||||||
|
<SelectValue placeholder="Type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Object.keys(AlertColors).map(type => (
|
||||||
|
<SelectItem key={type} value={type}>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{type.charAt(0).toUpperCase() + type.slice(1)}
|
||||||
|
{" "}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"size-4 rounded-full border",
|
||||||
|
AlertColors[type as keyof typeof AlertColors],
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeNote(index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
{" "}
|
||||||
|
Remove Note
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
type NoteProps = {
|
||||||
|
script: Script;
|
||||||
|
setScript: (script: Script) => void;
|
||||||
|
setIsValid: (isValid: boolean) => void;
|
||||||
|
setZodErrors: (zodErrors: z.ZodError | null) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Note({
|
||||||
|
script,
|
||||||
|
setScript,
|
||||||
|
setIsValid,
|
||||||
|
setZodErrors,
|
||||||
|
}: NoteProps) {
|
||||||
|
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||||
|
|
||||||
|
const addNote = useCallback(() => {
|
||||||
|
setScript({
|
||||||
|
...script,
|
||||||
|
notes: [...script.notes, { text: "", type: "" }],
|
||||||
|
});
|
||||||
|
}, [script, setScript]);
|
||||||
|
|
||||||
|
const updateNote = useCallback((
|
||||||
|
index: number,
|
||||||
|
key: keyof Script["notes"][number],
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
|
const updated: Script = {
|
||||||
|
...script,
|
||||||
|
notes: script.notes.map((note, i) =>
|
||||||
|
i === index ? { ...note, [key]: value } : note,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
const result = ScriptSchema.safeParse(updated);
|
||||||
|
setIsValid(result.success);
|
||||||
|
setZodErrors(result.success ? null : result.error);
|
||||||
|
setScript(updated);
|
||||||
|
// Restore focus after state update
|
||||||
|
if (key === "text") {
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRefs.current[index]?.focus();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}, [script, setScript, setIsValid, setZodErrors]);
|
||||||
|
|
||||||
|
const removeNote = useCallback((index: number) => {
|
||||||
|
setScript({
|
||||||
|
...script,
|
||||||
|
notes: script.notes.filter((_, i) => i !== index),
|
||||||
|
});
|
||||||
|
}, [script, setScript]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="text-xl font-semibold">Notes</h3>
|
||||||
|
{script.notes.map((note, index) => (
|
||||||
|
<NoteItem key={index} note={note} index={index} updateNote={updateNote} removeNote={removeNote} />
|
||||||
|
))}
|
||||||
|
<Button type="button" size="sm" onClick={addNote}>
|
||||||
|
<PlusCircle className="mr-2 h-4 w-4" />
|
||||||
|
{" "}
|
||||||
|
Add Note
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
NoteItem.displayName = "NoteItem";
|
||||||
|
|
||||||
|
export default memo(Note);
|
||||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export const InstallMethodSchema = z.object({
|
export const InstallMethodSchema = z.object({
|
||||||
type: z.enum(["default", "alpine"], {
|
type: z.enum(["default", "alpine"], {
|
||||||
errorMap: () => ({ message: "Type must be either 'default' or 'alpine'" })
|
errorMap: () => ({ message: "Type must be either 'default' or 'alpine'" }),
|
||||||
}),
|
}),
|
||||||
script: z.string().min(1, "Script content cannot be empty"),
|
script: z.string().min(1, "Script content cannot be empty"),
|
||||||
resources: z.object({
|
resources: z.object({
|
||||||
@@ -25,7 +25,7 @@ export const ScriptSchema = z.object({
|
|||||||
categories: z.array(z.number()),
|
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"),
|
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", "pve", "addon", "turnkey"], {
|
type: z.enum(["vm", "ct", "pve", "addon", "turnkey"], {
|
||||||
errorMap: () => ({ message: "Type must be either 'vm', 'ct', 'pve', 'addon' or 'turnkey'" })
|
errorMap: () => ({ message: "Type must be either 'vm', 'ct', 'pve', 'addon' or 'turnkey'" }),
|
||||||
}),
|
}),
|
||||||
updateable: z.boolean(),
|
updateable: z.boolean(),
|
||||||
privileged: z.boolean(),
|
privileged: z.boolean(),
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import type { z } from "zod";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Calendar } from "@/components/ui/calendar";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { fetchCategories } from "@/lib/data";
|
|
||||||
import { Category } from "@/lib/types";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import { CalendarIcon, Check, Clipboard, Download } from "lucide-react";
|
import { CalendarIcon, Check, Clipboard, Download } from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { format } from "date-fns";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
|
||||||
import Categories from "./_components/Categories";
|
import type { Category } from "@/lib/types";
|
||||||
import InstallMethod from "./_components/InstallMethod";
|
|
||||||
import Note from "./_components/Note";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { ScriptSchema, type Script } from "./_schemas/schemas";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { fetchCategories } from "@/lib/data";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import type { Script } from "./_schemas/schemas";
|
||||||
|
|
||||||
|
import InstallMethod from "./_components/install-method";
|
||||||
|
import { ScriptSchema } from "./_schemas/schemas";
|
||||||
|
import Categories from "./_components/categories";
|
||||||
|
import Note from "./_components/note";
|
||||||
|
|
||||||
const initialScript: Script = {
|
const initialScript: Script = {
|
||||||
name: "",
|
name: "",
|
||||||
@@ -54,7 +60,7 @@ export default function JSONGenerator() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCategories()
|
fetchCategories()
|
||||||
.then(setCategories)
|
.then(setCategories)
|
||||||
.catch((error) => console.error("Error fetching categories:", error));
|
.catch(error => console.error("Error fetching categories:", error));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updateScript = useCallback((key: keyof Script, value: Script[keyof Script]) => {
|
const updateScript = useCallback((key: keyof Script, value: Script[keyof Script]) => {
|
||||||
@@ -67,11 +73,14 @@ export default function JSONGenerator() {
|
|||||||
|
|
||||||
if (updated.type === "pve") {
|
if (updated.type === "pve") {
|
||||||
scriptPath = `tools/pve/${updated.slug}.sh`;
|
scriptPath = `tools/pve/${updated.slug}.sh`;
|
||||||
} else if (updated.type === "addon") {
|
}
|
||||||
|
else if (updated.type === "addon") {
|
||||||
scriptPath = `tools/addon/${updated.slug}.sh`;
|
scriptPath = `tools/addon/${updated.slug}.sh`;
|
||||||
} else if (method.type === "alpine") {
|
}
|
||||||
|
else if (method.type === "alpine") {
|
||||||
scriptPath = `${updated.type}/alpine-${updated.slug}.sh`;
|
scriptPath = `${updated.type}/alpine-${updated.slug}.sh`;
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
scriptPath = `${updated.type}/${updated.slug}.sh`;
|
scriptPath = `${updated.type}/${updated.slug}.sh`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +145,10 @@ export default function JSONGenerator() {
|
|||||||
<div className="mt-2 space-y-1">
|
<div className="mt-2 space-y-1">
|
||||||
{zodErrors.errors.map((error, index) => (
|
{zodErrors.errors.map((error, index) => (
|
||||||
<AlertDescription key={index} className="p-1 text-red-500">
|
<AlertDescription key={index} className="p-1 text-red-500">
|
||||||
{error.path.join(".")} - {error.message}
|
{error.path.join(".")}
|
||||||
|
{" "}
|
||||||
|
-
|
||||||
|
{error.message}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -154,25 +166,31 @@ export default function JSONGenerator() {
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Name <span className="text-red-500">*</span>
|
Name
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</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>
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Slug <span className="text-red-500">*</span>
|
Slug
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</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>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Logo <span className="text-red-500">*</span>
|
Logo
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Full logo URL"
|
placeholder="Full logo URL"
|
||||||
value={script.logo || ""}
|
value={script.logo || ""}
|
||||||
onChange={(e) => updateScript("logo", e.target.value || null)}
|
onChange={e => updateScript("logo", e.target.value || null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -180,17 +198,19 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Path to config file"
|
placeholder="Path to config file"
|
||||||
value={script.config_path || ""}
|
value={script.config_path || ""}
|
||||||
onChange={(e) => updateScript("config_path", e.target.value || null)}
|
onChange={e => updateScript("config_path", e.target.value || null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Description <span className="text-red-500">*</span>
|
Description
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Example"
|
placeholder="Example"
|
||||||
value={script.description}
|
value={script.description}
|
||||||
onChange={(e) => updateScript("description", e.target.value)}
|
onChange={e => updateScript("description", e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Categories script={script} setScript={setScript} categories={categories} />
|
<Categories script={script} setScript={setScript} categories={categories} />
|
||||||
@@ -200,7 +220,7 @@ export default function JSONGenerator() {
|
|||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild className="flex-1">
|
<PopoverTrigger asChild className="flex-1">
|
||||||
<Button
|
<Button
|
||||||
variant={"outline"}
|
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>}
|
{formattedDate || <span>Pick a date</span>}
|
||||||
@@ -219,7 +239,7 @@ export default function JSONGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<Label>Type</Label>
|
<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">
|
<SelectTrigger className="flex-1">
|
||||||
<SelectValue placeholder="Type" />
|
<SelectValue placeholder="Type" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -234,11 +254,11 @@ export default function JSONGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full flex gap-5">
|
<div className="w-full flex gap-5">
|
||||||
<div className="flex items-center space-x-2">
|
<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>
|
<label>Updateable</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<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>
|
<label>Privileged</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -246,18 +266,18 @@ export default function JSONGenerator() {
|
|||||||
placeholder="Interface Port"
|
placeholder="Interface Port"
|
||||||
type="number"
|
type="number"
|
||||||
value={script.interface_port || ""}
|
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">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Website URL"
|
placeholder="Website URL"
|
||||||
value={script.website || ""}
|
value={script.website || ""}
|
||||||
onChange={(e) => updateScript("website", e.target.value || null)}
|
onChange={e => updateScript("website", e.target.value || null)}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Documentation URL"
|
placeholder="Documentation URL"
|
||||||
value={script.documentation || ""}
|
value={script.documentation || ""}
|
||||||
onChange={(e) => updateScript("documentation", e.target.value || null)}
|
onChange={e => updateScript("documentation", e.target.value || null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<InstallMethod script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
<InstallMethod script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||||
@@ -265,22 +285,20 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
value={script.default_credentials.username || ""}
|
value={script.default_credentials.username || ""}
|
||||||
onChange={(e) =>
|
onChange={e =>
|
||||||
updateScript("default_credentials", {
|
updateScript("default_credentials", {
|
||||||
...script.default_credentials,
|
...script.default_credentials,
|
||||||
username: e.target.value || null,
|
username: e.target.value || null,
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
value={script.default_credentials.password || ""}
|
value={script.default_credentials.password || ""}
|
||||||
onChange={(e) =>
|
onChange={e =>
|
||||||
updateScript("default_credentials", {
|
updateScript("default_credentials", {
|
||||||
...script.default_credentials,
|
...script.default_credentials,
|
||||||
password: e.target.value || null,
|
password: e.target.value || null,
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Note script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
<Note script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import Footer from "@/components/Footer";
|
|
||||||
import Navbar from "@/components/Navbar";
|
|
||||||
import QueryProvider from "@/components/query-provider";
|
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
|
||||||
import { analytics, basePath } from "@/config/siteConfig";
|
|
||||||
import "@/styles/globals.css";
|
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Inter } from "next/font/google";
|
|
||||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||||
|
import { Inter } from "next/font/google";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
|
import { analytics, basePath } from "@/config/site-config";
|
||||||
|
import "@/styles/globals.css";
|
||||||
|
import QueryProvider from "@/components/query-provider";
|
||||||
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
import Footer from "@/components/footer";
|
||||||
|
import Navbar from "@/components/navbar";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { basePath } from "@/config/siteConfig";
|
|
||||||
import type { MetadataRoute } from "next";
|
import type { MetadataRoute } from "next";
|
||||||
|
|
||||||
export const generateStaticParams = () => {
|
import { basePath } from "@/config/site-config";
|
||||||
|
|
||||||
|
export function generateStaticParams() {
|
||||||
return [];
|
return [];
|
||||||
};
|
}
|
||||||
|
|
||||||
export default function manifest(): MetadataRoute.Manifest {
|
export default function manifest(): MetadataRoute.Manifest {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import FAQ from "@/components/FAQ";
|
import { ArrowRightIcon, ExternalLink } from "lucide-react";
|
||||||
import AnimatedGradientText from "@/components/ui/animated-gradient-text";
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { FaGithub } from "react-icons/fa";
|
||||||
import { CardFooter } from "@/components/ui/card";
|
import { useTheme } from "next-themes";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -11,15 +13,14 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import Particles from "@/components/ui/particles";
|
import AnimatedGradientText from "@/components/ui/animated-gradient-text";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { basePath } from "@/config/siteConfig";
|
import { CardFooter } from "@/components/ui/card";
|
||||||
|
import Particles from "@/components/ui/particles";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { basePath } from "@/config/site-config";
|
||||||
|
import FAQ from "@/components/faq";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ArrowRightIcon, ExternalLink } from "lucide-react";
|
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { FaGithub } from "react-icons/fa";
|
|
||||||
|
|
||||||
function CustomArrowRightIcon() {
|
function CustomArrowRightIcon() {
|
||||||
return <ArrowRightIcon className="h-4 w-4" width={1} />;
|
return <ArrowRightIcon className="h-4 w-4" width={1} />;
|
||||||
@@ -50,7 +51,9 @@ export default function Page() {
|
|||||||
`p-px ![mask-composite:subtract]`,
|
`p-px ![mask-composite:subtract]`,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
❤️ <Separator className="mx-2 h-4" orientation="vertical" />
|
❤️
|
||||||
|
{" "}
|
||||||
|
<Separator className="mx-2 h-4" orientation="vertical" />
|
||||||
<span
|
<span
|
||||||
className={cn(
|
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`,
|
`animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
|
||||||
@@ -78,7 +81,9 @@ export default function Page() {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center justify-center"
|
className="flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<FaGithub className="mr-2 h-4 w-4" /> Tteck's GitHub
|
<FaGithub className="mr-2 h-4 w-4" />
|
||||||
|
{" "}
|
||||||
|
Tteck's GitHub
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="w-full" asChild>
|
<Button className="w-full" asChild>
|
||||||
@@ -88,7 +93,9 @@ export default function Page() {
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex items-center justify-center"
|
className="flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<ExternalLink className="mr-2 h-4 w-4" /> Proxmox Helper Scripts
|
<ExternalLink className="mr-2 h-4 w-4" />
|
||||||
|
{" "}
|
||||||
|
Proxmox Helper Scripts
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
@@ -104,7 +111,10 @@ export default function Page() {
|
|||||||
We are a community-driven initiative that simplifies the setup of Proxmox Virtual Environment (VE).
|
We are a community-driven initiative that simplifies the setup of Proxmox Virtual Environment (VE).
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
With 300+ scripts to help you manage your <b>Proxmox VE environment</b>. Whether you're a seasoned
|
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.
|
user or a newcomer, we've got you covered.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { basePath } from "@/config/siteConfig";
|
|
||||||
import type { MetadataRoute } from "next";
|
import type { MetadataRoute } from "next";
|
||||||
|
|
||||||
|
import { basePath } from "@/config/site-config";
|
||||||
|
|
||||||
export const dynamic = "force-static";
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
export default function robots(): MetadataRoute.Robots {
|
export default function robots(): MetadataRoute.Robots {
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
import CodeCopyButton from "@/components/ui/code-copy-button";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { basePath } from "@/config/siteConfig";
|
|
||||||
import { Script } from "@/lib/types";
|
|
||||||
import { getDisplayValueFromType } from "../ScriptInfoBlocks";
|
|
||||||
|
|
||||||
const getInstallCommand = (scriptPath = "", isAlpine = false) => {
|
|
||||||
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 defaultScript = item.install_methods.find((method) => method.type === "default");
|
|
||||||
|
|
||||||
const renderInstructions = (isAlpine = false) => (
|
|
||||||
<>
|
|
||||||
<p className="text-sm mt-2">
|
|
||||||
{isAlpine ? (
|
|
||||||
<>
|
|
||||||
As an alternative option, you can use Alpine Linux and the {item.name} package to create a {item.name}{" "}
|
|
||||||
{getDisplayValueFromType(item.type)} container with faster creation time and minimal system resource usage.
|
|
||||||
You are also obliged to adhere to updates provided by the package maintainer.
|
|
||||||
</>
|
|
||||||
) : item.type === "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 ? (
|
|
||||||
<>
|
|
||||||
{renderInstructions()}
|
|
||||||
<CodeCopyButton>{getInstallCommand(defaultScript.script)}</CodeCopyButton>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import handleCopy from "@/components/handleCopy";
|
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
|
||||||
import { Script } from "@/lib/types";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { ClipboardIcon } from "lucide-react";
|
|
||||||
|
|
||||||
export default function InterFaces({ item }: { item: Script }) {
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { Info } from "lucide-react";
|
||||||
|
|
||||||
|
import type { Script } from "@/lib/types";
|
||||||
|
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
import CodeCopyButton from "@/components/ui/code-copy-button";
|
||||||
|
import { basePath } from "@/config/site-config";
|
||||||
|
|
||||||
|
import { getDisplayValueFromType } from "../script-info-blocks";
|
||||||
|
|
||||||
|
function getInstallCommand(scriptPath = "", isAlpine = false, useGitea = false) {
|
||||||
|
const githubUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main/${scriptPath}`;
|
||||||
|
const giteaUrl = `https://git.community-scripts.org/community-scripts/${basePath}/raw/branch/main/${scriptPath}`;
|
||||||
|
const url = useGitea ? giteaUrl : githubUrl;
|
||||||
|
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 defaultScript = item.install_methods.find(method => method.type === "default");
|
||||||
|
|
||||||
|
const renderInstructions = (isAlpine = false) => (
|
||||||
|
<>
|
||||||
|
<p className="text-sm mt-2">
|
||||||
|
{isAlpine
|
||||||
|
? (
|
||||||
|
<>
|
||||||
|
As an alternative option, you can use Alpine Linux and the
|
||||||
|
{" "}
|
||||||
|
{item.name}
|
||||||
|
{" "}
|
||||||
|
package to create a
|
||||||
|
{" "}
|
||||||
|
{item.name}
|
||||||
|
{" "}
|
||||||
|
{getDisplayValueFromType(item.type)}
|
||||||
|
{" "}
|
||||||
|
container with faster creation time and minimal system resource usage.
|
||||||
|
You are also obliged to adhere to updates provided by the package maintainer.
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: item.type === "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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderGiteaInfo = () => (
|
||||||
|
<Alert className="mt-3 mb-3">
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<AlertDescription className="text-sm">
|
||||||
|
<strong>When to use Gitea:</strong>
|
||||||
|
{" "}
|
||||||
|
GitHub may have issues including slow connections, delayed updates after bug
|
||||||
|
fixes, no IPv6 support, API rate limits (60/hour). Use our Gitea mirror as a reliable alternative when
|
||||||
|
experiencing these issues.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderScriptTabs = (useGitea = false) => {
|
||||||
|
if (alpineScript) {
|
||||||
|
return (
|
||||||
|
<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, false, useGitea)}</CodeCopyButton>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="alpine">
|
||||||
|
{renderInstructions(true)}
|
||||||
|
<CodeCopyButton>{getInstallCommand(alpineScript.script, true, useGitea)}</CodeCopyButton>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (defaultScript?.script) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderInstructions()}
|
||||||
|
<CodeCopyButton>{getInstallCommand(defaultScript.script, false, useGitea)}</CodeCopyButton>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<Tabs defaultValue="github" className="w-full max-w-4xl">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="github">GitHub</TabsTrigger>
|
||||||
|
<TabsTrigger value="gitea">Gitea</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="github">
|
||||||
|
{renderScriptTabs(false)}
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="gitea">
|
||||||
|
{renderGiteaInfo()}
|
||||||
|
{renderScriptTabs(true)}
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { Category, Script } from "@/lib/types";
|
|
||||||
import ScriptAccordion from "./ScriptAccordion";
|
|
||||||
|
|
||||||
const Sidebar = ({
|
|
||||||
items,
|
|
||||||
selectedScript,
|
|
||||||
setSelectedScript,
|
|
||||||
}: {
|
|
||||||
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[]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex min-w-[350px] flex-col sm:max-w-[350px]">
|
|
||||||
<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={items}
|
|
||||||
selectedScript={selectedScript}
|
|
||||||
setSelectedScript={setSelectedScript}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Sidebar;
|
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
import { CPUIcon, HDDIcon, RAMIcon } from "@/components/icons/resource-icons";
|
import { CPUIcon, HDDIcon, RAMIcon } from "@/components/icons/resource-icons";
|
||||||
import { getDisplayValueFromRAM } from "@/lib/utils/resource-utils";
|
import { getDisplayValueFromRAM } from "@/lib/utils/resource-utils";
|
||||||
|
|
||||||
interface ResourceDisplayProps {
|
type ResourceDisplayProps = {
|
||||||
title: string;
|
title: string;
|
||||||
cpu: number | null;
|
cpu: number | null;
|
||||||
ram: number | null;
|
ram: number | null;
|
||||||
hdd: number | null;
|
hdd: number | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface IconTextProps {
|
type IconTextProps = {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
function IconText({ icon, label }: IconTextProps) {
|
function IconText({ icon, label }: IconTextProps) {
|
||||||
return (
|
return (
|
||||||
@@ -27,7 +27,8 @@ export function ResourceDisplay({ title, cpu, ram, hdd }: ResourceDisplayProps)
|
|||||||
const hasRAM = typeof ram === "number" && ram > 0;
|
const hasRAM = typeof ram === "number" && ram > 0;
|
||||||
const hasHDD = typeof hdd === "number" && hdd > 0;
|
const hasHDD = typeof hdd === "number" && hdd > 0;
|
||||||
|
|
||||||
if (!hasCPU && !hasRAM && !hasHDD) return null;
|
if (!hasCPU && !hasRAM && !hasHDD)
|
||||||
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import type { Category } from "@/lib/types";
|
||||||
|
|
||||||
import { formattedBadge } from "@/components/CommandMenu";
|
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
AccordionItem,
|
AccordionItem,
|
||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "@/components/ui/accordion";
|
} from "@/components/ui/accordion";
|
||||||
import { Category } from "@/lib/types";
|
import { formattedBadge } from "@/components/command-menu";
|
||||||
|
import { basePath } from "@/config/site-config";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { basePath } from "@/config/siteConfig";
|
|
||||||
|
|
||||||
export default function ScriptAccordion({
|
export default function ScriptAccordion({
|
||||||
items,
|
items,
|
||||||
@@ -41,8 +41,8 @@ export default function ScriptAccordion({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedScript) {
|
if (selectedScript) {
|
||||||
const category = items.find((category) =>
|
const category = items.find(category =>
|
||||||
category.scripts.some((script) => script.slug === selectedScript),
|
category.scripts.some(script => script.slug === selectedScript),
|
||||||
);
|
);
|
||||||
if (category) {
|
if (category) {
|
||||||
setExpandedItem(category.name);
|
setExpandedItem(category.name);
|
||||||
@@ -58,11 +58,11 @@ export default function ScriptAccordion({
|
|||||||
collapsible
|
collapsible
|
||||||
className="overflow-y-scroll max-h-[calc(100vh-225px)] overflow-x-hidden p-2"
|
className="overflow-y-scroll max-h-[calc(100vh-225px)] overflow-x-hidden p-2"
|
||||||
>
|
>
|
||||||
{items.map((category) => (
|
{items.map(category => (
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
key={category.id + ":category"}
|
key={`${category.id}:category`}
|
||||||
value={category.name}
|
value={category.name}
|
||||||
className={cn("sm:text-md flex flex-col border-none", {
|
className={cn("sm:text-sm flex flex-col border-none", {
|
||||||
"rounded-lg bg-accent/30": expandedItem === category.name,
|
"rounded-lg bg-accent/30": expandedItem === category.name,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
@@ -72,11 +72,15 @@ export default function ScriptAccordion({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="mr-2 flex w-full items-center justify-between">
|
<div className="mr-2 flex w-full items-center justify-between">
|
||||||
<span className="pl-2 text-left">{category.name} </span>
|
<span className="pl-2 text-left">
|
||||||
|
{category.name}
|
||||||
|
{" "}
|
||||||
|
</span>
|
||||||
<span className="rounded-full bg-gray-200 px-2 py-1 text-xs text-muted-foreground hover:no-underline dark:bg-blue-800/20">
|
<span className="rounded-full bg-gray-200 px-2 py-1 text-xs text-muted-foreground hover:no-underline dark:bg-blue-800/20">
|
||||||
{category.scripts.length}
|
{category.scripts.length}
|
||||||
</span>
|
</span>
|
||||||
</div>{" "}
|
</div>
|
||||||
|
{" "}
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent
|
<AccordionContent
|
||||||
data-state={expandedItem === category.name ? "open" : "closed"}
|
data-state={expandedItem === category.name ? "open" : "closed"}
|
||||||
@@ -109,10 +113,9 @@ export default function ScriptAccordion({
|
|||||||
height={16}
|
height={16}
|
||||||
width={16}
|
width={16}
|
||||||
unoptimized
|
unoptimized
|
||||||
onError={(e) =>
|
onError={e =>
|
||||||
((e.currentTarget as HTMLImageElement).src =
|
((e.currentTarget as HTMLImageElement).src
|
||||||
`/${basePath}/logo.png`)
|
= `/${basePath}/logo.png`)}
|
||||||
}
|
|
||||||
alt={script.name}
|
alt={script.name}
|
||||||
className="mr-1 w-4 h-4 rounded-full"
|
className="mr-1 w-4 h-4 rounded-full"
|
||||||
/>
|
/>
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { basePath, mostPopularScripts } from "@/config/siteConfig";
|
|
||||||
import { extractDate } from "@/lib/time";
|
|
||||||
import { Category, Script } from "@/lib/types";
|
|
||||||
import { CalendarPlus } from "lucide-react";
|
import { CalendarPlus } from "lucide-react";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useMemo, useState } from "react";
|
|
||||||
|
import type { Category, Script } from "@/lib/types";
|
||||||
|
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { basePath, mostPopularScripts } from "@/config/site-config";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { extractDate } from "@/lib/time";
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 3;
|
const ITEMS_PER_PAGE = 3;
|
||||||
|
|
||||||
export const getDisplayValueFromType = (type: string) => {
|
export function getDisplayValueFromType(type: string) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "ct":
|
case "ct":
|
||||||
return "LXC";
|
return "LXC";
|
||||||
@@ -22,15 +24,16 @@ export const getDisplayValueFromType = (type: string) => {
|
|||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export function LatestScripts({ items }: { items: Category[] }) {
|
export function LatestScripts({ items }: { items: Category[] }) {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
const latestScripts = useMemo(() => {
|
const latestScripts = useMemo(() => {
|
||||||
if (!items) return [];
|
if (!items)
|
||||||
|
return [];
|
||||||
|
|
||||||
const scripts = items.flatMap((category) => category.scripts || []);
|
const scripts = items.flatMap(category => category.scripts || []);
|
||||||
|
|
||||||
// Filter out duplicates by slug
|
// Filter out duplicates by slug
|
||||||
const uniqueScriptsMap = new Map<string, Script>();
|
const uniqueScriptsMap = new Map<string, Script>();
|
||||||
@@ -46,11 +49,11 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
|||||||
}, [items]);
|
}, [items]);
|
||||||
|
|
||||||
const goToNextPage = () => {
|
const goToNextPage = () => {
|
||||||
setPage((prevPage) => prevPage + 1);
|
setPage(prevPage => prevPage + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const goToPreviousPage = () => {
|
const goToPreviousPage = () => {
|
||||||
setPage((prevPage) => prevPage - 1);
|
setPage(prevPage => prevPage - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const startIndex = (page - 1) * ITEMS_PER_PAGE;
|
const startIndex = (page - 1) * ITEMS_PER_PAGE;
|
||||||
@@ -80,7 +83,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="min-w flex w-full flex-row flex-wrap gap-4">
|
<div className="min-w flex w-full flex-row flex-wrap gap-4">
|
||||||
{latestScripts.slice(startIndex, endIndex).map((script) => (
|
{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>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-3">
|
<CardTitle className="flex items-center gap-3">
|
||||||
@@ -91,13 +94,15 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
|||||||
height={64}
|
height={64}
|
||||||
width={64}
|
width={64}
|
||||||
alt=""
|
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"
|
className="h-11 w-11 object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className="text-lg line-clamp-1">
|
<p className="text-lg line-clamp-1">
|
||||||
{script.name} {getDisplayValueFromType(script.type)}
|
{script.name}
|
||||||
|
{" "}
|
||||||
|
{getDisplayValueFromType(script.type)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
||||||
<CalendarPlus className="h-4 w-4" />
|
<CalendarPlus className="h-4 w-4" />
|
||||||
@@ -130,7 +135,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
|||||||
|
|
||||||
export function MostViewedScripts({ items }: { items: Category[] }) {
|
export function MostViewedScripts({ items }: { items: Category[] }) {
|
||||||
const mostViewedScripts = items.reduce((acc: Script[], 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);
|
return acc.concat(foundScripts);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -142,7 +147,7 @@ export function MostViewedScripts({ items }: { items: Category[] }) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="min-w flex w-full flex-row flex-wrap gap-4">
|
<div className="min-w flex w-full flex-row flex-wrap gap-4">
|
||||||
{mostViewedScripts.map((script) => (
|
{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>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-3">
|
<CardTitle className="flex items-center gap-3">
|
||||||
@@ -153,13 +158,15 @@ export function MostViewedScripts({ items }: { items: Category[] }) {
|
|||||||
height={64}
|
height={64}
|
||||||
width={64}
|
width={64}
|
||||||
alt=""
|
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"
|
className="h-11 w-11 object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className="line-clamp-1 text-lg">
|
<p className="line-clamp-1 text-lg">
|
||||||
{script.name} {getDisplayValueFromType(script.type)}
|
{script.name}
|
||||||
|
{" "}
|
||||||
|
{getDisplayValueFromType(script.type)}
|
||||||
</p>
|
</p>
|
||||||
<p className="flex items-center gap-1 text-sm text-muted-foreground">
|
<p className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||||
<CalendarPlus className="h-4 w-4" />
|
<CalendarPlus className="h-4 w-4" />
|
||||||
@@ -1,31 +1,32 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { extractDate } from "@/lib/time";
|
|
||||||
import { AppVersion, Script } from "@/lib/types";
|
|
||||||
|
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
|
import { Suspense } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
import type { AppVersion, Script } from "@/lib/types";
|
||||||
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 ConfigFile from "./ScriptItems/ConfigFile";
|
|
||||||
import DefaultPassword from "./ScriptItems/DefaultPassword";
|
|
||||||
import Description from "./ScriptItems/Description";
|
|
||||||
import InstallCommand from "./ScriptItems/InstallCommand";
|
|
||||||
import InterFaces from "./ScriptItems/InterFaces";
|
|
||||||
import Tooltips from "./ScriptItems/Tooltips";
|
|
||||||
|
|
||||||
interface ScriptItemProps {
|
import { cleanSlug } from "@/lib/utils/resource-utils";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { useVersions } from "@/hooks/use-versions";
|
||||||
|
import { basePath } from "@/config/site-config";
|
||||||
|
import { extractDate } from "@/lib/time";
|
||||||
|
|
||||||
|
import { getDisplayValueFromType } from "./script-info-blocks";
|
||||||
|
import DefaultPassword from "./script-items/default-password";
|
||||||
|
import InstallCommand from "./script-items/install-command";
|
||||||
|
import { ResourceDisplay } from "./resource-display";
|
||||||
|
import Description from "./script-items/description";
|
||||||
|
import ConfigFile from "./script-items/config-file";
|
||||||
|
import InterFaces from "./script-items/interfaces";
|
||||||
|
import Tooltips from "./script-items/tool-tips";
|
||||||
|
import Buttons from "./script-items/buttons";
|
||||||
|
import Alerts from "./script-items/alerts";
|
||||||
|
|
||||||
|
type ScriptItemProps = {
|
||||||
item: Script;
|
item: Script;
|
||||||
setSelectedScript: (script: string | null) => void;
|
setSelectedScript: (script: string | null) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
function ScriptHeader({ item }: { item: Script }) {
|
function ScriptHeader({ item }: { item: Script }) {
|
||||||
const defaultInstallMethod = item.install_methods?.[0];
|
const defaultInstallMethod = item.install_methods?.[0];
|
||||||
@@ -40,7 +41,7 @@ function ScriptHeader({ item }: { item: Script }) {
|
|||||||
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"
|
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`}
|
src={item.logo || `/${basePath}/logo.png`}
|
||||||
width={400}
|
width={400}
|
||||||
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||||
height={400}
|
height={400}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
unoptimized
|
unoptimized
|
||||||
@@ -58,10 +59,15 @@ function ScriptHeader({ item }: { item: Script }) {
|
|||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div className="mt-1 flex items-center gap-3 text-sm text-muted-foreground">
|
<div className="mt-1 flex items-center gap-3 text-sm text-muted-foreground">
|
||||||
<span>Added {extractDate(item.date_created)}</span>
|
<span>
|
||||||
|
Added
|
||||||
|
{extractDate(item.date_created)}
|
||||||
|
</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span className=" capitalize">
|
<span className=" capitalize">
|
||||||
{os} {version}
|
{os}
|
||||||
|
{" "}
|
||||||
|
{version}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,10 +82,10 @@ function ScriptHeader({ item }: { item: Script }) {
|
|||||||
hdd={defaultInstallMethod.resources.hdd}
|
hdd={defaultInstallMethod.resources.hdd}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{item.install_methods.find((method) => method.type === "alpine")?.resources && (
|
{item.install_methods.find(method => method.type === "alpine")?.resources && (
|
||||||
<ResourceDisplay
|
<ResourceDisplay
|
||||||
title="Alpine"
|
title="Alpine"
|
||||||
{...item.install_methods.find((method) => method.type === "alpine")!.resources!}
|
{...item.install_methods.find(method => method.type === "alpine")!.resources!}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -108,7 +114,8 @@ function VersionInfo({ item }: { item: Script }) {
|
|||||||
return cleanName === cleanSlug(item.slug) || cleanName.includes(cleanSlug(item.slug));
|
return cleanName === cleanSlug(item.slug) || cleanName.includes(cleanSlug(item.slug));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!matchedVersion) return null;
|
if (!matchedVersion)
|
||||||
|
return null;
|
||||||
|
|
||||||
return <span className="font-medium text-sm">{matchedVersion.version}</span>;
|
return <span className="font-medium text-sm">{matchedVersion.version}</span>;
|
||||||
}
|
}
|
||||||
@@ -132,7 +139,7 @@ export function ScriptItem({ item, setSelectedScript }: ScriptItemProps) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-xl border border-border bg-gradient-to-b from-card/30 to-background/50 backdrop-blur-sm shadow-sm">
|
<div className="rounded-xl border border-border bg-accent/30 backdrop-blur-sm shadow-sm">
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
<Suspense fallback={<div className="animate-pulse h-32 bg-accent/20 rounded-xl" />}>
|
<Suspense fallback={<div className="animate-pulse h-32 bg-accent/20 rounded-xl" />}>
|
||||||
<ScriptHeader item={item} />
|
<ScriptHeader item={item} />
|
||||||
@@ -144,7 +151,9 @@ export function ScriptItem({ item, setSelectedScript }: ScriptItemProps) {
|
|||||||
<div className="mt-4 rounded-lg border shadow-sm">
|
<div className="mt-4 rounded-lg border shadow-sm">
|
||||||
<div className="flex gap-3 px-4 py-2 bg-accent/25">
|
<div className="flex gap-3 px-4 py-2 bg-accent/25">
|
||||||
<h2 className="text-lg font-semibold">
|
<h2 className="text-lg font-semibold">
|
||||||
How to {item.type === "pve" ? "use" : item.type === "addon" ? "apply" : "install"}
|
How to
|
||||||
|
{" "}
|
||||||
|
{item.type === "pve" ? "use" : item.type === "addon" ? "apply" : "install"}
|
||||||
</h2>
|
</h2>
|
||||||
<Tooltips item={item} />
|
<Tooltips item={item} />
|
||||||
</div>
|
</div>
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
import TextCopyBlock from "@/components/TextCopyBlock";
|
|
||||||
import { AlertColors } from "@/config/siteConfig";
|
|
||||||
import { Script } from "@/lib/types";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { AlertCircle, NotepadText } from "lucide-react";
|
import { AlertCircle, NotepadText } from "lucide-react";
|
||||||
|
|
||||||
|
import type { Script } from "@/lib/types";
|
||||||
|
|
||||||
|
import TextCopyBlock from "@/components/text-copy-block";
|
||||||
|
import { AlertColors } from "@/config/site-config";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type NoteProps = {
|
type NoteProps = {
|
||||||
text: string;
|
text: string;
|
||||||
type: keyof typeof AlertColors;
|
type: keyof typeof AlertColors;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function Alerts({ item }: { item: Script }) {
|
export default function Alerts({ item }: { item: Script }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{item?.notes?.length > 0 &&
|
{item?.notes?.length > 0
|
||||||
item.notes.map((note: NoteProps, index: number) => (
|
&& item.notes.map((note: NoteProps, index: number) => (
|
||||||
<div key={index} className="mt-4 flex flex-col shadow-sm gap-2">
|
<div key={index} className="mt-4 flex flex-col shadow-sm gap-2">
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -21,9 +23,11 @@ export default function Alerts({ item }: { item: Script }) {
|
|||||||
AlertColors[note.type],
|
AlertColors[note.type],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{note.type == "info" ? (
|
{note.type === "info"
|
||||||
|
? (
|
||||||
<NotepadText className="h-4 min-h-4 w-4 min-w-4" />
|
<NotepadText className="h-4 min-h-4 w-4 min-w-4" />
|
||||||
) : (
|
)
|
||||||
|
: (
|
||||||
<AlertCircle className="h-4 min-h-4 w-4 min-w-4" />
|
<AlertCircle className="h-4 min-h-4 w-4 min-w-4" />
|
||||||
)}
|
)}
|
||||||
<span>{TextCopyBlock(note.text)}</span>
|
<span>{TextCopyBlock(note.text)}</span>
|
||||||
@@ -1,20 +1,22 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { BookOpenText, Code, Globe, LinkIcon, RefreshCcw } from "lucide-react";
|
||||||
|
|
||||||
|
import type { Script } from "@/lib/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { basePath } from "@/config/siteConfig";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Script } from "@/lib/types";
|
import { basePath } from "@/config/site-config";
|
||||||
import { BookOpenText, Code, Globe, LinkIcon, RefreshCcw } from "lucide-react";
|
|
||||||
|
|
||||||
const generateInstallSourceUrl = (slug: string) => {
|
function generateInstallSourceUrl(slug: string) {
|
||||||
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
||||||
return `${baseUrl}/install/${slug}-install.sh`;
|
return `${baseUrl}/install/${slug}-install.sh`;
|
||||||
};
|
}
|
||||||
|
|
||||||
const generateSourceUrl = (slug: string, type: string) => {
|
function generateSourceUrl(slug: string, type: string) {
|
||||||
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -29,18 +31,18 @@ const generateSourceUrl = (slug: string, type: string) => {
|
|||||||
default:
|
default:
|
||||||
return `${baseUrl}/ct/${slug}.sh`; // fallback for "ct"
|
return `${baseUrl}/ct/${slug}.sh`; // fallback for "ct"
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const generateUpdateUrl = (slug: string) => {
|
function generateUpdateUrl(slug: string) {
|
||||||
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
||||||
return `${baseUrl}/ct/${slug}.sh`;
|
return `${baseUrl}/ct/${slug}.sh`;
|
||||||
};
|
}
|
||||||
|
|
||||||
interface LinkItem {
|
type LinkItem = {
|
||||||
href: string;
|
href: string;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
text: string;
|
text: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function Buttons({ item }: { item: Script }) {
|
export default function Buttons({ item }: { item: Script }) {
|
||||||
const isCtOrDefault = ["ct"].includes(item.type);
|
const isCtOrDefault = ["ct"].includes(item.type);
|
||||||
@@ -76,7 +78,8 @@ export default function Buttons({ item }: { item: Script }) {
|
|||||||
},
|
},
|
||||||
].filter(Boolean) as LinkItem[];
|
].filter(Boolean) as LinkItem[];
|
||||||
|
|
||||||
if (links.length === 0) return null;
|
if (links.length === 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import handleCopy from "@/components/handleCopy";
|
import type { Script } from "@/lib/types";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Script } from "@/lib/types";
|
import handleCopy from "@/components/handle-copy";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
export default function DefaultPassword({ item }: { item: Script }) {
|
export default function DefaultPassword({ item }: { item: Script }) {
|
||||||
const { username, password } = item.default_credentials;
|
const { username, password } = item.default_credentials;
|
||||||
const hasDefaultLogin = username || password;
|
const hasDefaultLogin = username || password;
|
||||||
|
|
||||||
if (!hasDefaultLogin) return null;
|
if (!hasDefaultLogin)
|
||||||
|
return null;
|
||||||
|
|
||||||
const copyCredential = (type: "username" | "password") => {
|
const copyCredential = (type: "username" | "password") => {
|
||||||
handleCopy(type, item.default_credentials[type] ?? "");
|
handleCopy(type, item.default_credentials[type] ?? "");
|
||||||
@@ -21,18 +23,27 @@ export default function DefaultPassword({ item }: { item: Script }) {
|
|||||||
<Separator className="w-full" />
|
<Separator className="w-full" />
|
||||||
<div className="flex flex-col gap-2 p-4">
|
<div className="flex flex-col gap-2 p-4">
|
||||||
<p className="mb-2 text-sm">
|
<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>
|
</p>
|
||||||
{["username", "password"].map((type) => {
|
{["username", "password"].map((type) => {
|
||||||
const value = item.default_credentials[type as "username" | "password"];
|
const value = item.default_credentials[type as "username" | "password"];
|
||||||
return value && value.trim() !== "" ? (
|
return value && value.trim() !== ""
|
||||||
|
? (
|
||||||
<div key={type} className="text-sm">
|
<div key={type} className="text-sm">
|
||||||
{type.charAt(0).toUpperCase() + type.slice(1)}:{" "}
|
{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")}>
|
||||||
{value}
|
{value}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
)
|
||||||
|
: null;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Script } from "@/lib/types";
|
import type { Script } from "@/lib/types";
|
||||||
|
|
||||||
export default function DefaultSettings({ item }: { item: Script }) {
|
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`);
|
||||||
@@ -8,15 +8,26 @@ export default function DefaultSettings({ item }: { item: Script }) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-md font-semibold">{title}</h2>
|
<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">
|
||||||
<p className="text-sm text-muted-foreground">RAM: {getDisplayValueFromRAM(ram ?? 0)}</p>
|
CPU:
|
||||||
<p className="text-sm text-muted-foreground">HDD: {hdd}GB</p>
|
{cpu}
|
||||||
|
vCPU
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
RAM:
|
||||||
|
{getDisplayValueFromRAM(ram ?? 0)}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
HDD:
|
||||||
|
{hdd}
|
||||||
|
GB
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultSettings = item.install_methods.find((method) => method.type === "default");
|
const defaultSettings = item.install_methods.find(method => method.type === "default");
|
||||||
const defaultAlpineSettings = item.install_methods.find((method) => method.type === "alpine");
|
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);
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import TextCopyBlock from "@/components/TextCopyBlock";
|
import type { Script } from "@/lib/types";
|
||||||
import { Script } from "@/lib/types";
|
|
||||||
|
import TextCopyBlock from "@/components/text-copy-block";
|
||||||
|
|
||||||
export default function Description({ item }: { item: Script }) {
|
export default function Description({ item }: { item: Script }) {
|
||||||
return (
|
return (
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import { Info } from "lucide-react";
|
||||||
|
|
||||||
|
import type { Script } from "@/lib/types";
|
||||||
|
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
import CodeCopyButton from "@/components/ui/code-copy-button";
|
||||||
|
import { basePath } from "@/config/site-config";
|
||||||
|
|
||||||
|
import { getDisplayValueFromType } from "../script-info-blocks";
|
||||||
|
|
||||||
|
function getInstallCommand(scriptPath = "", isAlpine = false, useGitea = false) {
|
||||||
|
const githubUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main/${scriptPath}`;
|
||||||
|
const giteaUrl = `https://git.community-scripts.org/community-scripts/${basePath}/raw/branch/main/${scriptPath}`;
|
||||||
|
const url = useGitea ? giteaUrl : githubUrl;
|
||||||
|
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 defaultScript = item.install_methods.find(method => method.type === "default");
|
||||||
|
|
||||||
|
const renderInstructions = (isAlpine = false) => (
|
||||||
|
<>
|
||||||
|
<p className="text-sm mt-2">
|
||||||
|
{isAlpine
|
||||||
|
? (
|
||||||
|
<>
|
||||||
|
As an alternative option, you can use Alpine Linux and the
|
||||||
|
{" "}
|
||||||
|
{item.name}
|
||||||
|
{" "}
|
||||||
|
package to create a
|
||||||
|
{" "}
|
||||||
|
{item.name}
|
||||||
|
{" "}
|
||||||
|
{getDisplayValueFromType(item.type)}
|
||||||
|
{" "}
|
||||||
|
container with faster creation time and minimal system resource usage.
|
||||||
|
You are also obliged to adhere to updates provided by the package maintainer.
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: item.type === "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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderGiteaInfo = () => (
|
||||||
|
<Alert className="mt-3 mb-3">
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<AlertDescription className="text-sm">
|
||||||
|
<strong>When to use Gitea:</strong>
|
||||||
|
{" "}
|
||||||
|
GitHub may have issues including slow connections, delayed updates after bug
|
||||||
|
fixes, no IPv6 support, API rate limits (60/hour). Use our Gitea mirror as a reliable alternative when
|
||||||
|
experiencing these issues.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderScriptTabs = (useGitea = false) => {
|
||||||
|
if (alpineScript) {
|
||||||
|
return (
|
||||||
|
<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, false, useGitea)}</CodeCopyButton>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="alpine">
|
||||||
|
{renderInstructions(true)}
|
||||||
|
<CodeCopyButton>{getInstallCommand(alpineScript.script, true, useGitea)}</CodeCopyButton>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (defaultScript?.script) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderInstructions()}
|
||||||
|
<CodeCopyButton>{getInstallCommand(defaultScript.script, false, useGitea)}</CodeCopyButton>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<Tabs defaultValue="github" className="w-full max-w-4xl">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="github">GitHub</TabsTrigger>
|
||||||
|
<TabsTrigger value="gitea">Gitea</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="github">
|
||||||
|
{renderScriptTabs(false)}
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="gitea">
|
||||||
|
{renderGiteaInfo()}
|
||||||
|
{renderScriptTabs(true)}
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { ClipboardIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import type { Script } from "@/lib/types";
|
||||||
|
|
||||||
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
import handleCopy from "@/components/handle-copy";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export default function InterFaces({ item }: { item: Script }) {
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,22 +1,27 @@
|
|||||||
import { Badge, type BadgeProps } from "@/components/ui/badge";
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
||||||
import { Script } from "@/lib/types";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { CircleHelp } from "lucide-react";
|
import { CircleHelp } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
interface TooltipProps {
|
import type { BadgeProps } from "@/components/ui/badge";
|
||||||
|
import type { Script } from "@/lib/types";
|
||||||
|
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type TooltipProps = {
|
||||||
variant: BadgeProps["variant"];
|
variant: BadgeProps["variant"];
|
||||||
label: string;
|
label: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const TooltipBadge: React.FC<TooltipProps> = ({ variant, label, content }) => (
|
const TooltipBadge: React.FC<TooltipProps> = ({ variant, label, content }) => (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={100}>
|
<Tooltip delayDuration={100}>
|
||||||
<TooltipTrigger className={cn("flex items-center", !content && "cursor-default")}>
|
<TooltipTrigger className={cn("flex items-center", !content && "cursor-default")}>
|
||||||
<Badge variant={variant} className="flex items-center gap-1">
|
<Badge variant={variant} className="flex items-center gap-1">
|
||||||
{label} {content && <CircleHelp className="size-3" />}
|
{label}
|
||||||
|
{" "}
|
||||||
|
{content && <CircleHelp className="size-3" />}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{content && (
|
{content && (
|
||||||
@@ -34,14 +39,14 @@ export default function Tooltips({ item }: { item: Script }) {
|
|||||||
{item.privileged && (
|
{item.privileged && (
|
||||||
<TooltipBadge variant="warning" label="Privileged" content="This script will be run in a privileged LXC" />
|
<TooltipBadge variant="warning" label="Privileged" content="This script will be run in a privileged LXC" />
|
||||||
)}
|
)}
|
||||||
{item.updateable && (
|
{(item.updateable || item.type !== "pve") && (
|
||||||
<TooltipBadge
|
<TooltipBadge
|
||||||
variant="success"
|
variant="success"
|
||||||
label="Updateable"
|
label="Updateable"
|
||||||
content={`To Update ${item.name}, run the command below (or type update) in the LXC Console.`}
|
content={`To Update ${item.name}, run the command below (or type update) in the LXC Console.`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!item.updateable && <TooltipBadge variant="failure" label="Not Updateable" />}
|
{!item.updateable && item.type !== "pve" && <TooltipBadge variant="failure" label="Not Updateable" />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
46
frontend/src/app/scripts/_components/sidebar.tsx
Normal file
46
frontend/src/app/scripts/_components/sidebar.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { Category, Script } from "@/lib/types";
|
||||||
|
|
||||||
|
import ScriptAccordion from "./script-accordion";
|
||||||
|
|
||||||
|
function Sidebar({
|
||||||
|
items,
|
||||||
|
selectedScript,
|
||||||
|
setSelectedScript,
|
||||||
|
}: {
|
||||||
|
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[]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-w-[350px] flex-col sm:max-w-[350px]">
|
||||||
|
<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={items}
|
||||||
|
selectedScript={selectedScript}
|
||||||
|
setSelectedScript={setSelectedScript}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Sidebar;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { AppVersion } from "@/lib/types";
|
import type { AppVersion } from "@/lib/types";
|
||||||
|
|
||||||
interface VersionBadgeProps {
|
type VersionBadgeProps = {
|
||||||
version: AppVersion;
|
version: AppVersion;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function VersionBadge({ version }: VersionBadgeProps) {
|
export function VersionBadge({ version }: VersionBadgeProps) {
|
||||||
return (
|
return (
|
||||||
@@ -1,18 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
import { Suspense, useEffect, useState } from "react";
|
||||||
export const dynamic = "force-static";
|
|
||||||
|
|
||||||
import { ScriptItem } from "@/app/scripts/_components/ScriptItem";
|
|
||||||
import { fetchCategories } from "@/lib/data";
|
|
||||||
import { Category, Script } from "@/lib/types";
|
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useQueryState } from "nuqs";
|
import { useQueryState } from "nuqs";
|
||||||
import { Suspense, useEffect, useState } from "react";
|
|
||||||
|
import type { Category, Script } from "@/lib/types";
|
||||||
|
|
||||||
|
import { ScriptItem } from "@/app/scripts/_components/script-item";
|
||||||
|
import { fetchCategories } from "@/lib/data";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LatestScripts,
|
LatestScripts,
|
||||||
MostViewedScripts,
|
MostViewedScripts,
|
||||||
} from "./_components/ScriptInfoBlocks";
|
} from "./_components/script-info-blocks";
|
||||||
import Sidebar from "./_components/Sidebar";
|
import Sidebar from "./_components/sidebar";
|
||||||
|
|
||||||
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
function ScriptContent() {
|
function ScriptContent() {
|
||||||
const [selectedScript, setSelectedScript] = useQueryState("id");
|
const [selectedScript, setSelectedScript] = useQueryState("id");
|
||||||
@@ -22,9 +24,9 @@ function ScriptContent() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedScript && links.length > 0) {
|
if (selectedScript && links.length > 0) {
|
||||||
const script = links
|
const script = links
|
||||||
.map((category) => category.scripts)
|
.map(category => category.scripts)
|
||||||
.flat()
|
.flat()
|
||||||
.find((script) => script.slug === selectedScript);
|
.find(script => script.slug === selectedScript);
|
||||||
setItem(script);
|
setItem(script);
|
||||||
}
|
}
|
||||||
}, [selectedScript, links]);
|
}, [selectedScript, links]);
|
||||||
@@ -34,7 +36,7 @@ function ScriptContent() {
|
|||||||
.then((categories) => {
|
.then((categories) => {
|
||||||
setLinks(categories);
|
setLinks(categories);
|
||||||
})
|
})
|
||||||
.catch((error) => console.error(error));
|
.catch(error => console.error(error));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -48,9 +50,11 @@ function ScriptContent() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-4 w-full sm:mx-0 sm:ml-4">
|
<div className="mx-4 w-full sm:mx-0 sm:ml-4">
|
||||||
{selectedScript && item ? (
|
{selectedScript && item
|
||||||
|
? (
|
||||||
<ScriptItem item={item} setSelectedScript={setSelectedScript} />
|
<ScriptItem item={item} setSelectedScript={setSelectedScript} />
|
||||||
) : (
|
)
|
||||||
|
: (
|
||||||
<div className="flex w-full flex-col gap-5">
|
<div className="flex w-full flex-col gap-5">
|
||||||
<LatestScripts items={links} />
|
<LatestScripts items={links} />
|
||||||
<MostViewedScripts items={links} />
|
<MostViewedScripts items={links} />
|
||||||
@@ -65,13 +69,13 @@ function ScriptContent() {
|
|||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={(
|
||||||
<div className="flex h-screen w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6">
|
<div className="flex h-screen w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6">
|
||||||
<div className="space-y-2 text-center">
|
<div className="space-y-2 text-center">
|
||||||
<Loader2 className="h-10 w-10 animate-spin" />
|
<Loader2 className="h-10 w-10 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
>
|
>
|
||||||
<ScriptContent />
|
<ScriptContent />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { basePath } from "@/config/siteConfig";
|
|
||||||
import type { MetadataRoute } from "next";
|
import type { MetadataRoute } from "next";
|
||||||
|
|
||||||
|
import { basePath } from "@/config/site-config";
|
||||||
|
|
||||||
export const dynamic = "force-static";
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||||
let domain = "community-scripts.github.io";
|
const domain = "community-scripts.github.io";
|
||||||
let protocol = "https";
|
const protocol = "https";
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
url: `${protocol}://${domain}/${basePath}`,
|
url: `${protocol}://${domain}/${basePath}`,
|
||||||
@@ -18,6 +19,6 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|||||||
{
|
{
|
||||||
url: `${protocol}://${domain}/${basePath}/json-editor`,
|
url: `${protocol}://${domain}/${basePath}/json-editor`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { navbarLinks } from "@/config/siteConfig";
|
|
||||||
|
|
||||||
import CommandMenu from "./CommandMenu";
|
|
||||||
import StarOnGithubButton from "./ui/star-on-github-button";
|
|
||||||
import { ThemeToggle } from "./ui/theme-toggle";
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
function Navbar() {
|
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleScroll = () => {
|
|
||||||
setIsScrolled(window.scrollY > 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("scroll", handleScroll);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("scroll", handleScroll);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={`fixed left-0 top-0 z-50 flex w-screen justify-center px-4 xl:px-0 ${
|
|
||||||
isScrolled ? "glass border-b bg-background/50" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex h-20 w-full max-w-[1440px] items-center justify-between sm:flex-row">
|
|
||||||
<Link
|
|
||||||
href={"/"}
|
|
||||||
className="flex cursor-pointer w-full justify-center sm:justify-start flex-row-reverse items-center gap-2 font-semibold sm:flex-row"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
height={18}
|
|
||||||
unoptimized
|
|
||||||
width={18}
|
|
||||||
alt="logo"
|
|
||||||
src="/ProxmoxVE/logo.png"
|
|
||||||
className=""
|
|
||||||
/>
|
|
||||||
<span className="hidden md:block">Proxmox VE Helper-Scripts</span>
|
|
||||||
</Link>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<CommandMenu />
|
|
||||||
<StarOnGithubButton />
|
|
||||||
{navbarLinks.map(({ href, event, icon, text, mobileHidden }) => (
|
|
||||||
<TooltipProvider key={event}>
|
|
||||||
<Tooltip delayDuration={100}>
|
|
||||||
<TooltipTrigger
|
|
||||||
className={mobileHidden ? "hidden lg:block" : ""}
|
|
||||||
>
|
|
||||||
<Button variant="ghost" size={"icon"} asChild>
|
|
||||||
<Link
|
|
||||||
target="_blank"
|
|
||||||
href={href}
|
|
||||||
data-umami-event={event}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
<span className="sr-only">{text}</span>
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom" className="text-xs">
|
|
||||||
{text}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
))}
|
|
||||||
<ThemeToggle />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Navbar;
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { ArcElement, Chart as ChartJS, Tooltip as ChartTooltip, Legend } from "chart.js";
|
||||||
import {
|
import ChartDataLabels from "chartjs-plugin-datalabels";
|
||||||
Dialog,
|
import { BarChart3, PieChart } from "lucide-react";
|
||||||
DialogContent,
|
import React, { useState } from "react";
|
||||||
DialogHeader,
|
import { Pie } from "react-chartjs-2";
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -21,21 +20,23 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { Chart as ChartJS, ArcElement, Tooltip as ChartTooltip, Legend } from "chart.js";
|
import {
|
||||||
import ChartDataLabels from "chartjs-plugin-datalabels";
|
Dialog,
|
||||||
import { BarChart3, PieChart } from "lucide-react";
|
DialogContent,
|
||||||
import React, { useState } from "react";
|
DialogHeader,
|
||||||
import { Pie, Bar } from "react-chartjs-2";
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
ChartJS.register(ArcElement, ChartTooltip, Legend, ChartDataLabels);
|
ChartJS.register(ArcElement, ChartTooltip, Legend, ChartDataLabels);
|
||||||
|
|
||||||
interface SummaryData {
|
type SummaryData = {
|
||||||
nsapp_count: Record<string, number>;
|
nsapp_count: Record<string, number>;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface ApplicationChartProps {
|
type ApplicationChartProps = {
|
||||||
data: SummaryData | null;
|
data: SummaryData | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 20;
|
const ITEMS_PER_PAGE = 20;
|
||||||
const CHART_COLORS = [
|
const CHART_COLORS = [
|
||||||
@@ -61,14 +62,15 @@ export default function ApplicationChart({ data }: ApplicationChartProps) {
|
|||||||
const [chartStartIndex, setChartStartIndex] = useState(0);
|
const [chartStartIndex, setChartStartIndex] = useState(0);
|
||||||
const [tableLimit, setTableLimit] = useState(ITEMS_PER_PAGE);
|
const [tableLimit, setTableLimit] = useState(ITEMS_PER_PAGE);
|
||||||
|
|
||||||
if (!data) return null;
|
if (!data)
|
||||||
|
return null;
|
||||||
|
|
||||||
const sortedApps = Object.entries(data.nsapp_count)
|
const sortedApps = Object.entries(data.nsapp_count)
|
||||||
.sort(([, a], [, b]) => b - a);
|
.sort(([, a], [, b]) => b - a);
|
||||||
|
|
||||||
const chartApps = sortedApps.slice(
|
const chartApps = sortedApps.slice(
|
||||||
chartStartIndex,
|
chartStartIndex,
|
||||||
chartStartIndex + ITEMS_PER_PAGE
|
chartStartIndex + ITEMS_PER_PAGE,
|
||||||
);
|
);
|
||||||
|
|
||||||
const chartData = {
|
const chartData = {
|
||||||
@@ -141,14 +143,18 @@ export default function ApplicationChart({ data }: ApplicationChartProps) {
|
|||||||
onClick={() => setChartStartIndex(Math.max(0, chartStartIndex - ITEMS_PER_PAGE))}
|
onClick={() => setChartStartIndex(Math.max(0, chartStartIndex - ITEMS_PER_PAGE))}
|
||||||
disabled={chartStartIndex === 0}
|
disabled={chartStartIndex === 0}
|
||||||
>
|
>
|
||||||
Previous {ITEMS_PER_PAGE}
|
Previous
|
||||||
|
{" "}
|
||||||
|
{ITEMS_PER_PAGE}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setChartStartIndex(chartStartIndex + ITEMS_PER_PAGE)}
|
onClick={() => setChartStartIndex(chartStartIndex + ITEMS_PER_PAGE)}
|
||||||
disabled={chartStartIndex + ITEMS_PER_PAGE >= sortedApps.length}
|
disabled={chartStartIndex + ITEMS_PER_PAGE >= sortedApps.length}
|
||||||
>
|
>
|
||||||
Next {ITEMS_PER_PAGE}
|
Next
|
||||||
|
{" "}
|
||||||
|
{ITEMS_PER_PAGE}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Sparkles } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import type { Category, Script } from "@/lib/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@@ -6,22 +13,16 @@ import {
|
|||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
import { basePath } from "@/config/siteConfig";
|
import { basePath } from "@/config/site-config";
|
||||||
import { fetchCategories } from "@/lib/data";
|
import { fetchCategories } from "@/lib/data";
|
||||||
import { Category, Script } from "@/lib/types";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import Image from "next/image";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import React from "react";
|
|
||||||
import { Badge } from "./ui/badge";
|
|
||||||
import { Button } from "./ui/button";
|
|
||||||
import { DialogTitle } from "./ui/dialog";
|
|
||||||
import { Sparkles } from "lucide-react";
|
|
||||||
import { TooltipContent, TooltipProvider } from "./ui/tooltip";
|
|
||||||
import { TooltipTrigger } from "./ui/tooltip";
|
|
||||||
import { Tooltip } from "./ui/tooltip";
|
|
||||||
|
|
||||||
export const formattedBadge = (type: string) => {
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
||||||
|
import { DialogTitle } from "./ui/dialog";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Badge } from "./ui/badge";
|
||||||
|
|
||||||
|
export function formattedBadge(type: string) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "vm":
|
case "vm":
|
||||||
return <Badge className="text-blue-500/75 border-blue-500/75">VM</Badge>;
|
return <Badge className="text-blue-500/75 border-blue-500/75">VM</Badge>;
|
||||||
@@ -33,12 +34,13 @@ export const formattedBadge = (type: string) => {
|
|||||||
return <Badge className="text-green-500/75 border-green-500/75">ADDON</Badge>;
|
return <Badge className="text-green-500/75 border-green-500/75">ADDON</Badge>;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
}
|
||||||
|
|
||||||
// random Script
|
// random Script
|
||||||
function getRandomScript(categories: Category[]): Script | null {
|
function getRandomScript(categories: Category[]): Script | null {
|
||||||
const allScripts = categories.flatMap((cat) => cat.scripts || []);
|
const allScripts = categories.flatMap(cat => cat.scripts || []);
|
||||||
if (allScripts.length === 0) return null;
|
if (allScripts.length === 0)
|
||||||
|
return null;
|
||||||
const idx = Math.floor(Math.random() * allScripts.length);
|
const idx = Math.floor(Math.random() * allScripts.length);
|
||||||
return allScripts[idx];
|
return allScripts[idx];
|
||||||
}
|
}
|
||||||
@@ -49,18 +51,6 @@ export default function CommandMenu() {
|
|||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const down = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
|
||||||
e.preventDefault();
|
|
||||||
fetchSortedCategories();
|
|
||||||
setOpen((open) => !open);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("keydown", down);
|
|
||||||
return () => document.removeEventListener("keydown", down);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchSortedCategories = () => {
|
const fetchSortedCategories = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
fetchCategories()
|
fetchCategories()
|
||||||
@@ -74,6 +64,18 @@ export default function CommandMenu() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const down = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
fetchSortedCategories();
|
||||||
|
setOpen(open => !open);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", down);
|
||||||
|
return () => document.removeEventListener("keydown", down);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const openRandomScript = async () => {
|
const openRandomScript = async () => {
|
||||||
if (links.length === 0) {
|
if (links.length === 0) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -84,10 +86,12 @@ export default function CommandMenu() {
|
|||||||
if (randomScript) {
|
if (randomScript) {
|
||||||
router.push(`/scripts?id=${randomScript.slug}`);
|
router.push(`/scripts?id=${randomScript.slug}`);
|
||||||
}
|
}
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
const randomScript = getRandomScript(links);
|
const randomScript = getRandomScript(links);
|
||||||
if (randomScript) {
|
if (randomScript) {
|
||||||
router.push(`/scripts?id=${randomScript.slug}`);
|
router.push(`/scripts?id=${randomScript.slug}`);
|
||||||
@@ -110,7 +114,8 @@ export default function CommandMenu() {
|
|||||||
>
|
>
|
||||||
<span className="inline-flex">Search scripts...</span>
|
<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">
|
<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
|
<span className="text-xs">⌘</span>
|
||||||
|
K
|
||||||
</kbd>
|
</kbd>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -134,9 +139,9 @@ export default function CommandMenu() {
|
|||||||
<CommandInput placeholder="Search for a script..." />
|
<CommandInput placeholder="Search for a script..." />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>{isLoading ? "Loading..." : "No scripts found."}</CommandEmpty>
|
<CommandEmpty>{isLoading ? "Loading..." : "No scripts found."}</CommandEmpty>
|
||||||
{links.map((category) => (
|
{links.map(category => (
|
||||||
<CommandGroup key={`category:${category.name}`} heading={category.name}>
|
<CommandGroup key={`category:${category.name}`} heading={category.name}>
|
||||||
{category.scripts.map((script) => (
|
{category.scripts.map(script => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={`script:${script.slug}`}
|
key={`script:${script.slug}`}
|
||||||
value={`${script.slug}-${script.name}`}
|
value={`${script.slug}-${script.name}`}
|
||||||
@@ -148,7 +153,7 @@ export default function CommandMenu() {
|
|||||||
<div className="flex gap-2" onClick={() => setOpen(false)}>
|
<div className="flex gap-2" onClick={() => setOpen(false)}>
|
||||||
<Image
|
<Image
|
||||||
src={script.logo || `/${basePath}/logo.png`}
|
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
|
unoptimized
|
||||||
width={16}
|
width={16}
|
||||||
height={16}
|
height={16}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import { FAQ_Items } from "../config/faqConfig";
|
|
||||||
import { Accordion, AccordionContent, AccordionItem } from "./ui/accordion";
|
import { Accordion, AccordionContent, AccordionItem } from "./ui/accordion";
|
||||||
|
import { FAQ_Items } from "../config/faq-config";
|
||||||
|
|
||||||
export default function FAQ() {
|
export default function FAQ() {
|
||||||
return (
|
return (
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
import { basePath } from "@/config/siteConfig";
|
import { FileJson, Server } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { FileJson, Server, ExternalLink } from "lucide-react";
|
|
||||||
import { buttonVariants } from "./ui/button";
|
import { basePath } from "@/config/site-config";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { buttonVariants } from "./ui/button";
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return (
|
return (
|
||||||
<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="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="mx-6 w-full flex justify-between text-xs sm:text-sm text-muted-foreground">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<p>
|
<p>
|
||||||
Website built by the community. The source code is available on{" "}
|
Website built by the community. The source code is available on
|
||||||
|
{" "}
|
||||||
<Link
|
<Link
|
||||||
href={`https://github.com/community-scripts/${basePath}/tree/main/frontend`}
|
href={`https://github.com/community-scripts/${basePath}/tree/main/frontend`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -28,13 +31,17 @@ export default function Footer() {
|
|||||||
href="/json-editor"
|
href="/json-editor"
|
||||||
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2")}
|
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2")}
|
||||||
>
|
>
|
||||||
<FileJson className="h-4 w-4" /> JSON Editor
|
<FileJson className="h-4 w-4" />
|
||||||
|
{" "}
|
||||||
|
JSON Editor
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/data"
|
href="/data"
|
||||||
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2")}
|
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2")}
|
||||||
>
|
>
|
||||||
<Server className="h-4 w-4" /> API Data
|
<Server className="h-4 w-4" />
|
||||||
|
{" "}
|
||||||
|
API Data
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2,14 +2,15 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
interface ModalProps {
|
type ModalProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
};
|
||||||
|
|
||||||
const Modal: React.FC<ModalProps> = ({ isOpen, onClose, children }) => {
|
const Modal: React.FC<ModalProps> = ({ isOpen, onClose, children }) => {
|
||||||
if (!isOpen) return null;
|
if (!isOpen)
|
||||||
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50">
|
||||||
86
frontend/src/components/navbar.tsx
Normal file
86
frontend/src/components/navbar.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { navbarLinks } from "@/config/site-config";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
||||||
|
import StarOnGithubButton from "./ui/star-on-github-button";
|
||||||
|
import { ThemeToggle } from "./ui/theme-toggle";
|
||||||
|
import CommandMenu from "./command-menu";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
function Navbar() {
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
setIsScrolled(window.scrollY > 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("scroll", handleScroll);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", handleScroll);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`fixed left-0 top-0 z-50 flex w-screen justify-center px-4 xl:px-0 ${
|
||||||
|
isScrolled ? "glass border-b bg-background/50" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex h-20 w-full max-w-[1440px] items-center justify-between sm:flex-row">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="flex cursor-pointer w-full justify-center sm:justify-start flex-row-reverse items-center gap-2 font-semibold sm:flex-row"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
height={18}
|
||||||
|
unoptimized
|
||||||
|
width={18}
|
||||||
|
alt="logo"
|
||||||
|
src="/ProxmoxVE/logo.png"
|
||||||
|
className=""
|
||||||
|
/>
|
||||||
|
<span className="hidden md:block">Proxmox VE Helper-Scripts</span>
|
||||||
|
</Link>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<CommandMenu />
|
||||||
|
<StarOnGithubButton />
|
||||||
|
{navbarLinks.map(({ href, event, icon, text, mobileHidden }) => (
|
||||||
|
<TooltipProvider key={event}>
|
||||||
|
<Tooltip delayDuration={100}>
|
||||||
|
<TooltipTrigger
|
||||||
|
className={mobileHidden ? "hidden lg:block" : ""}
|
||||||
|
>
|
||||||
|
<Button variant="ghost" size="icon" asChild>
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
href={href}
|
||||||
|
data-umami-event={event}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
<span className="sr-only">{text}</span>
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom" className="text-xs">
|
||||||
|
{text}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
))}
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Navbar;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ClipboardIcon } from "lucide-react";
|
import { ClipboardIcon } from "lucide-react";
|
||||||
import handleCopy from "./handleCopy";
|
|
||||||
|
import handleCopy from "./handle-copy";
|
||||||
|
|
||||||
export default function TextCopyBlock(description: string) {
|
export default function TextCopyBlock(description: string) {
|
||||||
const pattern = /`([^`]*)`/g;
|
const pattern = /`([^`]*)`/g;
|
||||||
@@ -19,7 +20,8 @@ export default function TextCopyBlock(description: string) {
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
return part;
|
return part;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import type { ThemeProviderProps } from "next-themes";
|
||||||
|
|
||||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||||
import { type ThemeProviderProps } from "next-themes/dist/types";
|
|
||||||
|
|
||||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import * as React from "react"
|
import type { VariantProps } from "class-variance-authority";
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cva } from "class-variance-authority";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const alertVariants = cva(
|
const alertVariants = cva(
|
||||||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||||
@@ -16,8 +18,8 @@ const alertVariants = cva(
|
|||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
|
|
||||||
const Alert = React.forwardRef<
|
const Alert = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
@@ -29,8 +31,8 @@ const Alert = React.forwardRef<
|
|||||||
className={cn(alertVariants({ variant }), className)}
|
className={cn(alertVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
Alert.displayName = "Alert"
|
Alert.displayName = "Alert";
|
||||||
|
|
||||||
const AlertTitle = React.forwardRef<
|
const AlertTitle = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
@@ -41,8 +43,8 @@ const AlertTitle = React.forwardRef<
|
|||||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
AlertTitle.displayName = "AlertTitle"
|
AlertTitle.displayName = "AlertTitle";
|
||||||
|
|
||||||
const AlertDescription = React.forwardRef<
|
const AlertDescription = React.forwardRef<
|
||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
@@ -53,7 +55,7 @@ const AlertDescription = React.forwardRef<
|
|||||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
AlertDescription.displayName = "AlertDescription"
|
AlertDescription.displayName = "AlertDescription";
|
||||||
|
|
||||||
export { Alert, AlertTitle, AlertDescription }
|
export { Alert, AlertDescription, AlertTitle };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export default function AnimatedGradientText({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-0 block h-full w-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] p-[1px] [border-radius:inherit] ![mask-composite:subtract] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]`}
|
className="absolute inset-0 block h-full w-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] p-[1px] [border-radius:inherit] ![mask-composite:subtract] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import type { VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cva } from "class-variance-authority";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -26,9 +28,7 @@ const badgeVariants = cva(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface BadgeProps
|
export type BadgeProps = {} & React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof badgeVariants>;
|
||||||
extends React.HTMLAttributes<HTMLDivElement>,
|
|
||||||
VariantProps<typeof badgeVariants> {}
|
|
||||||
|
|
||||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { cn } from "@/lib/utils";
|
import type { VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { Slot, Slottable } from "@radix-ui/react-slot";
|
import { Slot, Slottable } from "@radix-ui/react-slot";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva } from "class-variance-authority";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
{
|
{
|
||||||
@@ -47,21 +50,19 @@ const buttonVariants = cva(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
interface IconProps {
|
type IconProps = {
|
||||||
Icon: React.ElementType;
|
Icon: React.ElementType;
|
||||||
iconPlacement: "left" | "right";
|
iconPlacement: "left" | "right";
|
||||||
}
|
};
|
||||||
|
|
||||||
interface IconRefProps {
|
type IconRefProps = {
|
||||||
Icon?: never;
|
Icon?: never;
|
||||||
iconPlacement?: undefined;
|
iconPlacement?: undefined;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface ButtonProps
|
export type ButtonProps = {
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
||||||
VariantProps<typeof buttonVariants> {
|
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
}
|
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>;
|
||||||
|
|
||||||
export type ButtonIconProps = IconProps | IconRefProps;
|
export type ButtonIconProps = IconProps | IconRefProps;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react"
|
import { DayPicker } from "react-day-picker";
|
||||||
import { DayPicker } from "react-day-picker"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
import { buttonVariants } from "@/components/ui/button"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
||||||
|
|
||||||
function Calendar({
|
function Calendar({
|
||||||
className,
|
className,
|
||||||
@@ -27,7 +27,7 @@ function Calendar({
|
|||||||
nav: "space-x-1 flex items-center",
|
nav: "space-x-1 flex items-center",
|
||||||
nav_button: cn(
|
nav_button: cn(
|
||||||
buttonVariants({ variant: "outline" }),
|
buttonVariants({ variant: "outline" }),
|
||||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||||
),
|
),
|
||||||
nav_button_previous: "absolute left-1",
|
nav_button_previous: "absolute left-1",
|
||||||
nav_button_next: "absolute right-1",
|
nav_button_next: "absolute right-1",
|
||||||
@@ -39,7 +39,7 @@ function Calendar({
|
|||||||
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
||||||
day: cn(
|
day: cn(
|
||||||
buttonVariants({ variant: "ghost" }),
|
buttonVariants({ variant: "ghost" }),
|
||||||
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
|
"h-9 w-9 p-0 font-normal aria-selected:opacity-100",
|
||||||
),
|
),
|
||||||
day_range_end: "day-range-end",
|
day_range_end: "day-range-end",
|
||||||
day_selected:
|
day_selected:
|
||||||
@@ -54,13 +54,17 @@ function Calendar({
|
|||||||
...classNames,
|
...classNames,
|
||||||
}}
|
}}
|
||||||
components={{
|
components={{
|
||||||
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
|
Chevron: ({ ...props }) => {
|
||||||
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
|
if (props.orientation === "left") {
|
||||||
|
return <ChevronLeft className="h-4 w-4" />;
|
||||||
|
}
|
||||||
|
return <ChevronRight className="h-4 w-4" />;
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
Calendar.displayName = "Calendar"
|
Calendar.displayName = "Calendar";
|
||||||
|
|
||||||
export { Calendar }
|
export { Calendar };
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { CheckIcon, ClipboardIcon } from "lucide-react";
|
import { CheckIcon, ClipboardIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { Card } from "./card";
|
import { Card } from "./card";
|
||||||
|
|
||||||
export default function CodeCopyButton({
|
export default function CodeCopyButton({
|
||||||
@@ -26,7 +28,7 @@ export default function CodeCopyButton({
|
|||||||
|
|
||||||
setHasCopied(true);
|
setHasCopied(true);
|
||||||
|
|
||||||
let warning = localStorage.getItem("warning");
|
const warning = localStorage.getItem("warning");
|
||||||
|
|
||||||
if (warning === null) {
|
if (warning === null) {
|
||||||
localStorage.setItem("warning", "1");
|
localStorage.setItem("warning", "1");
|
||||||
@@ -50,9 +52,11 @@ export default function CodeCopyButton({
|
|||||||
className={cn("bg-muted px-3 py-4")}
|
className={cn("bg-muted px-3 py-4")}
|
||||||
title="Copy"
|
title="Copy"
|
||||||
>
|
>
|
||||||
{hasCopied ? (
|
{hasCopied
|
||||||
|
? (
|
||||||
<CheckIcon className="h-4 w-4" />
|
<CheckIcon className="h-4 w-4" />
|
||||||
) : (
|
)
|
||||||
|
: (
|
||||||
<ClipboardIcon className="h-4 w-4" />
|
<ClipboardIcon className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { basePath } from "@/config/siteConfig";
|
import type { VariantProps } from "class-variance-authority";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva } from "class-variance-authority";
|
||||||
import { Clipboard, Copy } from "lucide-react";
|
import { Clipboard, Copy } from "lucide-react";
|
||||||
import Link from "next/link";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "./button";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { basePath } from "@/config/site-config";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { Separator } from "./separator";
|
import { Separator } from "./separator";
|
||||||
|
import { Button } from "./button";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
@@ -40,23 +44,24 @@ const buttonVariants = cva(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCopy = (type: string, value: string) => {
|
function handleCopy(type: string, value: string) {
|
||||||
navigator.clipboard.writeText(value);
|
navigator.clipboard.writeText(value);
|
||||||
|
|
||||||
let amountOfScriptsCopied = localStorage.getItem("amountOfScriptsCopied");
|
let amountOfScriptsCopied = localStorage.getItem("amountOfScriptsCopied");
|
||||||
|
|
||||||
if (amountOfScriptsCopied === null) {
|
if (amountOfScriptsCopied === null) {
|
||||||
localStorage.setItem("amountOfScriptsCopied", "1");
|
localStorage.setItem("amountOfScriptsCopied", "1");
|
||||||
} else {
|
}
|
||||||
amountOfScriptsCopied = (parseInt(amountOfScriptsCopied) + 1).toString();
|
else {
|
||||||
|
amountOfScriptsCopied = (Number.parseInt(amountOfScriptsCopied) + 1).toString();
|
||||||
localStorage.setItem("amountOfScriptsCopied", amountOfScriptsCopied);
|
localStorage.setItem("amountOfScriptsCopied", amountOfScriptsCopied);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
parseInt(amountOfScriptsCopied) === 3 ||
|
Number.parseInt(amountOfScriptsCopied) === 3
|
||||||
parseInt(amountOfScriptsCopied) === 10 ||
|
|| Number.parseInt(amountOfScriptsCopied) === 10
|
||||||
parseInt(amountOfScriptsCopied) === 25 ||
|
|| Number.parseInt(amountOfScriptsCopied) === 25
|
||||||
parseInt(amountOfScriptsCopied) === 50 ||
|
|| Number.parseInt(amountOfScriptsCopied) === 50
|
||||||
parseInt(amountOfScriptsCopied) === 100
|
|| Number.parseInt(amountOfScriptsCopied) === 100
|
||||||
) {
|
) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
toast.info(
|
toast.info(
|
||||||
@@ -86,17 +91,20 @@ const handleCopy = (type: string, value: string) => {
|
|||||||
toast.success(
|
toast.success(
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Clipboard className="h-4 w-4" />
|
<Clipboard className="h-4 w-4" />
|
||||||
<span>Copied {type} to clipboard</span>
|
<span>
|
||||||
|
Copied
|
||||||
|
{type}
|
||||||
|
{" "}
|
||||||
|
to clipboard
|
||||||
|
</span>
|
||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export interface CodeBlockProps
|
export type CodeBlockProps = {
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
||||||
VariantProps<typeof buttonVariants> {
|
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
code: string;
|
code: string;
|
||||||
}
|
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>;
|
||||||
|
|
||||||
const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
|
const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||||
({ className, variant, size, asChild = false, code }, ref) => {
|
({ className, variant, size, asChild = false, code }, ref) => {
|
||||||
@@ -121,7 +129,10 @@ const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p className="flex items-center gap-2">
|
<p className="flex items-center gap-2">
|
||||||
{code} <Separator orientation="vertical" />{" "}
|
{code}
|
||||||
|
{" "}
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
{" "}
|
||||||
<Copy
|
<Copy
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
size={16}
|
size={16}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { type DialogProps } from "@radix-ui/react-dialog";
|
import type { DialogProps } from "@radix-ui/react-dialog";
|
||||||
|
|
||||||
import { Command as CommandPrimitive } from "cmdk";
|
import { Command as CommandPrimitive } from "cmdk";
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
@@ -23,9 +24,9 @@ const Command = React.forwardRef<
|
|||||||
));
|
));
|
||||||
Command.displayName = CommandPrimitive.displayName;
|
Command.displayName = CommandPrimitive.displayName;
|
||||||
|
|
||||||
interface CommandDialogProps extends DialogProps {}
|
type CommandDialogProps = {} & DialogProps;
|
||||||
|
|
||||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
function CommandDialog({ children, ...props }: CommandDialogProps) {
|
||||||
return (
|
return (
|
||||||
<Dialog {...props}>
|
<Dialog {...props}>
|
||||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||||
@@ -35,7 +36,7 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const CommandInput = React.forwardRef<
|
const CommandInput = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||||
@@ -126,10 +127,10 @@ const CommandItem = React.forwardRef<
|
|||||||
|
|
||||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||||
|
|
||||||
const CommandShortcut = ({
|
function CommandShortcut({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
}: React.HTMLAttributes<HTMLSpanElement>) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -139,7 +140,7 @@ const CommandShortcut = ({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
CommandShortcut.displayName = "CommandShortcut";
|
CommandShortcut.displayName = "CommandShortcut";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { CheckIcon, ClipboardIcon } from "lucide-react";
|
import { CheckIcon, ClipboardIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { Card } from "./card";
|
import { Card } from "./card";
|
||||||
|
|
||||||
export default function CodeCopyButton({
|
export default function CodeCopyButton({
|
||||||
@@ -26,7 +27,6 @@ export default function CodeCopyButton({
|
|||||||
|
|
||||||
setHasCopied(true);
|
setHasCopied(true);
|
||||||
|
|
||||||
|
|
||||||
// toast.success(`copied ${type} to clipboard`, {
|
// toast.success(`copied ${type} to clipboard`, {
|
||||||
// icon: <ClipboardCheck className="h-4 w-4" />,
|
// icon: <ClipboardCheck className="h-4 w-4" />,
|
||||||
// });
|
// });
|
||||||
@@ -42,9 +42,11 @@ export default function CodeCopyButton({
|
|||||||
className={cn(" right-0 cursor-pointer bg-muted px-3 py-4")}
|
className={cn(" right-0 cursor-pointer bg-muted px-3 py-4")}
|
||||||
onClick={() => handleCopy("install command", children)}
|
onClick={() => handleCopy("install command", children)}
|
||||||
>
|
>
|
||||||
{hasCopied ? (
|
{hasCopied
|
||||||
|
? (
|
||||||
<CheckIcon className="h-4 w-4" />
|
<CheckIcon className="h-4 w-4" />
|
||||||
) : (
|
)
|
||||||
|
: (
|
||||||
<ClipboardIcon className="h-4 w-4" />
|
<ClipboardIcon className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
<span className="sr-only">Copy</span>
|
<span className="sr-only">Copy</span>
|
||||||
|
|||||||
@@ -53,10 +53,11 @@ const DialogContent = React.forwardRef<
|
|||||||
));
|
));
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
const DialogHeader = ({
|
function DialogHeader({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
@@ -65,12 +66,14 @@ const DialogHeader = ({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
DialogHeader.displayName = "DialogHeader";
|
DialogHeader.displayName = "DialogHeader";
|
||||||
|
|
||||||
const DialogFooter = ({
|
function DialogFooter({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
@@ -79,6 +82,7 @@ const DialogFooter = ({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
DialogFooter.displayName = "DialogFooter";
|
DialogFooter.displayName = "DialogFooter";
|
||||||
|
|
||||||
const DialogTitle = React.forwardRef<
|
const DialogTitle = React.forwardRef<
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user