diff --git a/app/Models/Permission.php b/app/Models/Permission.php index 64c2800eb..81bb51e6c 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -63,10 +63,13 @@ class Permission extends Model public const ACTION_STARTUP_SOFTWARE = 'startup.software'; public const ACTION_SETTINGS_RENAME = 'settings.rename'; + public const ACTION_SETTINGS_MODRINTH = 'settings.modrinth'; public const ACTION_SETTINGS_REINSTALL = 'settings.reinstall'; public const ACTION_ACTIVITY_READ = 'activity.read'; + public const ACTION_MODRINTH_DOWNLOAD = 'modrinth.download'; + /** * Should timestamps be used on this model. */ @@ -199,7 +202,9 @@ class Permission extends Model 'description' => 'Permissions that control a user\'s access to the settings for this server.', 'keys' => [ 'rename' => 'Allows a user to rename this server and change the description of it.', + 'modrinth' => 'Allows a user to change what loader/version of mods to download', 'reinstall' => 'Allows a user to trigger a reinstall of this server.', + ], ], @@ -209,6 +214,12 @@ class Permission extends Model 'read' => 'Allows a user to view the activity logs for the server.', ], ], + 'modrinth' => [ + 'description' => 'Permissions that control a user\'s access to downloading mods using in app modrinth', + 'keys' => [ + 'download' => 'Allows a user to download mods to the server using modrinth', + ], + ], ]; /** diff --git a/resources/scripts/components/elements/CheckBoxMods.tsx b/resources/scripts/components/elements/CheckBoxMods.tsx new file mode 100644 index 000000000..d6a7e6d07 --- /dev/null +++ b/resources/scripts/components/elements/CheckBoxMods.tsx @@ -0,0 +1,31 @@ +import clsx from 'clsx'; +import { forwardRef } from 'react'; + +import styles from './styles.module.css'; + +type Props = Omit, 'type'> & { + label?: string; // Optional label text for better accessibility + inputField?: boolean; // Optional flag to display an input field +}; + +const CheckBox = forwardRef(({ className, label, inputField, ...props }, ref) => ( +
+ + {label && } + {inputField && } +
+)); + +export default CheckBox; diff --git a/resources/scripts/components/elements/CheckboxLabel.tsx b/resources/scripts/components/elements/CheckboxLabel.tsx new file mode 100644 index 000000000..2e5a490d1 --- /dev/null +++ b/resources/scripts/components/elements/CheckboxLabel.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +const Checkbox = React.forwardRef< + React.ElementRef<'div'>, + React.ComponentPropsWithoutRef<'div'> & { label?: string; onChange?: () => void } +>(({ className, label, onChange, ...props }, ref) => { + const [checked, setChecked] = React.useState(false); + + const toggleChecked = () => { + setChecked((prev) => { + const newCheckedState = !prev; + if (onChange) onChange(); // Call the onChange handler when the checkbox is toggled + return newCheckedState; + }); + }; + + return ( +
+ {label && ( + + {label} + + )} +
+ ); +}); + +Checkbox.displayName = 'Checkbox'; + +export { Checkbox }; diff --git a/resources/scripts/components/elements/LabelNew.tsx b/resources/scripts/components/elements/LabelNew.tsx new file mode 100644 index 000000000..0a1d69771 --- /dev/null +++ b/resources/scripts/components/elements/LabelNew.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import styled from 'styled-components'; + +interface CheckboxProps { + label?: string; + checked: boolean; + onChange: () => void; +} + +const CheckboxWrapper = styled.div` + display: flex; + align-items: center; + gap: 8px; + user-select: none; +`; + +const StyledInput = styled.input` + margin-right: 8px; +`; + +const StyledLabel = styled.label` + display: flex; + align-items: center; + cursor: pointer; +`; + +const Checkbox = React.forwardRef(({ label, checked, onChange }, ref) => { + return ( + + {label && ( + + + {label} + + )} + + ); +}); + +Checkbox.displayName = 'Checkbox'; + +export { Checkbox }; diff --git a/resources/scripts/components/elements/ModBox.tsx b/resources/scripts/components/elements/ModBox.tsx new file mode 100644 index 000000000..76aadac42 --- /dev/null +++ b/resources/scripts/components/elements/ModBox.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +const ModBox = React.forwardRef, React.ComponentPropsWithoutRef<'div'>>(({ ...props }, ref) => { + return
; +}); + +ModBox.displayName = 'ModBox'; + +export { ModBox }; diff --git a/resources/scripts/components/elements/ModrinthLogo.tsx b/resources/scripts/components/elements/ModrinthLogo.tsx new file mode 100644 index 000000000..e8189711e --- /dev/null +++ b/resources/scripts/components/elements/ModrinthLogo.tsx @@ -0,0 +1,15 @@ +// million-ignore +const Logo = () => { + return ( + + + + + ); +}; + +export default Logo; diff --git a/resources/scripts/components/elements/ScrollMenu.tsx b/resources/scripts/components/elements/ScrollMenu.tsx new file mode 100644 index 000000000..8ea457a21 --- /dev/null +++ b/resources/scripts/components/elements/ScrollMenu.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; + +import { Checkbox } from '@/components/elements/CheckboxLabel'; + +import { cn } from '@/lib/utils'; + +const ScrollMenu = React.forwardRef< + React.ElementRef<'div'>, + React.ComponentPropsWithoutRef<'div'> & { items: string[] } +>(({ className, items, ...props }, ref) => { + const [checkedItems, setCheckedItems] = React.useState([]); + + // Handle checkbox change + const handleCheckboxChange = (item: string) => { + // Update the checked state + setCheckedItems((prev) => { + const updatedItems = prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]; + + // Log the name of the item that was selected/deselected + console.log(`${item} is now ${updatedItems.includes(item) ? 'selected' : 'deselected'}`); + console.log(updatedItems); + + return updatedItems; + }); + }; + + return ( +
+
+
    + {items.map((item) => ( +
  • + handleCheckboxChange(item)} // Handle change + /> +
  • + ))} +
+
+
+ ); +}); + +ScrollMenu.displayName = 'ScrollMenu'; + +export { ScrollMenu }; diff --git a/resources/scripts/components/elements/inputs/Checkbox.tsx b/resources/scripts/components/elements/inputs/Checkbox.tsx index 4aa6e4815..7fd497680 100644 --- a/resources/scripts/components/elements/inputs/Checkbox.tsx +++ b/resources/scripts/components/elements/inputs/Checkbox.tsx @@ -3,8 +3,31 @@ import { forwardRef } from 'react'; import styles from './styles.module.css'; -type Props = Omit, 'type'>; +type Props = Omit, 'type'> & { + label?: string; // Optional label text for better accessibility + inputField?: boolean; // Optional flag to display an input field +}; -export default forwardRef(({ className, ...props }, ref) => ( - +const CheckBox = forwardRef(({ className, label, inputField, ...props }, ref) => ( +
+ + {label && } + {inputField && ( + + )} +
)); + +export default CheckBox; diff --git a/resources/scripts/components/server/modrinth/DisplayMods.tsx b/resources/scripts/components/server/modrinth/DisplayMods.tsx new file mode 100644 index 000000000..2c8970e4c --- /dev/null +++ b/resources/scripts/components/server/modrinth/DisplayMods.tsx @@ -0,0 +1,135 @@ +//? https://api.modrinth.com/v2/search?facets=[[%22categories:forge%22],[%22versions:1.17.1%22,%20%22versions:1.21.3%22],[%22project_type:mod%22],[%22license:mit%22]]&index=relevance +import { useEffect, useState } from 'react'; +import { toast } from 'sonner'; + +import ContentBox from '@/components/elements/ContentBox'; +import { ScrollMenu } from '@/components/elements/ScrollMenu'; + +interface Project { + project_id: string; + project_type: string; + slug: string; + author: string; + title: string; + description: string; + categories: string[]; + versions: string[]; + downloads: number; + follows: number; + icon_url: string; +} + +interface Props { + appVersion: string; + baseUrl: string; +} + +const ProjectSelector: React.FC = ({ appVersion, baseUrl }) => { + const [projects, setProjects] = useState([]); + const apiUrl = `${baseUrl}/search`; + + useEffect(() => { + async function fetchProjects() { + try { + const response = await fetch(apiUrl, { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': `pyrohost/pyrodactyl/${appVersion} (pyro.host)`, + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const data = await response.json(); + for (const x of data.hits) { + if (x['icon_url'] == '') { + x['icon_url'] = 'N/A'; + } + console.log(x.icon_url); + } + + setProjects(data.hits || []); // Safely access hits + } catch (error) { + toast.error('Failed to fetch projects.'); + console.error(error); + } + } + + if (appVersion) { + fetchProjects(); + } + }, [appVersion]); + + const filteredProjects = projects.filter((project) => { + return ['forge', 'fabric'].some((category) => project.categories.includes(category)); + }); + + return ( +
+ {filteredProjects.length > 0 ? ( +
+ {filteredProjects.map((project) => ( + // + +
+ + {project.icon_url && project.icon_url !== 'N/A' ? ( + + ) : ( + + )} + +
+

{project.title}

+

+ Author: {project.author} +

+

{project.description}

+

+ Downloads: {project.downloads} | Follows: {project.follows} +

+
+
+
+ ))} +
+ ) : ( +

No projects available...

+ )} +
+ ); +}; + +export default ProjectSelector; +// +// ({ +// id: project.project_id, +// name: project.title, +// description: project.description, +// icon: project.icon_url, +// }))} +// /> diff --git a/resources/scripts/components/server/modrinth/GameVersionSelector.tsx b/resources/scripts/components/server/modrinth/GameVersionSelector.tsx new file mode 100644 index 000000000..44e048215 --- /dev/null +++ b/resources/scripts/components/server/modrinth/GameVersionSelector.tsx @@ -0,0 +1,143 @@ +import { useEffect, useState } from 'react'; +import { toast } from 'sonner'; + +import { ScrollMenu } from '@/components/elements/ScrollMenu'; +import Checkbox from '@/components/elements/inputs/Checkbox'; + +interface GameVersion { + version: string; + version_type: 'release' | 'snapshot'; +} + +interface Props { + appVersion; + baseUrl: string; +} + +const GameVersionSelector: React.FC = ({ appVersion, baseUrl }) => { + const [minecraftVersions, setMinecraftVersions] = useState([]); + const [isSnapshotSelected, setIsSnapshotSelected] = useState(false); + const apiUrl = baseUrl + '/tag/game_version'; + + function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + useEffect(() => { + async function fetchGameVersions() { + try { + const response = await fetch(apiUrl, { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': `pyrohost/pyrodactyl/${appVersion} (pyro.host)`, + }, + }); + + const data = await response.json(); + setMinecraftVersions(data); + } catch (error) { + toast.error('Failed to fetch Minecraft versions.'); + console.error(error); + } + } + + if (appVersion) { + fetchGameVersions(); + } + }, [appVersion]); + + const filteredVersions = minecraftVersions.filter((version) => { + if (isSnapshotSelected) { + return version.version_type === 'snapshot'; + } else { + return version.version_type !== 'snapshot'; + } + }); + + return ( +
+ {filteredVersions.length > 0 ? ( + version.version)} /> + ) : ( +

No versions available...

+ )} + {filteredVersions.length > 0 ? ( +
+ setIsSnapshotSelected((prev) => !prev)} + /> +
+ ) : ( +

// Show nothing as it's weird to have a random button say "Show Snapshots" + )} +
+ ); +}; + +export default GameVersionSelector; + +{ + /* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%**+*+*###%%%%@%#++++++++++++#@@@@%%#@.. +%%%%%%%#%%%%%%%%%%%%%%%%%%%%%%#########%%%##%##############%#%%%##%%###%%@@@@@@@%%%%*++++++++++++*%%%%###%%. +%#%%%%%%%%%%%%%%%%%#######%%%%#########%%%%%%%%%%%%%%%%%%%%%%@@%@@@@@@@@@@@@@@@@@@@@#**++*+++++++*%%%%%%%%%. +%%%%%%%%%%%%%%%%%%%%%@@@@@@@%%%%%%%%%%%%%%%@@@%@@@@%%@@@%@@@@@@@@@@@@@@@@@@@@@@@@@@@%*******+++++*%%@@%%%%%: +%%%@%%%%%%%%%%%%%%%%@@@@@@@@@@%@@%%%%%%@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%****++++++++*%@@@%%%%%: +%%%%%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@%@%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%#********++++*%@%%@%#%%: +%%%%%%%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%******++==--::.......:. +%%%%%%%%%%%%%%%@@%%%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%@@@@@@@@@@@@@%%@%%@@%@@%%@@@%@%@%==-:.....::::......... +%%%%%@%%%%%%%%%%%%@%%%@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@%@@**%%=#@+%#***=++#%*%*+*-+:.....::--:-------------. +%%%@@%@%%%%%%%%%%%%%%%%@@@@@@@@@@@@@@@@@@%@@@@@%%@%#%#*=++=-=-:==-:-.::..:.::.::.....::-----------------===. +%%%%%%%%%%%%%%%@%%%%%%@@@%@@@@@@@@@@@@@@@%%@@#***#+=-------::......................::---:---:--=-:=---====+. +%%%%%@@@@@@@@@@@%%@@@@@@@%%@@@%@@@%%@%*#####*=+-:-::::-::-::.....................::==-:.::---:-=--=======++. +%%%@@@@@@@@@@@%%%%@%%#####@++++----==-=-==-----::-:::::::::::..................::-=----::::-:--=-======++++. +@@@@@@@@@@@**-:......................:::::---::-:-...::----::--::.............:---====+-=-=------=====+++++. +%%%%@@@@%#-.......:::::::............:::-:=-:::::::.:::----=-----:..........:-:::-====-----=--========+++++. +##%%%%@#-...:---:----::--::::........:::=-=-:::::...::-=+----=--:::::..:::::--:::--=====-----=-+=++==+++++=. +%%%%%#-..:-----=------:-:---::.....:::::---:::::....:--=========::::::::::--:--::-----=+==-=+**=*+++*++++==. +%%%@@%*:----===----:---------:....:::::--::::.......:---=----==--:::::::::-:--------+*#****++***+*****+====. +%%%@@@@%%==+++===-====-=-----:::::::::::::........:::-------==---:::::--:----:---=:+==+**#+*+*###*****+*+==. +%%%@@@@@%%****===++======----:::::::::::::.........:::-:--------::::::--:-:---=--+--+++*=+++=+*********+++=. +%%%@@@@%@%%%%#+###=======--:.:::::::::::............::::::-:::-:--:-------------=-==-+**-=+===*##**++++====. +%%@@@@@@%%@@@####=+*+=---::::::::::::.:..............:.:.:::::------------------=--=+=+=+-===*****++++====-. +@@@@@@@@@@@@@@@%*++=+==-:-----:::::::::..................:::::---=---------------=-=======-:--###*++=-==---. +@@@@@@@@@@@@@@@%%#%*+==+-==-::::::::......................::::----=-----:----------:--=----:--+**++=-==-=--. +@@%@@@@@@@@@%@@@@%%%%+-=--=--::::::::..:.....:.............::::-------::-:::--:---:-----=---::::==---------. +%%%%%%%@@@%%@@@@@@@%%+----:::::::..:..:.....................::---:::::.:::::::-:------::--::::::::::-------. +%%%%%%%@%@%%%@@%@@%%%%+=---::::...........................:::::::::::.::::..::...:--:::::::::::::::::------. +%%%%%%%%%@@%@%@@@%@%%#==-::::...........................::::::::::::::.:....::::--:::.::::::::::::::::-----. +%%%%%%%%%%%@@@@@@@@%%*-:..........................::..:::::::-::::::::::::::----::::.:...::::::::::::::----. +%%%%%%%%%%%@@@@@%@%%%+-:..........................:::::--------:---:::::--====-=--::..:::::::::----:-------. +%%%%%%%%%%%%@@@%%@%#+-..............:::::--:::::::::--=+=++++++++=+======+++-------::::-----::-------------. +%%%%%%%%%%%@@@@@%@%*=-.......:..:::-:--------::::::-==+++++++***#%@@@%**++==------------------------------=. +%%%%%%%%@@@@@@@@%%##+:.......:::-==+**++=---::::-::-=++=++++*%@@@@@@@%#*+==--==-=--=-===========--=-------=. +%%%%%@@@@@@@@@@@@@%%#=:........:=#%@@@@@%:--:::::::-=======+**+==-------==--=+==================-==-------=. +%%%@@@%%@@@@@@@@@@%%#=::::::...:-----:::-=-.:....:::::-===+**+==-==------:-=======================--------=. +%%%%@@@@@@@@@%%@%%%%%#*-:::::::::--::::--:--.......:::----=====----::::--========================----------. +%%%%@@@@@@@@@@@%@%%%@%@=:::::::::::::---:..........:::::::-:-:::......::---=====+=+++++===========---------. +%%%@@@@@@@@@@@@@@@%@@@#=--:::::..................::::-----::::::::.:::--==========++=++===========---------. +%%@@@@@@@@@@@@@@@@@@@@***=-::::::...............::----===---:.:::::---:--======+++++++==============-------. +%%%@@@@@@@%@%%%@@%@@%@@%%=+----:.::..:........:--::--=+=----::::::--=========+++++++++++++===========------. +%%%%%%@%%%%%%%%%%@@@@%%%%**--:--::::::......::-----==++*+==----::--==+++=+++++++++++++++=+===========------. +%%%%%%%####%%%%%@@@@@@%%%%++=---====-:..:.:-++=++*****#*+====-=----:----=====--=====+++++=+==========------. +%%%%%%%%#%#%%@@@%@@@%%@@%%%*+=----===..:...::++****%#*+++======--=---===+++++++++++=+++++++=============---. +%%%%%%%%%%%%%%%@@@%%@@%@%+#%*+====++-.::-::::-***###**+++++++=+++++++=--=+++++++++++++++==++===========----. +%%%%%%%%##%%%%@@%#%%@%%%%%%%%**==--=+::..::---=*####*+++++++++++=+++++-+++++=+=++++++++++++++=+========----. +%%%%%%%%%%%%%%%@%@%%%%%%###%#=##+*+++==--======+**#*********+*+=+=+++++++=+*+++++=++++++++++=====-===--=---. +%%%%%%%%%%%%%%%@%%%%%%%@#%%#%%###++==++--==-=++**####********+***+++=+++++++=+++++==+++=+++==+========-====. +%%%%%%%%%%%%%@@@%%%%#%%%##%####*++*##=**===+++****######*****+++*++++=++++=++++++++++++++++=============---. +%%%%%@%@@@@@@@%@%%%%%%%%%###%***####*#*=*+++++++****************+++++++=*++*++*+**+*+++++++++==============. +%%%%%@@@@@@@@@%@%%%%%%%%%%+*#*%%@%##@%*%***+++++++++++++******+++=+**++*+++*+++**+**++*++++++=============-. +%%%%%%%@@@@@@@@@%%%%####+*%%@@@@%%@%@*@#%##%***++++++++++++++++++*++=**++*****+*++++++++=================-=. +%%%%%%%%%###%%%%%%%%%%#%%%%@@@@@@@@@*%@@#@@%%#%*++++++++++++++++**++*+****=*+*****++++++=+=================. +%%%%%%%%%%%%#%%%%%%%#%%%#%%%%%%@#@@@%@@@@@@@@@%##++++++++++++++++*++*++***+++++++++++++=+==================. +%%%%%%%%%%%%%%%%%%%%%%#%%%%@@@@@@@*%@@@@@@@@@@@##*++++++++++++++++++++*+++*+*+++++++*++++++++==============. +%%%%%%%%%%%%%%%%%%%%%%%%%%@@@@@@@@%@@@@@@@@@@@@@%****++++++++++++++++++++++*++++++++++=++==================. +%%%%%%%%%%%%%%%%%%%%#%%%%%%%@@%@%##@@@@@@@@@@@@@@%#*+++++++++++++++++++++++***++=+=+=======================. +%%%%%%%%%%%%%#%%%%#%%%%@%%@@%@@@@*%@@@@@@@@@@@@@%%#**+++++=++++++++++++++++++*++++++=======================. +@%%@@@%%@%@@%%%%%#%%%%%%%%%@@@#@@%@@@@@@@@@@@@@@%#*+#*+++==++++++++++++++++=++++=++==================+=====. +@%@%%@@@@@@%%%%%#%%%%%%%%@%@%%@@%@@@%@@@@@@@@@@@@%#**++++======++++++++++++++++++====+====================-. + */ +} diff --git a/resources/scripts/components/server/modrinth/LoaderSelector.tsx b/resources/scripts/components/server/modrinth/LoaderSelector.tsx new file mode 100644 index 000000000..8e1fadfc9 --- /dev/null +++ b/resources/scripts/components/server/modrinth/LoaderSelector.tsx @@ -0,0 +1,65 @@ +import { useEffect, useState } from 'react'; +import { toast } from 'sonner'; + +import { ScrollMenu } from '@/components/elements/ScrollMenu'; + +interface GameLoaders { + icon: string; // SVG data(I probably wont use this) + name: string; + supported_project_types: string[]; +} + +interface Props { + appVersion; + baseUrl: string; +} +//! FIXME: We only want to show actual loaders like Fabric, Paper, Forge, not datapacks, Iris, Optifine +const LoaderSelector: React.FC = ({ appVersion, baseUrl }) => { + const [loaders, setLoaders] = useState([]); + const apiUrl = baseUrl + '/tag/loader'; + + useEffect(() => { + async function fetchLoaders() { + try { + const response = await fetch(apiUrl, { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': `pyrohost/pyrodactyl/${appVersion} (pyro.host)`, + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const data = await response.json(); + + setLoaders(data); + } catch (error) { + toast.error('Failed to fetch game loaders.'); + console.error(error); + } + } + + if (appVersion) { + fetchLoaders(); + console.log(); + } + }, [appVersion]); + + const filterLoaders = loaders.filter((loader) => { + return loader.name; + }); + + return ( +
+ {filterLoaders.length > 0 ? ( + loaders.name)} /> + ) : ( +

No Loaders available...

+ )} +
+ ); +}; + +export default LoaderSelector; diff --git a/resources/scripts/components/server/modrinth/ModrinthContainer.tsx b/resources/scripts/components/server/modrinth/ModrinthContainer.tsx new file mode 100644 index 000000000..b5696f97e --- /dev/null +++ b/resources/scripts/components/server/modrinth/ModrinthContainer.tsx @@ -0,0 +1,64 @@ +import { useEffect, useState } from 'react'; +import isEqual from 'react-fast-compare'; +import { toast } from 'sonner'; + +import Button from '@/components/elements/ButtonV2'; +import { Checkbox } from '@/components/elements/CheckboxLabel'; +import ContentBox from '@/components/elements/ContentBox'; +import { ModBox } from '@/components/elements/ModBox'; +import PageContentBlock from '@/components/elements/PageContentBlock'; +import { ScrollMenu } from '@/components/elements/ScrollMenu'; + +import ProjectSelector from './DisplayMods'; +import GameVersionSelector from './GameVersionSelector'; +import LoaderSelector from './LoaderSelector'; + +export default () => { + const headers: Headers = new Headers(); + const url = 'https://staging-api.modrinth.com/v2'; + async function getAppVersion(): Promise { + const response = await fetch('/api/client/version'); + const data = await response.json(); + return data.version; + } + + const appVersion = getAppVersion(); + + headers.set('Content-Type', 'application/json'); + headers.set('User-Agent', 'pyrohost/pyrodactyl/' + appVersion + ' (pyro.host)'); + + return ( + +
+ + + + + + + + + + + + + + + Client + Server + + + + + + +
+
+ ); +}; diff --git a/resources/scripts/routers/ServerRouter.tsx b/resources/scripts/routers/ServerRouter.tsx index da97dae6a..ec9afb18a 100644 --- a/resources/scripts/routers/ServerRouter.tsx +++ b/resources/scripts/routers/ServerRouter.tsx @@ -15,6 +15,7 @@ import { import ErrorBoundary from '@/components/elements/ErrorBoundary'; import MainSidebar from '@/components/elements/MainSidebar'; import MainWrapper from '@/components/elements/MainWrapper'; +import ModrinthLogo from '@/components/elements/ModrinthLogo'; import PermissionRoute from '@/components/elements/PermissionRoute'; import Logo from '@/components/elements/PyroLogo'; import { NotFound, ServerError } from '@/components/elements/ScreenBlock'; @@ -148,6 +149,7 @@ export default () => { const NavigationSchedules = useRef(null); const NavigationSettings = useRef(null); const NavigationActivity = useRef(null); + const NavigationMod = useRef(null); const NavigationShell = useRef(null); const calculateTop = (pathname: string) => { @@ -165,6 +167,7 @@ export default () => { const ButtonSettings = NavigationSettings.current; const ButtonShell = NavigationShell.current; const ButtonActivity = NavigationActivity.current; + const ButtonMod = NavigationMod.current; // Perfectly center the page highlighter with simple math. // Height of navigation links (56) minus highlight height (40) equals 16. 16 devided by 2 is 8. @@ -196,6 +199,9 @@ export default () => { return (ButtonShell as any).offsetTop + HighlightOffset; if (pathname.endsWith(`/server/${id}/activity`) && ButtonActivity != null) return (ButtonActivity as any).offsetTop + HighlightOffset; + if (pathname.endsWith(`/server/${id}/mods`) && ButtonMod != null) + return (ButtonMod as any).offsetTop + HighlightOffset; + return '0'; }; @@ -266,7 +272,7 @@ export default () => { - - + {rootAdmin && ( Manage Server - + Staff @@ -405,6 +411,17 @@ export default () => {

Activity

+ + + +

Mods

+
+
)} diff --git a/resources/scripts/routers/routes.ts b/resources/scripts/routers/routes.ts index 6ce8218ea..3ee333f88 100644 --- a/resources/scripts/routers/routes.ts +++ b/resources/scripts/routers/routes.ts @@ -10,6 +10,7 @@ import BackupContainer from '@/components/server/backups/BackupContainer'; import ServerConsole from '@/components/server/console/ServerConsoleContainer'; import DatabasesContainer from '@/components/server/databases/DatabasesContainer'; import FileManagerContainer from '@/components/server/files/FileManagerContainer'; +import ModrinthContainer from '@/components/server/modrinth/ModrinthContainer'; import NetworkContainer from '@/components/server/network/NetworkContainer'; import ScheduleContainer from '@/components/server/schedules/ScheduleContainer'; import SettingsContainer from '@/components/server/settings/SettingsContainer'; @@ -165,6 +166,13 @@ export default { name: 'Software', component: ShellContainer, }, + { + route: 'mods/*', + path: 'mods', + permission: ['modrinth.download', 'settings.modrinth'], + name: 'Modrinth', + component: ModrinthContainer, + }, { route: 'activity/*', path: 'activity', diff --git a/resources/views/admin/index.blade.php b/resources/views/admin/index.blade.php index c7f7186eb..a011e2c4d 100644 --- a/resources/views/admin/index.blade.php +++ b/resources/views/admin/index.blade.php @@ -1,44 +1,44 @@ @extends('layouts.admin') @section('title') - Administration +Administration @endsection @section('content-header') -

Administrative OverviewA quick glance at your system.

- +

Administrative OverviewA quick glance at your system.

+ @endsection @section('content')
-
-
+
-
-

System Information

-
-
- You are running Pyrodactyl panel version {{ config('app.version') }}. -
-
+
+

System Information

+
+
+ You are running Pyrodactyl panel version {{ config('app.version') }}. +
+
@endsection diff --git a/routes/api-client.php b/routes/api-client.php index 66f44b827..3735d7b29 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -18,7 +18,9 @@ */ Route::get('/', [Client\ClientController::class, 'index'])->name('api:client.index'); Route::get('/permissions', [Client\ClientController::class, 'permissions']); - +Route::get('/version', function () { + return response()->json(['version' => config('app.version')]); +}); Route::prefix('/nests')->group(function () { Route::get('/', [Client\Nests\NestController::class, 'index'])->name('api:client.nests'); diff --git a/tailwind.config.cjs b/tailwind.config.cjs index cc1e1e13a..6a6a03c85 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -27,5 +27,6 @@ module.exports = { }, }, }, - plugins: [require('tailwindcss-animate'), require("tailwindcss-inner-border")], + plugins: [require('tailwindcss-animate'), require("tailwindcss-inner-border"), require('tailwind-scrollbar'), + ], };