Compare commits

...

84 Commits

Author SHA1 Message Date
8a91b87f4c Update CHANGELOG.md (#5633)
Some checks failed
Auto Update .app-files / update-app-files (push) Has been cancelled
Create Changelog Pull Request / update-changelog-pull-request (push) Has been cancelled
Close Discussion on PR Merge / close-discussion (push) Has been cancelled
Frontend CI/CD / build (push) Has been cancelled
Frontend CI/CD / deploy (push) Has been cancelled
Sync to Gitea / sync (push) Has been cancelled
Crawl Versions from newreleases.io / crawl-versions (push) Has been cancelled
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-07-02 01:15:44 +01:00
020a4b3597 Update versions.json (#5632)
Co-authored-by: GitHub Actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-07-02 02:15:06 +02:00
f93c568758 Update CHANGELOG.md (#5630)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-07-01 22:40:53 +01:00
d9d4444f08 Update CHANGELOG.md (#5629)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-07-01 22:38:21 +01:00
400a82e2cc Update CHANGELOG.md (#5628)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-07-01 22:37:47 +01:00
35a0d14110 Jellyfin GPU passthrough setup instruction (#5625) 2025-07-01 23:36:13 +02:00
a78dd20a2e Update CHANGELOG.md (#5627)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-07-01 22:35:55 +01:00
4af08a5c45 Update date in json (#5626)
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
2025-07-01 22:35:42 +01:00
2959d37b65 'Add new script' (#5614) 2025-07-01 23:35:18 +02:00
3275136db7 Update date in json (#5624)
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
2025-07-01 21:02:37 +01:00
c4c974a01d Update CHANGELOG.md (#5623)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-07-01 21:02:34 +01:00
4fe05d09a2 ITSM-NG (#5615)
* 'Add new script'

* Update itsm-ng-install.sh

* Update itsm-ng-install.sh

---------

Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
Co-authored-by: Slaviša Arežina <58952836+tremor021@users.noreply.github.com>
2025-07-01 22:02:06 +02:00
7b5dd6cd69 Update versions.json (#5613)
Some checks failed
Create Changelog Pull Request / update-changelog-pull-request (push) Has been cancelled
Close Discussion on PR Merge / close-discussion (push) Has been cancelled
Frontend CI/CD / build (push) Has been cancelled
Frontend CI/CD / deploy (push) Has been cancelled
Sync to Gitea / sync (push) Has been cancelled
Crawl Versions from newreleases.io / crawl-versions (push) Has been cancelled
Build and Publish Docker Image / build (push) Has been cancelled
Create Daily Release / create-daily-release (push) Has been cancelled
Co-authored-by: GitHub Actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-07-01 14:07:45 +02:00
28c779ef86 Update CHANGELOG.md (#5612)
Some checks failed
Create Changelog Pull Request / update-changelog-pull-request (push) Has been cancelled
Close Discussion on PR Merge / close-discussion (push) Has been cancelled
Sync to Gitea / sync (push) Has been cancelled
Crawl Versions from newreleases.io / crawl-versions (push) Has been cancelled
Auto Update .app-files / update-app-files (push) Has been cancelled
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-07-01 11:31:43 +01:00
8c1dac0583 Update tools.func (#5608) 2025-07-01 12:31:06 +02:00
2db514f666 Update CHANGELOG.md (#5610)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-07-01 08:46:41 +01:00
0232dbd89d Update openwebui.sh (#5601) 2025-07-01 09:46:07 +02:00
70f43cb904 Update CHANGELOG.md (#5606)
Some checks failed
Auto Update .app-files / update-app-files (push) Has been cancelled
Create Changelog Pull Request / update-changelog-pull-request (push) Has been cancelled
Close Discussion on PR Merge / close-discussion (push) Has been cancelled
Sync to Gitea / sync (push) Has been cancelled
Frontend CI/CD / build (push) Has been cancelled
Frontend CI/CD / deploy (push) Has been cancelled
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-07-01 01:17:34 +01:00
05e06d0782 Update versions.json (#5605)
Co-authored-by: GitHub Actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-07-01 02:17:00 +02:00
89c39783b4 Update CHANGELOG.md (#5602)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-30 21:36:52 +01:00
3f3278b022 Fixing Cloudflare DDNS - lack of resources (#5600) 2025-06-30 22:36:11 +02:00
bda9f482c1 Update CHANGELOG.md (#5599)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-30 21:09:58 +01:00
7292dbb8c6 Update .app files (#5597)
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
2025-06-30 21:46:06 +02:00
2bdf85db39 Update CHANGELOG.md (#5596)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-30 20:23:55 +01:00
546bff56fa Alpine Syncthing (#5586)
* Alpine-Syncthing

* Update syncthing.json
2025-06-30 21:18:45 +02:00
1896f2db0f Update CHANGELOG.md (#5594)
Some checks failed
Auto Update .app-files / update-app-files (push) Has been cancelled
Create Changelog Pull Request / update-changelog-pull-request (push) Has been cancelled
Close Discussion on PR Merge / close-discussion (push) Has been cancelled
Frontend CI/CD / build (push) Has been cancelled
Frontend CI/CD / deploy (push) Has been cancelled
Sync to Gitea / sync (push) Has been cancelled
Crawl Versions from newreleases.io / crawl-versions (push) Has been cancelled
Build and Publish Docker Image / build (push) Has been cancelled
Create Daily Release / create-daily-release (push) Has been cancelled
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-30 19:26:17 +01:00
e47828f0a4 Update date in json (#5593)
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
2025-06-30 19:25:01 +01:00
c511f7d9e5 Kapowarr (#5584) 2025-06-30 20:24:26 +02:00
a106e7e358 Update CHANGELOG.md (#5592)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-30 19:23:49 +01:00
254f2b894d tools.func: optimize binary build installs with helper (#5588) 2025-06-30 20:23:25 +02:00
733251a0a2 Update apache-guacamole-install.sh (#5587) 2025-06-30 16:05:17 +02:00
bcfa05db47 Update CHANGELOG.md (#5585)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-30 14:22:34 +01:00
5313f00edb Immich: make changes to automatically enable QuickSync (#5560)
- In previous versions of the script, transcoding was enabled only if
you chose to enable OpenVINO
- Recently that was decoupled, but a couple of things were overlooked
- Now, even if you elect to not enable OpenVINO, the necessary
permission and group changes will be made to the immich user (or the
root user if choosing a privileged LXC) regardless.
2025-06-30 15:21:36 +02:00
f83bfd1598 fix jar 2025-06-30 15:12:23 +02:00
fd27524479 Update versions.json (#5583)
Co-authored-by: GitHub Actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-30 14:07:48 +02:00
8155fea034 Update CHANGELOG.md (#5581)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-30 12:26:34 +01:00
c853054067 Apache Guacamole: Install auth-jdbc component that matches release version (#5563)
* Pull jdbc auth matching release version

* jdbc 9.3

---------

Co-authored-by: CanbiZ <47820557+MickLesk@users.noreply.github.com>
2025-06-30 13:26:08 +02:00
a80ec39740 fix broken ip6 config file
Some checks failed
Create Changelog Pull Request / update-changelog-pull-request (push) Has been cancelled
Close Discussion on PR Merge / close-discussion (push) Has been cancelled
Sync to Gitea / sync (push) Has been cancelled
Crawl Versions from newreleases.io / crawl-versions (push) Has been cancelled
2025-06-30 12:39:29 +02:00
bb33d00829 Update api.func 2025-06-30 12:33:57 +02:00
be64a6700d Filebrowser: change exclude "folders" to "folderPaths" (#5576) 2025-06-30 12:12:14 +02:00
247bc549e8 Update CHANGELOG.md (#5577)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-30 11:12:11 +01:00
b26c5c9354 [core]: add ipv6 configuration support (#5575) 2025-06-30 12:11:44 +02:00
5e5c79ef29 Update CHANGELOG.md (#5572)
Some checks failed
Create Changelog Pull Request / update-changelog-pull-request (push) Has been cancelled
Close Discussion on PR Merge / close-discussion (push) Has been cancelled
Sync to Gitea / sync (push) Has been cancelled
Auto Update .app-files / update-app-files (push) Has been cancelled
Frontend CI/CD / build (push) Has been cancelled
Frontend CI/CD / deploy (push) Has been cancelled
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-30 01:16:40 +01:00
4db81b8c41 Update versions.json (#5571)
Co-authored-by: GitHub Actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-30 02:16:03 +02:00
0b97f26b13 Update CHANGELOG.md (#5569)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-29 21:02:26 +01:00
f2a21617f7 update readme with valid discord link. other one expired (#5567) 2025-06-29 22:01:36 +02:00
ed618b7144 Update CHANGELOG.md (#5568)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-29 21:01:29 +01:00
1ec71332bf Add cron-job api-key env variable to homarr script (#5204)
* Add cron-job api-key env variable to homarr script

* Update homarr.sh

---------

Co-authored-by: Tobias <96661824+CrazyWolf13@users.noreply.github.com>
2025-06-29 22:01:04 +02:00
5696dffd02 Update CHANGELOG.md (#5561)
Some checks failed
Auto Update .app-files / update-app-files (push) Has been cancelled
Create Changelog Pull Request / update-changelog-pull-request (push) Has been cancelled
Close Discussion on PR Merge / close-discussion (push) Has been cancelled
Frontend CI/CD / build (push) Has been cancelled
Frontend CI/CD / deploy (push) Has been cancelled
Sync to Gitea / sync (push) Has been cancelled
Crawl Versions from newreleases.io / crawl-versions (push) Has been cancelled
Build and Publish Docker Image / build (push) Has been cancelled
Create Daily Release / create-daily-release (push) Has been cancelled
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-29 15:35:04 +01:00
1e93f131d2 Update script-item.tsx (#5549)
* Update script-item.tsx

add space

* Update script-item.tsx

* Update script-item.tsx

---------

Co-authored-by: Bram Suurd <78373894+BramSuurdje@users.noreply.github.com>
2025-06-29 16:34:24 +02:00
022f88c30a Update versions.json (#5556)
Co-authored-by: GitHub Actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-29 14:07:10 +02:00
b661f3cbcc Update CHANGELOG.md (#5555)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-29 13:02:05 +01:00
9b97e4974a Update CHANGELOG.md (#5554)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-29 13:01:26 +01:00
e2b36b540f Linkwarden: Add backing up of data folder to the update function (#5548)
* Add backing up of data folder also

* Check for directories before backing  up
2025-06-29 14:00:38 +02:00
983a09c5db Update CHANGELOG.md (#5553)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-29 13:00:30 +01:00
f605085021 fix bug in tooltip that would always render 'updateable' (#5552)
* fix bug in tooltip that would always render 'updateable'

* Remove double InstallCommand component from ScriptItems
2025-06-29 14:00:01 +02:00
4a3b15ae0e Update CHANGELOG.md (#5547)
Some checks failed
Create Changelog Pull Request / update-changelog-pull-request (push) Has been cancelled
Close Discussion on PR Merge / close-discussion (push) Has been cancelled
Frontend CI/CD / build (push) Has been cancelled
Frontend CI/CD / deploy (push) Has been cancelled
Sync to Gitea / sync (push) Has been cancelled
Crawl Versions from newreleases.io / crawl-versions (push) Has been cancelled
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-29 01:17:25 +01:00
0fd5f366b3 Update versions.json (#5546)
Co-authored-by: GitHub Actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-29 02:16:55 +02:00
dd5b3cd1b9 push-to-gitea.yaml (#5542)
Some checks failed
Create Changelog Pull Request / update-changelog-pull-request (push) Has been cancelled
Close Discussion on PR Merge / close-discussion (push) Has been cancelled
Frontend CI/CD / build (push) Has been cancelled
Frontend CI/CD / deploy (push) Has been cancelled
Sync to Gitea / sync (push) Has been cancelled
Crawl Versions from newreleases.io / crawl-versions (push) Has been cancelled
Auto Update .app-files / update-app-files (push) Has been cancelled
Build and Publish Docker Image / build (push) Has been cancelled
Create Daily Release / create-daily-release (push) Has been cancelled
2025-06-28 16:04:20 +02:00
2b55f82aab Update versions.json (#5541)
Co-authored-by: GitHub Actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-28 14:07:13 +02:00
87c6f87faf Update CHANGELOG.md (#5539)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-28 11:22:35 +01:00
caad96f25a Update CHANGELOG.md (#5538)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-28 11:22:25 +01:00
8e7978713f Ollama: Clean up old Ollama files before running update (#5534)
* Update ollama.sh

* Update openwebui.sh
2025-06-28 12:21:55 +02:00
1e05867b4c Update CHANGELOG.md (#5537)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-28 11:21:49 +01:00
43dfe6dc33 Update onlyoffice-install.sh (#5535) 2025-06-28 12:21:22 +02:00
179812e55f Update CHANGELOG.md (#5533)
Some checks failed
Auto Update .app-files / update-app-files (push) Has been cancelled
Create Changelog Pull Request / update-changelog-pull-request (push) Has been cancelled
Close Discussion on PR Merge / close-discussion (push) Has been cancelled
Frontend CI/CD / deploy (push) Has been cancelled
Frontend CI/CD / build (push) Has been cancelled
Sync to Gitea / sync (push) Has been cancelled
Crawl Versions from newreleases.io / crawl-versions (push) Has been cancelled
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-28 07:23:53 +01:00
d09cf45a3c Update booklore.json (#5528) 2025-06-28 08:23:12 +02:00
e609868619 uptime-kuma fix 2025-06-28 08:11:40 +02:00
692ac62add Update CHANGELOG.md (#5532)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-28 01:15:22 +01:00
216cc7e5c3 Update versions.json (#5531)
Co-authored-by: GitHub Actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-28 02:14:47 +02:00
bcc113406a Update CHANGELOG.md (#5529)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-27 23:38:36 +01:00
0067075ed1 Remove npm legacy errors, created single source of truth for ESlint. updated analytics url. updated script background (#5498)
* Update ScriptAccordion and ScriptItem components for improved styling

* Add README.md for Proxmox VE Helper-Scripts Frontend

* Remove testing dependencies and related test files from the frontend project

* Update analytics URL in siteConfig to point to community-scripts.org

* Refactor ESLint configuration to have one source of truth and run "npm lint" to apply new changes

* Update lint script in package.json to remove npm

* Add 'next' option to ESLint configuration for improved compatibility

* Update package dependencies and versions in package.json and package-lock.json

* Refactor theme provider import and enhance calendar component for dynamic icon rendering

* rename sidebar, alerts and buttons

* rename description and interfaces files

* rename more files

* change folder name

* Refactor tooltip logic to improve updateable condition handling

* Enhance CommandMenu to prevent duplicate scripts across categories

* Remove test step from frontend CI/CD workflow
2025-06-28 00:38:09 +02:00
d60911a063 Update CHANGELOG.md (#5527)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-27 22:12:57 +01:00
abad754f61 Update CHANGELOG.md (#5526)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-27 22:12:02 +01:00
a632d315ab Update date in json (#5525)
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
2025-06-27 22:11:49 +01:00
520bae01d6 BookLore (#5524) 2025-06-27 23:11:24 +02:00
7057fba151 push-to-gitea.yaml (#5523) 2025-06-27 20:51:09 +02:00
e24ca6472c Update CHANGELOG.md (#5522)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-27 16:41:35 +01:00
028feb363f Update wireguard.json (#5514) 2025-06-27 17:40:59 +02:00
491b341fdf push-to-gitea.yaml (#5521) 2025-06-27 17:17:22 +02:00
db77e42a50 Update CHANGELOG.md (#5520)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-27 15:04:47 +01:00
cf3f790f03 wizarr: remove unneeded tmp file (#5517) 2025-06-27 16:04:11 +02:00
a0da56997c Workflow to gitea (forgot git add and commit) (#5513)
* New workflow to push to gitea and change links

* Update workflow
2025-06-27 16:03:27 +02:00
c000235d81 Update versions.json (#5518)
Co-authored-by: GitHub Actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-27 14:07:44 +02:00
133 changed files with 9262 additions and 4418 deletions

View File

@ -44,9 +44,6 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm ci --prefer-offline --legacy-peer-deps run: npm ci --prefer-offline --legacy-peer-deps
- name: Run tests
run: npm run test
- name: Configure Next.js for pages - name: Configure Next.js for pages
uses: actions/configure-pages@v5 uses: actions/configure-pages@v5
with: with:

View File

@ -26,19 +26,23 @@ jobs:
if [ -n "$files_with_github_urls" ]; then if [ -n "$files_with_github_urls" ]; then
echo "$files_with_github_urls" | while read file; do echo "$files_with_github_urls" | while read file; do
if [ -f "$file" ]; then 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 fi
done done
else else
echo "No files found containing GitHub raw URLs" echo "No files found containing GitHub raw URLs"
fi fi
- name: Push to Gitea - name: Push to Gitea
run: | run: |
git config --global user.name "Push From Github" git config --global user.name "Push From Github"
git config --global user.email "actions@github.com" 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 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: env:
GITEA_USER: ${{ secrets.GITEA_USERNAME }} GITEA_USER: ${{ secrets.GITEA_USERNAME }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}

View File

@ -14,14 +14,121 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
All LXC instances created using this repository come pre-installed with Midnight Commander, which is a command-line tool (`mc`) that offers a user-friendly file and directory management interface for the terminal environment. All LXC instances created using this repository come pre-installed with Midnight Commander, which is a command-line tool (`mc`) that offers a user-friendly file and directory management interface for the terminal environment.
## 2025-06-27 ## 2025-07-02
## 2025-07-01
### 🆕 New Scripts
- Librespeed Rust ([#5614](https://github.com/community-scripts/ProxmoxVE/pull/5614))
- ITSM-NG ([#5615](https://github.com/community-scripts/ProxmoxVE/pull/5615))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- Open WebUI: Fix Ollama update procedure [@tremor021](https://github.com/tremor021) ([#5601](https://github.com/community-scripts/ProxmoxVE/pull/5601))
- #### ✨ New Features
- [tools]: increase fetch_and_deploy with dns pre check [@MickLesk](https://github.com/MickLesk) ([#5608](https://github.com/community-scripts/ProxmoxVE/pull/5608))
### 🌐 Website
- #### 📝 Script Information
- Jellyfin GPU Passthrough NVIDIA Note [@austinpilz](https://github.com/austinpilz) ([#5625](https://github.com/community-scripts/ProxmoxVE/pull/5625))
## 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
- Fixing Cloudflare DDNS - lack of resources [@meszolym](https://github.com/meszolym) ([#5600](https://github.com/community-scripts/ProxmoxVE/pull/5600))
- #### 🐞 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 ### 🧰 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 - #### 📂 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)) - 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 ## 2025-06-26
### 🆕 New Scripts ### 🆕 New Scripts

View File

@ -13,7 +13,7 @@
<a href="https://helper-scripts.com"> <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" /> <img src="https://img.shields.io/badge/Website-4c9b3f?style=for-the-badge&logo=github&logoColor=white" alt="Website" />
</a> </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" /> <img src="https://img.shields.io/badge/Discord-7289da?style=for-the-badge&logo=discord&logoColor=white" alt="Discord" />
</a> </a>
<a href="https://ko-fi.com/community_scripts"> <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: 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). - **GitHub Discussions**: [Ask questions or report issues](https://github.com/community-scripts/ProxmoxVE/discussions).
## 🤝 Report a Bug or Feature Request ## 🤝 Report a Bug or Feature Request

45
ct/alpine-syncthing.sh Normal file
View 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
View 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}"

View File

@ -7,8 +7,8 @@ source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxV
APP="Cloudflare-DDNS" APP="Cloudflare-DDNS"
var_tags="${var_tags:-network}" var_tags="${var_tags:-network}"
var_cpu="${var_cpu:-1}" var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-512}" var_ram="${var_ram:-1024}"
var_disk="${var_disk:-3}" var_disk="${var_disk:-3}"
var_os="${var_os:-debian}" var_os="${var_os:-debian}"
var_version="${var_version:-12}" var_version="${var_version:-12}"

View File

@ -0,0 +1,6 @@
___ __ _ _____ __ __ _
/ | / /___ (_)___ ___ / ___/__ ______ _____/ /_/ /_ (_)___ ____ _
/ /| | / / __ \/ / __ \/ _ \______\__ \/ / / / __ \/ ___/ __/ __ \/ / __ \/ __ `/
/ ___ |/ / /_/ / / / / / __/_____/__/ / /_/ / / / / /__/ /_/ / / / / / / / /_/ /
/_/ |_/_/ .___/_/_/ /_/\___/ /____/\__, /_/ /_/\___/\__/_/ /_/_/_/ /_/\__, /
/_/ /____/ /____/

6
ct/headers/booklore Normal file
View File

@ -0,0 +1,6 @@
____ __ __
/ __ )____ ____ / /__/ / ____ ________
/ __ / __ \/ __ \/ //_/ / / __ \/ ___/ _ \
/ /_/ / /_/ / /_/ / ,< / /___/ /_/ / / / __/
/_____/\____/\____/_/|_/_____/\____/_/ \___/

6
ct/headers/itsm-ng Normal file
View File

@ -0,0 +1,6 @@
_______________ __ ___ _ ________
/ _/_ __/ ___// |/ / / | / / ____/
/ / / / \__ \/ /|_/ /_____/ |/ / / __
_/ / / / ___/ / / / /_____/ /| / /_/ /
/___/ /_/ /____/_/ /_/ /_/ |_/\____/

6
ct/headers/kapowarr Normal file
View File

@ -0,0 +1,6 @@
__ __
/ //_/___ _____ ____ _ ______ ___________
/ ,< / __ `/ __ \/ __ \ | /| / / __ `/ ___/ ___/
/ /| / /_/ / /_/ / /_/ / |/ |/ / /_/ / / / /
/_/ |_\__,_/ .___/\____/|__/|__/\__,_/_/ /_/
/_/

View File

@ -0,0 +1,6 @@
__ _ __ __ ____ __
/ / (_) /_ ________ _________ ___ ___ ____/ / / __ \__ _______/ /_
/ / / / __ \/ ___/ _ \/ ___/ __ \/ _ \/ _ \/ __ /_____/ /_/ / / / / ___/ __/
/ /___/ / /_/ / / / __(__ ) /_/ / __/ __/ /_/ /_____/ _, _/ /_/ (__ ) /_
/_____/_/_.___/_/ \___/____/ .___/\___/\___/\__,_/ /_/ |_|\__,_/____/\__/
/_/

View File

@ -48,6 +48,7 @@ source /opt/homarr/.env
set +a set +a
export DB_DIALECT='sqlite' export DB_DIALECT='sqlite'
export AUTH_SECRET=$(openssl rand -base64 32) 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 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 for dir in $(find /opt/homarr_db/migrations/migrations -mindepth 1 -maxdepth 1 -type d); do
dirname=$(basename "$dir") dirname=$(basename "$dir")
@ -114,6 +115,7 @@ source /opt/homarr/.env
set +a set +a
export DB_DIALECT='sqlite' export DB_DIALECT='sqlite'
export AUTH_SECRET=$(openssl rand -base64 32) 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 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 for dir in $(find /opt/homarr_db/migrations/migrations -mindepth 1 -maxdepth 1 -type d); do
dirname=$(basename "$dir") dirname=$(basename "$dir")

47
ct/itsm-ng.sh Normal file
View File

@ -0,0 +1,47 @@
#!/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: Florianb63
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://itsm-ng.com/
APP="ITSM-NG"
var_tags="${var_tags:-asset-management;foss}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-10}"
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/itsm-ng/config_db.php ]]; then
msg_error "No ${APP} Installation Found!"
exit 1
fi
msg_info "Updating ${APP} LXC"
$STD apt-get update
$STD apt-get -y upgrade
msg_ok "Updated Successfully"
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}${CL}"

66
ct/kapowarr.sh Normal file
View 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}"

54
ct/librespeed-rust.sh Normal file
View File

@ -0,0 +1,54 @@
#!/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: Joseph Stubberfield (stubbers)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/librespeed/speedtest-rust
APP="Librespeed-Rust"
var_tags="${var_tags:-network}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-512}"
var_disk="${var_disk:-4}"
var_os="${var_os:-debian}"
var_version="${var_version:-12}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /var/lib/librespeed-rs ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
RELEASE=$(curl -fsSL https://api.github.com/repos/librespeed/speedtest-rust/releases/latest | grep '"tag_name"' | sed -E 's/.*"tag_name": "v([^"]+).*/\1/')
if [[ "${RELEASE}" != "$(cat ~/.librespeed 2>/dev/null)" ]] || [[ ! -f ~/.librespeed ]]; then
msg_info "Stopping Services"
systemctl stop librespeed-rs
msg_ok "Services Stopped"
fetch_and_deploy_gh_release "librespeed-rust" "librespeed/speedtest-rust" "binary" "latest" "/opt/librespeed-rust" "librespeed-rs-x86_64-unknown-linux-gnu.deb"
msg_info "Starting Service"
systemctl start librespeed-rs
msg_ok "Started Service"
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}:8080${CL}"

View File

@ -37,6 +37,7 @@ function update_script() {
msg_info "Updating ${APP} to ${RELEASE}" msg_info "Updating ${APP} to ${RELEASE}"
mv /opt/linkwarden/.env /opt/.env mv /opt/linkwarden/.env /opt/.env
[ -d /opt/linkwarden/data ] && mv /opt/linkwarden/data /opt/data.bak
rm -rf /opt/linkwarden rm -rf /opt/linkwarden
fetch_and_deploy_gh_release "linkwarden" "linkwarden/linkwarden" fetch_and_deploy_gh_release "linkwarden" "linkwarden/linkwarden"
cd /opt/linkwarden cd /opt/linkwarden
@ -47,6 +48,7 @@ function update_script() {
$STD yarn prisma:generate $STD yarn prisma:generate
$STD yarn web:build $STD yarn web:build
$STD yarn prisma:deploy $STD yarn prisma:deploy
[ -d /opt/data.bak ] && mv /opt/data.bak /opt/linkwarden/data
msg_ok "Updated ${APP} to ${RELEASE}" msg_ok "Updated ${APP} to ${RELEASE}"
msg_info "Starting ${APP}" msg_info "Starting ${APP}"

View File

@ -38,6 +38,8 @@ function update_script() {
TMP_TAR=$(mktemp --suffix=.tgz) TMP_TAR=$(mktemp --suffix=.tgz)
curl -fL# -o "${TMP_TAR}" "https://github.com/ollama/ollama/releases/download/${RELEASE}/ollama-linux-amd64.tgz" curl -fL# -o "${TMP_TAR}" "https://github.com/ollama/ollama/releases/download/${RELEASE}/ollama-linux-amd64.tgz"
msg_info "Updating Ollama to ${RELEASE}" 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 tar -xzf "${TMP_TAR}" -C /usr/local/lib/ollama
ln -sf /usr/local/lib/ollama/bin/ollama /usr/local/bin/ollama ln -sf /usr/local/lib/ollama/bin/ollama /usr/local/bin/ollama
echo "${RELEASE}" >/opt/Ollama_version.txt echo "${RELEASE}" >/opt/Ollama_version.txt

View File

@ -33,6 +33,8 @@ function update_script() {
OLLAMA_VERSION=$(ollama -v | awk '{print $NF}') 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)}') 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 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 curl -fsSLO https://ollama.com/download/ollama-linux-amd64.tgz
tar -C /usr -xzf ollama-linux-amd64.tgz tar -C /usr -xzf ollama-linux-amd64.tgz
rm -rf ollama-linux-amd64.tgz rm -rf ollama-linux-amd64.tgz

View File

@ -60,7 +60,6 @@ function update_script() {
msg_info "Cleaning Up" msg_info "Cleaning Up"
rm -rf "$BACKUP_FILE" rm -rf "$BACKUP_FILE"
rm /tmp/"$RELEASE".zip
msg_ok "Cleanup Completed" msg_ok "Cleanup Completed"
msg_ok "Update Successful" msg_ok "Update Successful"
else else

View File

@ -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
View 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
View 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.
![Next.js](https://img.shields.io/badge/Next.js-15.2.4-black?style=flat-square&logo=next.js)
![React](https://img.shields.io/badge/React-19.0.0-blue?style=flat-square&logo=react)
![TypeScript](https://img.shields.io/badge/TypeScript-5.8.2-blue?style=flat-square&logo=typescript)
![Tailwind CSS](https://img.shields.io/badge/Tailwind-3.4.17-06B6D4?style=flat-square&logo=tailwindcss)
![License](https://img.shields.io/badge/License-MIT-green?style=flat-square)
## 🌟 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**

View 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"],
},
],
},
},
);

File diff suppressed because it is too large Load Diff

30
frontend/package.json generated
View File

@ -1,22 +1,18 @@
{ {
"name": "proxmox-helper-scripts-website", "name": "proxmox-helper-scripts-website",
"type": "module",
"version": "1.0.0", "version": "1.0.0",
"license": "MIT",
"private": true, "private": true,
"author": { "author": {
"name": "Bram Suurd", "name": "Bram Suurd",
"url": "https://github.com/community-scripts" "url": "https://github.com/community-scripts"
}, },
"type": "module", "license": "MIT",
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev --turbopack",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "eslint . --fix",
"test": "vitest",
"deploy": "next build && touch out/.nojekyll && git add out/ && git commit -m \"Deploy\" && git subtree push --prefix out origin gh-pages",
"format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache",
"format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
@ -45,7 +41,7 @@
"lucide-react": "^0.453.0", "lucide-react": "^0.453.0",
"mini-svg-data-uri": "^1.4.4", "mini-svg-data-uri": "^1.4.4",
"next": "15.2.4", "next": "15.2.4",
"next-themes": "^0.3.0", "next-themes": "^0.4.4",
"nuqs": "^2.4.1", "nuqs": "^2.4.1",
"pocketbase": "^0.21.5", "pocketbase": "^0.21.5",
"prettier-plugin-organize-imports": "^4.1.0", "prettier-plugin-organize-imports": "^4.1.0",
@ -53,7 +49,7 @@
"react-chartjs-2": "^5.3.0", "react-chartjs-2": "^5.3.0",
"react-code-blocks": "^0.1.6", "react-code-blocks": "^0.1.6",
"react-datepicker": "^7.6.0", "react-datepicker": "^7.6.0",
"react-day-picker": "8.10.1", "react-day-picker": "^9.4.3",
"react-dom": "19.0.0", "react-dom": "19.0.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-simple-typewriter": "^5.0.1", "react-simple-typewriter": "^5.0.1",
@ -64,9 +60,10 @@
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^4.16.1",
"@eslint-react/eslint-plugin": "^1.52.2",
"@next/eslint-plugin-next": "^15.3.4",
"@tanstack/eslint-plugin-query": "^5.68.0", "@tanstack/eslint-plugin-query": "^5.68.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/node": "^22.13.16", "@types/node": "^22.13.16",
"@types/react": "npm:types-react@19.0.0-rc.1", "@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
@ -75,6 +72,9 @@
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.23.0", "eslint": "^9.23.0",
"eslint-config-next": "15.0.2", "eslint-config-next": "15.0.2",
"eslint-plugin-format": "^1.0.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"jsdom": "^25.0.1", "jsdom": "^25.0.1",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"prettier": "^3.5.3", "prettier": "^3.5.3",
@ -83,11 +83,13 @@
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tailwindcss-animated": "^1.1.2", "tailwindcss-animated": "^1.1.2",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4"
"vitest": "^3.1.1"
}, },
"overrides": { "overrides": {
"@types/react": "npm:types-react@19.0.0-rc.1", "@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1" "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"date-fns": "^4.1.0",
"react": "19.0.0",
"react-dom": "19.0.0"
} }
} }

35
frontend/public/json/booklore.json generated Normal file
View 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": []
}

View File

@ -19,8 +19,8 @@
"type": "default", "type": "default",
"script": "ct/cloudflare-ddns.sh", "script": "ct/cloudflare-ddns.sh",
"resources": { "resources": {
"cpu": 1, "cpu": 2,
"ram": 512, "ram": 1024,
"hdd": 2, "hdd": 2,
"os": "Debian", "os": "Debian",
"version": "12" "version": "12"

35
frontend/public/json/itsm-ng.json generated Normal file
View File

@ -0,0 +1,35 @@
{
"name": "ITSM-NG",
"slug": "itsm-ng",
"categories": [
25
],
"date_created": "2025-07-01",
"type": "ct",
"updateable": true,
"privileged": false,
"interface_port": 80,
"documentation": "https://wiki.itsm-ng.org/en/home",
"website": "https://itsm-ng.com",
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/itsm-ng.svg",
"config_path": "/etc/itsm-ng",
"description": "ITSM-NG is a powerful, open-source IT Service Management (ITSM) solution designed for managing IT assets, software, licenses, and support processes in accordance with ITIL best practices. It offers integrated features for asset inventory, incident tracking, problem management, change requests, and service desk workflows.",
"install_methods": [
{
"type": "default",
"script": "ct/itsm-ng.sh",
"resources": {
"cpu": 2,
"ram": 2048,
"hdd": 10,
"os": "debian",
"version": "12"
}
}
],
"default_credentials": {
"username": "itsm",
"password": "itsm"
},
"notes": []
}

View File

@ -39,6 +39,10 @@
{ {
"text": "FFmpeg path: /usr/lib/jellyfin-ffmpeg/ffmpeg", "text": "FFmpeg path: /usr/lib/jellyfin-ffmpeg/ffmpeg",
"type": "info" "type": "info"
},
{
"text": "For NVIDIA graphics cards, you'll need to install the same drivers in the container that you did on the host. In the container, run the driver installation script and add the CLI arg --no-kernel-module",
"type": "info"
} }
] ]
} }

35
frontend/public/json/kapowarr.json generated Normal file
View 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": []
}

View File

@ -0,0 +1,35 @@
{
"name": "Librespeed Rust",
"slug": "librespeed-rust",
"categories": [
4
],
"date_created": "2025-07-01",
"type": "ct",
"updateable": true,
"privileged": false,
"config_path": "/var/lib/librespeed-rs/configs.toml",
"interface_port": 8080,
"documentation": "https://github.com/librespeed/speedtest-rust",
"website": "https://github.com/librespeed/speedtest-rust",
"logo": "https://raw.githubusercontent.com/selfhst/icons/refs/heads/main/svg/librespeed.svg",
"description": "Librespeed is a no flash, no java, no websocket speedtest server. This community script deploys the rust version for simplicity and low resource usage.",
"install_methods": [
{
"type": "default",
"script": "ct/librespeed-rust.sh",
"resources": {
"cpu": 1,
"ram": 512,
"hdd": 4,
"os": "Debian",
"version": "12"
}
}
],
"default_credentials": {
"username": null,
"password": null
},
"notes": []
}

View File

@ -12,7 +12,7 @@
"documentation": null, "documentation": null,
"website": "https://syncthing.net/", "website": "https://syncthing.net/",
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/syncthing.webp", "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.", "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": [ "install_methods": [
{ {
@ -25,8 +25,19 @@
"os": "debian", "os": "debian",
"version": "12" "version": "12"
} }
} },
], {
"type": "alpine",
"script": "ct/alpine-syncthing.sh",
"resources": {
"cpu": 1,
"ram": 256,
"hdd": 1,
"os": "alpine",
"version": "3.22"
}
}
],
"default_credentials": { "default_credentials": {
"username": null, "username": null,
"password": null "password": null

View File

@ -1,14 +1,279 @@
[ [
{
"name": "mongodb/mongo",
"version": "r8.1.2",
"date": "2025-07-01T22:39:32Z"
},
{
"name": "Threadfin/Threadfin",
"version": "1.2.35",
"date": "2025-07-01T21:37:20Z"
},
{
"name": "apache/tomcat",
"version": "10.1.43",
"date": "2025-07-01T21:32:34Z"
},
{
"name": "actualbudget/actual",
"version": "v25.7.0",
"date": "2025-07-01T21:02:27Z"
},
{
"name": "home-assistant/core",
"version": "2025.6.3",
"date": "2025-06-24T13:00:12Z"
},
{
"name": "TwiN/gatus",
"version": "v5.19.0",
"date": "2025-07-01T19:59:32Z"
},
{
"name": "Koenkk/zigbee2mqtt",
"version": "2.5.0",
"date": "2025-07-01T18:28:01Z"
},
{
"name": "hivemq/hivemq-community-edition",
"version": "2025.4",
"date": "2025-07-01T18:01:37Z"
},
{
"name": "HabitRPG/habitica",
"version": "v5.37.1",
"date": "2025-07-01T16:57:43Z"
},
{
"name": "navidrome/navidrome",
"version": "v0.57.0",
"date": "2025-07-01T16:47:46Z"
},
{
"name": "jenkinsci/jenkins",
"version": "jenkins-2.517",
"date": "2025-07-01T16:08:23Z"
},
{
"name": "element-hq/synapse",
"version": "v1.133.0",
"date": "2025-07-01T15:13:42Z"
},
{
"name": "sysadminsmedia/homebox",
"version": "v0.20.1",
"date": "2025-07-01T14:18:32Z"
},
{
"name": "keycloak/keycloak",
"version": "26.3.0",
"date": "2025-07-01T13:18:12Z"
},
{
"name": "syncthing/syncthing",
"version": "v1.30.0",
"date": "2025-07-01T11:29:11Z"
},
{
"name": "Checkmk/checkmk",
"version": "v2.2.0p44-rc1",
"date": "2025-07-01T11:10:25Z"
},
{
"name": "rcourtman/Pulse",
"version": "v99.99.99",
"date": "2025-07-01T08:26:41Z"
},
{
"name": "Jackett/Jackett",
"version": "v0.22.2101",
"date": "2025-07-01T05:56:59Z"
},
{
"name": "zabbix/zabbix",
"version": "7.4.0",
"date": "2025-07-01T04:36:51Z"
},
{
"name": "openobserve/openobserve",
"version": "v0.15.0-rc3",
"date": "2025-07-01T04:09:37Z"
},
{
"name": "wazuh/wazuh",
"version": "coverity-w27-4.13.0",
"date": "2025-07-01T03:17:32Z"
},
{
"name": "NginxProxyManager/nginx-proxy-manager",
"version": "v2.12.4",
"date": "2025-07-01T01:45:42Z"
},
{
"name": "MagicMirrorOrg/MagicMirror",
"version": "v2.32.0",
"date": "2025-06-30T22:12:48Z"
},
{
"name": "docker/compose",
"version": "v2.38.1",
"date": "2025-06-30T20:07:35Z"
},
{
"name": "jhuckaby/Cronicle",
"version": "v0.9.81",
"date": "2025-06-30T16:40:33Z"
},
{
"name": "ollama/ollama",
"version": "v0.9.4-rc6",
"date": "2025-06-30T15:59:03Z"
},
{
"name": "prometheus/prometheus",
"version": "v2.53.5",
"date": "2025-06-30T11:01:12Z"
},
{
"name": "n8n-io/n8n",
"version": "n8n@1.100.0",
"date": "2025-06-23T12:48:35Z"
},
{
"name": "jupyter/notebook",
"version": "v7.4.4",
"date": "2025-06-30T13:04:22Z"
},
{
"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": "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": "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": "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": "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": "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": "MediaBrowser/Emby.Releases", "name": "MediaBrowser/Emby.Releases",
"version": "4.9.1.2", "version": "4.9.1.2",
"date": "2025-06-26T22:08:00Z" "date": "2025-06-26T22:08:00Z"
}, },
{
"name": "prometheus/prometheus",
"version": "v3.4.2",
"date": "2025-06-26T21:45:21Z"
},
{ {
"name": "home-assistant/operating-system", "name": "home-assistant/operating-system",
"version": "15.2", "version": "15.2",
@ -19,16 +284,6 @@
"version": "v4.3.3", "version": "v4.3.3",
"date": "2025-06-26T18:42:56Z" "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", "name": "apache/tika",
"version": "3.2.1-rc2", "version": "3.2.1-rc2",
@ -39,11 +294,6 @@
"version": "v1.84.3", "version": "v1.84.3",
"date": "2025-06-26T16:31:57Z" "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", "name": "traefik/traefik",
"version": "v3.5.0-rc1", "version": "v3.5.0-rc1",
@ -64,36 +314,11 @@
"version": "4.1.0-beta.2", "version": "4.1.0-beta.2",
"date": "2025-06-26T14:23:26Z" "date": "2025-06-26T14:23:26Z"
}, },
{
"name": "plexguide/Huntarr.io",
"version": "8.1.9",
"date": "2025-06-26T14:16:45Z"
},
{ {
"name": "Dolibarr/dolibarr", "name": "Dolibarr/dolibarr",
"version": "18.0.7", "version": "18.0.7",
"date": "2025-06-26T09:16:33Z" "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", "name": "gristlabs/grist-core",
"version": "v1.6.1", "version": "v1.6.1",
@ -104,56 +329,21 @@
"version": "v4.101.2", "version": "v4.101.2",
"date": "2025-06-25T21:18:52Z" "date": "2025-06-25T21:18:52Z"
}, },
{
"name": "msgbyte/tianji",
"version": "v1.22.4",
"date": "2025-06-25T20:46:20Z"
},
{ {
"name": "influxdata/influxdb", "name": "influxdata/influxdb",
"version": "v3.2.0", "version": "v3.2.0",
"date": "2025-06-25T17:31:48Z" "date": "2025-06-25T17:31:48Z"
}, },
{
"name": "keycloak/keycloak",
"version": "26.2.5",
"date": "2025-05-28T06:49:43Z"
},
{ {
"name": "wavelog/wavelog", "name": "wavelog/wavelog",
"version": "2.0.5", "version": "2.0.5",
"date": "2025-06-25T14:53:31Z" "date": "2025-06-25T14:53:31Z"
}, },
{
"name": "jenkinsci/jenkins",
"version": "jenkins-2.504.3",
"date": "2025-06-25T14:43:01Z"
},
{ {
"name": "bunkerity/bunkerweb", "name": "bunkerity/bunkerweb",
"version": "testing", "version": "testing",
"date": "2025-06-16T18:10:42Z" "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",
"date": "2025-06-23T12:48:35Z"
},
{ {
"name": "moghtech/komodo", "name": "moghtech/komodo",
"version": "v1.18.4", "version": "v1.18.4",
@ -179,56 +369,26 @@
"version": "v2.18.0", "version": "v2.18.0",
"date": "2025-06-24T08:29:55Z" "date": "2025-06-24T08:29:55Z"
}, },
{
"name": "element-hq/synapse",
"version": "v1.132.0",
"date": "2025-06-17T13:49:30Z"
},
{
"name": "docker/compose",
"version": "v2.37.3",
"date": "2025-06-24T14:05:33Z"
},
{
"name": "Checkmk/checkmk",
"version": "v2.4.0p5",
"date": "2025-06-24T13:06:53Z"
},
{ {
"name": "fallenbagel/jellyseerr", "name": "fallenbagel/jellyseerr",
"version": "preview-fix-proxy-axios", "version": "preview-fix-proxy-axios",
"date": "2025-06-24T08:50:22Z" "date": "2025-06-24T08:50:22Z"
}, },
{
"name": "wazuh/wazuh",
"version": "coverity-w26-4.13.0",
"date": "2025-06-24T02:02:34Z"
},
{ {
"name": "minio/minio", "name": "minio/minio",
"version": "RELEASE.2025-06-13T11-33-47Z", "version": "RELEASE.2025-06-13T11-33-47Z",
"date": "2025-06-23T20:58:42Z" "date": "2025-06-23T20:58:42Z"
}, },
{
"name": "esphome/esphome",
"version": "2025.6.1",
"date": "2025-06-23T19:28:09Z"
},
{ {
"name": "runtipi/runtipi", "name": "runtipi/runtipi",
"version": "nightly", "version": "v4.2.1",
"date": "2025-06-23T19:10:33Z" "date": "2025-06-03T20:04:28Z"
}, },
{ {
"name": "VictoriaMetrics/VictoriaMetrics", "name": "VictoriaMetrics/VictoriaMetrics",
"version": "pmm-6401-v1.120.0", "version": "pmm-6401-v1.120.0",
"date": "2025-06-23T15:12:12Z" "date": "2025-06-23T15:12:12Z"
}, },
{
"name": "Graylog2/graylog2-server",
"version": "6.3.0-rc.2",
"date": "2025-06-23T11:31:38Z"
},
{ {
"name": "gotson/komga", "name": "gotson/komga",
"version": "1.22.0", "version": "1.22.0",
@ -244,11 +404,6 @@
"version": "release-5.1.1", "version": "release-5.1.1",
"date": "2025-06-22T21:41:17Z" "date": "2025-06-22T21:41:17Z"
}, },
{
"name": "pocket-id/pocket-id",
"version": "v1.4.1",
"date": "2025-06-22T19:38:08Z"
},
{ {
"name": "clusterzx/paperless-ai", "name": "clusterzx/paperless-ai",
"version": "v3.0.7", "version": "v3.0.7",
@ -264,36 +419,16 @@
"version": "0.17.14", "version": "0.17.14",
"date": "2025-06-21T23:43:04Z" "date": "2025-06-21T23:43:04Z"
}, },
{
"name": "HabitRPG/habitica",
"version": "v5.37.0",
"date": "2025-06-21T14:05:12Z"
},
{ {
"name": "rogerfar/rdt-client", "name": "rogerfar/rdt-client",
"version": "v2.0.114", "version": "v2.0.114",
"date": "2025-06-21T11:20:21Z" "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", "name": "pocketbase/pocketbase",
"version": "v0.28.4", "version": "v0.28.4",
"date": "2025-06-21T08:29:04Z" "date": "2025-06-21T08:29:04Z"
}, },
{
"name": "dgtlmoon/changedetection.io",
"version": "0.50.4",
"date": "2025-06-21T07:47:02Z"
},
{ {
"name": "go-gitea/gitea", "name": "go-gitea/gitea",
"version": "v1.24.2", "version": "v1.24.2",
@ -304,26 +439,11 @@
"version": "v1.135.3", "version": "v1.135.3",
"date": "2025-06-20T20:19:20Z" "date": "2025-06-20T20:19:20Z"
}, },
{
"name": "homarr-labs/homarr",
"version": "v1.25.0",
"date": "2025-06-20T19:15:43Z"
},
{ {
"name": "Sonarr/Sonarr", "name": "Sonarr/Sonarr",
"version": "v4.0.15.2941", "version": "v4.0.15.2941",
"date": "2025-06-20T17:20:54Z" "date": "2025-06-20T17:20:54Z"
}, },
{
"name": "zabbix/zabbix",
"version": "7.2.9",
"date": "2025-06-20T10:58:45Z"
},
{
"name": "syncthing/syncthing",
"version": "2.0.0-rc.19",
"date": "2025-06-02T17:56:25Z"
},
{ {
"name": "benzino77/tasmocompiler", "name": "benzino77/tasmocompiler",
"version": "v12.7.0", "version": "v12.7.0",
@ -334,11 +454,6 @@
"version": "v2.17.1", "version": "v2.17.1",
"date": "2025-06-19T19:35:01Z" "date": "2025-06-19T19:35:01Z"
}, },
{
"name": "rclone/rclone",
"version": "v1.70.1",
"date": "2025-06-19T13:19:02Z"
},
{ {
"name": "icereed/paperless-gpt", "name": "icereed/paperless-gpt",
"version": "v0.21.0", "version": "v0.21.0",
@ -429,11 +544,6 @@
"version": "2025.6.1", "version": "2025.6.1",
"date": "2025-06-17T12:45:39Z" "date": "2025-06-17T12:45:39Z"
}, },
{
"name": "sabnzbd/sabnzbd",
"version": "4.5.1",
"date": "2025-04-11T09:57:47Z"
},
{ {
"name": "crowdsecurity/crowdsec", "name": "crowdsecurity/crowdsec",
"version": "v1.6.9", "version": "v1.6.9",
@ -464,26 +574,11 @@
"version": "2.36.1", "version": "2.36.1",
"date": "2025-06-16T19:20:54Z" "date": "2025-06-16T19:20:54Z"
}, },
{
"name": "goauthentik/authentik",
"version": "version/2025.6.2",
"date": "2025-06-16T17:54:39Z"
},
{
"name": "emqx/emqx",
"version": "e5.9.1-alpha.1",
"date": "2025-06-16T15:34:01Z"
},
{ {
"name": "open-webui/open-webui", "name": "open-webui/open-webui",
"version": "v0.6.15", "version": "v0.6.15",
"date": "2025-06-16T14:34:42Z" "date": "2025-06-16T14:34:42Z"
}, },
{
"name": "grokability/snipe-it",
"version": "v8.1.16",
"date": "2025-06-16T13:49:37Z"
},
{ {
"name": "jellyfin/jellyfin", "name": "jellyfin/jellyfin",
"version": "v10.10.7", "version": "v10.10.7",
@ -504,11 +599,6 @@
"version": "cli/v0.25.0", "version": "cli/v0.25.0",
"date": "2025-06-15T17:48:29Z" "date": "2025-06-15T17:48:29Z"
}, },
{
"name": "tobychui/zoraxy",
"version": "v3.1.9",
"date": "2025-03-01T02:24:33Z"
},
{ {
"name": "Prowlarr/Prowlarr", "name": "Prowlarr/Prowlarr",
"version": "v1.37.0.5076", "version": "v1.37.0.5076",
@ -554,11 +644,6 @@
"version": "v3.3.25", "version": "v3.3.25",
"date": "2025-06-14T02:52:44Z" "date": "2025-06-14T02:52:44Z"
}, },
{
"name": "FlowiseAI/Flowise",
"version": "flowise@3.0.2",
"date": "2025-06-12T22:48:11Z"
},
{ {
"name": "leiweibau/Pi.Alert", "name": "leiweibau/Pi.Alert",
"version": "v2025-06-12", "version": "v2025-06-12",
@ -569,11 +654,6 @@
"version": "v3.3.0", "version": "v3.3.0",
"date": "2025-06-12T06:54:48Z" "date": "2025-06-12T06:54:48Z"
}, },
{
"name": "documenso/documenso",
"version": "v1.12.0-rc.4",
"date": "2025-06-12T00:27:41Z"
},
{ {
"name": "autobrr/autobrr", "name": "autobrr/autobrr",
"version": "v1.63.1", "version": "v1.63.1",
@ -584,11 +664,6 @@
"version": "v3.4.1", "version": "v3.4.1",
"date": "2025-06-11T07:53:44Z" "date": "2025-06-11T07:53:44Z"
}, },
{
"name": "openobserve/openobserve",
"version": "v0.15.0-rc2",
"date": "2025-06-11T04:29:22Z"
},
{ {
"name": "OctoPrint/OctoPrint", "name": "OctoPrint/OctoPrint",
"version": "1.11.2", "version": "1.11.2",
@ -649,11 +724,6 @@
"version": "v0.26.1", "version": "v0.26.1",
"date": "2025-06-06T11:22:02Z" "date": "2025-06-06T11:22:02Z"
}, },
{
"name": "apache/tomcat",
"version": "10.1.42",
"date": "2025-06-05T22:39:40Z"
},
{ {
"name": "benjaminjonard/koillection", "name": "benjaminjonard/koillection",
"version": "1.6.14", "version": "1.6.14",
@ -669,11 +739,6 @@
"version": "mariadb-11.8.2", "version": "mariadb-11.8.2",
"date": "2025-06-04T13:35:16Z" "date": "2025-06-04T13:35:16Z"
}, },
{
"name": "actualbudget/actual",
"version": "v25.6.1",
"date": "2025-06-04T22:24:31Z"
},
{ {
"name": "rabbitmq/rabbitmq-server", "name": "rabbitmq/rabbitmq-server",
"version": "v4.1.1", "version": "v4.1.1",
@ -709,16 +774,6 @@
"version": "v1.3.2", "version": "v1.3.2",
"date": "2025-06-01T19:02:46Z" "date": "2025-06-01T19:02:46Z"
}, },
{
"name": "Koenkk/zigbee2mqtt",
"version": "2.4.0",
"date": "2025-06-01T18:08:44Z"
},
{
"name": "TwiN/gatus",
"version": "v5.18.1",
"date": "2025-05-31T23:06:08Z"
},
{ {
"name": "blakeblackshear/frigate", "name": "blakeblackshear/frigate",
"version": "v0.14.1", "version": "v0.14.1",
@ -734,11 +789,6 @@
"version": "0.26.3", "version": "0.26.3",
"date": "2025-05-29T21:18:15Z" "date": "2025-05-29T21:18:15Z"
}, },
{
"name": "navidrome/navidrome",
"version": "v0.56.1",
"date": "2025-05-29T19:09:16Z"
},
{ {
"name": "readeck/readeck", "name": "readeck/readeck",
"version": "0.19.2", "version": "0.19.2",
@ -759,21 +809,11 @@
"version": "v1.12.3", "version": "v1.12.3",
"date": "2025-05-27T20:43:10Z" "date": "2025-05-27T20:43:10Z"
}, },
{
"name": "Threadfin/Threadfin",
"version": "1.2.34",
"date": "2025-05-27T18:18:00Z"
},
{ {
"name": "dani-garcia/vaultwarden", "name": "dani-garcia/vaultwarden",
"version": "1.34.1", "version": "1.34.1",
"date": "2025-05-26T21:40:54Z" "date": "2025-05-26T21:40:54Z"
}, },
{
"name": "jupyter/notebook",
"version": "v7.4.3",
"date": "2025-05-26T14:27:27Z"
},
{ {
"name": "stonith404/pingvin-share", "name": "stonith404/pingvin-share",
"version": "v1.13.0", "version": "v1.13.0",
@ -884,21 +924,11 @@
"version": "2025-05-07-r1", "version": "2025-05-07-r1",
"date": "2025-05-07T12:18:42Z" "date": "2025-05-07T12:18:42Z"
}, },
{
"name": "sysadminsmedia/homebox",
"version": "v0.19.0",
"date": "2025-05-06T18:05:42Z"
},
{ {
"name": "garethgeorge/backrest", "name": "garethgeorge/backrest",
"version": "v1.8.1", "version": "v1.8.1",
"date": "2025-05-06T04:27:00Z" "date": "2025-05-06T04:27:00Z"
}, },
{
"name": "linkwarden/linkwarden",
"version": "v2.10.2",
"date": "2025-05-06T03:12:53Z"
},
{ {
"name": "postgres/postgres", "name": "postgres/postgres",
"version": "REL_13_21", "version": "REL_13_21",
@ -914,21 +944,11 @@
"version": "v1.13.5", "version": "v1.13.5",
"date": "2025-05-03T09:48:44Z" "date": "2025-05-03T09:48:44Z"
}, },
{
"name": "jhuckaby/Cronicle",
"version": "v0.9.80",
"date": "2025-05-02T16:48:15Z"
},
{ {
"name": "WordPress/WordPress", "name": "WordPress/WordPress",
"version": "6.8.1", "version": "6.8.1",
"date": "2025-04-30T16:44:16Z" "date": "2025-04-30T16:44:16Z"
}, },
{
"name": "hivemq/hivemq-community-edition",
"version": "2025.3",
"date": "2025-04-30T02:52:28Z"
},
{ {
"name": "hargata/lubelog", "name": "hargata/lubelog",
"version": "v1.4.7", "version": "v1.4.7",
@ -1034,11 +1054,6 @@
"version": "2.3", "version": "2.3",
"date": "2025-04-05T18:05:36Z" "date": "2025-04-05T18:05:36Z"
}, },
{
"name": "MagicMirrorOrg/MagicMirror",
"version": "v2.31.0",
"date": "2025-04-01T18:12:45Z"
},
{ {
"name": "Kometa-Team/Kometa", "name": "Kometa-Team/Kometa",
"version": "v2.2.0", "version": "v2.2.0",
@ -1179,11 +1194,6 @@
"version": "v2.7.1", "version": "v2.7.1",
"date": "2025-02-22T01:14:41Z" "date": "2025-02-22T01:14:41Z"
}, },
{
"name": "typesense/typesense",
"version": "v28.0",
"date": "2025-02-18T15:49:57Z"
},
{ {
"name": "recyclarr/recyclarr", "name": "recyclarr/recyclarr",
"version": "v7.4.1", "version": "v7.4.1",
@ -1204,16 +1214,6 @@
"version": "v25.2.1", "version": "v25.2.1",
"date": "2025-02-06T20:41:28Z" "date": "2025-02-06T20:41:28Z"
}, },
{
"name": "NginxProxyManager/nginx-proxy-manager",
"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", "name": "AmruthPillai/Reactive-Resume",
"version": "v4.4.4", "version": "v4.4.4",

View File

@ -44,7 +44,7 @@
}, },
"notes": [ "notes": [
{ {
"text": "Wireguard and WGDashboard are not the same. More info: `https://donaldzou.github.io/WGDashboard-Documentation/what-is-wireguard-what-is-wgdashboard.html`", "text": "Wireguard and WGDashboard are not the same. More info: `https://docs.wgdashboard.dev/what-is-wireguard-what-is-wgdashboard.html`",
"type": "info" "type": "info"
} }
] ]

View File

@ -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();
});
});

View File

@ -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)
});
});
})

View File

@ -1,4 +0,0 @@
import { vi } from "vitest";
// Mock canvas getContext
HTMLCanvasElement.prototype.getContext = vi.fn();

View File

@ -1,7 +1,8 @@
import { Metadata, Script } from "@/lib/types";
import { promises as fs } from "fs";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import path from "path"; import { promises as fs } from "node:fs";
import path from "node:path";
import type { Metadata, Script } from "@/lib/types";
export const dynamic = "force-static"; export const dynamic = "force-static";
@ -10,21 +11,21 @@ const metadataFileName = "metadata.json";
const versionFileName = "version.json"; const versionFileName = "version.json";
const encoding = "utf-8"; const encoding = "utf-8";
const getMetadata = async () => { async function getMetadata() {
const filePath = path.resolve(jsonDir, metadataFileName); const filePath = path.resolve(jsonDir, metadataFileName);
const fileContent = await fs.readFile(filePath, encoding); const fileContent = await fs.readFile(filePath, encoding);
const metadata: Metadata = JSON.parse(fileContent); const metadata: Metadata = JSON.parse(fileContent);
return metadata; return metadata;
}; }
const getScripts = async () => { async function getScripts() {
const filePaths = (await fs.readdir(jsonDir)) const filePaths = (await fs.readdir(jsonDir))
.filter((fileName) => .filter(fileName =>
fileName.endsWith(".json") && fileName.endsWith(".json")
fileName !== metadataFileName && && fileName !== metadataFileName
fileName !== versionFileName && fileName !== versionFileName,
) )
.map((fileName) => path.resolve(jsonDir, fileName)); .map(fileName => path.resolve(jsonDir, fileName));
const scripts = await Promise.all( const scripts = await Promise.all(
filePaths.map(async (filePath) => { filePaths.map(async (filePath) => {
@ -34,7 +35,7 @@ const getScripts = async () => {
}), }),
); );
return scripts; return scripts;
}; }
export async function GET() { export async function GET() {
try { try {
@ -43,7 +44,7 @@ export async function GET() {
const categories = metadata.categories const categories = metadata.categories
.map((category) => { .map((category) => {
category.scripts = scripts.filter((script) => category.scripts = scripts.filter(script =>
script.categories?.includes(category.id), script.categories?.includes(category.id),
); );
return category; return category;
@ -51,7 +52,8 @@ export async function GET() {
.sort((a, b) => a.sort_order - b.sort_order); .sort((a, b) => a.sort_order - b.sort_order);
return NextResponse.json(categories); return NextResponse.json(categories);
} catch (error) { }
catch (error) {
console.error(error as Error); console.error(error as Error);
return NextResponse.json( return NextResponse.json(
{ error: "Failed to fetch categories" }, { error: "Failed to fetch categories" },

View File

@ -1,9 +1,9 @@
import { AppVersion } from "@/lib/types";
import { error } from "console";
import { promises as fs } from "fs";
// import Error from "next/error"; // import Error from "next/error";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import path from "path"; import { promises as fs } from "node:fs";
import path from "node:path";
import type { AppVersion } from "@/lib/types";
export const dynamic = "force-static"; export const dynamic = "force-static";
@ -11,33 +11,32 @@ const jsonDir = "public/json";
const versionsFileName = "versions.json"; const versionsFileName = "versions.json";
const encoding = "utf-8"; const encoding = "utf-8";
const getVersions = async () => { async function getVersions() {
const filePath = path.resolve(jsonDir, versionsFileName); const filePath = path.resolve(jsonDir, versionsFileName);
const fileContent = await fs.readFile(filePath, encoding); const fileContent = await fs.readFile(filePath, encoding);
const versions: AppVersion[] = JSON.parse(fileContent); const versions: AppVersion[] = JSON.parse(fileContent);
const modifiedVersions = versions.map(version => { const modifiedVersions = versions.map((version) => {
let newName = version.name; let newName = version.name;
newName = newName.toLowerCase().replace(/[^a-z0-9/]/g, ''); newName = newName.toLowerCase().replace(/[^a-z0-9/]/g, "");
return { ...version, name: newName, date: new Date(version.date) }; return { ...version, name: newName, date: new Date(version.date) };
}); });
return modifiedVersions; return modifiedVersions;
}; }
export async function GET() { export async function GET() {
try { try {
const versions = await getVersions(); const versions = await getVersions();
return NextResponse.json(versions); return NextResponse.json(versions);
}
} catch (error) { catch (error) {
console.error(error); console.error(error);
const err = error as globalThis.Error; const err = error as globalThis.Error;
return NextResponse.json({ return NextResponse.json({
name: err.name, name: err.name,
message: err.message || "An unexpected error occurred", message: err.message || "An unexpected error occurred",
version: "No version found - Error" version: "No version found - Error",
}, { }, {
status: 500, status: 500,
}); });

View File

@ -1,18 +1,20 @@
"use client"; "use client";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Category } from "@/lib/types";
import { ChevronLeft, ChevronRight } from "lucide-react"; import { ChevronLeft, ChevronRight } from "lucide-react";
import { useRouter } from "next/navigation";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import type { Category } from "@/lib/types";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
const defaultLogo = "/default-logo.png"; // Fallback logo path const defaultLogo = "/default-logo.png"; // Fallback logo path
const MAX_DESCRIPTION_LENGTH = 100; // Set max length for description const MAX_DESCRIPTION_LENGTH = 100; // Set max length for description
const MAX_LOGOS = 5; // Max logos to display at once const MAX_LOGOS = 5; // Max logos to display at once
const formattedBadge = (type: string) => { function formattedBadge(type: string) {
switch (type) { switch (type) {
case "vm": case "vm":
return <Badge className="text-blue-500/75 border-blue-500/75 badge">VM</Badge>; return <Badge className="text-blue-500/75 border-blue-500/75 badge">VM</Badge>;
@ -24,9 +26,9 @@ const formattedBadge = (type: string) => {
return <Badge className="text-green-500/75 border-green-500/75 badge">ADDON</Badge>; return <Badge className="text-green-500/75 border-green-500/75 badge">ADDON</Badge>;
} }
return null; return null;
}; }
const CategoryView = () => { function CategoryView() {
const [categories, setCategories] = useState<Category[]>([]); const [categories, setCategories] = useState<Category[]>([]);
const [selectedCategoryIndex, setSelectedCategoryIndex] = useState<number | null>(null); const [selectedCategoryIndex, setSelectedCategoryIndex] = useState<number | null>(null);
const [currentScripts, setCurrentScripts] = useState<any[]>([]); const [currentScripts, setCurrentScripts] = useState<any[]>([]);
@ -36,6 +38,7 @@ const CategoryView = () => {
useEffect(() => { useEffect(() => {
const fetchCategories = async () => { const fetchCategories = async () => {
try { try {
// eslint-disable-next-line node/no-process-env
const basePath = process.env.NODE_ENV === "production" ? "/ProxmoxVE" : ""; const basePath = process.env.NODE_ENV === "production" ? "/ProxmoxVE" : "";
const response = await fetch(`${basePath}/api/categories`); const response = await fetch(`${basePath}/api/categories`);
if (!response.ok) { if (!response.ok) {
@ -50,7 +53,8 @@ const CategoryView = () => {
initialLogoIndices[category.name] = 0; initialLogoIndices[category.name] = 0;
}); });
setLogoIndices(initialLogoIndices); setLogoIndices(initialLogoIndices);
} catch (error) { }
catch (error) {
console.error("Error fetching categories:", error); console.error("Error fetching categories:", error);
} }
}; };
@ -74,8 +78,8 @@ const CategoryView = () => {
const navigateCategory = (direction: "prev" | "next") => { const navigateCategory = (direction: "prev" | "next") => {
if (selectedCategoryIndex !== null) { if (selectedCategoryIndex !== null) {
const newIndex = const newIndex
direction === "prev" = direction === "prev"
? (selectedCategoryIndex - 1 + categories.length) % categories.length ? (selectedCategoryIndex - 1 + categories.length) % categories.length
: (selectedCategoryIndex + 1) % categories.length; : (selectedCategoryIndex + 1) % categories.length;
setSelectedCategoryIndex(newIndex); setSelectedCategoryIndex(newIndex);
@ -86,12 +90,13 @@ const CategoryView = () => {
const switchLogos = (categoryName: string, direction: "prev" | "next") => { const switchLogos = (categoryName: string, direction: "prev" | "next") => {
setLogoIndices((prev) => { setLogoIndices((prev) => {
const currentIndex = prev[categoryName] || 0; const currentIndex = prev[categoryName] || 0;
const category = categories.find((cat) => cat.name === categoryName); const category = categories.find(cat => cat.name === categoryName);
if (!category || !category.scripts) return prev; if (!category || !category.scripts)
return prev;
const totalLogos = category.scripts.length; const totalLogos = category.scripts.length;
const newIndex = const newIndex
direction === "prev" = direction === "prev"
? (currentIndex - MAX_LOGOS + totalLogos) % totalLogos ? (currentIndex - MAX_LOGOS + totalLogos) % totalLogos
: (currentIndex + MAX_LOGOS) % totalLogos; : (currentIndex + MAX_LOGOS) % totalLogos;
@ -109,35 +114,49 @@ const CategoryView = () => {
const hdd = script.install_methods[0]?.resources.hdd; const hdd = script.install_methods[0]?.resources.hdd;
const resourceParts = []; const resourceParts = [];
if (cpu) if (cpu) {
resourceParts.push( resourceParts.push(
<span key="cpu"> <span key="cpu">
<b>CPU:</b> {cpu}vCPU <b>CPU:</b>
{" "}
{cpu}
vCPU
</span>, </span>,
); );
if (ram) }
if (ram) {
resourceParts.push( resourceParts.push(
<span key="ram"> <span key="ram">
<b>RAM:</b> {ram}MB <b>RAM:</b>
{" "}
{ram}
MB
</span>, </span>,
); );
if (hdd) }
if (hdd) {
resourceParts.push( resourceParts.push(
<span key="hdd"> <span key="hdd">
<b>HDD:</b> {hdd}GB <b>HDD:</b>
{" "}
{hdd}
GB
</span>, </span>,
); );
}
return resourceParts.length > 0 ? ( return resourceParts.length > 0
<div className="text-sm text-gray-400"> ? (
{resourceParts.map((part, index) => ( <div className="text-sm text-gray-400">
<React.Fragment key={index}> {resourceParts.map((part, index) => (
{part} <React.Fragment key={index}>
{index < resourceParts.length - 1 && " | "} {part}
</React.Fragment> {index < resourceParts.length - 1 && " | "}
))} </React.Fragment>
</div> ))}
) : null; </div>
)
: null;
}; };
return ( return (
@ -145,145 +164,151 @@ const CategoryView = () => {
{categories.length === 0 && ( {categories.length === 0 && (
<p className="text-center text-gray-500">No categories available. Please check the API endpoint.</p> <p className="text-center text-gray-500">No categories available. Please check the API endpoint.</p>
)} )}
{selectedCategoryIndex !== null ? ( {selectedCategoryIndex !== null
<div> ? (
{/* Header with Navigation */} <div>
<div className="flex items-center justify-between mb-6"> {/* Header with Navigation */}
<Button <div className="flex items-center justify-between mb-6">
variant="ghost" <Button
onClick={() => navigateCategory("prev")} variant="ghost"
className="p-2 transition-transform duration-300 hover:scale-105" 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)}
> >
<CardContent className="flex flex-col gap-4"> <ChevronLeft className="h-6 w-6" />
<h3 className="text-lg font-bold script-text text-center hover:text-blue-600 transition-colors duration-300"> </Button>
{script.name} <h2 className="text-3xl font-semibold transition-opacity duration-300 hover:opacity-90">
</h3> {categories[selectedCategoryIndex].name}
<img </h2>
src={script.logo || defaultLogo} <Button
alt={script.name || "Script logo"} variant="ghost"
className="h-12 w-12 object-contain mx-auto" onClick={() => navigateCategory("next")}
/> className="p-2 transition-transform duration-300 hover:scale-105"
<p className="text-sm text-gray-500 text-center"> >
<b>Created at:</b> {script.date_created || "No date available"} <ChevronRight className="h-6 w-6" />
</p> </Button>
<p </div>
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 */} {/* Scripts Grid */}
<div className="mt-8 text-center"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
<Button {currentScripts
variant="default" .sort((a, b) => a.name.localeCompare(b.name))
onClick={handleBackClick} .map(script => (
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" <Card
> key={script.name}
Back to Categories className="p-4 cursor-pointer hover:shadow-md transition-shadow duration-300"
</Button> onClick={() => handleScriptClick(script.slug)}
</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" /> <CardContent className="flex flex-col gap-4">
</Button> <h3 className="text-lg font-bold script-text text-center hover:text-blue-600 transition-colors duration-300">
{category.scripts && {script.name}
category.scripts </h3>
.slice(logoIndices[category.name] || 0, (logoIndices[category.name] || 0) + MAX_LOGOS) <img
.map((script, i) => ( src={script.logo || defaultLogo}
<div key={i} className="flex flex-col items-center"> alt={script.name || "Script logo"}
<img className="h-12 w-12 object-contain mx-auto"
src={script.logo || defaultLogo} />
alt={script.name || "Script logo"} <p className="text-sm text-gray-500 text-center">
title={script.name} <b>Created at:</b>
className="h-8 w-8 object-contain cursor-pointer" {" "}
onClick={(e) => { {script.date_created || "No date available"}
e.stopPropagation(); </p>
handleScriptClick(script.slug); <p
}} className="text-sm text-gray-700 hover:text-gray-900 text-center transition-colors duration-300"
/> title={script.description || "No description available."}
{formattedBadge(script.type)} >
</div> {truncateDescription(script.description || "No description available.")}
))} </p>
<Button {renderResources(script)}
variant="ghost" </CardContent>
onClick={(e) => { </Card>
e.stopPropagation(); ))}
switchLogos(category.name, "next"); </div>
}}
className="p-1 transition-transform duration-300 hover:scale-110" {/* Back to Categories Button */}
> <div className="mt-8 text-center">
<ChevronRight className="h-4 w-4" /> <Button
</Button> variant="default"
</div> onClick={handleBackClick}
<p className="text-sm text-gray-400 text-center"> 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"
{(category as any).description || "No description available."} >
</p> Back to Categories
</CardContent> </Button>
</Card> </div>
))} </div>
</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> </div>
); );
}; }
export default CategoryView; export default CategoryView;

View File

@ -1,11 +1,11 @@
"use client"; "use client";
import React, { JSX, useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import DatePicker from 'react-datepicker'; import "react-datepicker/dist/react-datepicker.css";
import 'react-datepicker/dist/react-datepicker.css';
import ApplicationChart from "../../components/ApplicationChart";
interface DataModel { import ApplicationChart from "../../components/application-chart";
type DataModel = {
id: number; id: number;
ct_type: number; ct_type: number;
disk_size: number; disk_size: number;
@ -22,13 +22,13 @@ interface DataModel {
error: string; error: string;
type: string; type: string;
[key: string]: any; [key: string]: any;
} };
interface SummaryData { type SummaryData = {
total_entries: number; total_entries: number;
status_count: Record<string, number>; status_count: Record<string, number>;
nsapp_count: Record<string, number>; nsapp_count: Record<string, number>;
} };
const DataFetcher: React.FC = () => { const DataFetcher: React.FC = () => {
const [data, setData] = useState<DataModel[]>([]); const [data, setData] = useState<DataModel[]>([]);
@ -37,16 +37,18 @@ const DataFetcher: React.FC = () => {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(25); const [itemsPerPage, setItemsPerPage] = useState(25);
const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'ascending' | 'descending' } | null>(null); const [sortConfig, setSortConfig] = useState<{ key: string; direction: "ascending" | "descending" } | null>(null);
useEffect(() => { useEffect(() => {
const fetchSummary = async () => { const fetchSummary = async () => {
try { try {
const response = await fetch("https://api.htl-braunau.at/data/summary"); const response = await fetch("https://api.htl-braunau.at/data/summary");
if (!response.ok) throw new Error(`Failed to fetch summary: ${response.statusText}`); if (!response.ok)
throw new Error(`Failed to fetch summary: ${response.statusText}`);
const result: SummaryData = await response.json(); const result: SummaryData = await response.json();
setSummary(result); setSummary(result);
} catch (err) { }
catch (err) {
setError((err as Error).message); setError((err as Error).message);
} }
}; };
@ -58,13 +60,16 @@ const DataFetcher: React.FC = () => {
const fetchPaginatedData = async () => { const fetchPaginatedData = async () => {
setLoading(true); setLoading(true);
try { try {
const response = await fetch(`https://api.htl-braunau.at/data/paginated?page=${currentPage}&limit=${itemsPerPage === 0 ? '' : itemsPerPage}`); const response = await fetch(`https://api.htl-braunau.at/data/paginated?page=${currentPage}&limit=${itemsPerPage === 0 ? "" : itemsPerPage}`);
if (!response.ok) throw new Error(`Failed to fetch data: ${response.statusText}`); if (!response.ok)
throw new Error(`Failed to fetch data: ${response.statusText}`);
const result: DataModel[] = await response.json(); const result: DataModel[] = await response.json();
setData(result); setData(result);
} catch (err) { }
catch (err) {
setError((err as Error).message); setError((err as Error).message);
} finally { }
finally {
setLoading(false); setLoading(false);
} }
}; };
@ -73,26 +78,35 @@ const DataFetcher: React.FC = () => {
}, [currentPage, itemsPerPage]); }, [currentPage, itemsPerPage]);
const sortedData = React.useMemo(() => { const sortedData = React.useMemo(() => {
if (!sortConfig) return data; if (!sortConfig)
return data;
const sorted = [...data].sort((a, b) => { const sorted = [...data].sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) { if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === 'ascending' ? -1 : 1; return sortConfig.direction === "ascending" ? -1 : 1;
} }
if (a[sortConfig.key] > b[sortConfig.key]) { if (a[sortConfig.key] > b[sortConfig.key]) {
return sortConfig.direction === 'ascending' ? 1 : -1; return sortConfig.direction === "ascending" ? 1 : -1;
} }
return 0; return 0;
}); });
return sorted; return sorted;
}, [data, sortConfig]); }, [data, sortConfig]);
if (loading) return <p>Loading...</p>; if (loading)
if (error) return <p>Error: {error}</p>; return <p>Loading...</p>;
if (error) {
return (
<p>
Error:
{error}
</p>
);
}
const requestSort = (key: string) => { const requestSort = (key: string) => {
let direction: 'ascending' | 'descending' = 'ascending'; let direction: "ascending" | "descending" = "ascending";
if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') { if (sortConfig && sortConfig.key === key && sortConfig.direction === "ascending") {
direction = 'descending'; direction = "descending";
} }
setSortConfig({ key, direction }); setSortConfig({ key, direction });
}; };
@ -102,8 +116,8 @@ const DataFetcher: React.FC = () => {
const year = date.getFullYear(); const year = date.getFullYear();
const month = date.getMonth() + 1; const month = date.getMonth() + 1;
const day = date.getDate(); const day = date.getDate();
const hours = String(date.getHours()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, "0");
const timezoneOffset = dateString.slice(-6); const timezoneOffset = dateString.slice(-6);
return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`; return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`;
}; };
@ -114,49 +128,76 @@ const DataFetcher: React.FC = () => {
<ApplicationChart data={summary} /> <ApplicationChart data={summary} />
<p className="text-lg font-bold mt-4"> </p> <p className="text-lg font-bold mt-4"> </p>
<div className="mb-4 flex justify-between items-center"> <div className="mb-4 flex justify-between items-center">
<p className="text-lg font-bold">{summary?.total_entries} results found</p> <p className="text-lg font-bold">
<p className="text-lg font">Status Legend: 🔄 installing {summary?.status_count["installing"] ?? 0} | completed {summary?.status_count["done"] ?? 0} | failed {summary?.status_count["failed"] ?? 0} | unknown</p> {summary?.total_entries}
</div> {" "}
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-x-auto">
<div className="overflow-y-auto lg:overflow-y-visible"> <div className="overflow-y-auto lg:overflow-y-visible">
<table className="min-w-full table-auto border-collapse"> <table className="min-w-full table-auto border-collapse">
<thead> <thead>
<tr> <tr>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('status')}>Status</th> <th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("status")}>Status</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('type')}>Type</th> <th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("type")}>Type</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('nsapp')}>Application</th> <th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("nsapp")}>Application</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_type')}>OS</th> <th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("os_type")}>OS</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('os_version')}>OS Version</th> <th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("os_version")}>OS Version</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('disk_size')}>Disk Size</th> <th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("disk_size")}>Disk Size</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('core_count')}>Core Count</th> <th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("core_count")}>Core Count</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('ram_size')}>RAM Size</th> <th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("ram_size")}>RAM Size</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('method')}>Method</th> <th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("method")}>Method</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('pve_version')}>PVE Version</th> <th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("pve_version")}>PVE Version</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('error')}>Error Message</th> <th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("error")}>Error Message</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort('created_at')}>Created At</th> <th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("created_at")}>Created At</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{sortedData.map((item, index) => ( {sortedData.map((item, index) => (
<tr key={index}> <tr key={index}>
<td className="px-4 py-2 border-b"> <td className="px-4 py-2 border-b">
{item.status === "done" ? ( {item.status === "done"
"✔️" ? (
) : item.status === "failed" ? ( "✔️"
"❌" )
) : item.status === "installing" ? ( : item.status === "failed"
"🔄" ? (
) : ( "❌"
item.status )
)} : item.status === "installing"
? (
"🔄"
)
: (
item.status
)}
</td>
<td className="px-4 py-2 border-b">
{item.type === "lxc"
? (
"📦"
)
: item.type === "vm"
? (
"🖥️"
)
: (
item.type
)}
</td> </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.nsapp}</td>
<td className="px-4 py-2 border-b">{item.os_type}</td> <td className="px-4 py-2 border-b">{item.os_type}</td>
<td className="px-4 py-2 border-b">{item.os_version}</td> <td className="px-4 py-2 border-b">{item.os_version}</td>
@ -175,11 +216,14 @@ const DataFetcher: React.FC = () => {
</div> </div>
<div className="mt-4 flex justify-between items-center"> <div className="mt-4 flex justify-between items-center">
<button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage === 1} className="p-2 border">Previous</button> <button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage === 1} className="p-2 border">Previous</button>
<span>Page {currentPage}</span> <span>
Page
{currentPage}
</span>
<button onClick={() => setCurrentPage(prev => prev + 1)} className="p-2 border">Next</button> <button onClick={() => setCurrentPage(prev => prev + 1)} className="p-2 border">Next</button>
<select <select
value={itemsPerPage} value={itemsPerPage}
onChange={(e) => setItemsPerPage(Number(e.target.value))} onChange={e => setItemsPerPage(Number(e.target.value))}
className="p-2 border" className="p-2 border"
> >
<option value={10}>10</option> <option value={10}>10</option>

View File

@ -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);

View File

@ -1,4 +1,9 @@
import { Label } from "@/components/ui/label"; import type { z } from "zod";
import { memo } from "react";
import type { Category } from "@/lib/types";
import { import {
Select, Select,
SelectContent, SelectContent,
@ -6,11 +11,10 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Category } from "@/lib/types"; import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { z } from "zod";
import { type Script } from "../_schemas/schemas"; import type { Script } from "../_schemas/schemas";
import { memo } from "react";
type CategoryProps = { type CategoryProps = {
script: Script; script: Script;
@ -20,10 +24,10 @@ type CategoryProps = {
categories: Category[]; categories: Category[];
}; };
const CategoryTag = memo(({ const CategoryTag = memo(({
category, category,
onRemove onRemove,
}: { }: {
category: Category; category: Category;
onRemove: () => void; onRemove: () => void;
}) => ( }) => (
@ -53,7 +57,7 @@ const CategoryTag = memo(({
</span> </span>
)); ));
CategoryTag.displayName = 'CategoryTag'; CategoryTag.displayName = "CategoryTag";
function Categories({ function Categories({
script, script,
@ -79,14 +83,16 @@ function Categories({
return ( return (
<div> <div>
<Label> <Label>
Category <span className="text-red-500">*</span> Category
{" "}
<span className="text-red-500">*</span>
</Label> </Label>
<Select onValueChange={(value) => addCategory(Number(value))}> <Select onValueChange={value => addCategory(Number(value))}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a category" /> <SelectValue placeholder="Select a category" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{categories.map((category) => ( {categories.map(category => (
<SelectItem key={category.id} value={category.id.toString()}> <SelectItem key={category.id} value={category.id.toString()}>
{category.name} {category.name}
</SelectItem> </SelectItem>
@ -101,13 +107,15 @@ function Categories({
> >
{script.categories.map((categoryId) => { {script.categories.map((categoryId) => {
const category = categoryMap.get(categoryId); const category = categoryMap.get(categoryId);
return category ? ( return category
<CategoryTag ? (
key={categoryId} <CategoryTag
category={category} key={categoryId}
onRemove={() => removeCategory(categoryId)} category={category}
/> onRemove={() => removeCategory(categoryId)}
) : null; />
)
: null;
})} })}
</div> </div>
</div> </div>

View File

@ -1,11 +1,16 @@
import { Button } from "@/components/ui/button"; import type { z } from "zod";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { OperatingSystems } from "@/config/siteConfig";
import { PlusCircle, Trash2 } from "lucide-react"; import { PlusCircle, Trash2 } from "lucide-react";
import { memo, useCallback, useRef } from "react"; import { memo, useCallback, useRef } from "react";
import { z } from "zod";
import { InstallMethodSchema, ScriptSchema, type Script } from "../_schemas/schemas"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { OperatingSystems } from "@/config/site-config";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import type { Script } from "../_schemas/schemas";
import { InstallMethodSchema, ScriptSchema } from "../_schemas/schemas";
type InstallMethodProps = { type InstallMethodProps = {
script: Script; script: Script;
@ -28,9 +33,11 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
if (type === "pve") { if (type === "pve") {
scriptPath = `tools/pve/${slug}.sh`; scriptPath = `tools/pve/${slug}.sh`;
} else if (type === "addon") { }
else if (type === "addon") {
scriptPath = `tools/addon/${slug}.sh`; scriptPath = `tools/addon/${slug}.sh`;
} else { }
else {
scriptPath = `${type}/${slug}.sh`; scriptPath = `${type}/${slug}.sh`;
} }
@ -65,8 +72,8 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
const updatedMethod = { ...method, [key]: value }; const updatedMethod = { ...method, [key]: value };
if (key === "type") { if (key === "type") {
updatedMethod.script = updatedMethod.script
value === "alpine" ? `${prev.type}/alpine-${prev.slug}.sh` : `${prev.type}/${prev.slug}.sh`; = value === "alpine" ? `${prev.type}/alpine-${prev.slug}.sh` : `${prev.type}/${prev.slug}.sh`;
// Set OS to Alpine and reset version if type is alpine // Set OS to Alpine and reset version if type is alpine
if (value === "alpine") { if (value === "alpine") {
@ -89,7 +96,8 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
setIsValid(result.success); setIsValid(result.success);
if (!result.success) { if (!result.success) {
setZodErrors(result.error); setZodErrors(result.error);
} else { }
else {
setZodErrors(null); setZodErrors(null);
} }
return updated; return updated;
@ -100,7 +108,7 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
const removeInstallMethod = useCallback( const removeInstallMethod = useCallback(
(index: number) => { (index: number) => {
setScript((prev) => ({ setScript(prev => ({
...prev, ...prev,
install_methods: prev.install_methods.filter((_, i) => i !== index), install_methods: prev.install_methods.filter((_, i) => i !== index),
})); }));
@ -113,7 +121,7 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
<h3 className="text-xl font-semibold">Install Methods</h3> <h3 className="text-xl font-semibold">Install Methods</h3>
{script.install_methods.map((method, index) => ( {script.install_methods.map((method, index) => (
<div key={index} className="space-y-2 border p-4 rounded"> <div key={index} className="space-y-2 border p-4 rounded">
<Select value={method.type} onValueChange={(value) => updateInstallMethod(index, "type", value)}> <Select value={method.type} onValueChange={value => updateInstallMethod(index, "type", value)}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Type" /> <SelectValue placeholder="Type" />
</SelectTrigger> </SelectTrigger>
@ -130,12 +138,11 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
placeholder="CPU in Cores" placeholder="CPU in Cores"
type="number" type="number"
value={method.resources.cpu || ""} value={method.resources.cpu || ""}
onChange={(e) => onChange={e =>
updateInstallMethod(index, "resources", { updateInstallMethod(index, "resources", {
...method.resources, ...method.resources,
cpu: e.target.value ? Number(e.target.value) : null, cpu: e.target.value ? Number(e.target.value) : null,
}) })}
}
/> />
<Input <Input
ref={(el) => { ref={(el) => {
@ -144,12 +151,11 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
placeholder="RAM in MB" placeholder="RAM in MB"
type="number" type="number"
value={method.resources.ram || ""} value={method.resources.ram || ""}
onChange={(e) => onChange={e =>
updateInstallMethod(index, "resources", { updateInstallMethod(index, "resources", {
...method.resources, ...method.resources,
ram: e.target.value ? Number(e.target.value) : null, ram: e.target.value ? Number(e.target.value) : null,
}) })}
}
/> />
<Input <Input
ref={(el) => { ref={(el) => {
@ -158,31 +164,29 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
placeholder="HDD in GB" placeholder="HDD in GB"
type="number" type="number"
value={method.resources.hdd || ""} value={method.resources.hdd || ""}
onChange={(e) => onChange={e =>
updateInstallMethod(index, "resources", { updateInstallMethod(index, "resources", {
...method.resources, ...method.resources,
hdd: e.target.value ? Number(e.target.value) : null, hdd: e.target.value ? Number(e.target.value) : null,
}) })}
}
/> />
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Select <Select
value={method.resources.os || undefined} value={method.resources.os || undefined}
onValueChange={(value) => onValueChange={value =>
updateInstallMethod(index, "resources", { updateInstallMethod(index, "resources", {
...method.resources, ...method.resources,
os: value || null, os: value || null,
version: null, // Reset version when OS changes version: null, // Reset version when OS changes
}) })}
}
disabled={method.type === "alpine"} disabled={method.type === "alpine"}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="OS" /> <SelectValue placeholder="OS" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{OperatingSystems.map((os) => ( {OperatingSystems.map(os => (
<SelectItem key={os.name} value={os.name}> <SelectItem key={os.name} value={os.name}>
{os.name} {os.name}
</SelectItem> </SelectItem>
@ -191,19 +195,18 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
</Select> </Select>
<Select <Select
value={method.resources.version || undefined} value={method.resources.version || undefined}
onValueChange={(value) => onValueChange={value =>
updateInstallMethod(index, "resources", { updateInstallMethod(index, "resources", {
...method.resources, ...method.resources,
version: value || null, version: value || null,
}) })}
}
disabled={method.type === "alpine"} disabled={method.type === "alpine"}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Version" /> <SelectValue placeholder="Version" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{OperatingSystems.find((os) => os.name === method.resources.os)?.versions.map((version) => ( {OperatingSystems.find(os => os.name === method.resources.os)?.versions.map(version => (
<SelectItem key={version.slug} value={version.name}> <SelectItem key={version.slug} value={version.name}>
{version.name} {version.name}
</SelectItem> </SelectItem>
@ -212,12 +215,16 @@ function InstallMethod({ script, setScript, setIsValid, setZodErrors }: InstallM
</Select> </Select>
</div> </div>
<Button variant="destructive" size="sm" type="button" onClick={() => removeInstallMethod(index)}> <Button variant="destructive" size="sm" type="button" onClick={() => removeInstallMethod(index)}>
<Trash2 className="mr-2 h-4 w-4" /> Remove Install Method <Trash2 className="mr-2 h-4 w-4" />
{" "}
Remove Install Method
</Button> </Button>
</div> </div>
))} ))}
<Button type="button" size="sm" disabled={script.install_methods.length >= 2} onClick={addInstallMethod}> <Button type="button" size="sm" disabled={script.install_methods.length >= 2} onClick={addInstallMethod}>
<PlusCircle className="mr-2 h-4 w-4" /> Add Install Method <PlusCircle className="mr-2 h-4 w-4" />
{" "}
Add Install Method
</Button> </Button>
</> </>
); );

View 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);

View File

@ -2,7 +2,7 @@ import { z } from "zod";
export const InstallMethodSchema = z.object({ export const InstallMethodSchema = z.object({
type: z.enum(["default", "alpine"], { type: z.enum(["default", "alpine"], {
errorMap: () => ({ message: "Type must be either 'default' or 'alpine'" }) errorMap: () => ({ message: "Type must be either 'default' or 'alpine'" }),
}), }),
script: z.string().min(1, "Script content cannot be empty"), script: z.string().min(1, "Script content cannot be empty"),
resources: z.object({ resources: z.object({
@ -25,7 +25,7 @@ export const ScriptSchema = z.object({
categories: z.array(z.number()), categories: z.array(z.number()),
date_created: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format").min(1, "Date is required"), date_created: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format").min(1, "Date is required"),
type: z.enum(["vm", "ct", "pve", "addon", "turnkey"], { type: z.enum(["vm", "ct", "pve", "addon", "turnkey"], {
errorMap: () => ({ message: "Type must be either 'vm', 'ct', 'pve', 'addon' or 'turnkey'" }) errorMap: () => ({ message: "Type must be either 'vm', 'ct', 'pve', 'addon' or 'turnkey'" }),
}), }),
updateable: z.boolean(), updateable: z.boolean(),
privileged: z.boolean(), privileged: z.boolean(),

View File

@ -1,26 +1,32 @@
"use client"; "use client";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import type { z } from "zod";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { fetchCategories } from "@/lib/data";
import { Category } from "@/lib/types";
import { cn } from "@/lib/utils";
import { format } from "date-fns";
import { CalendarIcon, Check, Clipboard, Download } from "lucide-react"; import { CalendarIcon, Check, Clipboard, Download } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { format } from "date-fns";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod";
import Categories from "./_components/Categories"; import type { Category } from "@/lib/types";
import InstallMethod from "./_components/InstallMethod";
import Note from "./_components/Note"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ScriptSchema, type Script } from "./_schemas/schemas"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Calendar } from "@/components/ui/calendar";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { fetchCategories } from "@/lib/data";
import { cn } from "@/lib/utils";
import type { Script } from "./_schemas/schemas";
import InstallMethod from "./_components/install-method";
import { ScriptSchema } from "./_schemas/schemas";
import Categories from "./_components/categories";
import Note from "./_components/note";
const initialScript: Script = { const initialScript: Script = {
name: "", name: "",
@ -54,7 +60,7 @@ export default function JSONGenerator() {
useEffect(() => { useEffect(() => {
fetchCategories() fetchCategories()
.then(setCategories) .then(setCategories)
.catch((error) => console.error("Error fetching categories:", error)); .catch(error => console.error("Error fetching categories:", error));
}, []); }, []);
const updateScript = useCallback((key: keyof Script, value: Script[keyof Script]) => { const updateScript = useCallback((key: keyof Script, value: Script[keyof Script]) => {
@ -67,11 +73,14 @@ export default function JSONGenerator() {
if (updated.type === "pve") { if (updated.type === "pve") {
scriptPath = `tools/pve/${updated.slug}.sh`; scriptPath = `tools/pve/${updated.slug}.sh`;
} else if (updated.type === "addon") { }
else if (updated.type === "addon") {
scriptPath = `tools/addon/${updated.slug}.sh`; scriptPath = `tools/addon/${updated.slug}.sh`;
} else if (method.type === "alpine") { }
else if (method.type === "alpine") {
scriptPath = `${updated.type}/alpine-${updated.slug}.sh`; scriptPath = `${updated.type}/alpine-${updated.slug}.sh`;
} else { }
else {
scriptPath = `${updated.type}/${updated.slug}.sh`; scriptPath = `${updated.type}/${updated.slug}.sh`;
} }
@ -136,7 +145,10 @@ export default function JSONGenerator() {
<div className="mt-2 space-y-1"> <div className="mt-2 space-y-1">
{zodErrors.errors.map((error, index) => ( {zodErrors.errors.map((error, index) => (
<AlertDescription key={index} className="p-1 text-red-500"> <AlertDescription key={index} className="p-1 text-red-500">
{error.path.join(".")} - {error.message} {error.path.join(".")}
{" "}
-
{error.message}
</AlertDescription> </AlertDescription>
))} ))}
</div> </div>
@ -154,25 +166,31 @@ export default function JSONGenerator() {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<Label> <Label>
Name <span className="text-red-500">*</span> Name
{" "}
<span className="text-red-500">*</span>
</Label> </Label>
<Input placeholder="Example" value={script.name} onChange={(e) => updateScript("name", e.target.value)} /> <Input placeholder="Example" value={script.name} onChange={e => updateScript("name", e.target.value)} />
</div> </div>
<div> <div>
<Label> <Label>
Slug <span className="text-red-500">*</span> Slug
{" "}
<span className="text-red-500">*</span>
</Label> </Label>
<Input placeholder="example" value={script.slug} onChange={(e) => updateScript("slug", e.target.value)} /> <Input placeholder="example" value={script.slug} onChange={e => updateScript("slug", e.target.value)} />
</div> </div>
</div> </div>
<div> <div>
<Label> <Label>
Logo <span className="text-red-500">*</span> Logo
{" "}
<span className="text-red-500">*</span>
</Label> </Label>
<Input <Input
placeholder="Full logo URL" placeholder="Full logo URL"
value={script.logo || ""} value={script.logo || ""}
onChange={(e) => updateScript("logo", e.target.value || null)} onChange={e => updateScript("logo", e.target.value || null)}
/> />
</div> </div>
<div> <div>
@ -180,17 +198,19 @@ export default function JSONGenerator() {
<Input <Input
placeholder="Path to config file" placeholder="Path to config file"
value={script.config_path || ""} value={script.config_path || ""}
onChange={(e) => updateScript("config_path", e.target.value || null)} onChange={e => updateScript("config_path", e.target.value || null)}
/> />
</div> </div>
<div> <div>
<Label> <Label>
Description <span className="text-red-500">*</span> Description
{" "}
<span className="text-red-500">*</span>
</Label> </Label>
<Textarea <Textarea
placeholder="Example" placeholder="Example"
value={script.description} value={script.description}
onChange={(e) => updateScript("description", e.target.value)} onChange={e => updateScript("description", e.target.value)}
/> />
</div> </div>
<Categories script={script} setScript={setScript} categories={categories} /> <Categories script={script} setScript={setScript} categories={categories} />
@ -200,7 +220,7 @@ export default function JSONGenerator() {
<Popover> <Popover>
<PopoverTrigger asChild className="flex-1"> <PopoverTrigger asChild className="flex-1">
<Button <Button
variant={"outline"} variant="outline"
className={cn("pl-3 text-left font-normal w-full", !script.date_created && "text-muted-foreground")} className={cn("pl-3 text-left font-normal w-full", !script.date_created && "text-muted-foreground")}
> >
{formattedDate || <span>Pick a date</span>} {formattedDate || <span>Pick a date</span>}
@ -219,7 +239,7 @@ export default function JSONGenerator() {
</div> </div>
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
<Label>Type</Label> <Label>Type</Label>
<Select value={script.type} onValueChange={(value) => updateScript("type", value)}> <Select value={script.type} onValueChange={value => updateScript("type", value)}>
<SelectTrigger className="flex-1"> <SelectTrigger className="flex-1">
<SelectValue placeholder="Type" /> <SelectValue placeholder="Type" />
</SelectTrigger> </SelectTrigger>
@ -234,11 +254,11 @@ export default function JSONGenerator() {
</div> </div>
<div className="w-full flex gap-5"> <div className="w-full flex gap-5">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Switch checked={script.updateable} onCheckedChange={(checked) => updateScript("updateable", checked)} /> <Switch checked={script.updateable} onCheckedChange={checked => updateScript("updateable", checked)} />
<label>Updateable</label> <label>Updateable</label>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Switch checked={script.privileged} onCheckedChange={(checked) => updateScript("privileged", checked)} /> <Switch checked={script.privileged} onCheckedChange={checked => updateScript("privileged", checked)} />
<label>Privileged</label> <label>Privileged</label>
</div> </div>
</div> </div>
@ -246,18 +266,18 @@ export default function JSONGenerator() {
placeholder="Interface Port" placeholder="Interface Port"
type="number" type="number"
value={script.interface_port || ""} value={script.interface_port || ""}
onChange={(e) => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)} onChange={e => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
/> />
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
placeholder="Website URL" placeholder="Website URL"
value={script.website || ""} value={script.website || ""}
onChange={(e) => updateScript("website", e.target.value || null)} onChange={e => updateScript("website", e.target.value || null)}
/> />
<Input <Input
placeholder="Documentation URL" placeholder="Documentation URL"
value={script.documentation || ""} value={script.documentation || ""}
onChange={(e) => updateScript("documentation", e.target.value || null)} onChange={e => updateScript("documentation", e.target.value || null)}
/> />
</div> </div>
<InstallMethod script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} /> <InstallMethod script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
@ -265,22 +285,20 @@ export default function JSONGenerator() {
<Input <Input
placeholder="Username" placeholder="Username"
value={script.default_credentials.username || ""} value={script.default_credentials.username || ""}
onChange={(e) => onChange={e =>
updateScript("default_credentials", { updateScript("default_credentials", {
...script.default_credentials, ...script.default_credentials,
username: e.target.value || null, username: e.target.value || null,
}) })}
}
/> />
<Input <Input
placeholder="Password" placeholder="Password"
value={script.default_credentials.password || ""} value={script.default_credentials.password || ""}
onChange={(e) => onChange={e =>
updateScript("default_credentials", { updateScript("default_credentials", {
...script.default_credentials, ...script.default_credentials,
password: e.target.value || null, password: e.target.value || null,
}) })}
}
/> />
<Note script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} /> <Note script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
</form> </form>

View File

@ -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 type { Metadata } from "next";
import { Inter } from "next/font/google";
import { NuqsAdapter } from "nuqs/adapters/next/app"; import { NuqsAdapter } from "nuqs/adapters/next/app";
import { Inter } from "next/font/google";
import React from "react"; import React from "react";
import { ThemeProvider } from "@/components/theme-provider";
import { analytics, basePath } from "@/config/site-config";
import "@/styles/globals.css";
import QueryProvider from "@/components/query-provider";
import { Toaster } from "@/components/ui/sonner";
import Footer from "@/components/footer";
import Navbar from "@/components/navbar";
const inter = Inter({ subsets: ["latin"] }); const inter = Inter({ subsets: ["latin"] });
export const metadata : Metadata = { export const metadata: Metadata = {
title: "Proxmox VE Helper-Scripts", title: "Proxmox VE Helper-Scripts",
description: description:
"The official website for the Proxmox VE Helper-Scripts (Community) Repository. Featuring over 300+ scripts to help you manage your Proxmox VE environment.", "The official website for the Proxmox VE Helper-Scripts (Community) Repository. Featuring over 300+ scripts to help you manage your Proxmox VE environment.",

View File

@ -1,9 +1,10 @@
import { basePath } from "@/config/siteConfig";
import type { MetadataRoute } from "next"; import type { MetadataRoute } from "next";
export const generateStaticParams = () => { import { basePath } from "@/config/site-config";
export function generateStaticParams() {
return []; return [];
}; }
export default function manifest(): MetadataRoute.Manifest { export default function manifest(): MetadataRoute.Manifest {
return { return {

View File

@ -1,8 +1,10 @@
"use client"; "use client";
import FAQ from "@/components/FAQ"; import { ArrowRightIcon, ExternalLink } from "lucide-react";
import AnimatedGradientText from "@/components/ui/animated-gradient-text"; import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button"; import { FaGithub } from "react-icons/fa";
import { CardFooter } from "@/components/ui/card"; import { useTheme } from "next-themes";
import Link from "next/link";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -11,15 +13,14 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import Particles from "@/components/ui/particles"; import AnimatedGradientText from "@/components/ui/animated-gradient-text";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { basePath } from "@/config/siteConfig"; import { CardFooter } from "@/components/ui/card";
import Particles from "@/components/ui/particles";
import { Button } from "@/components/ui/button";
import { basePath } from "@/config/site-config";
import FAQ from "@/components/faq";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ArrowRightIcon, ExternalLink } from "lucide-react";
import { useTheme } from "next-themes";
import Link from "next/link";
import { useEffect, useState } from "react";
import { FaGithub } from "react-icons/fa";
function CustomArrowRightIcon() { function CustomArrowRightIcon() {
return <ArrowRightIcon className="h-4 w-4" width={1} />; return <ArrowRightIcon className="h-4 w-4" width={1} />;
@ -50,7 +51,9 @@ export default function Page() {
`p-px ![mask-composite:subtract]`, `p-px ![mask-composite:subtract]`,
)} )}
/> />
<Separator className="mx-2 h-4" orientation="vertical" />
{" "}
<Separator className="mx-2 h-4" orientation="vertical" />
<span <span
className={cn( className={cn(
`animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`, `animate-gradient bg-gradient-to-r from-[#ffaa40] via-[#9c40ff] to-[#ffaa40] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
@ -78,7 +81,9 @@ export default function Page() {
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center justify-center" className="flex items-center justify-center"
> >
<FaGithub className="mr-2 h-4 w-4" /> Tteck&apos;s GitHub <FaGithub className="mr-2 h-4 w-4" />
{" "}
Tteck&apos;s GitHub
</a> </a>
</Button> </Button>
<Button className="w-full" asChild> <Button className="w-full" asChild>
@ -88,7 +93,9 @@ export default function Page() {
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center justify-center" className="flex items-center justify-center"
> >
<ExternalLink className="mr-2 h-4 w-4" /> Proxmox Helper Scripts <ExternalLink className="mr-2 h-4 w-4" />
{" "}
Proxmox Helper Scripts
</a> </a>
</Button> </Button>
</CardFooter> </CardFooter>
@ -104,7 +111,10 @@ export default function Page() {
We are a community-driven initiative that simplifies the setup of Proxmox Virtual Environment (VE). We are a community-driven initiative that simplifies the setup of Proxmox Virtual Environment (VE).
</p> </p>
<p> <p>
With 300+ scripts to help you manage your <b>Proxmox VE environment</b>. Whether you&#39;re a seasoned With 300+ scripts to help you manage your
{" "}
<b>Proxmox VE environment</b>
. Whether you&#39;re a seasoned
user or a newcomer, we&#39;ve got you covered. user or a newcomer, we&#39;ve got you covered.
</p> </p>
</div> </div>

View File

@ -1,6 +1,7 @@
import { basePath } from "@/config/siteConfig";
import type { MetadataRoute } from "next"; import type { MetadataRoute } from "next";
import { basePath } from "@/config/site-config";
export const dynamic = "force-static"; export const dynamic = "force-static";
export default function robots(): MetadataRoute.Robots { export default function robots(): MetadataRoute.Robots {

View File

@ -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>
);
}

View File

@ -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;

View File

@ -1,17 +1,17 @@
import { CPUIcon, HDDIcon, RAMIcon } from "@/components/icons/resource-icons"; import { CPUIcon, HDDIcon, RAMIcon } from "@/components/icons/resource-icons";
import { getDisplayValueFromRAM } from "@/lib/utils/resource-utils"; import { getDisplayValueFromRAM } from "@/lib/utils/resource-utils";
interface ResourceDisplayProps { type ResourceDisplayProps = {
title: string; title: string;
cpu: number | null; cpu: number | null;
ram: number | null; ram: number | null;
hdd: number | null; hdd: number | null;
} };
interface IconTextProps { type IconTextProps = {
icon: React.ReactNode; icon: React.ReactNode;
label: string; label: string;
} };
function IconText({ icon, label }: IconTextProps) { function IconText({ icon, label }: IconTextProps) {
return ( return (
@ -27,7 +27,8 @@ export function ResourceDisplay({ title, cpu, ram, hdd }: ResourceDisplayProps)
const hasRAM = typeof ram === "number" && ram > 0; const hasRAM = typeof ram === "number" && ram > 0;
const hasHDD = typeof hdd === "number" && hdd > 0; const hasHDD = typeof hdd === "number" && hdd > 0;
if (!hasCPU && !hasRAM && !hasHDD) return null; if (!hasCPU && !hasRAM && !hasHDD)
return null;
return ( return (
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">

View File

@ -1,18 +1,18 @@
import { useCallback, useEffect, useRef } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import Image from "next/image";
import Link from "next/link";
import type { Category } from "@/lib/types";
import { formattedBadge } from "@/components/CommandMenu";
import { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
AccordionItem, AccordionItem,
AccordionTrigger, AccordionTrigger,
} from "@/components/ui/accordion"; } from "@/components/ui/accordion";
import { Category } from "@/lib/types"; import { formattedBadge } from "@/components/command-menu";
import { basePath } from "@/config/site-config";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { basePath } from "@/config/siteConfig";
export default function ScriptAccordion({ export default function ScriptAccordion({
items, items,
@ -41,8 +41,8 @@ export default function ScriptAccordion({
useEffect(() => { useEffect(() => {
if (selectedScript) { if (selectedScript) {
const category = items.find((category) => const category = items.find(category =>
category.scripts.some((script) => script.slug === selectedScript), category.scripts.some(script => script.slug === selectedScript),
); );
if (category) { if (category) {
setExpandedItem(category.name); setExpandedItem(category.name);
@ -58,11 +58,11 @@ export default function ScriptAccordion({
collapsible collapsible
className="overflow-y-scroll max-h-[calc(100vh-225px)] overflow-x-hidden p-2" className="overflow-y-scroll max-h-[calc(100vh-225px)] overflow-x-hidden p-2"
> >
{items.map((category) => ( {items.map(category => (
<AccordionItem <AccordionItem
key={category.id + ":category"} key={`${category.id}:category`}
value={category.name} value={category.name}
className={cn("sm:text-md flex flex-col border-none", { className={cn("sm:text-sm flex flex-col border-none", {
"rounded-lg bg-accent/30": expandedItem === category.name, "rounded-lg bg-accent/30": expandedItem === category.name,
})} })}
> >
@ -72,11 +72,15 @@ export default function ScriptAccordion({
)} )}
> >
<div className="mr-2 flex w-full items-center justify-between"> <div className="mr-2 flex w-full items-center justify-between">
<span className="pl-2 text-left">{category.name} </span> <span className="pl-2 text-left">
{category.name}
{" "}
</span>
<span className="rounded-full bg-gray-200 px-2 py-1 text-xs text-muted-foreground hover:no-underline dark:bg-blue-800/20"> <span className="rounded-full bg-gray-200 px-2 py-1 text-xs text-muted-foreground hover:no-underline dark:bg-blue-800/20">
{category.scripts.length} {category.scripts.length}
</span> </span>
</div>{" "} </div>
{" "}
</AccordionTrigger> </AccordionTrigger>
<AccordionContent <AccordionContent
data-state={expandedItem === category.name ? "open" : "closed"} data-state={expandedItem === category.name ? "open" : "closed"}
@ -109,10 +113,9 @@ export default function ScriptAccordion({
height={16} height={16}
width={16} width={16}
unoptimized unoptimized
onError={(e) => onError={e =>
((e.currentTarget as HTMLImageElement).src = ((e.currentTarget as HTMLImageElement).src
`/${basePath}/logo.png`) = `/${basePath}/logo.png`)}
}
alt={script.name} alt={script.name}
className="mr-1 w-4 h-4 rounded-full" className="mr-1 w-4 h-4 rounded-full"
/> />

View File

@ -1,16 +1,18 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { basePath, mostPopularScripts } from "@/config/siteConfig";
import { extractDate } from "@/lib/time";
import { Category, Script } from "@/lib/types";
import { CalendarPlus } from "lucide-react"; import { CalendarPlus } from "lucide-react";
import { useMemo, useState } from "react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useMemo, useState } from "react";
import type { Category, Script } from "@/lib/types";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { basePath, mostPopularScripts } from "@/config/site-config";
import { Button } from "@/components/ui/button";
import { extractDate } from "@/lib/time";
const ITEMS_PER_PAGE = 3; const ITEMS_PER_PAGE = 3;
export const getDisplayValueFromType = (type: string) => { export function getDisplayValueFromType(type: string) {
switch (type) { switch (type) {
case "ct": case "ct":
return "LXC"; return "LXC";
@ -22,15 +24,16 @@ export const getDisplayValueFromType = (type: string) => {
default: default:
return ""; return "";
} }
}; }
export function LatestScripts({ items }: { items: Category[] }) { export function LatestScripts({ items }: { items: Category[] }) {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const latestScripts = useMemo(() => { const latestScripts = useMemo(() => {
if (!items) return []; if (!items)
return [];
const scripts = items.flatMap((category) => category.scripts || []); const scripts = items.flatMap(category => category.scripts || []);
// Filter out duplicates by slug // Filter out duplicates by slug
const uniqueScriptsMap = new Map<string, Script>(); const uniqueScriptsMap = new Map<string, Script>();
@ -46,11 +49,11 @@ export function LatestScripts({ items }: { items: Category[] }) {
}, [items]); }, [items]);
const goToNextPage = () => { const goToNextPage = () => {
setPage((prevPage) => prevPage + 1); setPage(prevPage => prevPage + 1);
}; };
const goToPreviousPage = () => { const goToPreviousPage = () => {
setPage((prevPage) => prevPage - 1); setPage(prevPage => prevPage - 1);
}; };
const startIndex = (page - 1) * ITEMS_PER_PAGE; const startIndex = (page - 1) * ITEMS_PER_PAGE;
@ -80,7 +83,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
</div> </div>
)} )}
<div className="min-w flex w-full flex-row flex-wrap gap-4"> <div className="min-w flex w-full flex-row flex-wrap gap-4">
{latestScripts.slice(startIndex, endIndex).map((script) => ( {latestScripts.slice(startIndex, endIndex).map(script => (
<Card key={script.slug} className="min-w-[250px] flex-1 flex-grow bg-accent/30"> <Card key={script.slug} className="min-w-[250px] flex-1 flex-grow bg-accent/30">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-3"> <CardTitle className="flex items-center gap-3">
@ -91,13 +94,15 @@ export function LatestScripts({ items }: { items: Category[] }) {
height={64} height={64}
width={64} width={64}
alt="" alt=""
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)} onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
className="h-11 w-11 object-contain" className="h-11 w-11 object-contain"
/> />
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<p className="text-lg line-clamp-1"> <p className="text-lg line-clamp-1">
{script.name} {getDisplayValueFromType(script.type)} {script.name}
{" "}
{getDisplayValueFromType(script.type)}
</p> </p>
<p className="text-sm text-muted-foreground flex items-center gap-1"> <p className="text-sm text-muted-foreground flex items-center gap-1">
<CalendarPlus className="h-4 w-4" /> <CalendarPlus className="h-4 w-4" />
@ -130,7 +135,7 @@ export function LatestScripts({ items }: { items: Category[] }) {
export function MostViewedScripts({ items }: { items: Category[] }) { export function MostViewedScripts({ items }: { items: Category[] }) {
const mostViewedScripts = items.reduce((acc: Script[], category) => { const mostViewedScripts = items.reduce((acc: Script[], category) => {
const foundScripts = category.scripts.filter((script) => mostPopularScripts.includes(script.slug)); const foundScripts = category.scripts.filter(script => mostPopularScripts.includes(script.slug));
return acc.concat(foundScripts); return acc.concat(foundScripts);
}, []); }, []);
@ -142,7 +147,7 @@ export function MostViewedScripts({ items }: { items: Category[] }) {
</> </>
)} )}
<div className="min-w flex w-full flex-row flex-wrap gap-4"> <div className="min-w flex w-full flex-row flex-wrap gap-4">
{mostViewedScripts.map((script) => ( {mostViewedScripts.map(script => (
<Card key={script.slug} className="min-w-[250px] flex-1 flex-grow bg-accent/30"> <Card key={script.slug} className="min-w-[250px] flex-1 flex-grow bg-accent/30">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-3"> <CardTitle className="flex items-center gap-3">
@ -153,13 +158,15 @@ export function MostViewedScripts({ items }: { items: Category[] }) {
height={64} height={64}
width={64} width={64}
alt="" alt=""
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)} onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
className="h-11 w-11 object-contain" className="h-11 w-11 object-contain"
/> />
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<p className="line-clamp-1 text-lg"> <p className="line-clamp-1 text-lg">
{script.name} {getDisplayValueFromType(script.type)} {script.name}
{" "}
{getDisplayValueFromType(script.type)}
</p> </p>
<p className="flex items-center gap-1 text-sm text-muted-foreground"> <p className="flex items-center gap-1 text-sm text-muted-foreground">
<CalendarPlus className="h-4 w-4" /> <CalendarPlus className="h-4 w-4" />

View File

@ -1,31 +1,32 @@
"use client"; "use client";
import { extractDate } from "@/lib/time";
import { AppVersion, Script } from "@/lib/types";
import { X } from "lucide-react"; import { X } from "lucide-react";
import { Suspense } from "react";
import Image from "next/image"; import Image from "next/image";
import { Separator } from "@/components/ui/separator"; import type { AppVersion, Script } from "@/lib/types";
import { basePath } from "@/config/siteConfig";
import { useVersions } from "@/hooks/useVersions";
import { cleanSlug } from "@/lib/utils/resource-utils";
import { Suspense } from "react";
import { ResourceDisplay } from "./ResourceDisplay";
import { getDisplayValueFromType } from "./ScriptInfoBlocks";
import Alerts from "./ScriptItems/Alerts";
import Buttons from "./ScriptItems/Buttons";
import ConfigFile from "./ScriptItems/ConfigFile";
import DefaultPassword from "./ScriptItems/DefaultPassword";
import Description from "./ScriptItems/Description";
import InstallCommand from "./ScriptItems/InstallCommand";
import InterFaces from "./ScriptItems/InterFaces";
import Tooltips from "./ScriptItems/Tooltips";
interface ScriptItemProps { import { cleanSlug } from "@/lib/utils/resource-utils";
import { Separator } from "@/components/ui/separator";
import { useVersions } from "@/hooks/use-versions";
import { basePath } from "@/config/site-config";
import { extractDate } from "@/lib/time";
import { getDisplayValueFromType } from "./script-info-blocks";
import DefaultPassword from "./script-items/default-password";
import InstallCommand from "./script-items/install-command";
import { ResourceDisplay } from "./resource-display";
import Description from "./script-items/description";
import ConfigFile from "./script-items/config-file";
import InterFaces from "./script-items/interfaces";
import Tooltips from "./script-items/tool-tips";
import Buttons from "./script-items/buttons";
import Alerts from "./script-items/alerts";
type ScriptItemProps = {
item: Script; item: Script;
setSelectedScript: (script: string | null) => void; setSelectedScript: (script: string | null) => void;
} };
function ScriptHeader({ item }: { item: Script }) { function ScriptHeader({ item }: { item: Script }) {
const defaultInstallMethod = item.install_methods?.[0]; const defaultInstallMethod = item.install_methods?.[0];
@ -40,7 +41,7 @@ function ScriptHeader({ item }: { item: Script }) {
className="h-32 w-32 rounded-xl bg-gradient-to-br from-accent/40 to-accent/60 object-contain p-3 shadow-lg transition-transform hover:scale-105" className="h-32 w-32 rounded-xl bg-gradient-to-br from-accent/40 to-accent/60 object-contain p-3 shadow-lg transition-transform hover:scale-105"
src={item.logo || `/${basePath}/logo.png`} src={item.logo || `/${basePath}/logo.png`}
width={400} width={400}
onError={(e) => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)} onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
height={400} height={400}
alt={item.name} alt={item.name}
unoptimized unoptimized
@ -58,10 +59,16 @@ function ScriptHeader({ item }: { item: Script }) {
</span> </span>
</h1> </h1>
<div className="mt-1 flex items-center gap-3 text-sm text-muted-foreground"> <div className="mt-1 flex items-center gap-3 text-sm text-muted-foreground">
<span>Added {extractDate(item.date_created)}</span> <span>
Added
{" "}
{extractDate(item.date_created)}
</span>
<span></span> <span></span>
<span className=" capitalize"> <span className=" capitalize">
{os} {version} {os}
{" "}
{version}
</span> </span>
</div> </div>
</div> </div>
@ -76,10 +83,10 @@ function ScriptHeader({ item }: { item: Script }) {
hdd={defaultInstallMethod.resources.hdd} hdd={defaultInstallMethod.resources.hdd}
/> />
)} )}
{item.install_methods.find((method) => method.type === "alpine")?.resources && ( {item.install_methods.find(method => method.type === "alpine")?.resources && (
<ResourceDisplay <ResourceDisplay
title="Alpine" title="Alpine"
{...item.install_methods.find((method) => method.type === "alpine")!.resources!} {...item.install_methods.find(method => method.type === "alpine")!.resources!}
/> />
)} )}
</div> </div>
@ -108,7 +115,8 @@ function VersionInfo({ item }: { item: Script }) {
return cleanName === cleanSlug(item.slug) || cleanName.includes(cleanSlug(item.slug)); return cleanName === cleanSlug(item.slug) || cleanName.includes(cleanSlug(item.slug));
}); });
if (!matchedVersion) return null; if (!matchedVersion)
return null;
return <span className="font-medium text-sm">{matchedVersion.version}</span>; return <span className="font-medium text-sm">{matchedVersion.version}</span>;
} }
@ -132,7 +140,7 @@ export function ScriptItem({ item, setSelectedScript }: ScriptItemProps) {
</button> </button>
</div> </div>
<div className="rounded-xl border border-border bg-gradient-to-b from-card/30 to-background/50 backdrop-blur-sm shadow-sm"> <div className="rounded-xl border border-border bg-accent/30 backdrop-blur-sm shadow-sm">
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
<Suspense fallback={<div className="animate-pulse h-32 bg-accent/20 rounded-xl" />}> <Suspense fallback={<div className="animate-pulse h-32 bg-accent/20 rounded-xl" />}>
<ScriptHeader item={item} /> <ScriptHeader item={item} />
@ -144,7 +152,9 @@ export function ScriptItem({ item, setSelectedScript }: ScriptItemProps) {
<div className="mt-4 rounded-lg border shadow-sm"> <div className="mt-4 rounded-lg border shadow-sm">
<div className="flex gap-3 px-4 py-2 bg-accent/25"> <div className="flex gap-3 px-4 py-2 bg-accent/25">
<h2 className="text-lg font-semibold"> <h2 className="text-lg font-semibold">
How to {item.type === "pve" ? "use" : item.type === "addon" ? "apply" : "install"} How to
{" "}
{item.type === "pve" ? "use" : item.type === "addon" ? "apply" : "install"}
</h2> </h2>
<Tooltips item={item} /> <Tooltips item={item} />
</div> </div>

View File

@ -1,19 +1,21 @@
import TextCopyBlock from "@/components/TextCopyBlock";
import { AlertColors } from "@/config/siteConfig";
import { Script } from "@/lib/types";
import { cn } from "@/lib/utils";
import { AlertCircle, NotepadText } from "lucide-react"; import { AlertCircle, NotepadText } from "lucide-react";
import type { Script } from "@/lib/types";
import TextCopyBlock from "@/components/text-copy-block";
import { AlertColors } from "@/config/site-config";
import { cn } from "@/lib/utils";
type NoteProps = { type NoteProps = {
text: string; text: string;
type: keyof typeof AlertColors; type: keyof typeof AlertColors;
} };
export default function Alerts({ item }: { item: Script }) { export default function Alerts({ item }: { item: Script }) {
return ( return (
<> <>
{item?.notes?.length > 0 && {item?.notes?.length > 0
item.notes.map((note: NoteProps, index: number) => ( && item.notes.map((note: NoteProps, index: number) => (
<div key={index} className="mt-4 flex flex-col shadow-sm gap-2"> <div key={index} className="mt-4 flex flex-col shadow-sm gap-2">
<p <p
className={cn( className={cn(
@ -21,11 +23,13 @@ export default function Alerts({ item }: { item: Script }) {
AlertColors[note.type], AlertColors[note.type],
)} )}
> >
{note.type == "info" ? ( {note.type === "info"
<NotepadText className="h-4 min-h-4 w-4 min-w-4" /> ? (
) : ( <NotepadText className="h-4 min-h-4 w-4 min-w-4" />
<AlertCircle className="h-4 min-h-4 w-4 min-w-4" /> )
)} : (
<AlertCircle className="h-4 min-h-4 w-4 min-w-4" />
)}
<span>{TextCopyBlock(note.text)}</span> <span>{TextCopyBlock(note.text)}</span>
</p> </p>
</div> </div>

View File

@ -1,20 +1,22 @@
import { Button } from "@/components/ui/button"; import { BookOpenText, Code, Globe, LinkIcon, RefreshCcw } from "lucide-react";
import type { Script } from "@/lib/types";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { basePath } from "@/config/siteConfig"; import { Button } from "@/components/ui/button";
import { Script } from "@/lib/types"; import { basePath } from "@/config/site-config";
import { BookOpenText, Code, Globe, LinkIcon, RefreshCcw } from "lucide-react";
const generateInstallSourceUrl = (slug: string) => { function generateInstallSourceUrl(slug: string) {
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`; const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
return `${baseUrl}/install/${slug}-install.sh`; return `${baseUrl}/install/${slug}-install.sh`;
}; }
const generateSourceUrl = (slug: string, type: string) => { function generateSourceUrl(slug: string, type: string) {
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`; const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
switch (type) { switch (type) {
@ -29,18 +31,18 @@ const generateSourceUrl = (slug: string, type: string) => {
default: default:
return `${baseUrl}/ct/${slug}.sh`; // fallback for "ct" return `${baseUrl}/ct/${slug}.sh`; // fallback for "ct"
} }
}; }
const generateUpdateUrl = (slug: string) => { function generateUpdateUrl(slug: string) {
const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`; const baseUrl = `https://raw.githubusercontent.com/community-scripts/${basePath}/main`;
return `${baseUrl}/ct/${slug}.sh`; return `${baseUrl}/ct/${slug}.sh`;
}; }
interface LinkItem { type LinkItem = {
href: string; href: string;
icon: React.ReactNode; icon: React.ReactNode;
text: string; text: string;
} };
export default function Buttons({ item }: { item: Script }) { export default function Buttons({ item }: { item: Script }) {
const isCtOrDefault = ["ct"].includes(item.type); const isCtOrDefault = ["ct"].includes(item.type);
@ -76,7 +78,8 @@ export default function Buttons({ item }: { item: Script }) {
}, },
].filter(Boolean) as LinkItem[]; ].filter(Boolean) as LinkItem[];
if (links.length === 0) return null; if (links.length === 0)
return null;
return ( return (
<DropdownMenu> <DropdownMenu>

View File

@ -1,13 +1,15 @@
import handleCopy from "@/components/handleCopy"; import type { Script } from "@/lib/types";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Script } from "@/lib/types"; import handleCopy from "@/components/handle-copy";
import { Button } from "@/components/ui/button";
export default function DefaultPassword({ item }: { item: Script }) { export default function DefaultPassword({ item }: { item: Script }) {
const { username, password } = item.default_credentials; const { username, password } = item.default_credentials;
const hasDefaultLogin = username || password; const hasDefaultLogin = username || password;
if (!hasDefaultLogin) return null; if (!hasDefaultLogin)
return null;
const copyCredential = (type: "username" | "password") => { const copyCredential = (type: "username" | "password") => {
handleCopy(type, item.default_credentials[type] ?? ""); handleCopy(type, item.default_credentials[type] ?? "");
@ -21,18 +23,27 @@ export default function DefaultPassword({ item }: { item: Script }) {
<Separator className="w-full" /> <Separator className="w-full" />
<div className="flex flex-col gap-2 p-4"> <div className="flex flex-col gap-2 p-4">
<p className="mb-2 text-sm"> <p className="mb-2 text-sm">
You can use the following credentials to login to the {item.name} {item.type}. You can use the following credentials to login to the
{" "}
{item.name}
{" "}
{item.type}
.
</p> </p>
{["username", "password"].map((type) => { {["username", "password"].map((type) => {
const value = item.default_credentials[type as "username" | "password"]; const value = item.default_credentials[type as "username" | "password"];
return value && value.trim() !== "" ? ( return value && value.trim() !== ""
<div key={type} className="text-sm"> ? (
{type.charAt(0).toUpperCase() + type.slice(1)}:{" "} <div key={type} className="text-sm">
<Button variant="secondary" size="null" onClick={() => copyCredential(type as "username" | "password")}> {type.charAt(0).toUpperCase() + type.slice(1)}
{value} :
</Button> {" "}
</div> <Button variant="secondary" size="null" onClick={() => copyCredential(type as "username" | "password")}>
) : null; {value}
</Button>
</div>
)
: null;
})} })}
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
import { Script } from "@/lib/types"; import type { Script } from "@/lib/types";
export default function DefaultSettings({ item }: { item: Script }) { export default function DefaultSettings({ item }: { item: Script }) {
const getDisplayValueFromRAM = (ram: number) => (ram >= 1024 ? `${Math.floor(ram / 1024)}GB` : `${ram}MB`); const getDisplayValueFromRAM = (ram: number) => (ram >= 1024 ? `${Math.floor(ram / 1024)}GB` : `${ram}MB`);
@ -8,15 +8,26 @@ export default function DefaultSettings({ item }: { item: Script }) {
return ( return (
<div> <div>
<h2 className="text-md font-semibold">{title}</h2> <h2 className="text-md font-semibold">{title}</h2>
<p className="text-sm text-muted-foreground">CPU: {cpu}vCPU</p> <p className="text-sm text-muted-foreground">
<p className="text-sm text-muted-foreground">RAM: {getDisplayValueFromRAM(ram ?? 0)}</p> CPU:
<p className="text-sm text-muted-foreground">HDD: {hdd}GB</p> {cpu}
vCPU
</p>
<p className="text-sm text-muted-foreground">
RAM:
{getDisplayValueFromRAM(ram ?? 0)}
</p>
<p className="text-sm text-muted-foreground">
HDD:
{hdd}
GB
</p>
</div> </div>
); );
}; };
const defaultSettings = item.install_methods.find((method) => method.type === "default"); const defaultSettings = item.install_methods.find(method => method.type === "default");
const defaultAlpineSettings = item.install_methods.find((method) => method.type === "alpine"); const defaultAlpineSettings = item.install_methods.find(method => method.type === "alpine");
const hasDefaultSettings = defaultSettings?.resources && Object.values(defaultSettings.resources).some(Boolean); const hasDefaultSettings = defaultSettings?.resources && Object.values(defaultSettings.resources).some(Boolean);

View File

@ -1,5 +1,6 @@
import TextCopyBlock from "@/components/TextCopyBlock"; import type { Script } from "@/lib/types";
import { Script } from "@/lib/types";
import TextCopyBlock from "@/components/text-copy-block";
export default function Description({ item }: { item: Script }) { export default function Description({ item }: { item: Script }) {
return ( return (

View File

@ -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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { Info } from "lucide-react"; import CodeCopyButton from "@/components/ui/code-copy-button";
import { basePath } from "@/config/siteConfig"; import { basePath } from "@/config/site-config";
import { Script } from "@/lib/types";
import { getDisplayValueFromType } from "../ScriptInfoBlocks";
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 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 giteaUrl = `https://git.community-scripts.org/community-scripts/${basePath}/raw/branch/main/${scriptPath}`;
const url = useGitea ? giteaUrl : githubUrl; 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 }) { export default function InstallCommand({ item }: { item: Script }) {
const alpineScript = item.install_methods.find((method) => method.type === "alpine"); const alpineScript = item.install_methods.find(method => method.type === "alpine");
const defaultScript = item.install_methods.find((method) => method.type === "default"); const defaultScript = item.install_methods.find(method => method.type === "default");
const renderInstructions = (isAlpine = false) => ( const renderInstructions = (isAlpine = false) => (
<> <>
<p className="text-sm mt-2"> <p className="text-sm mt-2">
{isAlpine ? ( {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. As an alternative option, you can use Alpine Linux and the
You are also obliged to adhere to updates provided by the package maintainer. {" "}
</> {item.name}
) : item.type === "pve" ? ( {" "}
<> package to create a
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.name}
</> {" "}
) : item.type === "addon" ? ( {getDisplayValueFromType(item.type)}
<> {" "}
This script enhances an existing setup. You can use it inside a running LXC container or directly on the container with faster creation time and minimal system resource usage.
Proxmox VE host to extend functionality with {item.name}. You are also obliged to adhere to updates provided by the package maintainer.
</> </>
) : ( )
<> : item.type === "pve"
To create a new Proxmox VE {item.name} {getDisplayValueFromType(item.type)}, run the command below in the ? (
Proxmox VE Shell. <>
</> 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> </p>
{isAlpine && ( {isAlpine && (
<p className="mt-2 text-sm"> <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. the Proxmox VE Shell.
</p> </p>
)} )}
@ -56,7 +90,9 @@ export default function InstallCommand({ item }: { item: Script }) {
<Alert className="mt-3 mb-3"> <Alert className="mt-3 mb-3">
<Info className="h-4 w-4" /> <Info className="h-4 w-4" />
<AlertDescription className="text-sm"> <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 fixes, no IPv6 support, API rate limits (60/hour). Use our Gitea mirror as a reliable alternative when
experiencing these issues. experiencing these issues.
</AlertDescription> </AlertDescription>
@ -81,7 +117,8 @@ export default function InstallCommand({ item }: { item: Script }) {
</TabsContent> </TabsContent>
</Tabs> </Tabs>
); );
} else if (defaultScript?.script) { }
else if (defaultScript?.script) {
return ( return (
<> <>
{renderInstructions()} {renderInstructions()}
@ -109,4 +146,4 @@ export default function InstallCommand({ item }: { item: Script }) {
</Tabs> </Tabs>
</div> </div>
); );
} }

View File

@ -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>
);
}

View File

@ -1,22 +1,27 @@
import { Badge, type BadgeProps } from "@/components/ui/badge";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Script } from "@/lib/types";
import { cn } from "@/lib/utils";
import { CircleHelp } from "lucide-react"; import { CircleHelp } from "lucide-react";
import React from "react"; import React from "react";
interface TooltipProps { import type { BadgeProps } from "@/components/ui/badge";
import type { Script } from "@/lib/types";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
type TooltipProps = {
variant: BadgeProps["variant"]; variant: BadgeProps["variant"];
label: string; label: string;
content?: string; content?: string;
} };
const TooltipBadge: React.FC<TooltipProps> = ({ variant, label, content }) => ( const TooltipBadge: React.FC<TooltipProps> = ({ variant, label, content }) => (
<TooltipProvider> <TooltipProvider>
<Tooltip delayDuration={100}> <Tooltip delayDuration={100}>
<TooltipTrigger className={cn("flex items-center", !content && "cursor-default")}> <TooltipTrigger className={cn("flex items-center", !content && "cursor-default")}>
<Badge variant={variant} className="flex items-center gap-1"> <Badge variant={variant} className="flex items-center gap-1">
{label} {content && <CircleHelp className="size-3" />} {label}
{" "}
{content && <CircleHelp className="size-3" />}
</Badge> </Badge>
</TooltipTrigger> </TooltipTrigger>
{content && ( {content && (
@ -34,7 +39,7 @@ export default function Tooltips({ item }: { item: Script }) {
{item.privileged && ( {item.privileged && (
<TooltipBadge variant="warning" label="Privileged" content="This script will be run in a privileged LXC" /> <TooltipBadge variant="warning" label="Privileged" content="This script will be run in a privileged LXC" />
)} )}
{(item.updateable || item.type !== "pve") && ( {item.updateable && item.type !== "pve" && (
<TooltipBadge <TooltipBadge
variant="success" variant="success"
label="Updateable" label="Updateable"

View 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;

View File

@ -1,8 +1,8 @@
import { AppVersion } from "@/lib/types"; import type { AppVersion } from "@/lib/types";
interface VersionBadgeProps { type VersionBadgeProps = {
version: AppVersion; version: AppVersion;
} };
export function VersionBadge({ version }: VersionBadgeProps) { export function VersionBadge({ version }: VersionBadgeProps) {
return ( return (

View File

@ -1,18 +1,20 @@
"use client"; "use client";
import { Suspense, useEffect, useState } from "react";
export const dynamic = "force-static";
import { ScriptItem } from "@/app/scripts/_components/ScriptItem";
import { fetchCategories } from "@/lib/data";
import { Category, Script } from "@/lib/types";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { useQueryState } from "nuqs"; import { useQueryState } from "nuqs";
import { Suspense, useEffect, useState } from "react";
import type { Category, Script } from "@/lib/types";
import { ScriptItem } from "@/app/scripts/_components/script-item";
import { fetchCategories } from "@/lib/data";
import { import {
LatestScripts, LatestScripts,
MostViewedScripts, MostViewedScripts,
} from "./_components/ScriptInfoBlocks"; } from "./_components/script-info-blocks";
import Sidebar from "./_components/Sidebar"; import Sidebar from "./_components/sidebar";
export const dynamic = "force-static";
function ScriptContent() { function ScriptContent() {
const [selectedScript, setSelectedScript] = useQueryState("id"); const [selectedScript, setSelectedScript] = useQueryState("id");
@ -22,9 +24,9 @@ function ScriptContent() {
useEffect(() => { useEffect(() => {
if (selectedScript && links.length > 0) { if (selectedScript && links.length > 0) {
const script = links const script = links
.map((category) => category.scripts) .map(category => category.scripts)
.flat() .flat()
.find((script) => script.slug === selectedScript); .find(script => script.slug === selectedScript);
setItem(script); setItem(script);
} }
}, [selectedScript, links]); }, [selectedScript, links]);
@ -34,7 +36,7 @@ function ScriptContent() {
.then((categories) => { .then((categories) => {
setLinks(categories); setLinks(categories);
}) })
.catch((error) => console.error(error)); .catch(error => console.error(error));
}, []); }, []);
return ( return (
@ -48,14 +50,16 @@ function ScriptContent() {
/> />
</div> </div>
<div className="mx-4 w-full sm:mx-0 sm:ml-4"> <div className="mx-4 w-full sm:mx-0 sm:ml-4">
{selectedScript && item ? ( {selectedScript && item
<ScriptItem item={item} setSelectedScript={setSelectedScript} /> ? (
) : ( <ScriptItem item={item} setSelectedScript={setSelectedScript} />
<div className="flex w-full flex-col gap-5"> )
<LatestScripts items={links} /> : (
<MostViewedScripts items={links} /> <div className="flex w-full flex-col gap-5">
</div> <LatestScripts items={links} />
)} <MostViewedScripts items={links} />
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@ -65,13 +69,13 @@ function ScriptContent() {
export default function Page() { export default function Page() {
return ( return (
<Suspense <Suspense
fallback={ fallback={(
<div className="flex h-screen w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6"> <div className="flex h-screen w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6">
<div className="space-y-2 text-center"> <div className="space-y-2 text-center">
<Loader2 className="h-10 w-10 animate-spin" /> <Loader2 className="h-10 w-10 animate-spin" />
</div> </div>
</div> </div>
} )}
> >
<ScriptContent /> <ScriptContent />
</Suspense> </Suspense>

View File

@ -1,11 +1,12 @@
import { basePath } from "@/config/siteConfig";
import type { MetadataRoute } from "next"; import type { MetadataRoute } from "next";
import { basePath } from "@/config/site-config";
export const dynamic = "force-static"; export const dynamic = "force-static";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> { export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
let domain = "community-scripts.github.io"; const domain = "community-scripts.github.io";
let protocol = "https"; const protocol = "https";
return [ return [
{ {
url: `${protocol}://${domain}/${basePath}`, url: `${protocol}://${domain}/${basePath}`,
@ -18,6 +19,6 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
{ {
url: `${protocol}://${domain}/${basePath}/json-editor`, url: `${protocol}://${domain}/${basePath}/json-editor`,
lastModified: new Date(), lastModified: new Date(),
} },
]; ];
} }

View File

@ -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;

View File

@ -1,12 +1,11 @@
"use client"; "use client";
import { Button } from "@/components/ui/button"; import { ArcElement, Chart as ChartJS, Tooltip as ChartTooltip, Legend } from "chart.js";
import { import ChartDataLabels from "chartjs-plugin-datalabels";
Dialog, import { BarChart3, PieChart } from "lucide-react";
DialogContent, import React, { useState } from "react";
DialogHeader, import { Pie } from "react-chartjs-2";
DialogTitle,
} from "@/components/ui/dialog";
import { import {
Table, Table,
TableBody, TableBody,
@ -21,21 +20,23 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { Chart as ChartJS, ArcElement, Tooltip as ChartTooltip, Legend } from "chart.js"; import {
import ChartDataLabels from "chartjs-plugin-datalabels"; Dialog,
import { BarChart3, PieChart } from "lucide-react"; DialogContent,
import React, { useState } from "react"; DialogHeader,
import { Pie, Bar } from "react-chartjs-2"; DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
ChartJS.register(ArcElement, ChartTooltip, Legend, ChartDataLabels); ChartJS.register(ArcElement, ChartTooltip, Legend, ChartDataLabels);
interface SummaryData { type SummaryData = {
nsapp_count: Record<string, number>; nsapp_count: Record<string, number>;
} };
interface ApplicationChartProps { type ApplicationChartProps = {
data: SummaryData | null; data: SummaryData | null;
} };
const ITEMS_PER_PAGE = 20; const ITEMS_PER_PAGE = 20;
const CHART_COLORS = [ const CHART_COLORS = [
@ -61,14 +62,15 @@ export default function ApplicationChart({ data }: ApplicationChartProps) {
const [chartStartIndex, setChartStartIndex] = useState(0); const [chartStartIndex, setChartStartIndex] = useState(0);
const [tableLimit, setTableLimit] = useState(ITEMS_PER_PAGE); const [tableLimit, setTableLimit] = useState(ITEMS_PER_PAGE);
if (!data) return null; if (!data)
return null;
const sortedApps = Object.entries(data.nsapp_count) const sortedApps = Object.entries(data.nsapp_count)
.sort(([, a], [, b]) => b - a); .sort(([, a], [, b]) => b - a);
const chartApps = sortedApps.slice( const chartApps = sortedApps.slice(
chartStartIndex, chartStartIndex,
chartStartIndex + ITEMS_PER_PAGE chartStartIndex + ITEMS_PER_PAGE,
); );
const chartData = { const chartData = {
@ -141,14 +143,18 @@ export default function ApplicationChart({ data }: ApplicationChartProps) {
onClick={() => setChartStartIndex(Math.max(0, chartStartIndex - ITEMS_PER_PAGE))} onClick={() => setChartStartIndex(Math.max(0, chartStartIndex - ITEMS_PER_PAGE))}
disabled={chartStartIndex === 0} disabled={chartStartIndex === 0}
> >
Previous {ITEMS_PER_PAGE} Previous
{" "}
{ITEMS_PER_PAGE}
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
onClick={() => setChartStartIndex(chartStartIndex + ITEMS_PER_PAGE)} onClick={() => setChartStartIndex(chartStartIndex + ITEMS_PER_PAGE)}
disabled={chartStartIndex + ITEMS_PER_PAGE >= sortedApps.length} disabled={chartStartIndex + ITEMS_PER_PAGE >= sortedApps.length}
> >
Next {ITEMS_PER_PAGE} Next
{" "}
{ITEMS_PER_PAGE}
</Button> </Button>
</div> </div>
</DialogContent> </DialogContent>
@ -190,4 +196,4 @@ export default function ApplicationChart({ data }: ApplicationChartProps) {
</Dialog> </Dialog>
</div> </div>
); );
} }

View File

@ -1,3 +1,10 @@
import { useRouter } from "next/navigation";
import { Sparkles } from "lucide-react";
import Image from "next/image";
import React from "react";
import type { Category, Script } from "@/lib/types";
import { import {
CommandDialog, CommandDialog,
CommandEmpty, CommandEmpty,
@ -6,22 +13,16 @@ import {
CommandItem, CommandItem,
CommandList, CommandList,
} from "@/components/ui/command"; } from "@/components/ui/command";
import { basePath } from "@/config/siteConfig"; import { basePath } from "@/config/site-config";
import { fetchCategories } from "@/lib/data"; import { fetchCategories } from "@/lib/data";
import { Category, Script } from "@/lib/types";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import Image from "next/image";
import { useRouter } from "next/navigation";
import React from "react";
import { Badge } from "./ui/badge";
import { Button } from "./ui/button";
import { DialogTitle } from "./ui/dialog";
import { Sparkles } from "lucide-react";
import { TooltipContent, TooltipProvider } from "./ui/tooltip";
import { TooltipTrigger } from "./ui/tooltip";
import { Tooltip } from "./ui/tooltip";
export const formattedBadge = (type: string) => { import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
import { DialogTitle } from "./ui/dialog";
import { Button } from "./ui/button";
import { Badge } from "./ui/badge";
export function formattedBadge(type: string) {
switch (type) { switch (type) {
case "vm": case "vm":
return <Badge className="text-blue-500/75 border-blue-500/75">VM</Badge>; return <Badge className="text-blue-500/75 border-blue-500/75">VM</Badge>;
@ -33,12 +34,13 @@ export const formattedBadge = (type: string) => {
return <Badge className="text-green-500/75 border-green-500/75">ADDON</Badge>; return <Badge className="text-green-500/75 border-green-500/75">ADDON</Badge>;
} }
return null; return null;
}; }
// random Script // random Script
function getRandomScript(categories: Category[]): Script | null { function getRandomScript(categories: Category[]): Script | null {
const allScripts = categories.flatMap((cat) => cat.scripts || []); const allScripts = categories.flatMap(cat => cat.scripts || []);
if (allScripts.length === 0) return null; if (allScripts.length === 0)
return null;
const idx = Math.floor(Math.random() * allScripts.length); const idx = Math.floor(Math.random() * allScripts.length);
return allScripts[idx]; return allScripts[idx];
} }
@ -49,18 +51,6 @@ export default function CommandMenu() {
const [isLoading, setIsLoading] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false);
const router = useRouter(); const router = useRouter();
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
fetchSortedCategories();
setOpen((open) => !open);
}
};
document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, []);
const fetchSortedCategories = () => { const fetchSortedCategories = () => {
setIsLoading(true); setIsLoading(true);
fetchCategories() fetchCategories()
@ -74,6 +64,18 @@ export default function CommandMenu() {
}); });
}; };
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
fetchSortedCategories();
setOpen(open => !open);
}
};
document.addEventListener("keydown", down);
return () => document.removeEventListener("keydown", down);
}, []);
const openRandomScript = async () => { const openRandomScript = async () => {
if (links.length === 0) { if (links.length === 0) {
setIsLoading(true); setIsLoading(true);
@ -84,10 +86,12 @@ export default function CommandMenu() {
if (randomScript) { if (randomScript) {
router.push(`/scripts?id=${randomScript.slug}`); router.push(`/scripts?id=${randomScript.slug}`);
} }
} finally { }
finally {
setIsLoading(false); setIsLoading(false);
} }
} else { }
else {
const randomScript = getRandomScript(links); const randomScript = getRandomScript(links);
if (randomScript) { if (randomScript) {
router.push(`/scripts?id=${randomScript.slug}`); router.push(`/scripts?id=${randomScript.slug}`);
@ -110,7 +114,8 @@ export default function CommandMenu() {
> >
<span className="inline-flex">Search scripts...</span> <span className="inline-flex">Search scripts...</span>
<kbd className="pointer-events-none absolute right-[0.3rem] top-[0.45rem] hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex"> <kbd className="pointer-events-none absolute right-[0.3rem] top-[0.45rem] hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
<span className="text-xs"></span>K <span className="text-xs"></span>
K
</kbd> </kbd>
</Button> </Button>
@ -134,54 +139,34 @@ export default function CommandMenu() {
<CommandInput placeholder="Search for a script..." /> <CommandInput placeholder="Search for a script..." />
<CommandList> <CommandList>
<CommandEmpty>{isLoading ? "Loading..." : "No scripts found."}</CommandEmpty> <CommandEmpty>{isLoading ? "Loading..." : "No scripts found."}</CommandEmpty>
{(() => { {links.map(category => (
// Track seen scripts globally to avoid duplicates across all categories <CommandGroup key={`category:${category.name}`} heading={category.name}>
const globalSeenScripts = new Set<string>(); {category.scripts.map(script => (
<CommandItem
return links.map((category) => { key={`script:${script.slug}`}
const uniqueScripts = category.scripts.filter((script) => { value={`${script.slug}-${script.name}`}
if (globalSeenScripts.has(script.slug)) { onSelect={() => {
return false; setOpen(false);
} router.push(`/scripts?id=${script.slug}`);
globalSeenScripts.add(script.slug); }}
return true; >
}); <div className="flex gap-2" onClick={() => setOpen(false)}>
<Image
// Only render category if it has unique scripts src={script.logo || `/${basePath}/logo.png`}
if (uniqueScripts.length === 0) { onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
return null; unoptimized
} width={16}
height={16}
return ( alt=""
<CommandGroup key={`category:${category.name}`} heading={category.name}> className="h-5 w-5"
{uniqueScripts.map((script) => ( />
<CommandItem <span>{script.name}</span>
key={`script:${script.slug}`} <span>{formattedBadge(script.type)}</span>
value={`${script.slug}-${script.name}`} </div>
onSelect={() => { </CommandItem>
setOpen(false); ))}
router.push(`/scripts?id=${script.slug}`); </CommandGroup>
}} ))}
>
<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> </CommandList>
</CommandDialog> </CommandDialog>
</> </>

View File

@ -1,7 +1,8 @@
import * as AccordionPrimitive from "@radix-ui/react-accordion"; import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { FAQ_Items } from "../config/faqConfig";
import { Accordion, AccordionContent, AccordionItem } from "./ui/accordion"; import { Accordion, AccordionContent, AccordionItem } from "./ui/accordion";
import { FAQ_Items } from "../config/faq-config";
export default function FAQ() { export default function FAQ() {
return ( return (

View File

@ -1,16 +1,19 @@
import { basePath } from "@/config/siteConfig"; import { FileJson, Server } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { FileJson, Server, ExternalLink } from "lucide-react";
import { buttonVariants } from "./ui/button"; import { basePath } from "@/config/site-config";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { buttonVariants } from "./ui/button";
export default function Footer() { export default function Footer() {
return ( return (
<div className="supports-backdrop-blur:bg-background/90 mt-auto border-t w-full flex justify-between border-border bg-background/40 py-4 backdrop-blur-lg"> <div className="supports-backdrop-blur:bg-background/90 mt-auto border-t w-full flex justify-between border-border bg-background/40 py-4 backdrop-blur-lg">
<div className="mx-6 w-full flex justify-between text-xs sm:text-sm text-muted-foreground"> <div className="mx-6 w-full flex justify-between text-xs sm:text-sm text-muted-foreground">
<div className="flex items-center"> <div className="flex items-center">
<p> <p>
Website built by the community. The source code is available on{" "} Website built by the community. The source code is available on
{" "}
<Link <Link
href={`https://github.com/community-scripts/${basePath}/tree/main/frontend`} href={`https://github.com/community-scripts/${basePath}/tree/main/frontend`}
target="_blank" target="_blank"
@ -28,13 +31,17 @@ export default function Footer() {
href="/json-editor" href="/json-editor"
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2")} className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2")}
> >
<FileJson className="h-4 w-4" /> JSON Editor <FileJson className="h-4 w-4" />
{" "}
JSON Editor
</Link> </Link>
<Link <Link
href="/data" href="/data"
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2")} className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2")}
> >
<Server className="h-4 w-4" /> API Data <Server className="h-4 w-4" />
{" "}
API Data
</Link> </Link>
</div> </div>
</div> </div>

View File

@ -2,14 +2,15 @@
import React from "react"; import React from "react";
interface ModalProps { type ModalProps = {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
children: React.ReactNode; children: React.ReactNode;
} };
const Modal: React.FC<ModalProps> = ({ isOpen, onClose, children }) => { const Modal: React.FC<ModalProps> = ({ isOpen, onClose, children }) => {
if (!isOpen) return null; if (!isOpen)
return null;
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50">

View 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;

View File

@ -1,5 +1,6 @@
import { ClipboardIcon } from "lucide-react"; import { ClipboardIcon } from "lucide-react";
import handleCopy from "./handleCopy";
import handleCopy from "./handle-copy";
export default function TextCopyBlock(description: string) { export default function TextCopyBlock(description: string) {
const pattern = /`([^`]*)`/g; const pattern = /`([^`]*)`/g;
@ -19,7 +20,8 @@ export default function TextCopyBlock(description: string) {
/> />
</span> </span>
); );
} else { }
else {
return part; return part;
} }
}); });

View File

@ -1,7 +1,8 @@
"use client"; "use client";
import type { ThemeProviderProps } from "next-themes";
import { ThemeProvider as NextThemesProvider } from "next-themes"; import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) { export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>; return <NextThemesProvider {...props}>{children}</NextThemesProvider>;

View File

@ -1,7 +1,9 @@
import * as React from "react" import type { VariantProps } from "class-variance-authority";
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cva } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const alertVariants = cva( const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
@ -16,8 +18,8 @@ const alertVariants = cva(
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
) );
const Alert = React.forwardRef< const Alert = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@ -29,8 +31,8 @@ const Alert = React.forwardRef<
className={cn(alertVariants({ variant }), className)} className={cn(alertVariants({ variant }), className)}
{...props} {...props}
/> />
)) ));
Alert.displayName = "Alert" Alert.displayName = "Alert";
const AlertTitle = React.forwardRef< const AlertTitle = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
@ -41,8 +43,8 @@ const AlertTitle = React.forwardRef<
className={cn("mb-1 font-medium leading-none tracking-tight", className)} className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props} {...props}
/> />
)) ));
AlertTitle.displayName = "AlertTitle" AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef< const AlertDescription = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
@ -53,7 +55,7 @@ const AlertDescription = React.forwardRef<
className={cn("text-sm [&_p]:leading-relaxed", className)} className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props} {...props}
/> />
)) ));
AlertDescription.displayName = "AlertDescription" AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription } export { Alert, AlertDescription, AlertTitle };

View File

@ -1,4 +1,4 @@
import { ReactNode } from "react"; import type { ReactNode } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -17,7 +17,7 @@ export default function AnimatedGradientText({
)} )}
> >
<div <div
className={`absolute inset-0 block h-full w-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] p-[1px] [border-radius:inherit] ![mask-composite:subtract] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]`} className="absolute inset-0 block h-full w-full animate-gradient bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:var(--bg-size)_100%] p-[1px] [border-radius:inherit] ![mask-composite:subtract] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]"
/> />
{children} {children}

View File

@ -1,4 +1,6 @@
import { cva, type VariantProps } from "class-variance-authority"; import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import * as React from "react"; import * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -26,9 +28,7 @@ const badgeVariants = cva(
}, },
); );
export interface BadgeProps export type BadgeProps = {} & React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof badgeVariants>;
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) { function Badge({ className, variant, ...props }: BadgeProps) {
return ( return (

View File

@ -1,8 +1,11 @@
import { cn } from "@/lib/utils"; import type { VariantProps } from "class-variance-authority";
import { Slot, Slottable } from "@radix-ui/react-slot"; import { Slot, Slottable } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority"; import { cva } from "class-variance-authority";
import * as React from "react"; import * as React from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{ {
@ -47,21 +50,19 @@ const buttonVariants = cva(
}, },
); );
interface IconProps { type IconProps = {
Icon: React.ElementType; Icon: React.ElementType;
iconPlacement: "left" | "right"; iconPlacement: "left" | "right";
} };
interface IconRefProps { type IconRefProps = {
Icon?: never; Icon?: never;
iconPlacement?: undefined; iconPlacement?: undefined;
} };
export interface ButtonProps export type ButtonProps = {
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean; asChild?: boolean;
} } & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>;
export type ButtonIconProps = IconProps | IconRefProps; export type ButtonIconProps = IconProps | IconRefProps;

View File

@ -1,13 +1,13 @@
"use client" "use client";
import * as React from "react" import { ChevronLeft, ChevronRight } from "lucide-react";
import { ChevronLeft, ChevronRight } from "lucide-react" import { DayPicker } from "react-day-picker";
import { DayPicker } from "react-day-picker" import * as React from "react";
import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button";
import { buttonVariants } from "@/components/ui/button" import { cn } from "@/lib/utils";
export type CalendarProps = React.ComponentProps<typeof DayPicker> export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({ function Calendar({
className, className,
@ -27,7 +27,7 @@ function Calendar({
nav: "space-x-1 flex items-center", nav: "space-x-1 flex items-center",
nav_button: cn( nav_button: cn(
buttonVariants({ variant: "outline" }), buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100" "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
), ),
nav_button_previous: "absolute left-1", nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1", nav_button_next: "absolute right-1",
@ -39,7 +39,7 @@ function Calendar({
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20", cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn( day: cn(
buttonVariants({ variant: "ghost" }), buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100" "h-9 w-9 p-0 font-normal aria-selected:opacity-100",
), ),
day_range_end: "day-range-end", day_range_end: "day-range-end",
day_selected: day_selected:
@ -54,13 +54,17 @@ function Calendar({
...classNames, ...classNames,
}} }}
components={{ components={{
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />, Chevron: ({ ...props }) => {
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />, if (props.orientation === "left") {
return <ChevronLeft className="h-4 w-4" />;
}
return <ChevronRight className="h-4 w-4" />;
},
}} }}
{...props} {...props}
/> />
) );
} }
Calendar.displayName = "Calendar" Calendar.displayName = "Calendar";
export { Calendar } export { Calendar };

View File

@ -1,8 +1,10 @@
"use client"; "use client";
import { cn } from "@/lib/utils";
import { CheckIcon, ClipboardIcon } from "lucide-react"; import { CheckIcon, ClipboardIcon } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { Card } from "./card"; import { Card } from "./card";
export default function CodeCopyButton({ export default function CodeCopyButton({
@ -26,7 +28,7 @@ export default function CodeCopyButton({
setHasCopied(true); setHasCopied(true);
let warning = localStorage.getItem("warning"); const warning = localStorage.getItem("warning");
if (warning === null) { if (warning === null) {
localStorage.setItem("warning", "1"); localStorage.setItem("warning", "1");
@ -50,11 +52,13 @@ export default function CodeCopyButton({
className={cn("bg-muted px-3 py-4")} className={cn("bg-muted px-3 py-4")}
title="Copy" title="Copy"
> >
{hasCopied ? ( {hasCopied
<CheckIcon className="h-4 w-4" /> ? (
) : ( <CheckIcon className="h-4 w-4" />
<ClipboardIcon className="h-4 w-4" /> )
)} : (
<ClipboardIcon className="h-4 w-4" />
)}
</button> </button>
</Card> </Card>
</div> </div>

View File

@ -1,14 +1,18 @@
"use client"; "use client";
import { basePath } from "@/config/siteConfig"; import type { VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { cva, type VariantProps } from "class-variance-authority"; import { cva } from "class-variance-authority";
import { Clipboard, Copy } from "lucide-react"; import { Clipboard, Copy } from "lucide-react";
import Link from "next/link";
import * as React from "react"; import * as React from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "./button"; import Link from "next/link";
import { basePath } from "@/config/site-config";
import { cn } from "@/lib/utils";
import { Separator } from "./separator"; import { Separator } from "./separator";
import { Button } from "./button";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
@ -40,23 +44,24 @@ const buttonVariants = cva(
}, },
); );
const handleCopy = (type: string, value: string) => { function handleCopy(type: string, value: string) {
navigator.clipboard.writeText(value); navigator.clipboard.writeText(value);
let amountOfScriptsCopied = localStorage.getItem("amountOfScriptsCopied"); let amountOfScriptsCopied = localStorage.getItem("amountOfScriptsCopied");
if (amountOfScriptsCopied === null) { if (amountOfScriptsCopied === null) {
localStorage.setItem("amountOfScriptsCopied", "1"); localStorage.setItem("amountOfScriptsCopied", "1");
} else { }
amountOfScriptsCopied = (parseInt(amountOfScriptsCopied) + 1).toString(); else {
amountOfScriptsCopied = (Number.parseInt(amountOfScriptsCopied) + 1).toString();
localStorage.setItem("amountOfScriptsCopied", amountOfScriptsCopied); localStorage.setItem("amountOfScriptsCopied", amountOfScriptsCopied);
if ( if (
parseInt(amountOfScriptsCopied) === 3 || Number.parseInt(amountOfScriptsCopied) === 3
parseInt(amountOfScriptsCopied) === 10 || || Number.parseInt(amountOfScriptsCopied) === 10
parseInt(amountOfScriptsCopied) === 25 || || Number.parseInt(amountOfScriptsCopied) === 25
parseInt(amountOfScriptsCopied) === 50 || || Number.parseInt(amountOfScriptsCopied) === 50
parseInt(amountOfScriptsCopied) === 100 || Number.parseInt(amountOfScriptsCopied) === 100
) { ) {
setTimeout(() => { setTimeout(() => {
toast.info( toast.info(
@ -86,17 +91,20 @@ const handleCopy = (type: string, value: string) => {
toast.success( toast.success(
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Clipboard className="h-4 w-4" /> <Clipboard className="h-4 w-4" />
<span>Copied {type} to clipboard</span> <span>
Copied
{type}
{" "}
to clipboard
</span>
</div>, </div>,
); );
}; }
export interface CodeBlockProps export type CodeBlockProps = {
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean; asChild?: boolean;
code: string; code: string;
} } & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>;
const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>( const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
({ className, variant, size, asChild = false, code }, ref) => { ({ className, variant, size, asChild = false, code }, ref) => {
@ -121,7 +129,10 @@ const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
)} )}
> >
<p className="flex items-center gap-2"> <p className="flex items-center gap-2">
{code} <Separator orientation="vertical" />{" "} {code}
{" "}
<Separator orientation="vertical" />
{" "}
<Copy <Copy
className="cursor-pointer" className="cursor-pointer"
size={16} size={16}

View File

@ -1,6 +1,7 @@
"use client"; "use client";
import { type DialogProps } from "@radix-ui/react-dialog"; import type { DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk"; import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react"; import { Search } from "lucide-react";
import * as React from "react"; import * as React from "react";
@ -23,9 +24,9 @@ const Command = React.forwardRef<
)); ));
Command.displayName = CommandPrimitive.displayName; Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {} type CommandDialogProps = {} & DialogProps;
const CommandDialog = ({ children, ...props }: CommandDialogProps) => { function CommandDialog({ children, ...props }: CommandDialogProps) {
return ( return (
<Dialog {...props}> <Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg"> <DialogContent className="overflow-hidden p-0 shadow-lg">
@ -35,7 +36,7 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
}; }
const CommandInput = React.forwardRef< const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>, React.ElementRef<typeof CommandPrimitive.Input>,
@ -126,10 +127,10 @@ const CommandItem = React.forwardRef<
CommandItem.displayName = CommandPrimitive.Item.displayName; CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({ function CommandShortcut({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLSpanElement>) => { }: React.HTMLAttributes<HTMLSpanElement>) {
return ( return (
<span <span
className={cn( className={cn(
@ -139,7 +140,7 @@ const CommandShortcut = ({
{...props} {...props}
/> />
); );
}; }
CommandShortcut.displayName = "CommandShortcut"; CommandShortcut.displayName = "CommandShortcut";
export { export {

View File

@ -1,8 +1,9 @@
"use client"; "use client";
import { cn } from "@/lib/utils";
import { CheckIcon, ClipboardIcon } from "lucide-react"; import { CheckIcon, ClipboardIcon } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { Card } from "./card"; import { Card } from "./card";
export default function CodeCopyButton({ export default function CodeCopyButton({
@ -26,7 +27,6 @@ export default function CodeCopyButton({
setHasCopied(true); setHasCopied(true);
// toast.success(`copied ${type} to clipboard`, { // toast.success(`copied ${type} to clipboard`, {
// icon: <ClipboardCheck className="h-4 w-4" />, // icon: <ClipboardCheck className="h-4 w-4" />,
// }); // });
@ -42,11 +42,13 @@ export default function CodeCopyButton({
className={cn(" right-0 cursor-pointer bg-muted px-3 py-4")} className={cn(" right-0 cursor-pointer bg-muted px-3 py-4")}
onClick={() => handleCopy("install command", children)} onClick={() => handleCopy("install command", children)}
> >
{hasCopied ? ( {hasCopied
<CheckIcon className="h-4 w-4" /> ? (
) : ( <CheckIcon className="h-4 w-4" />
<ClipboardIcon className="h-4 w-4" /> )
)} : (
<ClipboardIcon className="h-4 w-4" />
)}
<span className="sr-only">Copy</span> <span className="sr-only">Copy</span>
</div> </div>
</Card> </Card>

View File

@ -53,32 +53,36 @@ const DialogContent = React.forwardRef<
)); ));
DialogContent.displayName = DialogPrimitive.Content.displayName; DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ function DialogHeader({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) {
<div return (
className={cn( <div
"flex flex-col space-y-1.5 text-center sm:text-left", className={cn(
className, "flex flex-col space-y-1.5 text-center sm:text-left",
)} className,
{...props} )}
/> {...props}
); />
);
}
DialogHeader.displayName = "DialogHeader"; DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({ function DialogFooter({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) {
<div return (
className={cn( <div
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className={cn(
className, "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
)} className,
{...props} )}
/> {...props}
); />
);
}
DialogFooter.displayName = "DialogFooter"; DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef< const DialogTitle = React.forwardRef<

View File

@ -37,8 +37,8 @@ const DropdownMenuSubTrigger = React.forwardRef<
<ChevronRight className="ml-auto h-4 w-4" /> <ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
)); ));
DropdownMenuSubTrigger.displayName = DropdownMenuSubTrigger.displayName
DropdownMenuPrimitive.SubTrigger.displayName; = DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef< const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@ -53,8 +53,8 @@ const DropdownMenuSubContent = React.forwardRef<
{...props} {...props}
/> />
)); ));
DropdownMenuSubContent.displayName = DropdownMenuSubContent.displayName
DropdownMenuPrimitive.SubContent.displayName; = DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef< const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>, React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@ -113,8 +113,8 @@ const DropdownMenuCheckboxItem = React.forwardRef<
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
)); ));
DropdownMenuCheckboxItem.displayName = DropdownMenuCheckboxItem.displayName
DropdownMenuPrimitive.CheckboxItem.displayName; = DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef< const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@ -168,17 +168,17 @@ const DropdownMenuSeparator = React.forwardRef<
)); ));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ function DropdownMenuShortcut({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLSpanElement>) => { }: React.HTMLAttributes<HTMLSpanElement>) {
return ( return (
<span <span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)} className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props} {...props}
/> />
); );
}; }
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export { export {

View File

@ -2,8 +2,7 @@ import * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export interface InputProps export type InputProps = {} & React.InputHTMLAttributes<HTMLInputElement>;
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => { ({ className, type, ...props }, ref) => {

View File

@ -1,26 +1,28 @@
"use client" "use client";
import * as React from "react" import type { VariantProps } from "class-variance-authority";
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, 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( 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< const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
VariantProps<typeof labelVariants> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<LabelPrimitive.Root <LabelPrimitive.Root
ref={ref} ref={ref}
className={cn(labelVariants(), className)} className={cn(labelVariants(), className)}
{...props} {...props}
/> />
)) ));
Label.displayName = LabelPrimitive.Root.displayName Label.displayName = LabelPrimitive.Root.displayName;
export { Label } export { Label };

View File

@ -53,7 +53,8 @@ const NavigationMenuTrigger = React.forwardRef<
className={cn(navigationMenuTriggerStyle(), "group", className)} className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props} {...props}
> >
{children}{" "} {children}
{" "}
<ChevronDown <ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180" className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true" aria-hidden="true"
@ -94,8 +95,8 @@ const NavigationMenuViewport = React.forwardRef<
/> />
</div> </div>
)); ));
NavigationMenuViewport.displayName = NavigationMenuViewport.displayName
NavigationMenuPrimitive.Viewport.displayName; = NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef< const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>, 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" /> <div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator> </NavigationMenuPrimitive.Indicator>
)); ));
NavigationMenuIndicator.displayName = NavigationMenuIndicator.displayName
NavigationMenuPrimitive.Indicator.displayName; = NavigationMenuPrimitive.Indicator.displayName;
export { export {
NavigationMenu, NavigationMenu,

View File

@ -30,10 +30,10 @@ export default function NumberTicker({
}); });
useEffect(() => { useEffect(() => {
isInView && isInView
setTimeout(() => { && setTimeout(() => {
motionValue.set(direction === "down" ? 0 : value); motionValue.set(direction === "down" ? 0 : value);
}, delay * 1000); }, delay * 1000);
}, [motionValue, isInView, delay, value, direction]); }, [motionValue, isInView, delay, value, direction]);
useEffect( useEffect(

View File

@ -1,12 +1,13 @@
"use client"; "use client";
import { cn } from "@/lib/utils";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
interface MousePosition { import { cn } from "@/lib/utils";
type MousePosition = {
x: number; x: number;
y: number; y: number;
} };
function MousePosition(): MousePosition { function MousePosition(): MousePosition {
const [mousePosition, setMousePosition] = useState<MousePosition>({ const [mousePosition, setMousePosition] = useState<MousePosition>({
@ -29,7 +30,7 @@ function MousePosition(): MousePosition {
return mousePosition; return mousePosition;
} }
interface ParticlesProps { type ParticlesProps = {
className?: string; className?: string;
quantity?: number; quantity?: number;
staticity?: number; staticity?: number;
@ -39,18 +40,18 @@ interface ParticlesProps {
color?: string; color?: string;
vx?: number; vx?: number;
vy?: number; vy?: number;
} };
function hexToRgb(hex: string): number[] { function hexToRgb(hex: string): number[] {
hex = hex.replace("#", ""); hex = hex.replace("#", "");
if (hex.length === 3) { if (hex.length === 3) {
hex = hex hex = hex
.split("") .split("")
.map((char) => char + char) .map(char => char + char)
.join(""); .join("");
} }
const hexInt = parseInt(hex, 16); const hexInt = Number.parseInt(hex, 16);
const red = (hexInt >> 16) & 255; const red = (hexInt >> 16) & 255;
const green = (hexInt >> 8) & 255; const green = (hexInt >> 8) & 255;
const blue = hexInt & 255; const blue = hexInt & 255;
@ -150,7 +151,7 @@ const Particles: React.FC<ParticlesProps> = ({
const translateY = 0; const translateY = 0;
const pSize = Math.floor(Math.random() * 2) + size; const pSize = Math.floor(Math.random() * 2) + size;
const alpha = 0; 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 dx = (Math.random() - 0.5) * 0.1;
const dy = (Math.random() - 0.5) * 0.1; const dy = (Math.random() - 0.5) * 0.1;
const magnetism = 0.1 + Math.random() * 4; const magnetism = 0.1 + Math.random() * 4;
@ -213,8 +214,8 @@ const Particles: React.FC<ParticlesProps> = ({
start2: number, start2: number,
end2: number, end2: number,
): number => { ): number => {
const remapped = const remapped
((value - start1) * (end2 - start2)) / (end1 - start1) + start2; = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
return remapped > 0 ? remapped : 0; 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 canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
]; ];
const closestEdge = edge.reduce((a, b) => Math.min(a, b)); 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), remapValue(closestEdge, 0, 20, 0, 1).toFixed(2),
); );
if (remapClosestEdge > 1) { if (remapClosestEdge > 1) {
@ -237,26 +238,27 @@ const Particles: React.FC<ParticlesProps> = ({
if (circle.alpha > circle.targetAlpha) { if (circle.alpha > circle.targetAlpha) {
circle.alpha = circle.targetAlpha; circle.alpha = circle.targetAlpha;
} }
} else { }
else {
circle.alpha = circle.targetAlpha * remapClosestEdge; circle.alpha = circle.targetAlpha * remapClosestEdge;
} }
circle.x += circle.dx + vx; circle.x += circle.dx + vx;
circle.y += circle.dy + vy; circle.y += circle.dy + vy;
circle.translateX += circle.translateX
(mouse.current.x / (staticity / circle.magnetism) - circle.translateX) / += (mouse.current.x / (staticity / circle.magnetism) - circle.translateX)
ease; / ease;
circle.translateY += circle.translateY
(mouse.current.y / (staticity / circle.magnetism) - circle.translateY) / += (mouse.current.y / (staticity / circle.magnetism) - circle.translateY)
ease; / ease;
drawCircle(circle, true); drawCircle(circle, true);
// circle gets out of the canvas // circle gets out of the canvas
if ( if (
circle.x < -circle.size || circle.x < -circle.size
circle.x > canvasSize.current.w + circle.size || || circle.x > canvasSize.current.w + circle.size
circle.y < -circle.size || || circle.y < -circle.size
circle.y > canvasSize.current.h + circle.size || circle.y > canvasSize.current.h + circle.size
) { ) {
// remove the circle from the array // remove the circle from the array
circles.current.splice(i, 1); circles.current.splice(i, 1);

View File

@ -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< const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>, React.ElementRef<typeof PopoverPrimitive.Content>,
@ -20,12 +20,12 @@ const PopoverContent = React.forwardRef<
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( 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", "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} {...props}
/> />
</PopoverPrimitive.Portal> </PopoverPrimitive.Portal>
)) ));
PopoverContent.displayName = PopoverPrimitive.Content.displayName PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent } export { Popover, PopoverContent, PopoverTrigger };

View File

@ -1,16 +1,16 @@
"use client" "use client";
import * as React from "react" import { Check, ChevronDown, ChevronUp } from "lucide-react";
import * as SelectPrimitive from "@radix-ui/react-select" import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react" 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< const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>, React.ElementRef<typeof SelectPrimitive.Trigger>,
@ -20,7 +20,7 @@ const SelectTrigger = React.forwardRef<
ref={ref} ref={ref}
className={cn( 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", "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} {...props}
> >
@ -29,8 +29,8 @@ const SelectTrigger = React.forwardRef<
<ChevronDown className="h-4 w-4 opacity-50" /> <ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon> </SelectPrimitive.Icon>
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
)) ));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef< const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
@ -40,14 +40,14 @@ const SelectScrollUpButton = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"flex cursor-default items-center justify-center py-1", "flex cursor-default items-center justify-center py-1",
className className,
)} )}
{...props} {...props}
> >
<ChevronUp className="h-4 w-4" /> <ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton> </SelectPrimitive.ScrollUpButton>
)) ));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef< const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
@ -57,15 +57,15 @@ const SelectScrollDownButton = React.forwardRef<
ref={ref} ref={ref}
className={cn( className={cn(
"flex cursor-default items-center justify-center py-1", "flex cursor-default items-center justify-center py-1",
className className,
)} )}
{...props} {...props}
> >
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton> </SelectPrimitive.ScrollDownButton>
)) ));
SelectScrollDownButton.displayName = SelectScrollDownButton.displayName
SelectPrimitive.ScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef< const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>, React.ElementRef<typeof SelectPrimitive.Content>,
@ -76,9 +76,9 @@ const SelectContent = React.forwardRef<
ref={ref} ref={ref}
className={cn( 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", "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" && 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", && "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 className,
)} )}
position={position} position={position}
{...props} {...props}
@ -87,8 +87,8 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Viewport <SelectPrimitive.Viewport
className={cn( className={cn(
"p-1", "p-1",
position === "popper" && position === "popper"
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)} )}
> >
{children} {children}
@ -96,8 +96,8 @@ const SelectContent = React.forwardRef<
<SelectScrollDownButton /> <SelectScrollDownButton />
</SelectPrimitive.Content> </SelectPrimitive.Content>
</SelectPrimitive.Portal> </SelectPrimitive.Portal>
)) ));
SelectContent.displayName = SelectPrimitive.Content.displayName SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef< const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>, 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)} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props} {...props}
/> />
)) ));
SelectLabel.displayName = SelectPrimitive.Label.displayName SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef< const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>, React.ElementRef<typeof SelectPrimitive.Item>,
@ -119,7 +119,7 @@ const SelectItem = React.forwardRef<
ref={ref} ref={ref}
className={cn( 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", "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} {...props}
> >
@ -131,8 +131,8 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item> </SelectPrimitive.Item>
)) ));
SelectItem.displayName = SelectPrimitive.Item.displayName SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef< const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>, React.ElementRef<typeof SelectPrimitive.Separator>,
@ -143,18 +143,18 @@ const SelectSeparator = React.forwardRef<
className={cn("-mx-1 my-1 h-px bg-muted", className)} className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props} {...props}
/> />
)) ));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export { export {
Select, Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent, SelectContent,
SelectLabel, SelectGroup,
SelectItem, SelectItem,
SelectSeparator, SelectLabel,
SelectScrollUpButton,
SelectScrollDownButton, SelectScrollDownButton,
} SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};

View File

@ -1,7 +1,9 @@
"use client"; "use client";
import type { VariantProps } from "class-variance-authority";
import * as SheetPrimitive from "@radix-ui/react-dialog"; 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 { X } from "lucide-react";
import * as React from "react"; import * as React from "react";
@ -49,9 +51,7 @@ const sheetVariants = cva(
}, },
); );
interface SheetContentProps type SheetContentProps = {} & React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content> & VariantProps<typeof sheetVariants>;
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef< const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>, React.ElementRef<typeof SheetPrimitive.Content>,
@ -74,32 +74,36 @@ const SheetContent = React.forwardRef<
)); ));
SheetContent.displayName = SheetPrimitive.Content.displayName; SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({ function SheetHeader({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) {
<div return (
className={cn( <div
"flex flex-col space-y-2 text-center sm:text-left", className={cn(
className, "flex flex-col space-y-2 text-center sm:text-left",
)} className,
{...props} )}
/> {...props}
); />
);
}
SheetHeader.displayName = "SheetHeader"; SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({ function SheetFooter({
className, className,
...props ...props
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) {
<div return (
className={cn( <div
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className={cn(
className, "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
)} className,
{...props} )}
/> {...props}
); />
);
}
SheetFooter.displayName = "SheetFooter"; SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef< const SheetTitle = React.forwardRef<

Some files were not shown because too many files have changed in this diff Show More