mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2025-07-01 11:37:38 +00:00
Compare commits
64 Commits
michelroeg
...
tremor021-
Author | SHA1 | Date | |
---|---|---|---|
31bc105591 | |||
bda9f482c1 | |||
7292dbb8c6 | |||
2bdf85db39 | |||
546bff56fa | |||
1896f2db0f | |||
e47828f0a4 | |||
c511f7d9e5 | |||
a106e7e358 | |||
254f2b894d | |||
733251a0a2 | |||
bcfa05db47 | |||
5313f00edb | |||
f83bfd1598 | |||
fd27524479 | |||
8155fea034 | |||
c853054067 | |||
a80ec39740 | |||
bb33d00829 | |||
be64a6700d | |||
247bc549e8 | |||
b26c5c9354 | |||
5e5c79ef29 | |||
4db81b8c41 | |||
0b97f26b13 | |||
f2a21617f7 | |||
ed618b7144 | |||
1ec71332bf | |||
5696dffd02 | |||
1e93f131d2 | |||
022f88c30a | |||
b661f3cbcc | |||
9b97e4974a | |||
e2b36b540f | |||
983a09c5db | |||
f605085021 | |||
4a3b15ae0e | |||
0fd5f366b3 | |||
dd5b3cd1b9 | |||
2b55f82aab | |||
87c6f87faf | |||
caad96f25a | |||
8e7978713f | |||
1e05867b4c | |||
43dfe6dc33 | |||
179812e55f | |||
d09cf45a3c | |||
e609868619 | |||
692ac62add | |||
216cc7e5c3 | |||
bcc113406a | |||
0067075ed1 | |||
d60911a063 | |||
abad754f61 | |||
a632d315ab | |||
520bae01d6 | |||
7057fba151 | |||
e24ca6472c | |||
028feb363f | |||
491b341fdf | |||
db77e42a50 | |||
cf3f790f03 | |||
a0da56997c | |||
c000235d81 |
3
.github/workflows/frontend-cicd.yml
generated
vendored
3
.github/workflows/frontend-cicd.yml
generated
vendored
@ -44,9 +44,6 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci --prefer-offline --legacy-peer-deps
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: Configure Next.js for pages
|
||||
uses: actions/configure-pages@v5
|
||||
with:
|
||||
|
8
.github/workflows/push-to-gitea.yaml
generated
vendored
8
.github/workflows/push-to-gitea.yaml
generated
vendored
@ -26,19 +26,23 @@ jobs:
|
||||
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"
|
||||
sed -i 's|https://raw\.githubusercontent\.com/community-scripts/ProxmoxVE/|https://git.community-scripts.org/community-scripts/ProxmoxVE/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 push gitea --all
|
||||
git add .
|
||||
git commit -m "Sync to Gitea"
|
||||
git push gitea --all --force
|
||||
env:
|
||||
GITEA_USER: ${{ secrets.GITEA_USERNAME }}
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
|
82
CHANGELOG.md
82
CHANGELOG.md
@ -14,14 +14,94 @@ 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.
|
||||
|
||||
|
||||
## 2025-06-27
|
||||
## 2025-06-30
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Alpine Syncthing [@MickLesk](https://github.com/MickLesk) ([#5586](https://github.com/community-scripts/ProxmoxVE/pull/5586))
|
||||
- Kapowarr ([#5584](https://github.com/community-scripts/ProxmoxVE/pull/5584))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Immich: make changes to automatically enable QuickSync [@vhsdream](https://github.com/vhsdream) ([#5560](https://github.com/community-scripts/ProxmoxVE/pull/5560))
|
||||
- Apache Guacamole: Install auth-jdbc component that matches release version [@tremor021](https://github.com/tremor021) ([#5563](https://github.com/community-scripts/ProxmoxVE/pull/5563))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- tools.func: optimize binary installs with fetch_and_deploy helper [@MickLesk](https://github.com/MickLesk) ([#5588](https://github.com/community-scripts/ProxmoxVE/pull/5588))
|
||||
- [core]: add ipv6 configuration support [@MickLesk](https://github.com/MickLesk) ([#5575](https://github.com/community-scripts/ProxmoxVE/pull/5575))
|
||||
|
||||
## 2025-06-29
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Linkwarden: Add backing up of data folder to the update function [@tremor021](https://github.com/tremor021) ([#5548](https://github.com/community-scripts/ProxmoxVE/pull/5548))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- Add cron-job api-key env variable to homarr script [@Meierschlumpf](https://github.com/Meierschlumpf) ([#5204](https://github.com/community-scripts/ProxmoxVE/pull/5204))
|
||||
|
||||
### 🧰 Maintenance
|
||||
|
||||
- #### 📝 Documentation
|
||||
|
||||
- update readme with valid discord link. other one expired [@BramSuurdje](https://github.com/BramSuurdje) ([#5567](https://github.com/community-scripts/ProxmoxVE/pull/5567))
|
||||
|
||||
### 🌐 Website
|
||||
|
||||
- Update script-item.tsx [@ape364](https://github.com/ape364) ([#5549](https://github.com/community-scripts/ProxmoxVE/pull/5549))
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix bug in tooltip that would always render 'updateable' [@BramSuurdje](https://github.com/BramSuurdje) ([#5552](https://github.com/community-scripts/ProxmoxVE/pull/5552))
|
||||
|
||||
## 2025-06-28
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Ollama: Clean up old Ollama files before running update [@tremor021](https://github.com/tremor021) ([#5534](https://github.com/community-scripts/ProxmoxVE/pull/5534))
|
||||
- ONLYOFFICE: Update install script to manually create RabbitMQ user [@tremor021](https://github.com/tremor021) ([#5535](https://github.com/community-scripts/ProxmoxVE/pull/5535))
|
||||
|
||||
### 🌐 Website
|
||||
|
||||
- #### 📝 Script Information
|
||||
|
||||
- Booklore: Correct documentation and website [@pieman3000](https://github.com/pieman3000) ([#5528](https://github.com/community-scripts/ProxmoxVE/pull/5528))
|
||||
|
||||
## 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
|
||||
|
@ -13,7 +13,7 @@
|
||||
<a href="https://helper-scripts.com">
|
||||
<img src="https://img.shields.io/badge/Website-4c9b3f?style=for-the-badge&logo=github&logoColor=white" alt="Website" />
|
||||
</a>
|
||||
<a href="https://discord.gg/jsYVk5JBxq">
|
||||
<a href="https://discord.gg/3AnUqsXnmK">
|
||||
<img src="https://img.shields.io/badge/Discord-7289da?style=for-the-badge&logo=discord&logoColor=white" alt="Discord" />
|
||||
</a>
|
||||
<a href="https://ko-fi.com/community_scripts">
|
||||
@ -82,7 +82,7 @@ We appreciate any contributions to the project—whether it's bug reports, featu
|
||||
|
||||
Join our community for support:
|
||||
|
||||
- **Discord**: Join our [Proxmox Helper Scripts Discord server](https://discord.gg/jsYVk5JBxq) for real-time support.
|
||||
- **Discord**: Join our [Proxmox Helper Scripts Discord server](https://discord.gg/3AnUqsXnmK) for real-time support.
|
||||
- **GitHub Discussions**: [Ask questions or report issues](https://github.com/community-scripts/ProxmoxVE/discussions).
|
||||
|
||||
## 🤝 Report a Bug or Feature Request
|
||||
|
45
ct/alpine-syncthing.sh
Normal file
45
ct/alpine-syncthing.sh
Normal file
@ -0,0 +1,45 @@
|
||||
#!/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://syncthing.net/
|
||||
|
||||
APP="Alpine-Syncthing"
|
||||
var_tags="${var_tags:-alpine;networking}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-256}"
|
||||
var_disk="${var_disk:-1}"
|
||||
var_os="${var_os:-alpine}"
|
||||
var_version="${var_version:-3.22}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
msg_info "Updating Alpine Packages"
|
||||
$STD apk -U upgrade
|
||||
msg_ok "Updated Alpine Packages"
|
||||
|
||||
msg_info "Updating Syncthing"
|
||||
$STD apk upgrade syncthing
|
||||
msg_ok "Updated Syncthing"
|
||||
|
||||
msg_info "Restarting Syncthing"
|
||||
$STD rc-service syncthing restart
|
||||
msg_ok "Restarted Syncthing"
|
||||
|
||||
exit 1
|
||||
}
|
||||
|
||||
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}:8384${CL}"
|
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}"
|
6
ct/headers/alpine-syncthing
Normal file
6
ct/headers/alpine-syncthing
Normal file
@ -0,0 +1,6 @@
|
||||
___ __ _ _____ __ __ _
|
||||
/ | / /___ (_)___ ___ / ___/__ ______ _____/ /_/ /_ (_)___ ____ _
|
||||
/ /| | / / __ \/ / __ \/ _ \______\__ \/ / / / __ \/ ___/ __/ __ \/ / __ \/ __ `/
|
||||
/ ___ |/ / /_/ / / / / / __/_____/__/ / /_/ / / / / /__/ /_/ / / / / / / / /_/ /
|
||||
/_/ |_/_/ .___/_/_/ /_/\___/ /____/\__, /_/ /_/\___/\__/_/ /_/_/_/ /_/\__, /
|
||||
/_/ /____/ /____/
|
6
ct/headers/booklore
Normal file
6
ct/headers/booklore
Normal file
@ -0,0 +1,6 @@
|
||||
____ __ __
|
||||
/ __ )____ ____ / /__/ / ____ ________
|
||||
/ __ / __ \/ __ \/ //_/ / / __ \/ ___/ _ \
|
||||
/ /_/ / /_/ / /_/ / ,< / /___/ /_/ / / / __/
|
||||
/_____/\____/\____/_/|_/_____/\____/_/ \___/
|
||||
|
6
ct/headers/kapowarr
Normal file
6
ct/headers/kapowarr
Normal file
@ -0,0 +1,6 @@
|
||||
__ __
|
||||
/ //_/___ _____ ____ _ ______ ___________
|
||||
/ ,< / __ `/ __ \/ __ \ | /| / / __ `/ ___/ ___/
|
||||
/ /| / /_/ / /_/ / /_/ / |/ |/ / /_/ / / / /
|
||||
/_/ |_\__,_/ .___/\____/|__/|__/\__,_/_/ /_/
|
||||
/_/
|
@ -48,6 +48,7 @@ source /opt/homarr/.env
|
||||
set +a
|
||||
export DB_DIALECT='sqlite'
|
||||
export AUTH_SECRET=$(openssl rand -base64 32)
|
||||
export CRON_JOB_API_KEY=$(openssl rand -base64 32)
|
||||
node /opt/homarr_db/migrations/$DB_DIALECT/migrate.cjs /opt/homarr_db/migrations/$DB_DIALECT
|
||||
for dir in $(find /opt/homarr_db/migrations/migrations -mindepth 1 -maxdepth 1 -type d); do
|
||||
dirname=$(basename "$dir")
|
||||
@ -114,6 +115,7 @@ source /opt/homarr/.env
|
||||
set +a
|
||||
export DB_DIALECT='sqlite'
|
||||
export AUTH_SECRET=$(openssl rand -base64 32)
|
||||
export CRON_JOB_API_KEY=$(openssl rand -base64 32)
|
||||
node /opt/homarr_db/migrations/$DB_DIALECT/migrate.cjs /opt/homarr_db/migrations/$DB_DIALECT
|
||||
for dir in $(find /opt/homarr_db/migrations/migrations -mindepth 1 -maxdepth 1 -type d); do
|
||||
dirname=$(basename "$dir")
|
||||
|
66
ct/kapowarr.sh
Normal file
66
ct/kapowarr.sh
Normal file
@ -0,0 +1,66 @@
|
||||
#!/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: Slaviša Arežina (tremor021)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/Casvt/Kapowarr
|
||||
|
||||
APP="Kapowarr"
|
||||
var_tags="${var_tags:-Arr}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-256}"
|
||||
var_disk="${var_disk:-2}"
|
||||
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 [[ ! -f /etc/systemd/system/kapowarr.service ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
RELEASE=$(curl -s https://api.github.com/repos/Casvt/Kapowarr/releases/latest | grep "tag_name" | awk '{print substr($2, 2, length($2)-3) }')
|
||||
if [[ "${RELEASE}" != "$(cat $HOME/.kapowarr)" ]] || [[ ! -f $HOME/.kapowarr ]]; then
|
||||
setup_uv
|
||||
|
||||
msg_info "Stopping $APP"
|
||||
systemctl stop kapowarr
|
||||
msg_ok "Stopped $APP"
|
||||
|
||||
msg_info "Creating Backup"
|
||||
mv /opt/kapowarr/db /opt/
|
||||
msg_ok "Backup Created"
|
||||
|
||||
fetch_and_deploy_gh_release "kapowarr" "Casvt/Kapowarr"
|
||||
msg_info "Updating $APP to ${RELEASE}"
|
||||
mv /opt/db /opt/kapowarr
|
||||
msg_ok "Updated $APP to ${RELEASE}"
|
||||
|
||||
msg_info "Starting $APP"
|
||||
systemctl start kapowarr
|
||||
msg_ok "Started $APP"
|
||||
|
||||
msg_ok "Update Successful"
|
||||
else
|
||||
msg_ok "No update required. ${APP} is already at ${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}:5656${CL}"
|
@ -37,6 +37,7 @@ function update_script() {
|
||||
|
||||
msg_info "Updating ${APP} to ${RELEASE}"
|
||||
mv /opt/linkwarden/.env /opt/.env
|
||||
[ -d /opt/linkwarden/data ] && mv /opt/linkwarden/data /opt/data.bak
|
||||
rm -rf /opt/linkwarden
|
||||
fetch_and_deploy_gh_release "linkwarden" "linkwarden/linkwarden"
|
||||
cd /opt/linkwarden
|
||||
@ -47,6 +48,7 @@ function update_script() {
|
||||
$STD yarn prisma:generate
|
||||
$STD yarn web:build
|
||||
$STD yarn prisma:deploy
|
||||
[ -d /opt/data.bak ] && mv /opt/data.bak /opt/linkwarden/data
|
||||
msg_ok "Updated ${APP} to ${RELEASE}"
|
||||
|
||||
msg_info "Starting ${APP}"
|
||||
|
@ -38,6 +38,8 @@ function update_script() {
|
||||
TMP_TAR=$(mktemp --suffix=.tgz)
|
||||
curl -fL# -o "${TMP_TAR}" "https://github.com/ollama/ollama/releases/download/${RELEASE}/ollama-linux-amd64.tgz"
|
||||
msg_info "Updating Ollama to ${RELEASE}"
|
||||
rm -rf /usr/local/lib/ollama
|
||||
rm -rf /usr/local/bin/ollama
|
||||
tar -xzf "${TMP_TAR}" -C /usr/local/lib/ollama
|
||||
ln -sf /usr/local/lib/ollama/bin/ollama /usr/local/bin/ollama
|
||||
echo "${RELEASE}" >/opt/Ollama_version.txt
|
||||
|
@ -33,6 +33,8 @@ function update_script() {
|
||||
OLLAMA_VERSION=$(ollama -v | awk '{print $NF}')
|
||||
RELEASE=$(curl -s https://api.github.com/repos/ollama/ollama/releases/latest | grep "tag_name" | awk '{print substr($2, 3, length($2)-4)}')
|
||||
if [ "$OLLAMA_VERSION" != "$RELEASE" ]; then
|
||||
rm -rf /usr/lib/ollama
|
||||
rm -rf /usr/bin/ollama
|
||||
curl -fsSLO https://ollama.com/download/ollama-linux-amd64.tgz
|
||||
tar -C /usr -xzf ollama-linux-amd64.tgz
|
||||
rm -rf ollama-linux-amd64.tgz
|
||||
|
@ -60,7 +60,6 @@ function update_script() {
|
||||
|
||||
msg_info "Cleaning Up"
|
||||
rm -rf "$BACKUP_FILE"
|
||||
rm /tmp/"$RELEASE".zip
|
||||
msg_ok "Cleanup Completed"
|
||||
msg_ok "Update Successful"
|
||||
else
|
||||
|
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"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
8376
frontend/package-lock.json
generated
8376
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",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "Bram Suurd",
|
||||
"url": "https://github.com/community-scripts"
|
||||
},
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "vitest",
|
||||
"deploy": "next build && touch out/.nojekyll && git add out/ && git commit -m \"Deploy\" && git subtree push --prefix out origin gh-pages",
|
||||
"format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache",
|
||||
"format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache",
|
||||
"lint": "eslint . --fix",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -45,7 +41,7 @@
|
||||
"lucide-react": "^0.453.0",
|
||||
"mini-svg-data-uri": "^1.4.4",
|
||||
"next": "15.2.4",
|
||||
"next-themes": "^0.3.0",
|
||||
"next-themes": "^0.4.4",
|
||||
"nuqs": "^2.4.1",
|
||||
"pocketbase": "^0.21.5",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
@ -53,7 +49,7 @@
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-code-blocks": "^0.1.6",
|
||||
"react-datepicker": "^7.6.0",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-day-picker": "^9.4.3",
|
||||
"react-dom": "19.0.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-simple-typewriter": "^5.0.1",
|
||||
@ -64,9 +60,10 @@
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"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",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@types/node": "^22.13.16",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
@ -75,6 +72,9 @@
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.23.0",
|
||||
"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",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.3",
|
||||
@ -83,11 +83,13 @@
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tailwindcss-animated": "^1.1.2",
|
||||
"typescript": "^5.8.2",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.1.1"
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"overrides": {
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
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/adityachandelgit/BookLore",
|
||||
"website": "https://github.com/adityachandelgit/BookLore",
|
||||
"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": []
|
||||
}
|
35
frontend/public/json/kapowarr.json
generated
Normal file
35
frontend/public/json/kapowarr.json
generated
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "Kapowarr",
|
||||
"slug": "kapowarr",
|
||||
"categories": [
|
||||
14
|
||||
],
|
||||
"date_created": "2025-06-30",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 5656,
|
||||
"documentation": "https://casvt.github.io/Kapowarr/general_info/workings/",
|
||||
"website": "https://casvt.github.io/Kapowarr/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/kapowarr.webp",
|
||||
"config_path": "",
|
||||
"description": "Kapowarr allows you to build a digital library of comics. You can add volumes, map them to a folder and start managing! Download, rename, move and convert issues of the volume (including TPB's, One Shots, Hard Covers, and more). The whole process is automated and can be customised in the settings.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/kapowarr.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 256,
|
||||
"hdd": 2,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": []
|
||||
}
|
17
frontend/public/json/syncthing.json
generated
17
frontend/public/json/syncthing.json
generated
@ -12,7 +12,7 @@
|
||||
"documentation": null,
|
||||
"website": "https://syncthing.net/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/syncthing.webp",
|
||||
"config_path": "/root/.local/state/syncthing/config.xml",
|
||||
"config_path": "/root/.local/state/syncthing/config.xml - Alpine: /var/lib/syncthing/.local/state/syncthing/config.xml",
|
||||
"description": "Syncthing is an open-source file syncing tool that allows users to keep their files in sync across multiple devices by using peer-to-peer synchronization. It doesn't rely on any central server, so all data transfers are directly between devices.",
|
||||
"install_methods": [
|
||||
{
|
||||
@ -25,8 +25,19 @@
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "alpine",
|
||||
"script": "ct/alpine-syncthing.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 256,
|
||||
"hdd": 1,
|
||||
"os": "alpine",
|
||||
"version": "3.22"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
|
334
frontend/public/json/versions.json
generated
334
frontend/public/json/versions.json
generated
@ -1,14 +1,174 @@
|
||||
[
|
||||
{
|
||||
"name": "rcourtman/Pulse",
|
||||
"version": "v3.33.1",
|
||||
"date": "2025-06-30T11:29:32Z"
|
||||
},
|
||||
{
|
||||
"name": "Graylog2/graylog2-server",
|
||||
"version": "6.3.0",
|
||||
"date": "2025-06-30T11:26:45Z"
|
||||
},
|
||||
{
|
||||
"name": "grokability/snipe-it",
|
||||
"version": "v8.1.17",
|
||||
"date": "2025-06-30T11:26:27Z"
|
||||
},
|
||||
{
|
||||
"name": "prometheus/prometheus",
|
||||
"version": "v2.53.5",
|
||||
"date": "2025-06-30T11:01:12Z"
|
||||
},
|
||||
{
|
||||
"name": "documenso/documenso",
|
||||
"version": "v1.12.0-rc.8",
|
||||
"date": "2025-06-30T09:47:37Z"
|
||||
},
|
||||
{
|
||||
"name": "PrivateBin/PrivateBin",
|
||||
"version": "1.7.8",
|
||||
"date": "2025-06-30T09:00:54Z"
|
||||
},
|
||||
{
|
||||
"name": "fuma-nama/fumadocs",
|
||||
"version": "fumadocs-mdx@11.6.10",
|
||||
"date": "2025-06-30T07:07:36Z"
|
||||
},
|
||||
{
|
||||
"name": "mattermost/mattermost",
|
||||
"version": "preview-v0.1",
|
||||
"date": "2025-06-27T14:35:47Z"
|
||||
},
|
||||
{
|
||||
"name": "Jackett/Jackett",
|
||||
"version": "v0.22.2097",
|
||||
"date": "2025-06-30T05:53:30Z"
|
||||
},
|
||||
{
|
||||
"name": "typesense/typesense",
|
||||
"version": "v29.0",
|
||||
"date": "2025-06-30T03:52:33Z"
|
||||
},
|
||||
{
|
||||
"name": "firefly-iii/firefly-iii",
|
||||
"version": "v6.2.19",
|
||||
"date": "2025-06-28T06:53:45Z"
|
||||
},
|
||||
{
|
||||
"name": "keycloak/keycloak",
|
||||
"version": "26.2.5",
|
||||
"date": "2025-05-28T06:49:43Z"
|
||||
},
|
||||
{
|
||||
"name": "sysadminsmedia/homebox",
|
||||
"version": "v0.20.0",
|
||||
"date": "2025-06-29T18:50:03Z"
|
||||
},
|
||||
{
|
||||
"name": "dgtlmoon/changedetection.io",
|
||||
"version": "0.50.5",
|
||||
"date": "2025-06-29T08:54:47Z"
|
||||
},
|
||||
{
|
||||
"name": "emqx/emqx",
|
||||
"version": "e5.9.1-rc.1",
|
||||
"date": "2025-06-29T07:27:21Z"
|
||||
},
|
||||
{
|
||||
"name": "theonedev/onedev",
|
||||
"version": "v11.11.2",
|
||||
"date": "2025-06-29T01:40:39Z"
|
||||
},
|
||||
{
|
||||
"name": "home-assistant/core",
|
||||
"version": "2025.6.3",
|
||||
"date": "2025-06-24T13:00:12Z"
|
||||
},
|
||||
{
|
||||
"name": "linkwarden/linkwarden",
|
||||
"version": "v2.11.2",
|
||||
"date": "2025-06-28T17:33:38Z"
|
||||
},
|
||||
{
|
||||
"name": "msgbyte/tianji",
|
||||
"version": "v1.22.5",
|
||||
"date": "2025-06-28T16:06:19Z"
|
||||
},
|
||||
{
|
||||
"name": "Luligu/matterbridge",
|
||||
"version": "3.1.0",
|
||||
"date": "2025-06-28T09:02:38Z"
|
||||
},
|
||||
{
|
||||
"name": "esphome/esphome",
|
||||
"version": "2025.6.2",
|
||||
"date": "2025-06-28T03:47:16Z"
|
||||
},
|
||||
{
|
||||
"name": "plexguide/Huntarr.io",
|
||||
"version": "8.1.11",
|
||||
"date": "2025-06-28T03:42:46Z"
|
||||
},
|
||||
{
|
||||
"name": "tobychui/zoraxy",
|
||||
"version": "v3.2.4",
|
||||
"date": "2025-06-28T02:47:31Z"
|
||||
},
|
||||
{
|
||||
"name": "pocket-id/pocket-id",
|
||||
"version": "v1.5.0",
|
||||
"date": "2025-06-27T22:04:32Z"
|
||||
},
|
||||
{
|
||||
"name": "homarr-labs/homarr",
|
||||
"version": "v1.26.0",
|
||||
"date": "2025-06-27T19:15:24Z"
|
||||
},
|
||||
{
|
||||
"name": "ollama/ollama",
|
||||
"version": "v0.9.4-rc1",
|
||||
"date": "2025-06-27T18:45:33Z"
|
||||
},
|
||||
{
|
||||
"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": "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": "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",
|
||||
@ -19,16 +179,6 @@
|
||||
"version": "v4.3.3",
|
||||
"date": "2025-06-26T18:42:56Z"
|
||||
},
|
||||
{
|
||||
"name": "home-assistant/core",
|
||||
"version": "2025.6.3",
|
||||
"date": "2025-06-24T13:00:12Z"
|
||||
},
|
||||
{
|
||||
"name": "ollama/ollama",
|
||||
"version": "v0.9.4-rc0",
|
||||
"date": "2025-06-26T17:32:48Z"
|
||||
},
|
||||
{
|
||||
"name": "apache/tika",
|
||||
"version": "3.2.1-rc2",
|
||||
@ -39,11 +189,6 @@
|
||||
"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",
|
||||
@ -64,36 +209,16 @@
|
||||
"version": "4.1.0-beta.2",
|
||||
"date": "2025-06-26T14:23:26Z"
|
||||
},
|
||||
{
|
||||
"name": "plexguide/Huntarr.io",
|
||||
"version": "8.1.9",
|
||||
"date": "2025-06-26T14:16:45Z"
|
||||
},
|
||||
{
|
||||
"name": "Dolibarr/dolibarr",
|
||||
"version": "18.0.7",
|
||||
"date": "2025-06-26T09:16:33Z"
|
||||
},
|
||||
{
|
||||
"name": "Jackett/Jackett",
|
||||
"version": "v0.22.2056",
|
||||
"date": "2025-06-26T05:51:50Z"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
@ -104,21 +229,11 @@
|
||||
"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": "keycloak/keycloak",
|
||||
"version": "26.2.5",
|
||||
"date": "2025-05-28T06:49:43Z"
|
||||
},
|
||||
{
|
||||
"name": "wavelog/wavelog",
|
||||
"version": "2.0.5",
|
||||
@ -134,21 +249,6 @@
|
||||
"version": "testing",
|
||||
"date": "2025-06-16T18:10:42Z"
|
||||
},
|
||||
{
|
||||
"name": "cockpit-project/cockpit",
|
||||
"version": "341",
|
||||
"date": "2025-06-25T11:49:28Z"
|
||||
},
|
||||
{
|
||||
"name": "nzbgetcom/nzbget",
|
||||
"version": "v25.0",
|
||||
"date": "2025-05-12T09:12:04Z"
|
||||
},
|
||||
{
|
||||
"name": "mattermost/mattermost",
|
||||
"version": "v9.11.17",
|
||||
"date": "2025-06-18T08:12:05Z"
|
||||
},
|
||||
{
|
||||
"name": "n8n-io/n8n",
|
||||
"version": "n8n@1.100.0",
|
||||
@ -209,26 +309,16 @@
|
||||
"version": "RELEASE.2025-06-13T11-33-47Z",
|
||||
"date": "2025-06-23T20:58:42Z"
|
||||
},
|
||||
{
|
||||
"name": "esphome/esphome",
|
||||
"version": "2025.6.1",
|
||||
"date": "2025-06-23T19:28:09Z"
|
||||
},
|
||||
{
|
||||
"name": "runtipi/runtipi",
|
||||
"version": "nightly",
|
||||
"date": "2025-06-23T19:10:33Z"
|
||||
"version": "v4.2.1",
|
||||
"date": "2025-06-03T20:04:28Z"
|
||||
},
|
||||
{
|
||||
"name": "VictoriaMetrics/VictoriaMetrics",
|
||||
"version": "pmm-6401-v1.120.0",
|
||||
"date": "2025-06-23T15:12:12Z"
|
||||
},
|
||||
{
|
||||
"name": "Graylog2/graylog2-server",
|
||||
"version": "6.3.0-rc.2",
|
||||
"date": "2025-06-23T11:31:38Z"
|
||||
},
|
||||
{
|
||||
"name": "gotson/komga",
|
||||
"version": "1.22.0",
|
||||
@ -244,11 +334,6 @@
|
||||
"version": "release-5.1.1",
|
||||
"date": "2025-06-22T21:41:17Z"
|
||||
},
|
||||
{
|
||||
"name": "pocket-id/pocket-id",
|
||||
"version": "v1.4.1",
|
||||
"date": "2025-06-22T19:38:08Z"
|
||||
},
|
||||
{
|
||||
"name": "clusterzx/paperless-ai",
|
||||
"version": "v3.0.7",
|
||||
@ -274,26 +359,11 @@
|
||||
"version": "v2.0.114",
|
||||
"date": "2025-06-21T11:20:21Z"
|
||||
},
|
||||
{
|
||||
"name": "Luligu/matterbridge",
|
||||
"version": "3.0.7",
|
||||
"date": "2025-06-21T09:24:21Z"
|
||||
},
|
||||
{
|
||||
"name": "theonedev/onedev",
|
||||
"version": "v11.11.1",
|
||||
"date": "2025-06-21T09:23:39Z"
|
||||
},
|
||||
{
|
||||
"name": "pocketbase/pocketbase",
|
||||
"version": "v0.28.4",
|
||||
"date": "2025-06-21T08:29:04Z"
|
||||
},
|
||||
{
|
||||
"name": "dgtlmoon/changedetection.io",
|
||||
"version": "0.50.4",
|
||||
"date": "2025-06-21T07:47:02Z"
|
||||
},
|
||||
{
|
||||
"name": "go-gitea/gitea",
|
||||
"version": "v1.24.2",
|
||||
@ -304,21 +374,11 @@
|
||||
"version": "v1.135.3",
|
||||
"date": "2025-06-20T20:19:20Z"
|
||||
},
|
||||
{
|
||||
"name": "homarr-labs/homarr",
|
||||
"version": "v1.25.0",
|
||||
"date": "2025-06-20T19:15:43Z"
|
||||
},
|
||||
{
|
||||
"name": "Sonarr/Sonarr",
|
||||
"version": "v4.0.15.2941",
|
||||
"date": "2025-06-20T17:20:54Z"
|
||||
},
|
||||
{
|
||||
"name": "zabbix/zabbix",
|
||||
"version": "7.2.9",
|
||||
"date": "2025-06-20T10:58:45Z"
|
||||
},
|
||||
{
|
||||
"name": "syncthing/syncthing",
|
||||
"version": "2.0.0-rc.19",
|
||||
@ -334,11 +394,6 @@
|
||||
"version": "v2.17.1",
|
||||
"date": "2025-06-19T19:35:01Z"
|
||||
},
|
||||
{
|
||||
"name": "rclone/rclone",
|
||||
"version": "v1.70.1",
|
||||
"date": "2025-06-19T13:19:02Z"
|
||||
},
|
||||
{
|
||||
"name": "icereed/paperless-gpt",
|
||||
"version": "v0.21.0",
|
||||
@ -429,11 +484,6 @@
|
||||
"version": "2025.6.1",
|
||||
"date": "2025-06-17T12:45:39Z"
|
||||
},
|
||||
{
|
||||
"name": "sabnzbd/sabnzbd",
|
||||
"version": "4.5.1",
|
||||
"date": "2025-04-11T09:57:47Z"
|
||||
},
|
||||
{
|
||||
"name": "crowdsecurity/crowdsec",
|
||||
"version": "v1.6.9",
|
||||
@ -464,26 +514,11 @@
|
||||
"version": "2.36.1",
|
||||
"date": "2025-06-16T19:20:54Z"
|
||||
},
|
||||
{
|
||||
"name": "goauthentik/authentik",
|
||||
"version": "version/2025.6.2",
|
||||
"date": "2025-06-16T17:54:39Z"
|
||||
},
|
||||
{
|
||||
"name": "emqx/emqx",
|
||||
"version": "e5.9.1-alpha.1",
|
||||
"date": "2025-06-16T15:34:01Z"
|
||||
},
|
||||
{
|
||||
"name": "open-webui/open-webui",
|
||||
"version": "v0.6.15",
|
||||
"date": "2025-06-16T14:34:42Z"
|
||||
},
|
||||
{
|
||||
"name": "grokability/snipe-it",
|
||||
"version": "v8.1.16",
|
||||
"date": "2025-06-16T13:49:37Z"
|
||||
},
|
||||
{
|
||||
"name": "jellyfin/jellyfin",
|
||||
"version": "v10.10.7",
|
||||
@ -504,11 +539,6 @@
|
||||
"version": "cli/v0.25.0",
|
||||
"date": "2025-06-15T17:48:29Z"
|
||||
},
|
||||
{
|
||||
"name": "tobychui/zoraxy",
|
||||
"version": "v3.1.9",
|
||||
"date": "2025-03-01T02:24:33Z"
|
||||
},
|
||||
{
|
||||
"name": "Prowlarr/Prowlarr",
|
||||
"version": "v1.37.0.5076",
|
||||
@ -554,11 +584,6 @@
|
||||
"version": "v3.3.25",
|
||||
"date": "2025-06-14T02:52:44Z"
|
||||
},
|
||||
{
|
||||
"name": "FlowiseAI/Flowise",
|
||||
"version": "flowise@3.0.2",
|
||||
"date": "2025-06-12T22:48:11Z"
|
||||
},
|
||||
{
|
||||
"name": "leiweibau/Pi.Alert",
|
||||
"version": "v2025-06-12",
|
||||
@ -569,11 +594,6 @@
|
||||
"version": "v3.3.0",
|
||||
"date": "2025-06-12T06:54:48Z"
|
||||
},
|
||||
{
|
||||
"name": "documenso/documenso",
|
||||
"version": "v1.12.0-rc.4",
|
||||
"date": "2025-06-12T00:27:41Z"
|
||||
},
|
||||
{
|
||||
"name": "autobrr/autobrr",
|
||||
"version": "v1.63.1",
|
||||
@ -884,21 +904,11 @@
|
||||
"version": "2025-05-07-r1",
|
||||
"date": "2025-05-07T12:18:42Z"
|
||||
},
|
||||
{
|
||||
"name": "sysadminsmedia/homebox",
|
||||
"version": "v0.19.0",
|
||||
"date": "2025-05-06T18:05:42Z"
|
||||
},
|
||||
{
|
||||
"name": "garethgeorge/backrest",
|
||||
"version": "v1.8.1",
|
||||
"date": "2025-05-06T04:27:00Z"
|
||||
},
|
||||
{
|
||||
"name": "linkwarden/linkwarden",
|
||||
"version": "v2.10.2",
|
||||
"date": "2025-05-06T03:12:53Z"
|
||||
},
|
||||
{
|
||||
"name": "postgres/postgres",
|
||||
"version": "REL_13_21",
|
||||
@ -1179,11 +1189,6 @@
|
||||
"version": "v2.7.1",
|
||||
"date": "2025-02-22T01:14:41Z"
|
||||
},
|
||||
{
|
||||
"name": "typesense/typesense",
|
||||
"version": "v28.0",
|
||||
"date": "2025-02-18T15:49:57Z"
|
||||
},
|
||||
{
|
||||
"name": "recyclarr/recyclarr",
|
||||
"version": "v7.4.1",
|
||||
@ -1209,11 +1214,6 @@
|
||||
"version": "v2.12.3",
|
||||
"date": "2025-02-06T11:07:07Z"
|
||||
},
|
||||
{
|
||||
"name": "PrivateBin/PrivateBin",
|
||||
"version": "1.7.6",
|
||||
"date": "2025-02-01T09:50:52Z"
|
||||
},
|
||||
{
|
||||
"name": "AmruthPillai/Reactive-Resume",
|
||||
"version": "v4.4.4",
|
||||
|
2
frontend/public/json/wireguard.json
generated
2
frontend/public/json/wireguard.json
generated
@ -44,7 +44,7 @@
|
||||
},
|
||||
"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"
|
||||
}
|
||||
]
|
||||
|
@ -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 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";
|
||||
|
||||
@ -10,21 +11,21 @@ const metadataFileName = "metadata.json";
|
||||
const versionFileName = "version.json";
|
||||
const encoding = "utf-8";
|
||||
|
||||
const getMetadata = async () => {
|
||||
async function getMetadata() {
|
||||
const filePath = path.resolve(jsonDir, metadataFileName);
|
||||
const fileContent = await fs.readFile(filePath, encoding);
|
||||
const metadata: Metadata = JSON.parse(fileContent);
|
||||
return metadata;
|
||||
};
|
||||
}
|
||||
|
||||
const getScripts = async () => {
|
||||
async function getScripts() {
|
||||
const filePaths = (await fs.readdir(jsonDir))
|
||||
.filter((fileName) =>
|
||||
fileName.endsWith(".json") &&
|
||||
fileName !== metadataFileName &&
|
||||
fileName !== versionFileName
|
||||
.filter(fileName =>
|
||||
fileName.endsWith(".json")
|
||||
&& fileName !== metadataFileName
|
||||
&& fileName !== versionFileName,
|
||||
)
|
||||
.map((fileName) => path.resolve(jsonDir, fileName));
|
||||
.map(fileName => path.resolve(jsonDir, fileName));
|
||||
|
||||
const scripts = await Promise.all(
|
||||
filePaths.map(async (filePath) => {
|
||||
@ -34,7 +35,7 @@ const getScripts = async () => {
|
||||
}),
|
||||
);
|
||||
return scripts;
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
@ -43,7 +44,7 @@ export async function GET() {
|
||||
|
||||
const categories = metadata.categories
|
||||
.map((category) => {
|
||||
category.scripts = scripts.filter((script) =>
|
||||
category.scripts = scripts.filter(script =>
|
||||
script.categories?.includes(category.id),
|
||||
);
|
||||
return category;
|
||||
@ -51,7 +52,8 @@ export async function GET() {
|
||||
.sort((a, b) => a.sort_order - b.sort_order);
|
||||
|
||||
return NextResponse.json(categories);
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error as Error);
|
||||
return NextResponse.json(
|
||||
{ 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 { 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";
|
||||
|
||||
@ -11,33 +11,32 @@ const jsonDir = "public/json";
|
||||
const versionsFileName = "versions.json";
|
||||
const encoding = "utf-8";
|
||||
|
||||
const getVersions = async () => {
|
||||
async function getVersions() {
|
||||
const filePath = path.resolve(jsonDir, versionsFileName);
|
||||
const fileContent = await fs.readFile(filePath, encoding);
|
||||
const versions: AppVersion[] = JSON.parse(fileContent);
|
||||
|
||||
const modifiedVersions = versions.map(version => {
|
||||
const modifiedVersions = versions.map((version) => {
|
||||
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 modifiedVersions;
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
|
||||
const versions = await getVersions();
|
||||
return NextResponse.json(versions);
|
||||
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
const err = error as globalThis.Error;
|
||||
return NextResponse.json({
|
||||
name: err.name,
|
||||
message: err.message || "An unexpected error occurred",
|
||||
version: "No version found - Error"
|
||||
version: "No version found - Error",
|
||||
}, {
|
||||
status: 500,
|
||||
});
|
||||
|
@ -1,18 +1,20 @@
|
||||
"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 { useRouter } from "next/navigation";
|
||||
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 MAX_DESCRIPTION_LENGTH = 100; // Set max length for description
|
||||
const MAX_LOGOS = 5; // Max logos to display at once
|
||||
|
||||
const formattedBadge = (type: string) => {
|
||||
function formattedBadge(type: string) {
|
||||
switch (type) {
|
||||
case "vm":
|
||||
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 null;
|
||||
};
|
||||
}
|
||||
|
||||
const CategoryView = () => {
|
||||
function CategoryView() {
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [selectedCategoryIndex, setSelectedCategoryIndex] = useState<number | null>(null);
|
||||
const [currentScripts, setCurrentScripts] = useState<any[]>([]);
|
||||
@ -36,6 +38,7 @@ const CategoryView = () => {
|
||||
useEffect(() => {
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
// eslint-disable-next-line node/no-process-env
|
||||
const basePath = process.env.NODE_ENV === "production" ? "/ProxmoxVE" : "";
|
||||
const response = await fetch(`${basePath}/api/categories`);
|
||||
if (!response.ok) {
|
||||
@ -50,7 +53,8 @@ const CategoryView = () => {
|
||||
initialLogoIndices[category.name] = 0;
|
||||
});
|
||||
setLogoIndices(initialLogoIndices);
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error fetching categories:", error);
|
||||
}
|
||||
};
|
||||
@ -74,8 +78,8 @@ const CategoryView = () => {
|
||||
|
||||
const navigateCategory = (direction: "prev" | "next") => {
|
||||
if (selectedCategoryIndex !== null) {
|
||||
const newIndex =
|
||||
direction === "prev"
|
||||
const newIndex
|
||||
= direction === "prev"
|
||||
? (selectedCategoryIndex - 1 + categories.length) % categories.length
|
||||
: (selectedCategoryIndex + 1) % categories.length;
|
||||
setSelectedCategoryIndex(newIndex);
|
||||
@ -86,12 +90,13 @@ const CategoryView = () => {
|
||||
const switchLogos = (categoryName: string, direction: "prev" | "next") => {
|
||||
setLogoIndices((prev) => {
|
||||
const currentIndex = prev[categoryName] || 0;
|
||||
const category = categories.find((cat) => cat.name === categoryName);
|
||||
if (!category || !category.scripts) return prev;
|
||||
const category = categories.find(cat => cat.name === categoryName);
|
||||
if (!category || !category.scripts)
|
||||
return prev;
|
||||
|
||||
const totalLogos = category.scripts.length;
|
||||
const newIndex =
|
||||
direction === "prev"
|
||||
const newIndex
|
||||
= direction === "prev"
|
||||
? (currentIndex - MAX_LOGOS + totalLogos) % totalLogos
|
||||
: (currentIndex + MAX_LOGOS) % totalLogos;
|
||||
|
||||
@ -109,35 +114,49 @@ const CategoryView = () => {
|
||||
const hdd = script.install_methods[0]?.resources.hdd;
|
||||
|
||||
const resourceParts = [];
|
||||
if (cpu)
|
||||
if (cpu) {
|
||||
resourceParts.push(
|
||||
<span key="cpu">
|
||||
<b>CPU:</b> {cpu}vCPU
|
||||
<b>CPU:</b>
|
||||
{" "}
|
||||
{cpu}
|
||||
vCPU
|
||||
</span>,
|
||||
);
|
||||
if (ram)
|
||||
}
|
||||
if (ram) {
|
||||
resourceParts.push(
|
||||
<span key="ram">
|
||||
<b>RAM:</b> {ram}MB
|
||||
<b>RAM:</b>
|
||||
{" "}
|
||||
{ram}
|
||||
MB
|
||||
</span>,
|
||||
);
|
||||
if (hdd)
|
||||
}
|
||||
if (hdd) {
|
||||
resourceParts.push(
|
||||
<span key="hdd">
|
||||
<b>HDD:</b> {hdd}GB
|
||||
<b>HDD:</b>
|
||||
{" "}
|
||||
{hdd}
|
||||
GB
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
return resourceParts.length > 0 ? (
|
||||
<div className="text-sm text-gray-400">
|
||||
{resourceParts.map((part, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{part}
|
||||
{index < resourceParts.length - 1 && " | "}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
) : null;
|
||||
return resourceParts.length > 0
|
||||
? (
|
||||
<div className="text-sm text-gray-400">
|
||||
{resourceParts.map((part, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{part}
|
||||
{index < resourceParts.length - 1 && " | "}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
: null;
|
||||
};
|
||||
|
||||
return (
|
||||
@ -145,145 +164,151 @@ const CategoryView = () => {
|
||||
{categories.length === 0 && (
|
||||
<p className="text-center text-gray-500">No categories available. Please check the API endpoint.</p>
|
||||
)}
|
||||
{selectedCategoryIndex !== null ? (
|
||||
<div>
|
||||
{/* Header with Navigation */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => navigateCategory("prev")}
|
||||
className="p-2 transition-transform duration-300 hover:scale-105"
|
||||
>
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
</Button>
|
||||
<h2 className="text-3xl font-semibold transition-opacity duration-300 hover:opacity-90">
|
||||
{categories[selectedCategoryIndex].name}
|
||||
</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => navigateCategory("next")}
|
||||
className="p-2 transition-transform duration-300 hover:scale-105"
|
||||
>
|
||||
<ChevronRight className="h-6 w-6" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Scripts Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
{currentScripts
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((script) => (
|
||||
<Card
|
||||
key={script.name}
|
||||
className="p-4 cursor-pointer hover:shadow-md transition-shadow duration-300"
|
||||
onClick={() => handleScriptClick(script.slug)}
|
||||
{selectedCategoryIndex !== null
|
||||
? (
|
||||
<div>
|
||||
{/* Header with Navigation */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => navigateCategory("prev")}
|
||||
className="p-2 transition-transform duration-300 hover:scale-105"
|
||||
>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<h3 className="text-lg font-bold script-text text-center hover:text-blue-600 transition-colors duration-300">
|
||||
{script.name}
|
||||
</h3>
|
||||
<img
|
||||
src={script.logo || defaultLogo}
|
||||
alt={script.name || "Script logo"}
|
||||
className="h-12 w-12 object-contain mx-auto"
|
||||
/>
|
||||
<p className="text-sm text-gray-500 text-center">
|
||||
<b>Created at:</b> {script.date_created || "No date available"}
|
||||
</p>
|
||||
<p
|
||||
className="text-sm text-gray-700 hover:text-gray-900 text-center transition-colors duration-300"
|
||||
title={script.description || "No description available."}
|
||||
>
|
||||
{truncateDescription(script.description || "No description available.")}
|
||||
</p>
|
||||
{renderResources(script)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
</Button>
|
||||
<h2 className="text-3xl font-semibold transition-opacity duration-300 hover:opacity-90">
|
||||
{categories[selectedCategoryIndex].name}
|
||||
</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => navigateCategory("next")}
|
||||
className="p-2 transition-transform duration-300 hover:scale-105"
|
||||
>
|
||||
<ChevronRight className="h-6 w-6" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Back to Categories Button */}
|
||||
<div className="mt-8 text-center">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={handleBackClick}
|
||||
className="px-6 py-2 text-white bg-blue-600 hover:bg-blue-700 rounded-lg shadow-md transition-transform duration-300 hover:scale-105"
|
||||
>
|
||||
Back to Categories
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{/* Categories Grid */}
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-semibold mb-4">Categories</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
{categories.reduce((total, category) => total + (category.scripts?.length || 0), 0)} Total scripts
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8">
|
||||
{categories.map((category, index) => (
|
||||
<Card
|
||||
key={category.name}
|
||||
onClick={() => handleCategoryClick(index)}
|
||||
className="cursor-pointer hover:shadow-lg flex flex-col items-center justify-center py-6 transition-shadow duration-300"
|
||||
>
|
||||
<CardContent className="flex flex-col items-center">
|
||||
<h3 className="text-xl font-bold mb-4 category-title transition-colors duration-300 hover:text-blue-600">
|
||||
{category.name}
|
||||
</h3>
|
||||
<div className="flex justify-center items-center gap-2 mb-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
switchLogos(category.name, "prev");
|
||||
}}
|
||||
className="p-1 transition-transform duration-300 hover:scale-110"
|
||||
{/* Scripts Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
{currentScripts
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(script => (
|
||||
<Card
|
||||
key={script.name}
|
||||
className="p-4 cursor-pointer hover:shadow-md transition-shadow duration-300"
|
||||
onClick={() => handleScriptClick(script.slug)}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
{category.scripts &&
|
||||
category.scripts
|
||||
.slice(logoIndices[category.name] || 0, (logoIndices[category.name] || 0) + MAX_LOGOS)
|
||||
.map((script, i) => (
|
||||
<div key={i} className="flex flex-col items-center">
|
||||
<img
|
||||
src={script.logo || defaultLogo}
|
||||
alt={script.name || "Script logo"}
|
||||
title={script.name}
|
||||
className="h-8 w-8 object-contain cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleScriptClick(script.slug);
|
||||
}}
|
||||
/>
|
||||
{formattedBadge(script.type)}
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
switchLogos(category.name, "next");
|
||||
}}
|
||||
className="p-1 transition-transform duration-300 hover:scale-110"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 text-center">
|
||||
{(category as any).description || "No description available."}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<h3 className="text-lg font-bold script-text text-center hover:text-blue-600 transition-colors duration-300">
|
||||
{script.name}
|
||||
</h3>
|
||||
<img
|
||||
src={script.logo || defaultLogo}
|
||||
alt={script.name || "Script logo"}
|
||||
className="h-12 w-12 object-contain mx-auto"
|
||||
/>
|
||||
<p className="text-sm text-gray-500 text-center">
|
||||
<b>Created at:</b>
|
||||
{" "}
|
||||
{script.date_created || "No date available"}
|
||||
</p>
|
||||
<p
|
||||
className="text-sm text-gray-700 hover:text-gray-900 text-center transition-colors duration-300"
|
||||
title={script.description || "No description available."}
|
||||
>
|
||||
{truncateDescription(script.description || "No description available.")}
|
||||
</p>
|
||||
{renderResources(script)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Back to Categories Button */}
|
||||
<div className="mt-8 text-center">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={handleBackClick}
|
||||
className="px-6 py-2 text-white bg-blue-600 hover:bg-blue-700 rounded-lg shadow-md transition-transform duration-300 hover:scale-105"
|
||||
>
|
||||
Back to Categories
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div>
|
||||
{/* Categories Grid */}
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-semibold mb-4">Categories</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
{categories.reduce((total, category) => total + (category.scripts?.length || 0), 0)}
|
||||
{" "}
|
||||
Total scripts
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8">
|
||||
{categories.map((category, index) => (
|
||||
<Card
|
||||
key={category.name}
|
||||
onClick={() => handleCategoryClick(index)}
|
||||
className="cursor-pointer hover:shadow-lg flex flex-col items-center justify-center py-6 transition-shadow duration-300"
|
||||
>
|
||||
<CardContent className="flex flex-col items-center">
|
||||
<h3 className="text-xl font-bold mb-4 category-title transition-colors duration-300 hover:text-blue-600">
|
||||
{category.name}
|
||||
</h3>
|
||||
<div className="flex justify-center items-center gap-2 mb-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
switchLogos(category.name, "prev");
|
||||
}}
|
||||
className="p-1 transition-transform duration-300 hover:scale-110"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
{category.scripts
|
||||
&& category.scripts
|
||||
.slice(logoIndices[category.name] || 0, (logoIndices[category.name] || 0) + MAX_LOGOS)
|
||||
.map((script, i) => (
|
||||
<div key={i} className="flex flex-col items-center">
|
||||
<img
|
||||
src={script.logo || defaultLogo}
|
||||
alt={script.name || "Script logo"}
|
||||
title={script.name}
|
||||
className="h-8 w-8 object-contain cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleScriptClick(script.slug);
|
||||
}}
|
||||
/>
|
||||
{formattedBadge(script.type)}
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
switchLogos(category.name, "next");
|
||||
}}
|
||||
className="p-1 transition-transform duration-300 hover:scale-110"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 text-center">
|
||||
{(category as any).description || "No description available."}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default CategoryView;
|
||||
|
@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import React, { JSX, useEffect, useState } from "react";
|
||||
import DatePicker from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import ApplicationChart from "../../components/ApplicationChart";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
|
||||
interface DataModel {
|
||||
import ApplicationChart from "../../components/application-chart";
|
||||
|
||||
type DataModel = {
|
||||
id: number;
|
||||
ct_type: number;
|
||||
disk_size: number;
|
||||
@ -22,13 +22,13 @@ interface DataModel {
|
||||
error: string;
|
||||
type: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
};
|
||||
|
||||
interface SummaryData {
|
||||
type SummaryData = {
|
||||
total_entries: number;
|
||||
status_count: Record<string, number>;
|
||||
nsapp_count: Record<string, number>;
|
||||
}
|
||||
};
|
||||
|
||||
const DataFetcher: React.FC = () => {
|
||||
const [data, setData] = useState<DataModel[]>([]);
|
||||
@ -37,16 +37,18 @@ const DataFetcher: React.FC = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
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(() => {
|
||||
const fetchSummary = async () => {
|
||||
try {
|
||||
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();
|
||||
setSummary(result);
|
||||
} catch (err) {
|
||||
}
|
||||
catch (err) {
|
||||
setError((err as Error).message);
|
||||
}
|
||||
};
|
||||
@ -58,13 +60,16 @@ const DataFetcher: React.FC = () => {
|
||||
const fetchPaginatedData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
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}`);
|
||||
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}`);
|
||||
const result: DataModel[] = await response.json();
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
}
|
||||
catch (err) {
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
}
|
||||
finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
@ -73,26 +78,35 @@ const DataFetcher: React.FC = () => {
|
||||
}, [currentPage, itemsPerPage]);
|
||||
|
||||
const sortedData = React.useMemo(() => {
|
||||
if (!sortConfig) return data;
|
||||
if (!sortConfig)
|
||||
return data;
|
||||
const sorted = [...data].sort((a, b) => {
|
||||
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]) {
|
||||
return sortConfig.direction === 'ascending' ? 1 : -1;
|
||||
return sortConfig.direction === "ascending" ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return sorted;
|
||||
}, [data, sortConfig]);
|
||||
|
||||
if (loading) return <p>Loading...</p>;
|
||||
if (error) return <p>Error: {error}</p>;
|
||||
if (loading)
|
||||
return <p>Loading...</p>;
|
||||
if (error) {
|
||||
return (
|
||||
<p>
|
||||
Error:
|
||||
{error}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
const requestSort = (key: string) => {
|
||||
let direction: 'ascending' | 'descending' = 'ascending';
|
||||
if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') {
|
||||
direction = 'descending';
|
||||
let direction: "ascending" | "descending" = "ascending";
|
||||
if (sortConfig && sortConfig.key === key && sortConfig.direction === "ascending") {
|
||||
direction = "descending";
|
||||
}
|
||||
setSortConfig({ key, direction });
|
||||
};
|
||||
@ -102,8 +116,8 @@ const DataFetcher: React.FC = () => {
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, "0");
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
const timezoneOffset = dateString.slice(-6);
|
||||
return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`;
|
||||
};
|
||||
@ -114,49 +128,76 @@ const DataFetcher: React.FC = () => {
|
||||
<ApplicationChart data={summary} />
|
||||
<p className="text-lg font-bold mt-4"> </p>
|
||||
<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">Status Legend: 🔄 installing {summary?.status_count["installing"] ?? 0} | ✔️ completed {summary?.status_count["done"] ?? 0} | ❌ failed {summary?.status_count["failed"] ?? 0} | ❓ unknown</p>
|
||||
</div>
|
||||
<p className="text-lg font-bold">
|
||||
{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 className="overflow-x-auto">
|
||||
<div className="overflow-y-auto lg:overflow-y-visible">
|
||||
<table className="min-w-full table-auto border-collapse">
|
||||
<thead>
|
||||
<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('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('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('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('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('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('created_at')}>Created At</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("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_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("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("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("error")}>Error Message</th>
|
||||
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("created_at")}>Created At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedData.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td className="px-4 py-2 border-b">
|
||||
{item.status === "done" ? (
|
||||
"✔️"
|
||||
) : item.status === "failed" ? (
|
||||
"❌"
|
||||
) : item.status === "installing" ? (
|
||||
"🔄"
|
||||
) : (
|
||||
item.status
|
||||
)}
|
||||
{item.status === "done"
|
||||
? (
|
||||
"✔️"
|
||||
)
|
||||
: item.status === "failed"
|
||||
? (
|
||||
"❌"
|
||||
)
|
||||
: item.status === "installing"
|
||||
? (
|
||||
"🔄"
|
||||
)
|
||||
: (
|
||||
item.status
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 border-b">
|
||||
{item.type === "lxc"
|
||||
? (
|
||||
"📦"
|
||||
)
|
||||
: item.type === "vm"
|
||||
? (
|
||||
"🖥️"
|
||||
)
|
||||
: (
|
||||
item.type
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 border-b">{item.type === "lxc" ? (
|
||||
"📦"
|
||||
) : item.type === "vm" ? (
|
||||
"🖥️"
|
||||
) : (
|
||||
item.type
|
||||
)}</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_version}</td>
|
||||
@ -175,11 +216,14 @@ const DataFetcher: React.FC = () => {
|
||||
</div>
|
||||
<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>
|
||||
<span>Page {currentPage}</span>
|
||||
<span>
|
||||
Page
|
||||
{currentPage}
|
||||
</span>
|
||||
<button onClick={() => setCurrentPage(prev => prev + 1)} className="p-2 border">Next</button>
|
||||
<select
|
||||
value={itemsPerPage}
|
||||
onChange={(e) => setItemsPerPage(Number(e.target.value))}
|
||||
onChange={e => setItemsPerPage(Number(e.target.value))}
|
||||
className="p-2 border"
|
||||
>
|
||||
<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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
@ -6,11 +11,10 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Category } from "@/lib/types";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { z } from "zod";
|
||||
import { type Script } from "../_schemas/schemas";
|
||||
import { memo } from "react";
|
||||
|
||||
import type { Script } from "../_schemas/schemas";
|
||||
|
||||
type CategoryProps = {
|
||||
script: Script;
|
||||
@ -20,10 +24,10 @@ type CategoryProps = {
|
||||
categories: Category[];
|
||||
};
|
||||
|
||||
const CategoryTag = memo(({
|
||||
category,
|
||||
onRemove
|
||||
}: {
|
||||
const CategoryTag = memo(({
|
||||
category,
|
||||
onRemove,
|
||||
}: {
|
||||
category: Category;
|
||||
onRemove: () => void;
|
||||
}) => (
|
||||
@ -53,7 +57,7 @@ const CategoryTag = memo(({
|
||||
</span>
|
||||
));
|
||||
|
||||
CategoryTag.displayName = 'CategoryTag';
|
||||
CategoryTag.displayName = "CategoryTag";
|
||||
|
||||
function Categories({
|
||||
script,
|
||||
@ -79,14 +83,16 @@ function Categories({
|
||||
return (
|
||||
<div>
|
||||
<Label>
|
||||
Category <span className="text-red-500">*</span>
|
||||
Category
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Select onValueChange={(value) => addCategory(Number(value))}>
|
||||
<Select onValueChange={value => addCategory(Number(value))}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map((category) => (
|
||||
{categories.map(category => (
|
||||
<SelectItem key={category.id} value={category.id.toString()}>
|
||||
{category.name}
|
||||
</SelectItem>
|
||||
@ -101,13 +107,15 @@ function Categories({
|
||||
>
|
||||
{script.categories.map((categoryId) => {
|
||||
const category = categoryMap.get(categoryId);
|
||||
return category ? (
|
||||
<CategoryTag
|
||||
key={categoryId}
|
||||
category={category}
|
||||
onRemove={() => removeCategory(categoryId)}
|
||||
/>
|
||||
) : null;
|
||||
return category
|
||||
? (
|
||||
<CategoryTag
|
||||
key={categoryId}
|
||||
category={category}
|
||||
onRemove={() => removeCategory(categoryId)}
|
||||
/>
|
||||
)
|
||||
: null;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
@ -1,11 +1,16 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { OperatingSystems } from "@/config/siteConfig";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { PlusCircle, Trash2 } from "lucide-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 = {
|
||||
script: Script;
|
||||
@ -28,9 +33,11 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
||||
|
||||
if (type === "pve") {
|
||||
scriptPath = `tools/pve/${slug}.sh`;
|
||||
} else if (type === "addon") {
|
||||
}
|
||||
else if (type === "addon") {
|
||||
scriptPath = `tools/addon/${slug}.sh`;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
scriptPath = `${type}/${slug}.sh`;
|
||||
}
|
||||
|
||||
@ -65,8 +72,8 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
||||
const updatedMethod = { ...method, [key]: value };
|
||||
|
||||
if (key === "type") {
|
||||
updatedMethod.script =
|
||||
value === "alpine" ? `${prev.type}/alpine-${prev.slug}.sh` : `${prev.type}/${prev.slug}.sh`;
|
||||
updatedMethod.script
|
||||
= value === "alpine" ? `${prev.type}/alpine-${prev.slug}.sh` : `${prev.type}/${prev.slug}.sh`;
|
||||
|
||||
// Set OS to Alpine and reset version if type is alpine
|
||||
if (value === "alpine") {
|
||||
@ -89,7 +96,8 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
||||
setIsValid(result.success);
|
||||
if (!result.success) {
|
||||
setZodErrors(result.error);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
setZodErrors(null);
|
||||
}
|
||||
return updated;
|
||||
@ -100,7 +108,7 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
||||
|
||||
const removeInstallMethod = useCallback(
|
||||
(index: number) => {
|
||||
setScript((prev) => ({
|
||||
setScript(prev => ({
|
||||
...prev,
|
||||
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>
|
||||
{script.install_methods.map((method, index) => (
|
||||
<div key={index} className="space-y-2 border p-4 rounded">
|
||||
<Select value={method.type} onValueChange={(value) => updateInstallMethod(index, "type", value)}>
|
||||
<Select value={method.type} onValueChange={value => updateInstallMethod(index, "type", value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Type" />
|
||||
</SelectTrigger>
|
||||
@ -130,12 +138,11 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
||||
placeholder="CPU in Cores"
|
||||
type="number"
|
||||
value={method.resources.cpu || ""}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
cpu: e.target.value ? Number(e.target.value) : null,
|
||||
})
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
ref={(el) => {
|
||||
@ -144,12 +151,11 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
||||
placeholder="RAM in MB"
|
||||
type="number"
|
||||
value={method.resources.ram || ""}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
ram: e.target.value ? Number(e.target.value) : null,
|
||||
})
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
ref={(el) => {
|
||||
@ -158,31 +164,29 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
||||
placeholder="HDD in GB"
|
||||
type="number"
|
||||
value={method.resources.hdd || ""}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
hdd: e.target.value ? Number(e.target.value) : null,
|
||||
})
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={method.resources.os || undefined}
|
||||
onValueChange={(value) =>
|
||||
onValueChange={value =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
os: value || null,
|
||||
version: null, // Reset version when OS changes
|
||||
})
|
||||
}
|
||||
})}
|
||||
disabled={method.type === "alpine"}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="OS" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{OperatingSystems.map((os) => (
|
||||
{OperatingSystems.map(os => (
|
||||
<SelectItem key={os.name} value={os.name}>
|
||||
{os.name}
|
||||
</SelectItem>
|
||||
@ -191,19 +195,18 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
||||
</Select>
|
||||
<Select
|
||||
value={method.resources.version || undefined}
|
||||
onValueChange={(value) =>
|
||||
onValueChange={value =>
|
||||
updateInstallMethod(index, "resources", {
|
||||
...method.resources,
|
||||
version: value || null,
|
||||
})
|
||||
}
|
||||
})}
|
||||
disabled={method.type === "alpine"}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Version" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{OperatingSystems.find((os) => os.name === method.resources.os)?.versions.map((version) => (
|
||||
{OperatingSystems.find(os => os.name === method.resources.os)?.versions.map(version => (
|
||||
<SelectItem key={version.slug} value={version.name}>
|
||||
{version.name}
|
||||
</SelectItem>
|
||||
@ -212,12 +215,16 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
|
||||
</Select>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
))}
|
||||
<Button type="button" size="sm" disabled={script.install_methods.length >= 2} onClick={addInstallMethod}>
|
||||
<PlusCircle className="mr-2 h-4 w-4" /> Add Install Method
|
||||
<PlusCircle className="mr-2 h-4 w-4" />
|
||||
{" "}
|
||||
Add Install Method
|
||||
</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({
|
||||
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"),
|
||||
resources: z.object({
|
||||
@ -25,7 +25,7 @@ export const ScriptSchema = z.object({
|
||||
categories: z.array(z.number()),
|
||||
date_created: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format").min(1, "Date is required"),
|
||||
type: z.enum(["vm", "ct", "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(),
|
||||
privileged: z.boolean(),
|
||||
|
@ -1,26 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { Category } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { format } from "date-fns";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { CalendarIcon, Check, Clipboard, Download } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { format } from "date-fns";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import Categories from "./_components/Categories";
|
||||
import InstallMethod from "./_components/InstallMethod";
|
||||
import Note from "./_components/Note";
|
||||
import { ScriptSchema, type Script } from "./_schemas/schemas";
|
||||
|
||||
import type { Category } from "@/lib/types";
|
||||
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
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 = {
|
||||
name: "",
|
||||
@ -54,7 +60,7 @@ export default function JSONGenerator() {
|
||||
useEffect(() => {
|
||||
fetchCategories()
|
||||
.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]) => {
|
||||
@ -67,11 +73,14 @@ export default function JSONGenerator() {
|
||||
|
||||
if (updated.type === "pve") {
|
||||
scriptPath = `tools/pve/${updated.slug}.sh`;
|
||||
} else if (updated.type === "addon") {
|
||||
}
|
||||
else if (updated.type === "addon") {
|
||||
scriptPath = `tools/addon/${updated.slug}.sh`;
|
||||
} else if (method.type === "alpine") {
|
||||
}
|
||||
else if (method.type === "alpine") {
|
||||
scriptPath = `${updated.type}/alpine-${updated.slug}.sh`;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
scriptPath = `${updated.type}/${updated.slug}.sh`;
|
||||
}
|
||||
|
||||
@ -136,7 +145,10 @@ export default function JSONGenerator() {
|
||||
<div className="mt-2 space-y-1">
|
||||
{zodErrors.errors.map((error, index) => (
|
||||
<AlertDescription key={index} className="p-1 text-red-500">
|
||||
{error.path.join(".")} - {error.message}
|
||||
{error.path.join(".")}
|
||||
{" "}
|
||||
-
|
||||
{error.message}
|
||||
</AlertDescription>
|
||||
))}
|
||||
</div>
|
||||
@ -154,25 +166,31 @@ export default function JSONGenerator() {
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>
|
||||
Name <span className="text-red-500">*</span>
|
||||
Name
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input placeholder="Example" value={script.name} onChange={(e) => updateScript("name", e.target.value)} />
|
||||
<Input placeholder="Example" value={script.name} onChange={e => updateScript("name", e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
Slug <span className="text-red-500">*</span>
|
||||
Slug
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input placeholder="example" value={script.slug} onChange={(e) => updateScript("slug", e.target.value)} />
|
||||
<Input placeholder="example" value={script.slug} onChange={e => updateScript("slug", e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
Logo <span className="text-red-500">*</span>
|
||||
Logo
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Full logo URL"
|
||||
value={script.logo || ""}
|
||||
onChange={(e) => updateScript("logo", e.target.value || null)}
|
||||
onChange={e => updateScript("logo", e.target.value || null)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@ -180,17 +198,19 @@ export default function JSONGenerator() {
|
||||
<Input
|
||||
placeholder="Path to config file"
|
||||
value={script.config_path || ""}
|
||||
onChange={(e) => updateScript("config_path", e.target.value || null)}
|
||||
onChange={e => updateScript("config_path", e.target.value || null)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>
|
||||
Description <span className="text-red-500">*</span>
|
||||
Description
|
||||
{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
placeholder="Example"
|
||||
value={script.description}
|
||||
onChange={(e) => updateScript("description", e.target.value)}
|
||||
onChange={e => updateScript("description", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Categories script={script} setScript={setScript} categories={categories} />
|
||||
@ -200,7 +220,7 @@ export default function JSONGenerator() {
|
||||
<Popover>
|
||||
<PopoverTrigger asChild className="flex-1">
|
||||
<Button
|
||||
variant={"outline"}
|
||||
variant="outline"
|
||||
className={cn("pl-3 text-left font-normal w-full", !script.date_created && "text-muted-foreground")}
|
||||
>
|
||||
{formattedDate || <span>Pick a date</span>}
|
||||
@ -219,7 +239,7 @@ export default function JSONGenerator() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<Label>Type</Label>
|
||||
<Select value={script.type} onValueChange={(value) => updateScript("type", value)}>
|
||||
<Select value={script.type} onValueChange={value => updateScript("type", value)}>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="Type" />
|
||||
</SelectTrigger>
|
||||
@ -234,11 +254,11 @@ export default function JSONGenerator() {
|
||||
</div>
|
||||
<div className="w-full flex gap-5">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch checked={script.updateable} onCheckedChange={(checked) => updateScript("updateable", checked)} />
|
||||
<Switch checked={script.updateable} onCheckedChange={checked => updateScript("updateable", checked)} />
|
||||
<label>Updateable</label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch checked={script.privileged} onCheckedChange={(checked) => updateScript("privileged", checked)} />
|
||||
<Switch checked={script.privileged} onCheckedChange={checked => updateScript("privileged", checked)} />
|
||||
<label>Privileged</label>
|
||||
</div>
|
||||
</div>
|
||||
@ -246,18 +266,18 @@ export default function JSONGenerator() {
|
||||
placeholder="Interface Port"
|
||||
type="number"
|
||||
value={script.interface_port || ""}
|
||||
onChange={(e) => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
|
||||
onChange={e => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Website URL"
|
||||
value={script.website || ""}
|
||||
onChange={(e) => updateScript("website", e.target.value || null)}
|
||||
onChange={e => updateScript("website", e.target.value || null)}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Documentation URL"
|
||||
value={script.documentation || ""}
|
||||
onChange={(e) => updateScript("documentation", e.target.value || null)}
|
||||
onChange={e => updateScript("documentation", e.target.value || null)}
|
||||
/>
|
||||
</div>
|
||||
<InstallMethod script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||
@ -265,22 +285,20 @@ export default function JSONGenerator() {
|
||||
<Input
|
||||
placeholder="Username"
|
||||
value={script.default_credentials.username || ""}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
updateScript("default_credentials", {
|
||||
...script.default_credentials,
|
||||
username: e.target.value || null,
|
||||
})
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Password"
|
||||
value={script.default_credentials.password || ""}
|
||||
onChange={(e) =>
|
||||
onChange={e =>
|
||||
updateScript("default_credentials", {
|
||||
...script.default_credentials,
|
||||
password: e.target.value || null,
|
||||
})
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<Note script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||
</form>
|
||||
|
@ -1,18 +1,20 @@
|
||||
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 { Inter } from "next/font/google";
|
||||
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||
import { Inter } from "next/font/google";
|
||||
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"] });
|
||||
|
||||
export const metadata : Metadata = {
|
||||
export const metadata: Metadata = {
|
||||
title: "Proxmox VE Helper-Scripts",
|
||||
description:
|
||||
"The official website for the Proxmox VE Helper-Scripts (Community) Repository. Featuring over 300+ scripts to help you manage your Proxmox VE environment.",
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export const generateStaticParams = () => {
|
||||
import { basePath } from "@/config/site-config";
|
||||
|
||||
export function generateStaticParams() {
|
||||
return [];
|
||||
};
|
||||
}
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
|
@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
import FAQ from "@/components/FAQ";
|
||||
import AnimatedGradientText from "@/components/ui/animated-gradient-text";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardFooter } from "@/components/ui/card";
|
||||
import { ArrowRightIcon, ExternalLink } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FaGithub } from "react-icons/fa";
|
||||
import { useTheme } from "next-themes";
|
||||
import Link from "next/link";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@ -11,15 +13,14 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} 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 { 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 { ArrowRightIcon, ExternalLink } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FaGithub } from "react-icons/fa";
|
||||
|
||||
function CustomArrowRightIcon() {
|
||||
return <ArrowRightIcon className="h-4 w-4" width={1} />;
|
||||
@ -50,7 +51,9 @@ export default function Page() {
|
||||
`p-px ![mask-composite:subtract]`,
|
||||
)}
|
||||
/>
|
||||
❤️ <Separator className="mx-2 h-4" orientation="vertical" />
|
||||
❤️
|
||||
{" "}
|
||||
<Separator className="mx-2 h-4" orientation="vertical" />
|
||||
<span
|
||||
className={cn(
|
||||
`animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
|
||||
@ -78,7 +81,9 @@ export default function Page() {
|
||||
rel="noopener noreferrer"
|
||||
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>
|
||||
</Button>
|
||||
<Button className="w-full" asChild>
|
||||
@ -88,7 +93,9 @@ export default function Page() {
|
||||
rel="noopener noreferrer"
|
||||
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>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
@ -104,7 +111,10 @@ export default function Page() {
|
||||
We are a community-driven initiative that simplifies the setup of Proxmox Virtual Environment (VE).
|
||||
</p>
|
||||
<p>
|
||||
With 300+ scripts to help you manage your <b>Proxmox VE environment</b>. Whether you're a seasoned
|
||||
With 300+ scripts to help you manage your
|
||||
{" "}
|
||||
<b>Proxmox VE environment</b>
|
||||
. Whether you're a seasoned
|
||||
user or a newcomer, we've got you covered.
|
||||
</p>
|
||||
</div>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
import { basePath } from "@/config/site-config";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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 { getDisplayValueFromRAM } from "@/lib/utils/resource-utils";
|
||||
|
||||
interface ResourceDisplayProps {
|
||||
type ResourceDisplayProps = {
|
||||
title: string;
|
||||
cpu: number | null;
|
||||
ram: number | null;
|
||||
hdd: number | null;
|
||||
}
|
||||
};
|
||||
|
||||
interface IconTextProps {
|
||||
type IconTextProps = {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
}
|
||||
};
|
||||
|
||||
function IconText({ icon, label }: IconTextProps) {
|
||||
return (
|
||||
@ -27,7 +27,8 @@ export function ResourceDisplay({ title, cpu, ram, hdd }: ResourceDisplayProps)
|
||||
const hasRAM = typeof ram === "number" && ram > 0;
|
||||
const hasHDD = typeof hdd === "number" && hdd > 0;
|
||||
|
||||
if (!hasCPU && !hasRAM && !hasHDD) return null;
|
||||
if (!hasCPU && !hasRAM && !hasHDD)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<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 {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} 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 Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
|
||||
export default function ScriptAccordion({
|
||||
items,
|
||||
@ -41,8 +41,8 @@ export default function ScriptAccordion({
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedScript) {
|
||||
const category = items.find((category) =>
|
||||
category.scripts.some((script) => script.slug === selectedScript),
|
||||
const category = items.find(category =>
|
||||
category.scripts.some(script => script.slug === selectedScript),
|
||||
);
|
||||
if (category) {
|
||||
setExpandedItem(category.name);
|
||||
@ -58,11 +58,11 @@ export default function ScriptAccordion({
|
||||
collapsible
|
||||
className="overflow-y-scroll max-h-[calc(100vh-225px)] overflow-x-hidden p-2"
|
||||
>
|
||||
{items.map((category) => (
|
||||
{items.map(category => (
|
||||
<AccordionItem
|
||||
key={category.id + ":category"}
|
||||
key={`${category.id}:category`}
|
||||
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,
|
||||
})}
|
||||
>
|
||||
@ -72,11 +72,15 @@ export default function ScriptAccordion({
|
||||
)}
|
||||
>
|
||||
<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">
|
||||
{category.scripts.length}
|
||||
</span>
|
||||
</div>{" "}
|
||||
</div>
|
||||
{" "}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent
|
||||
data-state={expandedItem === category.name ? "open" : "closed"}
|
||||
@ -109,10 +113,9 @@ export default function ScriptAccordion({
|
||||
height={16}
|
||||
width={16}
|
||||
unoptimized
|
||||
onError={(e) =>
|
||||
((e.currentTarget as HTMLImageElement).src =
|
||||
`/${basePath}/logo.png`)
|
||||
}
|
||||
onError={e =>
|
||||
((e.currentTarget as HTMLImageElement).src
|
||||
= `/${basePath}/logo.png`)}
|
||||
alt={script.name}
|
||||
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 { useMemo, useState } from "react";
|
||||
import Image from "next/image";
|
||||
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;
|
||||
|
||||
export const getDisplayValueFromType = (type: string) => {
|
||||
export function getDisplayValueFromType(type: string) {
|
||||
switch (type) {
|
||||
case "ct":
|
||||
return "LXC";
|
||||
@ -22,15 +24,16 @@ export const getDisplayValueFromType = (type: string) => {
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function LatestScripts({ items }: { items: Category[] }) {
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
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
|
||||
const uniqueScriptsMap = new Map<string, Script>();
|
||||
@ -46,11 +49,11 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
||||
}, [items]);
|
||||
|
||||
const goToNextPage = () => {
|
||||
setPage((prevPage) => prevPage + 1);
|
||||
setPage(prevPage => prevPage + 1);
|
||||
};
|
||||
|
||||
const goToPreviousPage = () => {
|
||||
setPage((prevPage) => prevPage - 1);
|
||||
setPage(prevPage => prevPage - 1);
|
||||
};
|
||||
|
||||
const startIndex = (page - 1) * ITEMS_PER_PAGE;
|
||||
@ -80,7 +83,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
||||
</div>
|
||||
)}
|
||||
<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">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-3">
|
||||
@ -91,13 +94,15 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
||||
height={64}
|
||||
width={64}
|
||||
alt=""
|
||||
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||
onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||
className="h-11 w-11 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-lg line-clamp-1">
|
||||
{script.name} {getDisplayValueFromType(script.type)}
|
||||
{script.name}
|
||||
{" "}
|
||||
{getDisplayValueFromType(script.type)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
||||
<CalendarPlus className="h-4 w-4" />
|
||||
@ -130,7 +135,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
|
||||
|
||||
export function MostViewedScripts({ items }: { items: Category[] }) {
|
||||
const mostViewedScripts = items.reduce((acc: Script[], category) => {
|
||||
const foundScripts = category.scripts.filter((script) => mostPopularScripts.includes(script.slug));
|
||||
const foundScripts = category.scripts.filter(script => mostPopularScripts.includes(script.slug));
|
||||
return acc.concat(foundScripts);
|
||||
}, []);
|
||||
|
||||
@ -142,7 +147,7 @@ export function MostViewedScripts({ items }: { items: Category[] }) {
|
||||
</>
|
||||
)}
|
||||
<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">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-3">
|
||||
@ -153,13 +158,15 @@ export function MostViewedScripts({ items }: { items: Category[] }) {
|
||||
height={64}
|
||||
width={64}
|
||||
alt=""
|
||||
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||
onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||
className="h-11 w-11 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="line-clamp-1 text-lg">
|
||||
{script.name} {getDisplayValueFromType(script.type)}
|
||||
{script.name}
|
||||
{" "}
|
||||
{getDisplayValueFromType(script.type)}
|
||||
</p>
|
||||
<p className="flex items-center gap-1 text-sm text-muted-foreground">
|
||||
<CalendarPlus className="h-4 w-4" />
|
@ -1,31 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { extractDate } from "@/lib/time";
|
||||
import { AppVersion, Script } from "@/lib/types";
|
||||
|
||||
import { X } from "lucide-react";
|
||||
import { Suspense } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { useVersions } from "@/hooks/useVersions";
|
||||
import { cleanSlug } from "@/lib/utils/resource-utils";
|
||||
import { Suspense } from "react";
|
||||
import { ResourceDisplay } from "./ResourceDisplay";
|
||||
import { getDisplayValueFromType } from "./ScriptInfoBlocks";
|
||||
import Alerts from "./ScriptItems/Alerts";
|
||||
import Buttons from "./ScriptItems/Buttons";
|
||||
import 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";
|
||||
import type { AppVersion, Script } from "@/lib/types";
|
||||
|
||||
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;
|
||||
setSelectedScript: (script: string | null) => void;
|
||||
}
|
||||
};
|
||||
|
||||
function ScriptHeader({ item }: { item: Script }) {
|
||||
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"
|
||||
src={item.logo || `/${basePath}/logo.png`}
|
||||
width={400}
|
||||
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||
onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||
height={400}
|
||||
alt={item.name}
|
||||
unoptimized
|
||||
@ -58,10 +59,16 @@ function ScriptHeader({ item }: { item: Script }) {
|
||||
</span>
|
||||
</h1>
|
||||
<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 className=" capitalize">
|
||||
{os} {version}
|
||||
{os}
|
||||
{" "}
|
||||
{version}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -76,10 +83,10 @@ function ScriptHeader({ item }: { item: Script }) {
|
||||
hdd={defaultInstallMethod.resources.hdd}
|
||||
/>
|
||||
)}
|
||||
{item.install_methods.find((method) => method.type === "alpine")?.resources && (
|
||||
{item.install_methods.find(method => method.type === "alpine")?.resources && (
|
||||
<ResourceDisplay
|
||||
title="Alpine"
|
||||
{...item.install_methods.find((method) => method.type === "alpine")!.resources!}
|
||||
{...item.install_methods.find(method => method.type === "alpine")!.resources!}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -108,7 +115,8 @@ function VersionInfo({ item }: { item: Script }) {
|
||||
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>;
|
||||
}
|
||||
@ -132,7 +140,7 @@ export function ScriptItem({ item, setSelectedScript }: ScriptItemProps) {
|
||||
</button>
|
||||
</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">
|
||||
<Suspense fallback={<div className="animate-pulse h-32 bg-accent/20 rounded-xl" />}>
|
||||
<ScriptHeader item={item} />
|
||||
@ -144,7 +152,9 @@ export function ScriptItem({ item, setSelectedScript }: ScriptItemProps) {
|
||||
<div className="mt-4 rounded-lg border shadow-sm">
|
||||
<div className="flex gap-3 px-4 py-2 bg-accent/25">
|
||||
<h2 className="text-lg font-semibold">
|
||||
How to {item.type === "pve" ? "use" : item.type === "addon" ? "apply" : "install"}
|
||||
How to
|
||||
{" "}
|
||||
{item.type === "pve" ? "use" : item.type === "addon" ? "apply" : "install"}
|
||||
</h2>
|
||||
<Tooltips item={item} />
|
||||
</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 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 = {
|
||||
text: string;
|
||||
type: keyof typeof AlertColors;
|
||||
}
|
||||
};
|
||||
|
||||
export default function Alerts({ item }: { item: Script }) {
|
||||
return (
|
||||
<>
|
||||
{item?.notes?.length > 0 &&
|
||||
item.notes.map((note: NoteProps, index: number) => (
|
||||
{item?.notes?.length > 0
|
||||
&& item.notes.map((note: NoteProps, index: number) => (
|
||||
<div key={index} className="mt-4 flex flex-col shadow-sm gap-2">
|
||||
<p
|
||||
className={cn(
|
||||
@ -21,11 +23,13 @@ export default function Alerts({ item }: { item: Script }) {
|
||||
AlertColors[note.type],
|
||||
)}
|
||||
>
|
||||
{note.type == "info" ? (
|
||||
<NotepadText className="h-4 min-h-4 w-4 min-w-4" />
|
||||
) : (
|
||||
<AlertCircle className="h-4 min-h-4 w-4 min-w-4" />
|
||||
)}
|
||||
{note.type === "info"
|
||||
? (
|
||||
<NotepadText className="h-4 min-h-4 w-4 min-w-4" />
|
||||
)
|
||||
: (
|
||||
<AlertCircle className="h-4 min-h-4 w-4 min-w-4" />
|
||||
)}
|
||||
<span>{TextCopyBlock(note.text)}</span>
|
||||
</p>
|
||||
</div>
|
@ -1,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 {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { Script } from "@/lib/types";
|
||||
import { BookOpenText, Code, Globe, LinkIcon, RefreshCcw } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { basePath } from "@/config/site-config";
|
||||
|
||||
const generateInstallSourceUrl = (slug: string) => {
|
||||
function generateInstallSourceUrl(slug: string) {
|
||||
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
|
||||
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`;
|
||||
|
||||
switch (type) {
|
||||
@ -29,18 +31,18 @@ const generateSourceUrl = (slug: string, type: string) => {
|
||||
default:
|
||||
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`;
|
||||
return `${baseUrl}/ct/${slug}.sh`;
|
||||
};
|
||||
}
|
||||
|
||||
interface LinkItem {
|
||||
type LinkItem = {
|
||||
href: string;
|
||||
icon: React.ReactNode;
|
||||
text: string;
|
||||
}
|
||||
};
|
||||
|
||||
export default function Buttons({ item }: { item: Script }) {
|
||||
const isCtOrDefault = ["ct"].includes(item.type);
|
||||
@ -76,7 +78,8 @@ export default function Buttons({ item }: { item: Script }) {
|
||||
},
|
||||
].filter(Boolean) as LinkItem[];
|
||||
|
||||
if (links.length === 0) return null;
|
||||
if (links.length === 0)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
@ -1,13 +1,15 @@
|
||||
import handleCopy from "@/components/handleCopy";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { Script } from "@/lib/types";
|
||||
|
||||
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 }) {
|
||||
const { username, password } = item.default_credentials;
|
||||
const hasDefaultLogin = username || password;
|
||||
|
||||
if (!hasDefaultLogin) return null;
|
||||
if (!hasDefaultLogin)
|
||||
return null;
|
||||
|
||||
const copyCredential = (type: "username" | "password") => {
|
||||
handleCopy(type, item.default_credentials[type] ?? "");
|
||||
@ -21,18 +23,27 @@ export default function DefaultPassword({ item }: { item: Script }) {
|
||||
<Separator className="w-full" />
|
||||
<div className="flex flex-col gap-2 p-4">
|
||||
<p className="mb-2 text-sm">
|
||||
You can use the following credentials to login to the {item.name} {item.type}.
|
||||
You can use the following credentials to login to the
|
||||
{" "}
|
||||
{item.name}
|
||||
{" "}
|
||||
{item.type}
|
||||
.
|
||||
</p>
|
||||
{["username", "password"].map((type) => {
|
||||
const value = item.default_credentials[type as "username" | "password"];
|
||||
return value && value.trim() !== "" ? (
|
||||
<div key={type} className="text-sm">
|
||||
{type.charAt(0).toUpperCase() + type.slice(1)}:{" "}
|
||||
<Button variant="secondary" size="null" onClick={() => copyCredential(type as "username" | "password")}>
|
||||
{value}
|
||||
</Button>
|
||||
</div>
|
||||
) : null;
|
||||
return value && value.trim() !== ""
|
||||
? (
|
||||
<div key={type} className="text-sm">
|
||||
{type.charAt(0).toUpperCase() + type.slice(1)}
|
||||
:
|
||||
{" "}
|
||||
<Button variant="secondary" size="null" onClick={() => copyCredential(type as "username" | "password")}>
|
||||
{value}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
: null;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
@ -1,4 +1,4 @@
|
||||
import { Script } from "@/lib/types";
|
||||
import type { Script } from "@/lib/types";
|
||||
|
||||
export default function DefaultSettings({ item }: { item: Script }) {
|
||||
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 (
|
||||
<div>
|
||||
<h2 className="text-md font-semibold">{title}</h2>
|
||||
<p className="text-sm text-muted-foreground">CPU: {cpu}vCPU</p>
|
||||
<p className="text-sm text-muted-foreground">RAM: {getDisplayValueFromRAM(ram ?? 0)}</p>
|
||||
<p className="text-sm text-muted-foreground">HDD: {hdd}GB</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
CPU:
|
||||
{cpu}
|
||||
vCPU
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
RAM:
|
||||
{getDisplayValueFromRAM(ram ?? 0)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
HDD:
|
||||
{hdd}
|
||||
GB
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultSettings = item.install_methods.find((method) => method.type === "default");
|
||||
const defaultAlpineSettings = item.install_methods.find((method) => method.type === "alpine");
|
||||
const defaultSettings = item.install_methods.find(method => method.type === "default");
|
||||
const defaultAlpineSettings = item.install_methods.find(method => method.type === "alpine");
|
||||
|
||||
const hasDefaultSettings = defaultSettings?.resources && Object.values(defaultSettings.resources).some(Boolean);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import TextCopyBlock from "@/components/TextCopyBlock";
|
||||
import { Script } from "@/lib/types";
|
||||
import type { Script } from "@/lib/types";
|
||||
|
||||
import TextCopyBlock from "@/components/text-copy-block";
|
||||
|
||||
export default function Description({ item }: { item: Script }) {
|
||||
return (
|
@ -1,51 +1,85 @@
|
||||
import CodeCopyButton from "@/components/ui/code-copy-button";
|
||||
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 { Info } from "lucide-react";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { Script } from "@/lib/types";
|
||||
import { getDisplayValueFromType } from "../ScriptInfoBlocks";
|
||||
import CodeCopyButton from "@/components/ui/code-copy-button";
|
||||
import { basePath } from "@/config/site-config";
|
||||
|
||||
const getInstallCommand = (scriptPath = "", isAlpine = false, useGitea = false) => {
|
||||
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 `bash -c "$(curl -fsSL ${url})"`;
|
||||
};
|
||||
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 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.
|
||||
</>
|
||||
)}
|
||||
{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
|
||||
To create a new Proxmox VE Alpine-
|
||||
{item.name}
|
||||
{" "}
|
||||
{getDisplayValueFromType(item.type)}
|
||||
, run the command below in
|
||||
the Proxmox VE Shell.
|
||||
</p>
|
||||
)}
|
||||
@ -56,7 +90,9 @@ export default function InstallCommand({ item }: { item: Script }) {
|
||||
<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
|
||||
<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>
|
||||
@ -81,7 +117,8 @@ export default function InstallCommand({ item }: { item: Script }) {
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
} else if (defaultScript?.script) {
|
||||
}
|
||||
else if (defaultScript?.script) {
|
||||
return (
|
||||
<>
|
||||
{renderInstructions()}
|
||||
@ -109,4 +146,4 @@ export default function InstallCommand({ item }: { item: Script }) {
|
||||
</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 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"];
|
||||
label: string;
|
||||
content?: string;
|
||||
}
|
||||
};
|
||||
|
||||
const TooltipBadge: React.FC<TooltipProps> = ({ variant, label, content }) => (
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger className={cn("flex items-center", !content && "cursor-default")}>
|
||||
<Badge variant={variant} className="flex items-center gap-1">
|
||||
{label} {content && <CircleHelp className="size-3" />}
|
||||
{label}
|
||||
{" "}
|
||||
{content && <CircleHelp className="size-3" />}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
{content && (
|
||||
@ -34,7 +39,7 @@ export default function Tooltips({ item }: { item: Script }) {
|
||||
{item.privileged && (
|
||||
<TooltipBadge variant="warning" label="Privileged" content="This script will be run in a privileged LXC" />
|
||||
)}
|
||||
{(item.updateable || item.type !== "pve") && (
|
||||
{item.updateable && item.type !== "pve" && (
|
||||
<TooltipBadge
|
||||
variant="success"
|
||||
label="Updateable"
|
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;
|
||||
}
|
||||
};
|
||||
|
||||
export function VersionBadge({ version }: VersionBadgeProps) {
|
||||
return (
|
@ -1,18 +1,20 @@
|
||||
"use client";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
import { ScriptItem } from "@/app/scripts/_components/ScriptItem";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { Category, Script } from "@/lib/types";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
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 {
|
||||
LatestScripts,
|
||||
MostViewedScripts,
|
||||
} from "./_components/ScriptInfoBlocks";
|
||||
import Sidebar from "./_components/Sidebar";
|
||||
} from "./_components/script-info-blocks";
|
||||
import Sidebar from "./_components/sidebar";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
function ScriptContent() {
|
||||
const [selectedScript, setSelectedScript] = useQueryState("id");
|
||||
@ -22,9 +24,9 @@ function ScriptContent() {
|
||||
useEffect(() => {
|
||||
if (selectedScript && links.length > 0) {
|
||||
const script = links
|
||||
.map((category) => category.scripts)
|
||||
.map(category => category.scripts)
|
||||
.flat()
|
||||
.find((script) => script.slug === selectedScript);
|
||||
.find(script => script.slug === selectedScript);
|
||||
setItem(script);
|
||||
}
|
||||
}, [selectedScript, links]);
|
||||
@ -34,7 +36,7 @@ function ScriptContent() {
|
||||
.then((categories) => {
|
||||
setLinks(categories);
|
||||
})
|
||||
.catch((error) => console.error(error));
|
||||
.catch(error => console.error(error));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@ -48,14 +50,16 @@ function ScriptContent() {
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-4 w-full sm:mx-0 sm:ml-4">
|
||||
{selectedScript && item ? (
|
||||
<ScriptItem item={item} setSelectedScript={setSelectedScript} />
|
||||
) : (
|
||||
<div className="flex w-full flex-col gap-5">
|
||||
<LatestScripts items={links} />
|
||||
<MostViewedScripts items={links} />
|
||||
</div>
|
||||
)}
|
||||
{selectedScript && item
|
||||
? (
|
||||
<ScriptItem item={item} setSelectedScript={setSelectedScript} />
|
||||
)
|
||||
: (
|
||||
<div className="flex w-full flex-col gap-5">
|
||||
<LatestScripts items={links} />
|
||||
<MostViewedScripts items={links} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -65,13 +69,13 @@ function ScriptContent() {
|
||||
export default function Page() {
|
||||
return (
|
||||
<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="space-y-2 text-center">
|
||||
<Loader2 className="h-10 w-10 animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
>
|
||||
<ScriptContent />
|
||||
</Suspense>
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
import { basePath } from "@/config/site-config";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
let domain = "community-scripts.github.io";
|
||||
let protocol = "https";
|
||||
const domain = "community-scripts.github.io";
|
||||
const protocol = "https";
|
||||
return [
|
||||
{
|
||||
url: `${protocol}://${domain}/${basePath}`,
|
||||
@ -18,6 +19,6 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
{
|
||||
url: `${protocol}://${domain}/${basePath}/json-editor`,
|
||||
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";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { ArcElement, Chart as ChartJS, Tooltip as ChartTooltip, Legend } from "chart.js";
|
||||
import ChartDataLabels from "chartjs-plugin-datalabels";
|
||||
import { BarChart3, PieChart } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { Pie } from "react-chartjs-2";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@ -21,21 +20,23 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Chart as ChartJS, ArcElement, Tooltip as ChartTooltip, Legend } from "chart.js";
|
||||
import ChartDataLabels from "chartjs-plugin-datalabels";
|
||||
import { BarChart3, PieChart } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { Pie, Bar } from "react-chartjs-2";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
ChartJS.register(ArcElement, ChartTooltip, Legend, ChartDataLabels);
|
||||
|
||||
interface SummaryData {
|
||||
type SummaryData = {
|
||||
nsapp_count: Record<string, number>;
|
||||
}
|
||||
};
|
||||
|
||||
interface ApplicationChartProps {
|
||||
type ApplicationChartProps = {
|
||||
data: SummaryData | null;
|
||||
}
|
||||
};
|
||||
|
||||
const ITEMS_PER_PAGE = 20;
|
||||
const CHART_COLORS = [
|
||||
@ -61,14 +62,15 @@ export default function ApplicationChart({ data }: ApplicationChartProps) {
|
||||
const [chartStartIndex, setChartStartIndex] = useState(0);
|
||||
const [tableLimit, setTableLimit] = useState(ITEMS_PER_PAGE);
|
||||
|
||||
if (!data) return null;
|
||||
if (!data)
|
||||
return null;
|
||||
|
||||
const sortedApps = Object.entries(data.nsapp_count)
|
||||
.sort(([, a], [, b]) => b - a);
|
||||
|
||||
const chartApps = sortedApps.slice(
|
||||
chartStartIndex,
|
||||
chartStartIndex + ITEMS_PER_PAGE
|
||||
chartStartIndex + ITEMS_PER_PAGE,
|
||||
);
|
||||
|
||||
const chartData = {
|
||||
@ -141,14 +143,18 @@ export default function ApplicationChart({ data }: ApplicationChartProps) {
|
||||
onClick={() => setChartStartIndex(Math.max(0, chartStartIndex - ITEMS_PER_PAGE))}
|
||||
disabled={chartStartIndex === 0}
|
||||
>
|
||||
Previous {ITEMS_PER_PAGE}
|
||||
Previous
|
||||
{" "}
|
||||
{ITEMS_PER_PAGE}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setChartStartIndex(chartStartIndex + ITEMS_PER_PAGE)}
|
||||
disabled={chartStartIndex + ITEMS_PER_PAGE >= sortedApps.length}
|
||||
>
|
||||
Next {ITEMS_PER_PAGE}
|
||||
Next
|
||||
{" "}
|
||||
{ITEMS_PER_PAGE}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
@ -190,4 +196,4 @@ export default function ApplicationChart({ data }: ApplicationChartProps) {
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -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 {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
@ -6,22 +13,16 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { basePath } from "@/config/site-config";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { Category, Script } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React from "react";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Button } from "./ui/button";
|
||||
import { DialogTitle } from "./ui/dialog";
|
||||
import { 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) {
|
||||
case "vm":
|
||||
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 null;
|
||||
};
|
||||
}
|
||||
|
||||
// random Script
|
||||
function getRandomScript(categories: Category[]): Script | null {
|
||||
const allScripts = categories.flatMap((cat) => cat.scripts || []);
|
||||
if (allScripts.length === 0) return null;
|
||||
const allScripts = categories.flatMap(cat => cat.scripts || []);
|
||||
if (allScripts.length === 0)
|
||||
return null;
|
||||
const idx = Math.floor(Math.random() * allScripts.length);
|
||||
return allScripts[idx];
|
||||
}
|
||||
@ -49,18 +51,6 @@ export default function CommandMenu() {
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
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 = () => {
|
||||
setIsLoading(true);
|
||||
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 () => {
|
||||
if (links.length === 0) {
|
||||
setIsLoading(true);
|
||||
@ -84,10 +86,12 @@ export default function CommandMenu() {
|
||||
if (randomScript) {
|
||||
router.push(`/scripts?id=${randomScript.slug}`);
|
||||
}
|
||||
} finally {
|
||||
}
|
||||
finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
const randomScript = getRandomScript(links);
|
||||
if (randomScript) {
|
||||
router.push(`/scripts?id=${randomScript.slug}`);
|
||||
@ -110,7 +114,8 @@ export default function CommandMenu() {
|
||||
>
|
||||
<span className="inline-flex">Search scripts...</span>
|
||||
<kbd className="pointer-events-none absolute right-[0.3rem] top-[0.45rem] hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
|
||||
<span className="text-xs">⌘</span>K
|
||||
<span className="text-xs">⌘</span>
|
||||
K
|
||||
</kbd>
|
||||
</Button>
|
||||
|
||||
@ -134,54 +139,34 @@ export default function CommandMenu() {
|
||||
<CommandInput placeholder="Search for a script..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>{isLoading ? "Loading..." : "No scripts found."}</CommandEmpty>
|
||||
{(() => {
|
||||
// Track seen scripts globally to avoid duplicates across all categories
|
||||
const globalSeenScripts = new Set<string>();
|
||||
|
||||
return links.map((category) => {
|
||||
const uniqueScripts = category.scripts.filter((script) => {
|
||||
if (globalSeenScripts.has(script.slug)) {
|
||||
return false;
|
||||
}
|
||||
globalSeenScripts.add(script.slug);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Only render category if it has unique scripts
|
||||
if (uniqueScripts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandGroup key={`category:${category.name}`} heading={category.name}>
|
||||
{uniqueScripts.map((script) => (
|
||||
<CommandItem
|
||||
key={`script:${script.slug}`}
|
||||
value={`${script.slug}-${script.name}`}
|
||||
onSelect={() => {
|
||||
setOpen(false);
|
||||
router.push(`/scripts?id=${script.slug}`);
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-2" onClick={() => setOpen(false)}>
|
||||
<Image
|
||||
src={script.logo || `/${basePath}/logo.png`}
|
||||
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||
unoptimized
|
||||
width={16}
|
||||
height={16}
|
||||
alt=""
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
<span>{script.name}</span>
|
||||
<span>{formattedBadge(script.type)}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
{links.map(category => (
|
||||
<CommandGroup key={`category:${category.name}`} heading={category.name}>
|
||||
{category.scripts.map(script => (
|
||||
<CommandItem
|
||||
key={`script:${script.slug}`}
|
||||
value={`${script.slug}-${script.name}`}
|
||||
onSelect={() => {
|
||||
setOpen(false);
|
||||
router.push(`/scripts?id=${script.slug}`);
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-2" onClick={() => setOpen(false)}>
|
||||
<Image
|
||||
src={script.logo || `/${basePath}/logo.png`}
|
||||
onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
||||
unoptimized
|
||||
width={16}
|
||||
height={16}
|
||||
alt=""
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
<span>{script.name}</span>
|
||||
<span>{formattedBadge(script.type)}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
</>
|
@ -1,7 +1,8 @@
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { Plus } from "lucide-react";
|
||||
import { FAQ_Items } from "../config/faqConfig";
|
||||
|
||||
import { Accordion, AccordionContent, AccordionItem } from "./ui/accordion";
|
||||
import { FAQ_Items } from "../config/faq-config";
|
||||
|
||||
export default function FAQ() {
|
||||
return (
|
@ -1,16 +1,19 @@
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { FileJson, Server } from "lucide-react";
|
||||
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 { buttonVariants } from "./ui/button";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<div className="supports-backdrop-blur:bg-background/90 mt-auto border-t w-full flex justify-between border-border bg-background/40 py-4 backdrop-blur-lg">
|
||||
<div className="mx-6 w-full flex justify-between text-xs sm:text-sm text-muted-foreground">
|
||||
<div className="flex items-center">
|
||||
<p>
|
||||
Website built by the community. The source code is available on{" "}
|
||||
Website built by the community. The source code is available on
|
||||
{" "}
|
||||
<Link
|
||||
href={`https://github.com/community-scripts/${basePath}/tree/main/frontend`}
|
||||
target="_blank"
|
||||
@ -28,13 +31,17 @@ export default function Footer() {
|
||||
href="/json-editor"
|
||||
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
|
||||
href="/data"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
@ -2,14 +2,15 @@
|
||||
|
||||
import React from "react";
|
||||
|
||||
interface ModalProps {
|
||||
type ModalProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
};
|
||||
|
||||
const Modal: React.FC<ModalProps> = ({ isOpen, onClose, children }) => {
|
||||
if (!isOpen) return null;
|
||||
if (!isOpen)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<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 handleCopy from "./handleCopy";
|
||||
|
||||
import handleCopy from "./handle-copy";
|
||||
|
||||
export default function TextCopyBlock(description: string) {
|
||||
const pattern = /`([^`]*)`/g;
|
||||
@ -19,7 +20,8 @@ export default function TextCopyBlock(description: string) {
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
return part;
|
||||
}
|
||||
});
|
@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import type { ThemeProviderProps } from "next-themes";
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types";
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
|
@ -1,7 +1,9 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import 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(
|
||||
"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: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@ -29,8 +31,8 @@ const Alert = React.forwardRef<
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Alert.displayName = "Alert"
|
||||
));
|
||||
Alert.displayName = "Alert";
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
@ -41,8 +43,8 @@ const AlertTitle = React.forwardRef<
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertTitle.displayName = "AlertTitle"
|
||||
));
|
||||
AlertTitle.displayName = "AlertTitle";
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
@ -53,7 +55,7 @@ const AlertDescription = React.forwardRef<
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...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";
|
||||
|
||||
@ -17,7 +17,7 @@ export default function AnimatedGradientText({
|
||||
)}
|
||||
>
|
||||
<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}
|
||||
|
@ -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 { cn } from "@/lib/utils";
|
||||
@ -26,9 +28,7 @@ const badgeVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
export type BadgeProps = {} & React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof badgeVariants>;
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
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 { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cva } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
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",
|
||||
{
|
||||
@ -47,21 +50,19 @@ const buttonVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
interface IconProps {
|
||||
type IconProps = {
|
||||
Icon: React.ElementType;
|
||||
iconPlacement: "left" | "right";
|
||||
}
|
||||
};
|
||||
|
||||
interface IconRefProps {
|
||||
type IconRefProps = {
|
||||
Icon?: never;
|
||||
iconPlacement?: undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
export type ButtonProps = {
|
||||
asChild?: boolean;
|
||||
}
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>;
|
||||
|
||||
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 { DayPicker } from "react-day-picker"
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
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({
|
||||
className,
|
||||
@ -27,7 +27,7 @@ function Calendar({
|
||||
nav: "space-x-1 flex items-center",
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||
),
|
||||
nav_button_previous: "absolute left-1",
|
||||
nav_button_next: "absolute right-1",
|
||||
@ -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",
|
||||
day: cn(
|
||||
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_selected:
|
||||
@ -54,13 +54,17 @@ function Calendar({
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
|
||||
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
|
||||
Chevron: ({ ...props }) => {
|
||||
if (props.orientation === "left") {
|
||||
return <ChevronLeft className="h-4 w-4" />;
|
||||
}
|
||||
return <ChevronRight className="h-4 w-4" />;
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
Calendar.displayName = "Calendar"
|
||||
Calendar.displayName = "Calendar";
|
||||
|
||||
export { Calendar }
|
||||
export { Calendar };
|
||||
|
@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckIcon, ClipboardIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { Card } from "./card";
|
||||
|
||||
export default function CodeCopyButton({
|
||||
@ -26,7 +28,7 @@ export default function CodeCopyButton({
|
||||
|
||||
setHasCopied(true);
|
||||
|
||||
let warning = localStorage.getItem("warning");
|
||||
const warning = localStorage.getItem("warning");
|
||||
|
||||
if (warning === null) {
|
||||
localStorage.setItem("warning", "1");
|
||||
@ -50,11 +52,13 @@ export default function CodeCopyButton({
|
||||
className={cn("bg-muted px-3 py-4")}
|
||||
title="Copy"
|
||||
>
|
||||
{hasCopied ? (
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<ClipboardIcon className="h-4 w-4" />
|
||||
)}
|
||||
{hasCopied
|
||||
? (
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
)
|
||||
: (
|
||||
<ClipboardIcon className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</Card>
|
||||
</div>
|
||||
|
@ -1,14 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cva } from "class-variance-authority";
|
||||
import { Clipboard, Copy } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "./button";
|
||||
import Link from "next/link";
|
||||
|
||||
import { basePath } from "@/config/site-config";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { Separator } from "./separator";
|
||||
import { Button } from "./button";
|
||||
|
||||
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",
|
||||
@ -40,23 +44,24 @@ const buttonVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
const handleCopy = (type: string, value: string) => {
|
||||
function handleCopy(type: string, value: string) {
|
||||
navigator.clipboard.writeText(value);
|
||||
|
||||
let amountOfScriptsCopied = localStorage.getItem("amountOfScriptsCopied");
|
||||
|
||||
if (amountOfScriptsCopied === null) {
|
||||
localStorage.setItem("amountOfScriptsCopied", "1");
|
||||
} else {
|
||||
amountOfScriptsCopied = (parseInt(amountOfScriptsCopied) + 1).toString();
|
||||
}
|
||||
else {
|
||||
amountOfScriptsCopied = (Number.parseInt(amountOfScriptsCopied) + 1).toString();
|
||||
localStorage.setItem("amountOfScriptsCopied", amountOfScriptsCopied);
|
||||
|
||||
if (
|
||||
parseInt(amountOfScriptsCopied) === 3 ||
|
||||
parseInt(amountOfScriptsCopied) === 10 ||
|
||||
parseInt(amountOfScriptsCopied) === 25 ||
|
||||
parseInt(amountOfScriptsCopied) === 50 ||
|
||||
parseInt(amountOfScriptsCopied) === 100
|
||||
Number.parseInt(amountOfScriptsCopied) === 3
|
||||
|| Number.parseInt(amountOfScriptsCopied) === 10
|
||||
|| Number.parseInt(amountOfScriptsCopied) === 25
|
||||
|| Number.parseInt(amountOfScriptsCopied) === 50
|
||||
|| Number.parseInt(amountOfScriptsCopied) === 100
|
||||
) {
|
||||
setTimeout(() => {
|
||||
toast.info(
|
||||
@ -86,17 +91,20 @@ const handleCopy = (type: string, value: string) => {
|
||||
toast.success(
|
||||
<div className="flex items-center gap-2">
|
||||
<Clipboard className="h-4 w-4" />
|
||||
<span>Copied {type} to clipboard</span>
|
||||
<span>
|
||||
Copied
|
||||
{type}
|
||||
{" "}
|
||||
to clipboard
|
||||
</span>
|
||||
</div>,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export interface CodeBlockProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
export type CodeBlockProps = {
|
||||
asChild?: boolean;
|
||||
code: string;
|
||||
}
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>;
|
||||
|
||||
const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
({ className, variant, size, asChild = false, code }, ref) => {
|
||||
@ -121,7 +129,10 @@ const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
|
||||
)}
|
||||
>
|
||||
<p className="flex items-center gap-2">
|
||||
{code} <Separator orientation="vertical" />{" "}
|
||||
{code}
|
||||
{" "}
|
||||
<Separator orientation="vertical" />
|
||||
{" "}
|
||||
<Copy
|
||||
className="cursor-pointer"
|
||||
size={16}
|
||||
|
@ -1,6 +1,7 @@
|
||||
"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 { Search } from "lucide-react";
|
||||
import * as React from "react";
|
||||
@ -23,9 +24,9 @@ const Command = React.forwardRef<
|
||||
));
|
||||
Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
interface CommandDialogProps extends DialogProps {}
|
||||
type CommandDialogProps = {} & DialogProps;
|
||||
|
||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
function CommandDialog({ children, ...props }: CommandDialogProps) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
@ -35,7 +36,7 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
@ -126,10 +127,10 @@ const CommandItem = React.forwardRef<
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||
|
||||
const CommandShortcut = ({
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
@ -139,7 +140,7 @@ const CommandShortcut = ({
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
CommandShortcut.displayName = "CommandShortcut";
|
||||
|
||||
export {
|
||||
|
@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckIcon, ClipboardIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { Card } from "./card";
|
||||
|
||||
export default function CodeCopyButton({
|
||||
@ -26,7 +27,6 @@ export default function CodeCopyButton({
|
||||
|
||||
setHasCopied(true);
|
||||
|
||||
|
||||
// toast.success(`copied ${type} to clipboard`, {
|
||||
// icon: <ClipboardCheck className="h-4 w-4" />,
|
||||
// });
|
||||
@ -42,11 +42,13 @@ export default function CodeCopyButton({
|
||||
className={cn(" right-0 cursor-pointer bg-muted px-3 py-4")}
|
||||
onClick={() => handleCopy("install command", children)}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<ClipboardIcon className="h-4 w-4" />
|
||||
)}
|
||||
{hasCopied
|
||||
? (
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
)
|
||||
: (
|
||||
<ClipboardIcon className="h-4 w-4" />
|
||||
)}
|
||||
<span className="sr-only">Copy</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
@ -53,32 +53,36 @@ const DialogContent = React.forwardRef<
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
function DialogHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
function DialogFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
|
@ -37,8 +37,8 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
DropdownMenuSubTrigger.displayName
|
||||
= DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
@ -53,8 +53,8 @@ const DropdownMenuSubContent = React.forwardRef<
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
DropdownMenuSubContent.displayName
|
||||
= DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
@ -113,8 +113,8 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
DropdownMenuCheckboxItem.displayName
|
||||
= DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
@ -168,17 +168,17 @@ const DropdownMenuSeparator = React.forwardRef<
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
|
@ -2,8 +2,7 @@ import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
export type InputProps = {} & React.InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
|
@ -1,26 +1,28 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cva } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
& VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label }
|
||||
export { Label };
|
||||
|
@ -53,7 +53,8 @@ const NavigationMenuTrigger = React.forwardRef<
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
{children}
|
||||
{" "}
|
||||
<ChevronDown
|
||||
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
@ -94,8 +95,8 @@ const NavigationMenuViewport = React.forwardRef<
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
NavigationMenuViewport.displayName =
|
||||
NavigationMenuPrimitive.Viewport.displayName;
|
||||
NavigationMenuViewport.displayName
|
||||
= NavigationMenuPrimitive.Viewport.displayName;
|
||||
|
||||
const NavigationMenuIndicator = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||
@ -112,8 +113,8 @@ const NavigationMenuIndicator = React.forwardRef<
|
||||
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
));
|
||||
NavigationMenuIndicator.displayName =
|
||||
NavigationMenuPrimitive.Indicator.displayName;
|
||||
NavigationMenuIndicator.displayName
|
||||
= NavigationMenuPrimitive.Indicator.displayName;
|
||||
|
||||
export {
|
||||
NavigationMenu,
|
||||
|
@ -30,10 +30,10 @@ export default function NumberTicker({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
isInView &&
|
||||
setTimeout(() => {
|
||||
motionValue.set(direction === "down" ? 0 : value);
|
||||
}, delay * 1000);
|
||||
isInView
|
||||
&& setTimeout(() => {
|
||||
motionValue.set(direction === "down" ? 0 : value);
|
||||
}, delay * 1000);
|
||||
}, [motionValue, isInView, delay, value, direction]);
|
||||
|
||||
useEffect(
|
||||
|
@ -1,12 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface MousePosition {
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type MousePosition = {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
};
|
||||
|
||||
function MousePosition(): MousePosition {
|
||||
const [mousePosition, setMousePosition] = useState<MousePosition>({
|
||||
@ -29,7 +30,7 @@ function MousePosition(): MousePosition {
|
||||
return mousePosition;
|
||||
}
|
||||
|
||||
interface ParticlesProps {
|
||||
type ParticlesProps = {
|
||||
className?: string;
|
||||
quantity?: number;
|
||||
staticity?: number;
|
||||
@ -39,18 +40,18 @@ interface ParticlesProps {
|
||||
color?: string;
|
||||
vx?: number;
|
||||
vy?: number;
|
||||
}
|
||||
};
|
||||
function hexToRgb(hex: string): number[] {
|
||||
hex = hex.replace("#", "");
|
||||
|
||||
if (hex.length === 3) {
|
||||
hex = hex
|
||||
.split("")
|
||||
.map((char) => char + char)
|
||||
.map(char => char + char)
|
||||
.join("");
|
||||
}
|
||||
|
||||
const hexInt = parseInt(hex, 16);
|
||||
const hexInt = Number.parseInt(hex, 16);
|
||||
const red = (hexInt >> 16) & 255;
|
||||
const green = (hexInt >> 8) & 255;
|
||||
const blue = hexInt & 255;
|
||||
@ -150,7 +151,7 @@ const Particles: React.FC<ParticlesProps> = ({
|
||||
const translateY = 0;
|
||||
const pSize = Math.floor(Math.random() * 2) + size;
|
||||
const alpha = 0;
|
||||
const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
|
||||
const targetAlpha = Number.parseFloat((Math.random() * 0.6 + 0.1).toFixed(1));
|
||||
const dx = (Math.random() - 0.5) * 0.1;
|
||||
const dy = (Math.random() - 0.5) * 0.1;
|
||||
const magnetism = 0.1 + Math.random() * 4;
|
||||
@ -213,8 +214,8 @@ const Particles: React.FC<ParticlesProps> = ({
|
||||
start2: number,
|
||||
end2: number,
|
||||
): number => {
|
||||
const remapped =
|
||||
((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
|
||||
const remapped
|
||||
= ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
|
||||
return remapped > 0 ? remapped : 0;
|
||||
};
|
||||
|
||||
@ -229,7 +230,7 @@ const Particles: React.FC<ParticlesProps> = ({
|
||||
canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
|
||||
];
|
||||
const closestEdge = edge.reduce((a, b) => Math.min(a, b));
|
||||
const remapClosestEdge = parseFloat(
|
||||
const remapClosestEdge = Number.parseFloat(
|
||||
remapValue(closestEdge, 0, 20, 0, 1).toFixed(2),
|
||||
);
|
||||
if (remapClosestEdge > 1) {
|
||||
@ -237,26 +238,27 @@ const Particles: React.FC<ParticlesProps> = ({
|
||||
if (circle.alpha > circle.targetAlpha) {
|
||||
circle.alpha = circle.targetAlpha;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
circle.alpha = circle.targetAlpha * remapClosestEdge;
|
||||
}
|
||||
circle.x += circle.dx + vx;
|
||||
circle.y += circle.dy + vy;
|
||||
circle.translateX +=
|
||||
(mouse.current.x / (staticity / circle.magnetism) - circle.translateX) /
|
||||
ease;
|
||||
circle.translateY +=
|
||||
(mouse.current.y / (staticity / circle.magnetism) - circle.translateY) /
|
||||
ease;
|
||||
circle.translateX
|
||||
+= (mouse.current.x / (staticity / circle.magnetism) - circle.translateX)
|
||||
/ ease;
|
||||
circle.translateY
|
||||
+= (mouse.current.y / (staticity / circle.magnetism) - circle.translateY)
|
||||
/ ease;
|
||||
|
||||
drawCircle(circle, true);
|
||||
|
||||
// circle gets out of the canvas
|
||||
if (
|
||||
circle.x < -circle.size ||
|
||||
circle.x > canvasSize.current.w + circle.size ||
|
||||
circle.y < -circle.size ||
|
||||
circle.y > canvasSize.current.h + circle.size
|
||||
circle.x < -circle.size
|
||||
|| circle.x > canvasSize.current.w + circle.size
|
||||
|| circle.y < -circle.size
|
||||
|| circle.y > canvasSize.current.h + circle.size
|
||||
) {
|
||||
// remove the circle from the array
|
||||
circles.current.splice(i, 1);
|
||||
|
@ -1,13 +1,13 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
@ -20,12 +20,12 @@ const PopoverContent = React.forwardRef<
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent }
|
||||
export { Popover, PopoverContent, PopoverTrigger };
|
||||
|
@ -1,16 +1,16 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
@ -20,7 +20,7 @@ const SelectTrigger = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@ -29,8 +29,8 @@ const SelectTrigger = React.forwardRef<
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
@ -40,14 +40,14 @@ const SelectScrollUpButton = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
@ -57,15 +57,15 @@ const SelectScrollDownButton = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
));
|
||||
SelectScrollDownButton.displayName
|
||||
= SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
@ -76,9 +76,9 @@ const SelectContent = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
position === "popper"
|
||||
&& "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
@ -87,8 +87,8 @@ const SelectContent = React.forwardRef<
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
position === "popper"
|
||||
&& "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@ -96,8 +96,8 @@ const SelectContent = React.forwardRef<
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
@ -108,8 +108,8 @@ const SelectLabel = React.forwardRef<
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
@ -119,7 +119,7 @@ const SelectItem = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@ -131,8 +131,8 @@ const SelectItem = React.forwardRef<
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
@ -143,18 +143,18 @@ const SelectSeparator = React.forwardRef<
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
||||
|
@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
@ -49,9 +51,7 @@ const sheetVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
type SheetContentProps = {} & React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content> & VariantProps<typeof sheetVariants>;
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
@ -74,32 +74,36 @@ const SheetContent = React.forwardRef<
|
||||
));
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({
|
||||
function SheetHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
SheetHeader.displayName = "SheetHeader";
|
||||
|
||||
const SheetFooter = ({
|
||||
function SheetFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
SheetFooter.displayName = "SheetFooter";
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
|
@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { Toaster as Sonner } from "sonner";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>;
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
function Toaster({ ...props }: ToasterProps) {
|
||||
const { theme = "system" } = useTheme();
|
||||
|
||||
return (
|
||||
@ -26,6 +26,6 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export { Toaster };
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { basePath } from "@/config/siteConfig";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FaGithub, FaStar } from "react-icons/fa";
|
||||
import { buttonVariants } from "./button";
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { basePath } from "@/config/site-config";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import NumberTicker from "./number-ticker";
|
||||
import { buttonVariants } from "./button";
|
||||
|
||||
export default function StarOnGithubButton() {
|
||||
const [stars, setStars] = useState(0);
|
||||
@ -23,7 +25,8 @@ export default function StarOnGithubButton() {
|
||||
const data = await res.json();
|
||||
setStars(data.stargazers_count || stars);
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error fetching stars:", error);
|
||||
}
|
||||
};
|
||||
@ -43,7 +46,8 @@ export default function StarOnGithubButton() {
|
||||
<span className="absolute right-0 -mt-12 h-32 translate-x-12 rotate-12 bg-white opacity-10 transition-all duration-1000 ease-out group-hover:-translate-x-40" />
|
||||
<div className="flex items-center">
|
||||
<FaGithub className="size-4" />
|
||||
<span className="ml-1">Star on GitHub</span>{" "}
|
||||
<span className="ml-1">Star on GitHub</span>
|
||||
{" "}
|
||||
</div>
|
||||
<div className="ml-2 flex items-center gap-1 text-sm md:flex">
|
||||
<FaStar className="size-4 text-gray-500 transition-all duration-300 group-hover:text-yellow-300" />
|
||||
|
@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
@ -12,18 +12,18 @@ const Switch = React.forwardRef<
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
));
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||
|
||||
export { Switch }
|
||||
export { Switch };
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
@ -13,16 +13,16 @@ const Table = React.forwardRef<
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
));
|
||||
Table.displayName = "Table";
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
));
|
||||
TableHeader.displayName = "TableHeader";
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
@ -33,8 +33,8 @@ const TableBody = React.forwardRef<
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
));
|
||||
TableBody.displayName = "TableBody";
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
@ -44,12 +44,12 @@ const TableFooter = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
));
|
||||
TableFooter.displayName = "TableFooter";
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
@ -59,12 +59,12 @@ const TableRow = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
));
|
||||
TableRow.displayName = "TableRow";
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
@ -74,12 +74,12 @@ const TableHead = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
));
|
||||
TableHead.displayName = "TableHead";
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
@ -89,12 +89,12 @@ const TableCell = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
));
|
||||
TableCell.displayName = "TableCell";
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
@ -105,16 +105,16 @@ const TableCaption = React.forwardRef<
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
));
|
||||
TableCaption.displayName = "TableCaption";
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Textarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
@ -10,13 +10,13 @@ const Textarea = React.forwardRef<
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Textarea.displayName = "Textarea"
|
||||
);
|
||||
});
|
||||
Textarea.displayName = "Textarea";
|
||||
|
||||
export { Textarea }
|
||||
export { Textarea };
|
||||
|
@ -2,21 +2,24 @@
|
||||
|
||||
import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Button } from "./button";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "./tooltip";
|
||||
import { Button } from "./button";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme, theme: currentTheme } = useTheme();
|
||||
|
||||
const handleChangeTheme = (theme: "light" | "dark") => {
|
||||
if (theme === currentTheme) return;
|
||||
if (theme === currentTheme)
|
||||
return;
|
||||
|
||||
if (!document.startViewTransition) return setTheme(theme);
|
||||
if (!document.startViewTransition)
|
||||
return setTheme(theme);
|
||||
document.startViewTransition(() => setTheme(theme));
|
||||
};
|
||||
|
||||
@ -31,8 +34,7 @@ export function ThemeToggle() {
|
||||
className="px-2"
|
||||
aria-label="Toggle theme"
|
||||
onClick={() =>
|
||||
handleChangeTheme(currentTheme === "dark" ? "light" : "dark")
|
||||
}
|
||||
handleChangeTheme(currentTheme === "dark" ? "light" : "dark")}
|
||||
>
|
||||
<SunIcon className="size-[1.2rem] text-neutral-800 dark:hidden dark:text-neutral-200" />
|
||||
<MoonIcon className="hidden size-[1.2rem] text-neutral-800 dark:block dark:text-neutral-200" />
|
||||
|
@ -1,15 +1,15 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
@ -20,11 +20,11 @@ const TooltipContent = React.forwardRef<
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
|
||||
|
@ -25,9 +25,9 @@ export const FAQ_Items = [
|
||||
"Updates via our LXC scripts might not pull the absolute latest version for a few reasons:\n- A bug in the application's release naming on GitHub.\n- A bug in our update script.\n- We intentionally pinned the version. This happens if a newer version has breaking changes or serious bugs that could affect your data or LXC stability. We wait for fixes before allowing the update.",
|
||||
},
|
||||
{
|
||||
title: 'Why am I getting a "502 Bad Gateway" error?',
|
||||
title: "Why am I getting a \"502 Bad Gateway\" error?",
|
||||
content:
|
||||
'A "502 Bad Gateway" error usually means the application inside the LXC is not running or responding correctly. Check the application\'s logs first. If you use a reverse proxy, check its logs too. If you still have problems after checking the logs, report the issue, providing details from the logs.',
|
||||
"A \"502 Bad Gateway\" error usually means the application inside the LXC is not running or responding correctly. Check the application's logs first. If you use a reverse proxy, check its logs too. If you still have problems after checking the logs, report the issue, providing details from the logs.",
|
||||
},
|
||||
{
|
||||
title: "What should I do if a script fails during execution?",
|
72
frontend/src/config/site-config.tsx
Normal file
72
frontend/src/config/site-config.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { MessagesSquare, Scroll } from "lucide-react";
|
||||
import { FaDiscord, FaGithub } from "react-icons/fa";
|
||||
import React from "react";
|
||||
|
||||
import type { OperatingSystem } from "@/lib/types";
|
||||
|
||||
// eslint-disable-next-line node/no-process-env
|
||||
export const basePath = process.env.BASE_PATH || "";
|
||||
|
||||
export const navbarLinks = [
|
||||
{
|
||||
href: `https://github.com/community-scripts/${basePath}`,
|
||||
event: "Github",
|
||||
icon: <FaGithub className="h-4 w-4" />,
|
||||
text: "Github",
|
||||
},
|
||||
{
|
||||
href: `https://discord.gg/2wvnMDgdnU`,
|
||||
event: "Discord",
|
||||
icon: <FaDiscord className="h-4 w-4" />,
|
||||
text: "Discord",
|
||||
},
|
||||
{
|
||||
href: `https://github.com/community-scripts/${basePath}/blob/main/CHANGELOG.md`,
|
||||
event: "Change Log",
|
||||
icon: <Scroll className="h-4 w-4" />,
|
||||
text: "Change Log",
|
||||
mobileHidden: true,
|
||||
},
|
||||
{
|
||||
href: `https://github.com/community-scripts/${basePath}/discussions`,
|
||||
event: "Discussions",
|
||||
icon: <MessagesSquare className="h-4 w-4" />,
|
||||
text: "Discussions",
|
||||
mobileHidden: true,
|
||||
},
|
||||
].filter(Boolean) as {
|
||||
href: string;
|
||||
event: string;
|
||||
icon: React.ReactNode;
|
||||
text: string;
|
||||
mobileHidden?: boolean;
|
||||
}[];
|
||||
|
||||
export const mostPopularScripts = ["post-pve-install", "docker", "homeassistant"];
|
||||
|
||||
export const analytics = {
|
||||
url: "analytics.community-scripts.org",
|
||||
token: "aefee1b9-2a12-4ac2-9d82-a63113edc62e",
|
||||
};
|
||||
|
||||
export const AlertColors = {
|
||||
warning: "border-red-500/25 bg-destructive/25",
|
||||
info: "border-cyan-500/25 bg-cyan-50 dark:border-cyan-900 dark:bg-cyan-900/25",
|
||||
};
|
||||
|
||||
export const OperatingSystems: OperatingSystem[] = [
|
||||
{
|
||||
name: "Debian",
|
||||
versions: [
|
||||
{ name: "11", slug: "bullseye" },
|
||||
{ name: "12", slug: "bookworm" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Ubuntu",
|
||||
versions: [
|
||||
{ name: "22.04", slug: "jammy" },
|
||||
{ name: "24.04", slug: "noble" },
|
||||
],
|
||||
},
|
||||
];
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user