diff --git a/client/package-lock.json b/client/package-lock.json index a6e86f5..3b9ac77 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12,6 +12,7 @@ "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.15.6", "@mui/material": "^5.15.6", + "@mui/x-data-grid": "^7.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.21.3" @@ -1051,14 +1052,14 @@ } }, "node_modules/@mui/base": { - "version": "5.0.0-beta.39", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.39.tgz", - "integrity": "sha512-puyUptF7VJ+9/dMIRLF+DLR21cWfvejsA6OnatfJfqFp8aMhya7xQtvYLEfCch6ahvFZvNC9FFEGGR+qkgFjUg==", + "version": "5.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", + "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", "dependencies": { "@babel/runtime": "^7.23.9", "@floating-ui/react-dom": "^2.0.8", - "@mui/types": "^7.2.13", - "@mui/utils": "^5.15.13", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", "@popperjs/core": "^2.11.8", "clsx": "^2.1.0", "prop-types": "^15.8.1" @@ -1082,9 +1083,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.15.13", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.13.tgz", - "integrity": "sha512-ERsk9EWpiitSiKnmUdFJGshtFk647l4p7r+mjRWe/F1l5kT1NTTKkaeDLcK3/lsy0udXjMgcG0bNwzbYBdDdhQ==", + "version": "5.15.15", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.15.tgz", + "integrity": "sha512-aXnw29OWQ6I5A47iuWEI6qSSUfH6G/aCsW9KmW3LiFqr7uXZBK4Ks+z8G+qeIub8k0T5CMqlT2q0L+ZJTMrqpg==", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" @@ -1116,16 +1117,16 @@ } }, "node_modules/@mui/material": { - "version": "5.15.13", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.13.tgz", - "integrity": "sha512-E+QisOJcIzTTyeJ0o3lgYMcyrmCydb2S4cn9vTtGpIB9uR6fQ6La3dIGsXgYEGyeOB9YkWzQbNzYzvyODGEWKA==", + "version": "5.15.15", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.15.tgz", + "integrity": "sha512-3zvWayJ+E1kzoIsvwyEvkTUKVKt1AjchFFns+JtluHCuvxgKcLSRJTADw37k0doaRtVAsyh8bz9Afqzv+KYrIA==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/base": "5.0.0-beta.39", - "@mui/core-downloads-tracker": "^5.15.13", - "@mui/system": "^5.15.13", - "@mui/types": "^7.2.13", - "@mui/utils": "^5.15.13", + "@mui/base": "5.0.0-beta.40", + "@mui/core-downloads-tracker": "^5.15.15", + "@mui/system": "^5.15.15", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", "@types/react-transition-group": "^4.4.10", "clsx": "^2.1.0", "csstype": "^3.1.3", @@ -1160,12 +1161,12 @@ } }, "node_modules/@mui/private-theming": { - "version": "5.15.13", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.13.tgz", - "integrity": "sha512-j5Z2pRi6talCunIRIzpQERSaHwLd5EPdHMwIKDVCszro1RAzRZl7WmH68IMCgQmJMeglr+FalqNuq048qptGAg==", + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.14.tgz", + "integrity": "sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.15.13", + "@mui/utils": "^5.15.14", "prop-types": "^15.8.1" }, "engines": { @@ -1186,9 +1187,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.15.11", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.11.tgz", - "integrity": "sha512-So21AhAngqo07ces4S/JpX5UaMU2RHXpEA6hNzI6IQjd/1usMPxpgK8wkGgTe3JKmC2KDmH8cvoycq5H3Ii7/w==", + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz", + "integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==", "dependencies": { "@babel/runtime": "^7.23.9", "@emotion/cache": "^11.11.0", @@ -1217,15 +1218,15 @@ } }, "node_modules/@mui/system": { - "version": "5.15.13", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.13.tgz", - "integrity": "sha512-eHaX3sniZXNWkxX0lmcLxROhQ5La0HkOuF7zxbSdAoHUOk07gboQYmF6hSJ/VBFx/GLanIw67FMTn88vc8niLg==", + "version": "5.15.15", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.15.tgz", + "integrity": "sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.15.13", - "@mui/styled-engine": "^5.15.11", - "@mui/types": "^7.2.13", - "@mui/utils": "^5.15.13", + "@mui/private-theming": "^5.15.14", + "@mui/styled-engine": "^5.15.14", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -1256,9 +1257,9 @@ } }, "node_modules/@mui/types": { - "version": "7.2.13", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz", - "integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==", + "version": "7.2.14", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz", + "integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0" }, @@ -1269,9 +1270,9 @@ } }, "node_modules/@mui/utils": { - "version": "5.15.13", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.13.tgz", - "integrity": "sha512-qNlR9FLEhORC4zVZ3fzF48213EhP/92N71AcFbhHN73lPJjAbq9lUv+71P7uEdRHdrrOlm8+1zE8/OBy6MUqdg==", + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz", + "integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==", "dependencies": { "@babel/runtime": "^7.23.9", "@types/prop-types": "^15.7.11", @@ -1295,6 +1296,31 @@ } } }, + "node_modules/@mui/x-data-grid": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.2.0.tgz", + "integrity": "sha512-WKmFo0eKhj3W7Fv8u5n2XP4UcdzuJ+mEYALiMUDAYsah/hPBH9mA1miXn9DjXF3i3dxgzrTjdJemTgTJxAQZKg==", + "dependencies": { + "@babel/runtime": "^7.24.0", + "@mui/system": "^5.15.14", + "@mui/utils": "^5.15.14", + "clsx": "^2.1.0", + "prop-types": "^15.8.1", + "reselect": "^4.1.8" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.15.14", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6699,6 +6725,11 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", diff --git a/client/package.json b/client/package.json index 94ae2ae..cce66c3 100644 --- a/client/package.json +++ b/client/package.json @@ -15,6 +15,7 @@ "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.15.6", "@mui/material": "^5.15.6", + "@mui/x-data-grid": "^7.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.21.3" diff --git a/client/src/App.tsx b/client/src/App.tsx index 2d22f6d..59df985 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -9,6 +9,7 @@ import ServerData from './data/ServerData'; import About from './pages/About'; import Home from './pages/Home'; import NotFound from './pages/NotFound'; +import ServerDetails from './pages/ServerDetails'; export function App() { const [alertInfo, setAlertInfo] = useState<{ @@ -58,6 +59,10 @@ export function App() { /> } /> + } + /> } /> } /> diff --git a/client/src/apiUtil.tsx b/client/src/apiUtil.tsx index 2b168e2..43652d9 100644 --- a/client/src/apiUtil.tsx +++ b/client/src/apiUtil.tsx @@ -21,9 +21,9 @@ export const fetchDemo = async () => { } }; -export const fetchData = async (queryParams = {}) => { +export const fetchData = async (dir = 'servers', queryParams = {}) => { const queryString = new URLSearchParams(queryParams).toString(); - const url = `${import.meta.env.PROD ? 'https://api.ixion.dev' : 'http://localhost:8000'}/servers/${queryString ? `?${queryString}` : ''}`; + const url = `${import.meta.env.PROD ? 'https://api.ixion.dev' : 'http://localhost:8000'}/${dir}/${queryString ? `?${queryString}` : ''}`; try { const response = await fetch(url, { signal: AbortSignal.timeout(5000), diff --git a/client/src/components/AddServer.tsx b/client/src/components/AddServer.tsx index 7aca8df..42127aa 100644 --- a/client/src/components/AddServer.tsx +++ b/client/src/components/AddServer.tsx @@ -10,6 +10,7 @@ import { styled, } from '@mui/material'; import { useRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { postData } from '../apiUtil'; import ServerData from '../data/ServerData'; import { AlertResponse } from './Alert'; @@ -51,6 +52,7 @@ export default function AddServer({ servers: ServerData[]; setServers: React.Dispatch>; }) { + const navigate = useNavigate(); const containerRef = useRef(null); const inputRef = useRef(null); const [isLoading, setIsLoading] = useState(false); @@ -71,6 +73,7 @@ export default function AddServer({ severity: 'success', }); if (response.data && typeof response.data === 'object') { + navigate(`/server/${response.data.id}`); setServers([response.data, ...servers]); } if (inputRef.current) { diff --git a/client/src/components/ExpansionsBar.tsx b/client/src/components/ExpansionsBar.tsx new file mode 100644 index 0000000..c8d98c0 --- /dev/null +++ b/client/src/components/ExpansionsBar.tsx @@ -0,0 +1,68 @@ +import { ToggleButton, ToggleButtonGroup } from '@mui/material'; +import ServerData from '../data/ServerData'; + +const expansions = [ + + RotZ + , + + CoP + , + + ToAU + , + + WotG + , + + SoA + , +]; + +export default function ExpansionBar({ server }: { server: ServerData }) { + return ( + + {expansions} + + ); +} diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index baf1410..85351bd 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -10,7 +10,7 @@ import { import IconButton from '@mui/material/IconButton'; import { useState } from 'react'; import { Link } from 'react-router-dom'; -import { fetchData, fetchDemo } from '../apiUtil'; +import { fetchData } from '../apiUtil'; import SearchState from '../data/SearchState'; import ServerData from '../data/ServerData'; import AddServer from './AddServer'; @@ -52,11 +52,6 @@ export default function Header({ }); } } - setFetchLoading(50); - if (data.length === 0) { - data.push(await fetchDemo()); - } - setFetchLoading(75); setServers(data); setFetchLoading(100); setTimeout(() => { diff --git a/client/src/components/ServerCard.tsx b/client/src/components/ServerCard.tsx index 4e08755..5fb87f8 100644 --- a/client/src/components/ServerCard.tsx +++ b/client/src/components/ServerCard.tsx @@ -3,6 +3,7 @@ import { Close, ContentCopy, ExpandMore, + Info, Launch, Public, PublicOff, @@ -15,8 +16,6 @@ import { Chip, Divider, IconButton, - ToggleButton, - ToggleButtonGroup, Tooltip, Typography, alpha, @@ -29,6 +28,11 @@ import ServerData, { } from '../data/ServerData'; import CopyImageIcon from '../images/copy-image.png'; import { Accordion, AccordionDetails, AccordionSummary } from './Accordion'; +import ExpansionBar from './ExpansionsBar'; + +function scrollToTop() { + window.scrollTo({ top: 0, behavior: 'smooth' }); +} export default function ServerCard({ server }: { server: ServerData }) { const [clipboardTooltip, setClipboardTooltip] = useState('Copy server URL.'); @@ -48,7 +52,7 @@ export default function ServerCard({ server }: { server: ServerData }) { handleClipboardTooltipOpen(); setTimeout(() => { handleClipboardTooltipClose(); - setClipboardTooltip('Copy Server URL.'); + setClipboardTooltip('Copy server URL.'); }, 3000); } catch (error) { handleClipboardTooltipOpen(); @@ -59,7 +63,7 @@ export default function ServerCard({ server }: { server: ServerData }) { } setTimeout(() => { handleClipboardTooltipClose(); - setClipboardTooltip('Copy Server URL.'); + setClipboardTooltip('Copy server URL.'); }, 3000); } }; @@ -134,54 +138,6 @@ export default function ServerCard({ server }: { server: ServerData }) { ); }; - const expansions = [ - - RotZ - , - - CoP - , - - ToAU - , - - WotG - , - - SoA - , - ]; - return ( @@ -286,20 +242,7 @@ export default function ServerCard({ server }: { server: ServerData }) { )} - - {expansions} - + - - + + {server.active_sessions} active sessions {server.login_limit !== 1 && ( )} - + + + + + + Updated: {new Date(server.updated).toLocaleString()} diff --git a/client/src/data/ServerData.tsx b/client/src/data/ServerData.tsx index 7d3a14b..165acdb 100644 --- a/client/src/data/ServerData.tsx +++ b/client/src/data/ServerData.tsx @@ -7,7 +7,7 @@ export interface ServerSetting { transform?: (arg: boolean | string | number) => boolean | string | number; } -interface ServerSettings { +export interface ServerSettings { [key: string]: boolean | string | number; } @@ -16,6 +16,7 @@ export default interface ServerData { name: string; url: string; max_level: number; + settings?: ServerSettings; customizations: ServerSettings; login_limit: number; active_sessions: number; @@ -28,9 +29,16 @@ export const DemoServerData: ServerData = { name: 'LandSandBoat Demo', url: 'github.com/LandSandBoat/server', max_level: 99, + settings: LsbDefaults, customizations: { - ...LsbDefaults, 'API.WEBSITE': 'https://landsandboat.github.io/server/', + 'MAIN.ENABLE_TRUST_CASTING': 1, + 'MAP.LEVEL_SYNC_ENABLE': true, + 'LOGIN.RISE_OF_ZILART': true, + 'LOGIN.CHAINS_OF_PROMATHIA': true, + 'LOGIN.TREASURES_OF_AHT_URGHAN': true, + 'LOGIN.WINGS_OF_THE_GODDESS': true, + 'LOGIN.SEEKERS_OF_ADOULIN': true, }, login_limit: 0, active_sessions: 0, diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index 2548447..d3162b7 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -1,6 +1,6 @@ import { Box } from '@mui/material'; import { useEffect, useState } from 'react'; -import { fetchData, fetchDemo } from '../apiUtil'; +import { fetchData } from '../apiUtil'; import { AlertResponse } from '../components/Alert'; import ErrorCard from '../components/ErrorCard'; import ServerCard from '../components/ServerCard'; @@ -35,9 +35,7 @@ export default function Home({ setError('An unknown error occurred.'); } } - if (data.length === 0) { - data.push(await fetchDemo()); - } + setServers(data); }; @@ -161,7 +159,7 @@ export default function Home({ return ( - {error ? ( + {error || filteredServers.length === 0 ? ( ) : ( filteredServers.map((server: ServerData) => ( diff --git a/client/src/pages/ServerDetails.tsx b/client/src/pages/ServerDetails.tsx new file mode 100644 index 0000000..28248a1 --- /dev/null +++ b/client/src/pages/ServerDetails.tsx @@ -0,0 +1,307 @@ +import { + ContentCopy, + Launch, + Public, + PublicOff, + Warning, +} from '@mui/icons-material'; +import { + Box, + Card, + CardContent, + Divider, + IconButton, + Tooltip, + Typography, + alpha, +} from '@mui/material'; +import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { useEffect, useState } from 'react'; +import { Link, useParams } from 'react-router-dom'; +import { fetchData, fetchDemo } from '../apiUtil'; +import { AlertResponse } from '../components/Alert'; +import ErrorCard from '../components/ErrorCard'; +import ExpansionBar from '../components/ExpansionsBar'; +import ServerData, { + ServerSettings, + ServerSettingsInfo, +} from '../data/ServerData'; +import CopyImageIcon from '../images/copy-image.png'; + +interface KeyValueRow { + id: number; + key: string; + name: string; + value: string | number | boolean; +} + +const columns: GridColDef[] = [ + { field: 'key', headerName: 'Key', flex: 2 }, + { field: 'name', headerName: 'Name', flex: 1 }, + { field: 'value', headerName: 'Value', flex: 1 }, + { field: 'description', headerName: 'Description', flex: 3 }, +]; + +function ServerSettingsDataGrid({ + serverSettings, +}: { + serverSettings: ServerSettings; +}) { + const rows: KeyValueRow[] = Object.entries(serverSettings).map( + ([key, value], index) => ({ + id: index + 1, + key, + name: ServerSettingsInfo[key]?.name || 'none', + value, + description: ServerSettingsInfo[key]?.description || '', + }) + ); + + return ( + + + + ); +} + +export default function ServerDetails({ + setAlertInfo, +}: { + setAlertInfo: React.Dispatch>; +}) { + const { id } = useParams(); + const [server, setServer] = useState(); + const [error, setError] = useState(''); + + useEffect(() => { + let data: ServerData; + const fetchServerData = async () => { + try { + if (id === 'demo') { + data = await fetchDemo(); + } else { + data = await fetchData(`server/${id}`); + } + } catch (err) { + if (err instanceof Error) { + setAlertInfo({ + message: err.message, + severity: 'error', + }); + } else { + setError('An unknown error occurred.'); + } + } + + setServer(data); + }; + + fetchServerData(); + }, [id, setServer, setAlertInfo, setError]); + + const [clipboardTooltip, setClipboardTooltip] = useState('Copy server URL.'); + const [clipboardTooltipOpen, setClipboardTooltipOpen] = useState(false); + const handleClipboardTooltipClose = () => { + setClipboardTooltipOpen(false); + }; + + const handleClipboardTooltipOpen = () => { + setClipboardTooltipOpen(true); + }; + + const copyServerUrlToClipboard = async (text: string) => { + try { + navigator.clipboard.writeText(text); + setClipboardTooltip('Copied server URL!'); + handleClipboardTooltipOpen(); + setTimeout(() => { + handleClipboardTooltipClose(); + setClipboardTooltip('Copy server URL.'); + }, 3000); + } catch (err) { + handleClipboardTooltipOpen(); + if (err instanceof Error) { + setClipboardTooltip(err.message); + } else { + setClipboardTooltip('Something went wrong!'); + } + setTimeout(() => { + handleClipboardTooltipClose(); + setClipboardTooltip('Copy server URL.'); + }, 3000); + } + }; + + function formatExternalUrl(url: string): string { + // prepend 'https://' to the URL if it's not already there + if (!/^https?:\/\//i.test(url)) { + return `https://${url}`; + } + return url; + } + + return ( + + {error || !server ? ( + + ) : ( + + + + {server.customizations['LOGIN.MAINT_MODE'] === 1 ? ( + + + + ) : ( + + {server.inactivity_counter > 0 ? ( + + + + ) : ( + + + + )} + + )} + + + + alpha(theme.palette.text.primary, 0.87)} + sx={{ lineHeight: 1.0 }} + > + {server.name} + + {typeof server.customizations['API.WEBSITE'] === 'string' && + server.customizations['API.WEBSITE'] !== '' && ( + + { + event.stopPropagation(); + }} + disableRipple + > + + theme.typography.caption.fontSize, + }} + /> + + + )} + + + alpha(theme.palette.text.primary, 0.6)} + > + {server.url} + + {window.isSecureContext && ( + + { + event.stopPropagation(); + copyServerUrlToClipboard(server.url); + }} + disableRipple + > + + theme.typography.caption.fontSize, + }} + /> + + + )} + + + + + theme.palette.text.secondary} + > + {`Lv.${server.max_level}`} + + + + + + {server.settings && ( + + )} + + + + + {server.active_sessions} active sessions + {server.login_limit !== 1 && ( + + event.preventDefault()} + /> + + )} + + + Updated: {new Date(server.updated).toLocaleString()} + + + + )} + + ); +}