feat: tons of incomplete features for modrinth panel(TODO)

This commit is contained in:
KalebSchmidlkofer
2025-03-22 20:48:51 -07:00
parent 6a8b581398
commit 9cd3720de2
18 changed files with 970 additions and 739 deletions

View File

@@ -69,6 +69,6 @@ See our development pages on how to get started:
Pterodactyl® Copyright © 2015 - 2022 Dane Everitt and contributors.
Pyrodactyl™ Copyright © 2024-2025 Pyro Inc. and contributors.
Pyrodactyl™ Copyright © 2025 Pyro Inc. and contributors.
AGPL-3.0-or-later

988
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,12 @@ import { cn } from '@/lib/utils';
const CheckboxArrow = React.forwardRef<
React.ElementRef<'div'>,
React.ComponentPropsWithoutRef<'div'> & { label?: string; onChange?: () => void }
>(({ className, label, onChange, ...props }, ref) => {
React.ComponentPropsWithoutRef<'div'> & { label?: string; onChange?: () => void; toggleable?: boolean }
>(({ className, label, onChange, toggleable = true, ...props }, ref) => {
const [checked, setChecked] = React.useState(false);
const toggleChecked = () => {
if (!toggleable) return;
setChecked((prev) => {
const newCheckedState = !prev;
if (onChange) onChange();

View File

@@ -1,3 +1,5 @@
import { faDownload } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import axios from 'axios';
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
@@ -8,8 +10,7 @@ import HugeIconsDownload from '@/components/elements/hugeicons/Download';
import { ServerContext } from '@/state/server';
import DownloadModModel from './DownloadModel';
// import { useProjects } from './FetchProjects';
import { apiEndpoints, offset, settings } from './config';
import { apiEndpoints, offset, perpage, settings } from './config';
interface Project {
project_id: string;
@@ -34,19 +35,19 @@ interface Props {
const ProjectSelector: React.FC<Props> = ({ appVersion, baseUrl, nonApiUrl }) => {
const [projects, setProjects] = useState<Project[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isModalVisible, setModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [modalProject, setModalProject] = useState<string | null>(null);
const uuid = ServerContext.useStoreState((state) => state.server.data!);
const fetchProjects = async () => {
setIsLoading(true); // Start loading
setIsLoading(true);
try {
const facets = [
settings.loaders.length > 0 ? settings.loaders.map((loader) => `categories:${loader}`) : null,
settings.versions.length > 0 ? settings.versions.map((version) => `versions:${version}`) : null,
settings.environments.length > 0
? settings.environments.map((environment) => `project_type:${environment}`)
? settings.environments.map((env) => `project_type:${env}`)
: ['project_type:mod'],
].filter(Boolean);
@@ -54,7 +55,9 @@ const ProjectSelector: React.FC<Props> = ({ appVersion, baseUrl, nonApiUrl }) =>
facets: JSON.stringify(facets),
index: 'relevance',
offset: `${offset}`,
limit: `${perpage}`,
});
const query = settings.searchTerms.replace(/ /g, '-');
const apiUrl = `${baseUrl}${apiEndpoints.projects}?${searchParams.toString()}&query=${query}`;
@@ -65,12 +68,12 @@ const ProjectSelector: React.FC<Props> = ({ appVersion, baseUrl, nonApiUrl }) =>
},
});
const updatedProjects = response.data.hits.map((project: Project) => ({
...project,
icon_url: project.icon_url || 'N/A',
}));
setProjects(updatedProjects);
setProjects(
response.data.hits.map((project: Project) => ({
...project,
icon_url: project.icon_url || 'N/A',
})),
);
} catch (error) {
toast.error('Failed to fetch projects.');
console.error('Error fetching projects:', error);
@@ -84,20 +87,10 @@ const ProjectSelector: React.FC<Props> = ({ appVersion, baseUrl, nonApiUrl }) =>
}, []);
const formatNumber = (num: number): string => {
if (num >= 1_000_000_000) {
return (num / 1_000_000_000).toFixed(1) + 'B';
} else if (num >= 1_000_000) {
return (num / 1_000_000).toFixed(1) + 'M';
} else if (num >= 1_000) {
return (num / 1_000).toFixed(1) + 'K';
} else {
return num.toString();
}
};
const handleDownload = () => {
// Implement your download logic here
console.log('Downloading mod...');
if (num >= 1_000_000_000) return (num / 1_000_000_000).toFixed(1) + 'B';
if (num >= 1_000_000) return (num / 1_000_000).toFixed(1) + 'M';
if (num >= 1_000) return (num / 1_000).toFixed(1) + 'K';
return num.toString();
};
return (
@@ -110,19 +103,19 @@ const ProjectSelector: React.FC<Props> = ({ appVersion, baseUrl, nonApiUrl }) =>
>
{isLoading ? 'Loading...' : 'Fetch Projects'}
</button>
<p></p>
{isLoading ? (
<p className='text-white'>Loading projects...</p>
) : projects.length > 0 ? (
projects.map((project) => (
<ContentBox
key={project.project_id}
className='p-4 bg-[#ffffff09] border-[1px] border-white shadow-sm rounded-xl w-full mb-4 relative'
className='p-4 bg-[#ffffff09] border border-gray-600 shadow-sm rounded-xl w-full mb-4'
>
<div className='flex items-center'>
<ContentBox className=' pt-1 rounded-xl mr-4 '>
<ContentBox className='pt-1 rounded-xl mr-4'>
<a href={`${nonApiUrl}/mod/${project.project_id}`} target='_blank' rel='noreferrer'>
{project.icon_url && project.icon_url !== 'N/A' ? (
{project.icon_url !== 'N/A' ? (
<img src={project.icon_url} className='w-24 h-20 object-contain rounded' />
) : (
<svg
@@ -156,36 +149,29 @@ const ProjectSelector: React.FC<Props> = ({ appVersion, baseUrl, nonApiUrl }) =>
</p>
<p className='text-sm text-gray-400'>{project.description}</p>
</div>
<div className='flex flex-col py-2 whitespace-nowrap px-6 mx-6 justify-end'>
{/* Downloads */}
<p className='text-sm inline-block whitespace-nowrap'>
{formatNumber(project.downloads)}{' '}
<p className='text-gray-600 inline ml-1'>Downloads</p>
<div className='flex flex-col py-2 px-6 mx-6'>
<p className='text-sm'>
{formatNumber(project.downloads)} <span className='text-gray-600'>Downloads</span>
</p>
{/* Version */}
<p className='text-sm inline inline-block pt-2 whitespace-nowrap'>
{project.versions[project.versions.length - 1]}
<p className='text-gray-600 inline ml-2'>Latest</p>
<p className='text-sm pt-2'>
{project.versions.at(-1)} <span className='text-gray-600'>Latest</span>
</p>
</div>
<div className='flex flex-col py-2 whitespace-nowrap px-6 mx-6 justify-end'>
{/* Install */}
<a className='pt-4'>
<button
className='flex text-right border-2 border-solid rounded py-1 px-6 border-brand hover:border-white transition ease-in-out delay-300 hover:bg-red-600 hover:scale-110'
onClick={() => setModalVisible(true)}
>
<HugeIconsDownload className='px-2 mx-2' fill='currentColor' />
Install
</button>
{isModalVisible && (
<DownloadModModel
modid={project.project_id}
visible={isModalVisible} // This is needed if `asModal` adds extra props
onModalDismissed={() => setModalVisible(false)}
/>
)}
</a>
<div className='flex flex-col py-2 px-6 mx-6'>
<button
className='flex items-center border-2 border-solid rounded py-1 px-6 border-brand hover:border-white hover:bg-red-600 hover:scale-110 justify-center'
onClick={() => setModalProject(project.project_id)}
>
<HugeIconsDownload className='px-2 mx-2' fill='currentColor' /> Install
</button>
{modalProject === project.project_id && (
<DownloadModModel
modid={project.project_id}
modName={project.title}
visible
onModalDismissed={() => setModalProject(null)}
/>
)}
</div>
</div>
</ContentBox>

View File

@@ -10,64 +10,69 @@ interface DownloadProps {
const DownloadModrinth: React.FC<DownloadProps> = ({ url, serverUuid, directory = 'mods' }) => {
const [progress, setProgress] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(false);
const downloadAndUploadFile = async () => {
setLoading(true);
try {
// Download the file
const downloadResponse = await axios({
method: 'GET',
url: url,
toast.info('Downloading file from Modrinth...');
// 1⃣ Download the file from Modrinth
const downloadResponse = await axios.get(url, {
responseType: 'blob',
});
const fileName = url.split('/').pop();
const fileName = url.split('/').pop() || 'modrinth-file.jar';
const file = new Blob([downloadResponse.data], {
type: downloadResponse.headers['content-type'],
type: downloadResponse.headers['content-type'] || 'application/java-archive',
});
// 2⃣ Prepare FormData for Upload
const formData = new FormData();
formData.append('files', file, fileName);
// Upload the file
const uploadResponse = await axios.post(`/api/client/servers/${serverUuid}/files/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
params: {
directory: `/container/${directory}`,
},
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
setProgress(percentCompleted);
// 3 Upload to Pyrodactyl Server
toast.info(`Uploading ${fileName} to server...`);
await axios.post(`/api/client/servers/${serverUuid}/files/upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
params: { directory: `/container/${directory}` },
onUploadProgress: (event) => {
if (event.total) {
setProgress(Math.round((event.loaded * 100) / event.total));
}
},
});
toast.success('File uploaded successfully!');
return uploadResponse.data;
toast.success(`${fileName} uploaded successfully!`);
} catch (error) {
handleError(error);
} finally {
setLoading(false);
}
};
const handleError = (error: any) => {
if (axios.isCancel(error)) {
console.log('Request cancelled:', error.message);
toast.warning('Request cancelled.');
} else if (error.response) {
toast.error(`Server error! Status: ${error.response.status}`);
console.error(`Server error! Status: ${error.response.status}`);
} else if (error.request) {
toast.error('No response received from server.');
console.error('No response received from server.', error.request);
toast.error('No response from server.');
} else {
toast.error(`Error: ${error.message}`);
console.error(`Error: ${error.message}`);
}
};
return (
<div>
<button onClick={downloadAndUploadFile}>Download & Upload File</button>
{progress > 0 && <p>Upload Progress: {progress}%</p>}
<div className='p-4'>
<button
onClick={downloadAndUploadFile}
disabled={loading}
className='px-4 py-2 bg-blue-500 text-white rounded-lg'
>
{loading ? 'Processing...' : 'Download & Upload'}
</button>
{progress > 0 && <p className='mt-2 text-sm'>Upload Progress: {progress}%</p>}
</div>
);
};

View File

@@ -1,14 +1,14 @@
import { faDownload } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
// NOTE: This should be a middleware instead of whatever the fuck this is
import axios from 'axios';
import { useEffect, useState } from 'react';
import FlashMessageRender from '@/components/FlashMessageRender';
import ItemContainer from '@/components/elements/ItemContainer';
import { Button } from '@/components/elements/button/index';
import DropdownButton from '@/components/server/modrinth/Dropdown';
import asModal from '@/hoc/asModal';
import { ExpandableScrollBox, type ScrollItem } from './scroll-dropdown';
interface ModVersion {
id: string;
version_number: string;
@@ -26,6 +26,12 @@ const DownloadModModal = ({ modid, modName }: Props) => {
const [versions, setVersions] = useState<ModVersion[]>([]);
const [visibleCount, setVisibleCount] = useState(5);
const [loading, setLoading] = useState(true);
const [selectedItem, setSelectedItem] = useState<ScrollItem | null>(null);
const handleSelect = (item: ScrollItem) => {
setSelectedItem(item);
console.log(`Selected: ${item.label}`);
};
// Fetch mod versions from Modrinth API
useEffect(() => {
@@ -39,54 +45,40 @@ const DownloadModModal = ({ modid, modName }: Props) => {
.catch(() => setLoading(false));
}, [modid]);
const handleLoadMore = () => {
setVisibleCount((prev) => prev + 5);
};
return (
<div className='p-6 w-full'>
<h2 className='text-2xl font-bold text-white mb-4'>{modName}</h2>
<FlashMessageRender byKey={`mod-download-${modid}`} />
<div className='p-6 mb-12 w-full overscroll-none'>
<h2 className='text-2xl font-bold text-white mb-4 text-center '>{modName}</h2>
<div aria-hidden className='my-8 bg-[#ffffff33] min-h-[1px]'></div>
<FlashMessageRender byKey={`mod-download-${modid}`} />
{loading ? (
<p className='text-white'>Loading versions...</p>
) : versions.length === 0 ? (
<p className='text-white'>No versions available for this mod.</p>
<p className='text-white'>No versions available for this mod. 😔</p>
) : (
<>
{versions.slice(0, visibleCount).map((version) => (
<ItemContainer
key={version.id}
className='flex items-center justify-between py-2 border-b w-full'
>
<div className='flex flex-col text-left w-full'>
<span className='text-lg font-semibold text-white'>
Version: {version.version_number}
</span>
<span className='text-sm text-white'>
Published: {new Date(version.date_published).toLocaleDateString()}
</span>
<span className='text-sm text-white'>Downloads: {version.downloads}</span>
</div>
<Button
className='ml-4'
onClick={() => {
const file = version.files[0];
if (file) {
window.open(file.url, '_blank');
}
}}
>
<FontAwesomeIcon icon={faDownload} className='mr-2' /> Download
</Button>
</ItemContainer>
))}
{visibleCount < versions.length && (
<Button className='mt-4' onClick={handleLoadMore}>
Load More
</Button>
)}
</>
<div className='flex flex-col gap-4'>
<div className='w-full max-w-sm space-y-8'>
<h1 className='text-2xl font-bold text-center text-custom-light-gray'>
<span className='text-custom-red'>Selection</span> Box
</h1>
<ExpandableScrollBox
placeholder='Select an option'
items={versions}
maxHeight='250px'
onSelect={handleSelect}
/>
{/* Display selected item */}
<div className='p-4 bg-custom-dark-gray rounded-md text-custom-light-gray text-center'>
{selectedItem ? `You selected: ${selectedItem.label}` : 'No option selected yet'}
</div>
<div className='text-sm text-custom-light-gray text-center mt-4 opacity-70'>
The selected item appears in the button after selection
</div>
</div>
</div>
)}
</div>
);

View File

@@ -0,0 +1,75 @@
import { ChevronDownIcon } from '@radix-ui/react-icons';
import axios from 'axios';
import { useState } from 'react';
import Button from '../../elements/ButtonV2';
import { persistent, settings } from './config';
interface ApiRequest {
game_versions: string[];
loaders: string[];
id: string;
project_id: string;
author_id: string;
featured: boolean;
name: string;
version_number: string;
changelog: string;
changelog_url: string | null;
date_published: string;
downloads: number;
version_type: string;
status: string;
requested_status: string | null;
files: ApiFiles[];
}
interface ApiFiles {
hashes: { sha512: string; sha1: string };
url: string;
filename: string;
primary: boolean;
size: number;
file_type: string | null;
}
interface Props {
list: ApiRequest[];
}
const DropdownButton = ({ list }: Props) => {
const [selected, setSelected] = useState(list[0]);
const [open, setOpen] = useState(false);
return (
<div className='flex justify-center'>
<div className='relative inline-flex flex-col items-center w-3/4'>
<Button
className='flex items-center justify-between w-full px-4 py-2 rounded-lg overflow-show'
onClick={() => setOpen(!open)}
>
<span className='truncate'>Selected Version: {list[0].version_number}</span>
<ChevronDownIcon className='w-4 h-4 ml-2' />
</Button>
{open && (
<div className='absolute mt-1 w-full bg-[#ffffff09] border rounded-sm shadow-lg text-white'>
{list.map((option) => (
<div
key={option.id}
className='px-4 my-2 cursor-pointer text-white hover:bg-gray-700'
onClick={() => {
setSelected(option);
setOpen(false);
}}
>
{option.name}
</div>
))}
</div>
)}
</div>
</div>
);
};
export default DropdownButton;

View File

@@ -34,7 +34,7 @@ const EnvironmentSelector: React.FC<Props> = ({ items, onSelectionChange }) => {
{items.length > 5 && (
<div onClick={fetchNewProjects()}>
<div className='flex items-center gap-2 cursor-pointer mt-2' onClick={() => setShowAll(!showAll)}>
<CheckboxArrow label={showAll ? 'Show Less' : 'Show More'} />
<CheckboxArrow label={showAll ? 'Show Less' : 'Show More'} toggleable={false} />
<ArrowDownIcon
className={`transform transition-transform ${showAll ? 'rotate-180' : ''}`}
fill='currentColor'

View File

@@ -1,10 +1,10 @@
import { useEffect, useMemo, useState } from 'react';
import { toast } from 'sonner';
import { ScrollMenu } from '@/components/elements/ScrollMenu';
import { ScrollMenu } from '@/components/server/modrinth/ScrollMenu';
import Checkbox from '@/components/elements/inputs/Checkbox';
import { apiEndpoints, fetchHeaders, settings } from './config';
import { apiEndpoints, fetchHeaders, persistent, settings } from './config';
interface GameVersion {
version: string;
@@ -38,6 +38,7 @@ const GameVersionSelector: React.FC<Props> = ({ appVersion, baseUrl }) => {
const data = await response.json();
if (Array.isArray(data)) {
setMinecraftVersions(data);
persistent.gameVersions = data;
} else {
throw new Error('Invalid data format received from API.');
}
@@ -62,7 +63,6 @@ const GameVersionSelector: React.FC<Props> = ({ appVersion, baseUrl }) => {
const handleSelectionChange = (selectedItems: string[]) => {
settings.versions = selectedItems;
console.log('Updated settings.versions:', settings.versions);
};
const handleSnapshotToggle = () => {

View File

@@ -1,3 +1,4 @@
import axios from 'axios';
import debounce from 'debounce';
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
@@ -18,6 +19,7 @@ import { settings as localSettings } from './config';
export default () => {
const [appVersion, setAppVersion] = useState<string | null>(null);
const [settings, setSettings] = useState({
loaders: [],
versions: [],
@@ -28,8 +30,6 @@ export default () => {
const [searchTerm, setSearchTerm] = useState('');
const updateSearchTerms = () => {
// settings.searchTerms = setSearchTerm;
// settings.searchTerms = setSearchTerm;
localSettings.searchTerms = searchTerm;
fetchNewProjects();
return debounce(setSearchTerm, 50);
@@ -44,9 +44,8 @@ export default () => {
useEffect(() => {
async function getAppVersion() {
try {
const response = await fetch('/api/client/version');
const data = await response.json();
setAppVersion(data.version); // Set the app version state
const response = await axios.get('/api/client/version');
setAppVersion(response.data.version);
} catch (error) {
toast.error('Failed to fetch app version.');
}
@@ -62,18 +61,11 @@ export default () => {
return <div>Loading...</div>;
}
// Function to update settings
const updateSettings = (key: keyof typeof settings, value: any) => {
setSettings((prevSettings) => ({ ...prevSettings, [key]: value }));
};
const handleSearch = () => {
toast.success(`Searching for: ${searchQuery}`);
console.log(searchQuery);
};
return (
<PageContentBlock title={'Mods/Plugins'}>
<ContentBox className='p-8 bg-[#ffffff09] border-[1px] border-[#ffffff11] shadow-sm rounded-xl mb-5'>
{/* TODO: Add a navbar to cycle between Downloaded, Download, and Dependency resolver*/}
</ContentBox>
<div className='flex flex-wrap gap-4'>
<ContentBox
className='p-8 bg-[#ffffff09] border-[1px] border-[#ffffff11] shadow-sm rounded-xl md:w-1/6'

View File

@@ -4,7 +4,7 @@ import { Checkbox } from '@/components/elements/CheckboxLabel';
import { cn } from '@/lib/utils';
import { fetchNewProjects } from '../server/modrinth/config';
import { fetchNewProjects } from './config';
interface Props {
appVersion;

View File

@@ -5,10 +5,13 @@ interface Settings {
searchTerms: string;
}
export const gameLoaders = [];
export const gamerVersions = [];
export const persistent = {
gameLoaders: [],
gameVersions: [],
};
export const offset = 0;
export const perpage = 25;
export const apiEndpoints = {
projects: `/search`,

View File

@@ -0,0 +1,107 @@
'use client';
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons';
import * as React from 'react';
import { cn } from '@/lib/utils';
export interface ScrollItem {
id: string | number;
label: string;
}
interface ExpandableScrollBoxProps {
placeholder?: string;
items: ScrollItem[];
maxHeight?: string;
className?: string;
buttonClassName?: string;
boxClassName?: string;
onSelect?: (item: ScrollItem) => void;
}
export function ExpandableScrollBox({
placeholder = 'Select an option',
items = [],
maxHeight = '300px',
className = '',
buttonClassName = '',
boxClassName = '',
onSelect,
}: ExpandableScrollBoxProps) {
const [isOpen, setIsOpen] = React.useState(false);
const [selectedItem, setSelectedItem] = React.useState<ScrollItem | null>(null);
const handleSelect = (item: ScrollItem) => {
setSelectedItem(item);
setIsOpen(false);
if (onSelect) {
onSelect(item);
}
};
return (
<div className={cn('w-full', className)}>
{/* Selection Button */}
<button
className={cn(
'w-full px-6 py-3 rounded-md font-medium',
'bg-custom-red text-white',
'hover:bg-custom-red-hover focus:outline-none focus:ring-2 focus:ring-custom-red-hover focus:ring-offset-2 focus:ring-offset-black',
'transition-colors duration-200 shadow-md',
'flex items-center justify-between',
buttonClassName,
)}
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
aria-controls='scroll-box'
>
<span className='truncate'>{selectedItem ? selectedItem.label : placeholder}</span>
{isOpen ? (
<ChevronUpIcon className='ml-2 h-5 w-5 flex-shrink-0' />
) : (
<ChevronDownIcon className='ml-2 h-5 w-5 flex-shrink-0' />
)}
</button>
{/* Expandable Box with Scroll Menu */}
<div
id='scroll-box'
className={cn(
'w-full mt-4 rounded-md overflow-hidden',
'bg-custom-medium-gray border-2 border-custom-dark-gray',
'shadow-lg transition-all duration-300 ease-in-out',
isOpen ? 'max-h-[var(--max-height)] opacity-100 my-4' : 'max-h-0 opacity-0 my-0 border-0',
boxClassName,
)}
style={{ '--max-height': maxHeight } as React.CSSProperties}
>
{/* Scroll Container */}
<div
className={cn('overflow-y-auto transition-all', isOpen ? 'opacity-100' : 'opacity-0')}
style={{ maxHeight }}
>
<div className='py-1'>
{items.map((item) => (
<div
key={item.id}
className={cn(
'px-4 py-3 cursor-pointer transition-colors duration-150',
'flex items-center justify-between',
'hover:bg-custom-dark-gray text-white',
selectedItem?.id === item.id
? 'bg-custom-dark-gray border-l-2 border-custom-red'
: 'border-l-2 border-transparent',
)}
onClick={() => handleSelect(item)}
>
<span>{item.label}</span>
{selectedItem?.id === item.id && <Check className='h-4 w-4 text-custom-red' />}
</div>
))}
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,69 +0,0 @@
import { useState } from 'react';
import FlashMessageRender from '@/components/FlashMessageRender';
import Field from '@/components/elements/Field';
import Modal from '@/components/elements/Modal';
import { Button } from '@/components/elements/button/index';
import { ServerContext } from '@/state/server';
export default () => {
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
const [visible, setVisible] = useState(false);
return (
<>
{({ isSubmitting, resetForm }) => (
<Modal
visible={visible}
dismissable={!isSubmitting}
showSpinnerOverlay={isSubmitting}
onDismissed={() => {
resetForm();
setVisible(false);
}}
title='Create new database'
>
<div className='flex flex-col'>
<FlashMessageRender byKey={'database:create'} />
<Form>
<Field
type={'string'}
id={'database_name'}
name={'databaseName'}
label={'Database Name'}
description={'A descriptive name for your database instance.'}
/>
<div className={`mt-6`}>
<Field
type={'string'}
id={'connections_from'}
name={'connectionsFrom'}
label={'Connections From'}
description={
'Where connections should be allowed from. Leave blank to allow connections from anywhere.'
}
/>
</div>
<div className={`flex gap-3 justify-end my-6`}>
<Button type={'submit'}>Create Database</Button>
</div>
</Form>
</div>
</Modal>
)}
<button
style={{
background:
'radial-gradient(124.75% 124.75% at 50.01% -10.55%, rgb(36, 36, 36) 0%, rgb(20, 20, 20) 100%)',
}}
className='px-8 py-3 border-[1px] border-[#ffffff12] rounded-full text-sm font-bold shadow-md'
onClick={() => setVisible(true)}
>
New Database
</button>
</>
);
};

View File

@@ -1,16 +1,7 @@
// I know it's deprecated! We need to fix it!!!
import * as Sentry from '@sentry/react';
import { createRoot } from 'react-dom/client';
import App from '@/components/App';
Sentry.init({
// This is safe to be public.
// See https://docs.sentry.io/product/sentry-basics/concepts/dsn-explainer/ for more information.
dsn: 'https://b25e7066a7d647cea237cd72beec5c9f@app.glitchtip.com/6107',
integrations: [],
});
const container = document.getElementById('app');
if (container) {
const root = createRoot(container);

View File

@@ -246,7 +246,9 @@ export default () => {
</svg>
</button>
<MainSidebar className={`${isSidebarVisible ? '' : 'hidden'} lg:flex`}>
<MainSidebar
className={`${isSidebarVisible ? '' : 'hidden'} lg:flex fixed inset-y-0 left-0 z-[9999] w-[300px] bg-[#1a1a1a]`}
>
<div
className='absolute bg-brand w-[3px] h-10 left-0 rounded-full pointer-events-none'
style={{
@@ -286,7 +288,7 @@ export default () => {
</svg>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className='z-[99999] select-none' sideOffset={8}>
<DropdownMenuContent className='z-[99999] select-none relative' sideOffset={8}>
{rootAdmin && (
<DropdownMenuItem onSelect={onSelectManageServer}>
Manage Server
@@ -307,6 +309,7 @@ export default () => {
className='flex flex-row items-center'
ref={NavigationHome}
to={`/server/${id}`}
onClick={toggleSidebar}
end
>
<HugeIconsHome fill='currentColor' />
@@ -319,6 +322,7 @@ export default () => {
className='flex flex-row items-center'
ref={NavigationFiles}
to={`/server/${id}/files`}
onClick={toggleSidebar}
>
<HugeIconsFolder fill='currentColor' />
<p>Files</p>
@@ -329,6 +333,7 @@ export default () => {
className='flex flex-row items-center'
ref={NavigationDatabases}
to={`/server/${id}/databases`}
onClick={toggleSidebar}
end
>
<HugeIconsDatabase fill='currentColor' />
@@ -340,6 +345,7 @@ export default () => {
className='flex flex-row items-center'
ref={NavigationBackups}
to={`/server/${id}/backups`}
onClick={toggleSidebar}
end
>
<HugeIconsCloudUp fill='currentColor' />
@@ -351,6 +357,7 @@ export default () => {
className='flex flex-row items-center'
ref={NavigationNetworking}
to={`/server/${id}/network`}
onClick={toggleSidebar}
end
>
<HugeIconsConnections fill='currentColor' />
@@ -362,6 +369,7 @@ export default () => {
className='flex flex-row items-center'
ref={NavigationUsers}
to={`/server/${id}/users`}
onClick={toggleSidebar}
end
>
<HugeIconsPeople fill='currentColor' />
@@ -373,6 +381,7 @@ export default () => {
className='flex flex-row items-center'
ref={NavigationStartup}
to={`/server/${id}/startup`}
onClick={toggleSidebar}
end
>
<HugeIconsConsole fill='currentColor' />
@@ -384,6 +393,7 @@ export default () => {
className='flex flex-row items-center'
ref={NavigationSchedules}
to={`/server/${id}/schedules`}
onClick={toggleSidebar}
>
<HugeIconsClock fill='currentColor' />
<p>Schedules</p>
@@ -394,6 +404,7 @@ export default () => {
className='flex flex-row items-center'
ref={NavigationSettings}
to={`/server/${id}/settings`}
onClick={toggleSidebar}
end
>
<HugeIconsDashboardSettings fill='currentColor' />
@@ -405,23 +416,26 @@ export default () => {
className='flex flex-row items-center'
ref={NavigationActivity}
to={`/server/${id}/activity`}
onClick={toggleSidebar}
end
>
<HugeIconsPencil fill='currentColor' />
<p>Activity</p>
</NavLink>
</Can>
<Can action={['modrinth.*', 'modrinth.download']} matchAny>
{/* TODO: finish modrinth support *\}
{/* <Can action={['modrinth.*', 'modrinth.download']} matchAny>
<NavLink
className='flex flex-row items-center'
className='flex flex-row items-center sm:hidden md:show'
ref={NavigationMod}
to={`/server/${id}/mods`}
onClick={toggleSidebar}
end
>
<ModrinthLogo />
<p>Mods</p>
<p>Mods/Plugins</p>
</NavLink>
</Can>
</Can> */}
</>
)}
<Can action={'startup.software'}>
@@ -429,6 +443,7 @@ export default () => {
className='flex flex-row items-center'
ref={NavigationShell}
to={`/server/${id}/shell`}
onClick={toggleSidebar}
end
>
<HugeIconsController fill='currentColor' />

View File

@@ -0,0 +1,142 @@
@extends('layouts.admin')
@include('partials/admin.settings.nav', ['activeTab' => 'captcha'])
@section('title')
Captcha Settings
@endsection
@section('content-header')
<h1>Captcha Settings<small>Configure captcha settings for Pyrodactyl.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li class="active">Settings</li>
</ol>
@endsection
@section('content')
@yield('settings::nav')
<div class="row">
<div class="col-xs-12">
<form action="" method="POST">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">reCAPTCHA</h3>
</div>
<div class="box-body">
<div class="row">
<div class="form-group col-md-4">
<label class="control-label">Status</label>
<div>
<select class="form-control" name="recaptcha:enabled">
<option value="true">Enabled</option>
<option value="false" @if(old('recaptcha:enabled', config('recaptcha.enabled')) == '0') selected @endif>
Disabled</option>
</select>
<p class="text-muted small">If enabled, login forms and password reset forms will do a silent captcha
check and display a visible captcha if needed.</p>
</div>
</div>
<div class="form-group col-md-4">
<label class="control-label">Site Key</label>
<div>
<input type="text" required class="form-control" name="recaptcha:website_key"
value="{{ old('recaptcha:website_key', config('recaptcha.website_key')) }}">
</div>
</div>
<div class="form-group col-md-4">
<label class="control-label">Secret Key</label>
<div>
<input type="text" required class="form-control" name="recaptcha:secret_key"
value="{{ old('recaptcha:secret_key', config('recaptcha.secret_key')) }}">
<p class="text-muted small">Used for communication between your site and Google. Be sure to keep it a
secret.</p>
</div>
</div>
</div>
@if($showRecaptchaWarning)
<div class="row">
<div class="col-xs-12">
<div class="alert alert-warning no-margin">
You are currently using reCAPTCHA keys that were shipped with this Panel. For improved security it is
recommended to <a target="_blank" href="https://www.google.com/recaptcha/admin">generate new invisible
reCAPTCHA
keys</a> that tied specifically to your website.
</div>
</div>
</div>
@endif
</div>
</div>
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">HTTP Connections</h3>
</div>
<div class="box-body">
<div class="row">
<div class="form-group col-md-6">
<label class="control-label">Connection Timeout</label>
<div>
<input type="number" required class="form-control" name="pterodactyl:guzzle:connect_timeout"
value="{{ old('pterodactyl:guzzle:connect_timeout', config('pterodactyl.guzzle.connect_timeout')) }}">
<p class="text-muted small">The amount of time in seconds to wait for a connection to be opened before
throwing an error.</p>
</div>
</div>
<div class="form-group col-md-6">
<label class="control-label">Request Timeout</label>
<div>
<input type="number" required class="form-control" name="pterodactyl:guzzle:timeout"
value="{{ old('pterodactyl:guzzle:timeout', config('pterodactyl.guzzle.timeout')) }}">
<p class="text-muted small">The amount of time in seconds to wait for a request to be completed before
throwing an error.</p>
</div>
</div>
</div>
</div>
</div>
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Automatic Allocation Creation</h3>
</div>
<div class="box-body">
<div class="row">
<div class="form-group col-md-4">
<label class="control-label">Status</label>
<div>
<select class="form-control" name="pterodactyl:client_features:allocations:enabled">
<option value="false">Disabled</option>
<option value="true" @if(old('pterodactyl:client_features:allocations:enabled', config('pterodactyl.client_features.allocations.enabled'))) selected @endif>Enabled</option>
</select>
<p class="text-muted small">If enabled users will have the option to automatically create new
allocations for their server via the frontend.</p>
</div>
</div>
<div class="form-group col-md-4">
<label class="control-label">Starting Port</label>
<div>
<input type="number" class="form-control" name="pterodactyl:client_features:allocations:range_start"
value="{{ old('pterodactyl:client_features:allocations:range_start', config('pterodactyl.client_features.allocations.range_start')) }}">
<p class="text-muted small">The starting port in the range that can be automatically allocated.</p>
</div>
</div>
<div class="form-group col-md-4">
<label class="control-label">Ending Port</label>
<div>
<input type="number" class="form-control" name="pterodactyl:client_features:allocations:range_end"
value="{{ old('pterodactyl:client_features:allocations:range_end', config('pterodactyl.client_features.allocations.range_end')) }}">
<p class="text-muted small">The ending port in the range that can be automatically allocated.</p>
</div>
</div>
</div>
</div>
</div>
<div class="box box-primary">
<div class="box-footer">
{{ csrf_field() }}
<button type="submit" name="_method" value="PATCH" class="btn btn-sm btn-primary pull-right">Save</button>
</div>
</div>
</form>
</div>
</div>
@endsection

View File

@@ -9,7 +9,8 @@
<li @if($activeTab === 'basic')class="active"@endif><a href="{{ route('admin.settings') }}">General</a></li>
<li @if($activeTab === 'mail')class="active"@endif><a href="{{ route('admin.settings.mail') }}">Mail</a></li>
<li @if($activeTab === 'advanced')class="active"@endif><a href="{{ route('admin.settings.advanced') }}">Advanced</a></li>
</ul>
<li @if($activeTab === 'captcha')class="active"@endif><a href="{{ route('admin.settings.captcha') }}">Captcha</a></li>
</ul>
</div>
</div>
</div>