Change ServerCard to accept children

This allows me to pass in chips for summary view or data grid for detailed view.
Disable accordion features when data grid child is present.
This commit is contained in:
Corey 2024-05-07 05:31:53 +00:00
parent 5d041d5e77
commit ddf74e8343
8 changed files with 167 additions and 409 deletions

View File

@ -1,5 +1,5 @@
import { ToggleButton, ToggleButtonGroup } from '@mui/material';
import { ServerData } from '../data/ServerData';
import { ServerData } from '../../data/ServerData';
const expansions = [
<ToggleButton

View File

@ -0,0 +1,84 @@
import { Check, Close } from '@mui/icons-material';
import { Box, Chip, Tooltip } from '@mui/material';
import {
ServerData,
ServerSetting,
ServerSettingsInfo,
} from '../../data/ServerData';
export default function SettingsChipCloud({ server }: { server: ServerData }) {
const renderSettingsChip = ([key, value]: [
string,
boolean | string | number,
]) => {
if (!ServerSettingsInfo[key]) return null;
const transformValue = (
v: boolean | string | number,
setting: ServerSetting
): string | number | boolean => {
return setting.transform?.(v) ?? v;
};
let chipValue: string | number | boolean | JSX.Element = transformValue(
value,
ServerSettingsInfo[key]
);
if (typeof chipValue === 'boolean') {
chipValue = chipValue ? (
<Check color="success" />
) : (
<Close color="error" />
);
}
return (
<Tooltip
key={key}
arrow
disableInteractive
title={ServerSettingsInfo[key].description}
>
<Chip
label={`${ServerSettingsInfo[key].name === '' ? key : ServerSettingsInfo[key].name}`}
avatar={
typeof chipValue === 'object' ? undefined : (
<Chip label={chipValue} size="small" />
)
}
icon={typeof chipValue === 'object' ? chipValue : undefined}
size="small"
className="m-1 pr-1"
sx={{
'& .MuiChip-avatar': {
width: 'auto',
marginX: 0,
order: 2,
},
'& .MuiChip-icon': {
marginX: 0,
order: 2,
},
'&> .MuiChip-label': {
paddingRight: 0.5,
},
boxShadow: '0px 3px 3px rgba(0, 0, 0, .25)',
}}
/>
</Tooltip>
);
};
return (
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
maxWidth: '100%',
}}
>
{Object.entries(server.customizations).map(renderSettingsChip)}
</Box>
);
}

View File

@ -4,7 +4,7 @@ import {
ServerSetting,
ServerSettings,
ServerSettingsInfo,
} from '../data/ServerData';
} from '../../data/ServerData';
type KeyValueRow = {
id: number;
@ -23,11 +23,13 @@ const columns: GridColDef[] = [
{ field: 'description', headerName: 'Description', flex: 6 },
];
export default function ServerSettingsDataGrid({
serverSettings,
}: {
type SettingsDataGridProps = {
serverSettings: ServerSettings;
}) {
};
export default function SettingsDataGrid({
serverSettings,
}: SettingsDataGridProps) {
const transformValue = (
v: boolean | string | number,
setting: ServerSetting

View File

@ -1,6 +1,5 @@
import {
Check,
Close,
ArrowCircleUp,
ContentCopy,
ExpandMore,
Info,
@ -13,29 +12,40 @@ import {
Box,
Card,
CardContent,
Chip,
Divider,
IconButton,
Tooltip,
Typography,
alpha,
} from '@mui/material';
import { useState } from 'react';
import { Link } from 'react-router-dom';
import {
ServerData,
ServerSetting,
ServerSettingsInfo,
} from '../data/ServerData';
Children,
ElementType,
ReactNode,
isValidElement,
useState,
} from 'react';
import { Link } from 'react-router-dom';
import { ServerData } from '../data/ServerData';
import CopyImageIcon from '../images/copy-image.png';
import { Accordion, AccordionDetails, AccordionSummary } from './Accordion';
import ExpansionBar from './ExpansionsBar';
import ExpansionBar from './Server/ExpansionsBar';
import SettingsDataGrid from './Server/SettingsDataGrid';
import {
Accordion,
AccordionDetails,
AccordionSummary,
} from './Themed/Accordion';
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
export default function ServerCard({ server }: { server: ServerData }) {
type ServerCardProps = {
server: ServerData;
children: ReactNode;
};
export default function ServerCard({ server, children }: ServerCardProps) {
const [clipboardTooltip, setClipboardTooltip] = useState('Copy server URL.');
const [clipboardTooltipOpen, setClipboardTooltipOpen] = useState(false);
const handleClipboardTooltipClose = () => {
@ -77,72 +87,39 @@ export default function ServerCard({ server }: { server: ServerData }) {
return url;
}
const renderSettingsChip = ([key, value]: [
string,
boolean | string | number,
]) => {
if (!ServerSettingsInfo[key]) return null;
const transformValue = (
v: boolean | string | number,
setting: ServerSetting
): string | number | boolean => {
return setting.transform?.(v) ?? v;
};
let chipValue: string | number | boolean | JSX.Element = transformValue(
value,
ServerSettingsInfo[key]
// Check if this is a detailed server card with a data grid
const hasSpecificComponent = (
childrenProps: ReactNode,
componentType: ElementType
): boolean => {
const childrenArray = Children.toArray(childrenProps);
return childrenArray.some(
(child) => isValidElement(child) && child.type === componentType
);
if (typeof chipValue === 'boolean') {
chipValue = chipValue ? (
<Check color="success" />
) : (
<Close color="error" />
);
};
const isSettingsDataGridChild = hasSpecificComponent(
children,
SettingsDataGrid
);
// Manually handle the accordion state to disable closing when detailed view
const [expand, setExpand] = useState(isSettingsDataGridChild);
const toggleAcordion = () => {
if (!isSettingsDataGridChild) {
setExpand((prev) => !prev);
}
return (
<Tooltip
key={key}
arrow
disableInteractive
title={ServerSettingsInfo[key].description}
>
<Chip
label={`${ServerSettingsInfo[key].name === '' ? key : ServerSettingsInfo[key].name}`}
avatar={
typeof chipValue === 'object' ? undefined : (
<Chip label={chipValue} size="small" />
)
}
icon={typeof chipValue === 'object' ? chipValue : undefined}
size="small"
className="m-1 pr-1"
sx={{
'& .MuiChip-avatar': {
width: 'auto',
marginX: 0,
order: 2,
},
'& .MuiChip-icon': {
marginX: 0,
order: 2,
},
'&> .MuiChip-label': {
paddingRight: 0.5,
},
boxShadow: '0px 3px 3px rgba(0, 0, 0, .25)',
}}
/>
</Tooltip>
);
};
return (
<Card className="mb-2">
<Accordion className="my-0">
<AccordionSummary expandIcon={<ExpandMore />}>
<Accordion
className="my-0"
defaultExpanded={isSettingsDataGridChild}
expanded={expand}
>
<AccordionSummary
expandIcon={!isSettingsDataGridChild && <ExpandMore />}
onClick={toggleAcordion}
>
<Box className="flex flex-col items-center justify-center">
<Box className="flex content-center">
{server.customizations['LOGIN.MAINT_MODE'] === 1 ? (
@ -290,16 +267,7 @@ export default function ServerCard({ server }: { server: ServerData }) {
<Divider sx={{ marginY: 1 }} />
</>
)}
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
maxWidth: '100%',
}}
>
{Object.entries(server.customizations).map(renderSettingsChip)}
</Box>
{children}
</AccordionDetails>
</Accordion>
<Divider />
@ -328,7 +296,13 @@ export default function ServerCard({ server }: { server: ServerData }) {
</Box>
<Box className="flex items-center whitespace-nowrap">
{/* <ServerDetailsModal id={server.id} /> */}
<Tooltip title="View full settings." arrow disableInteractive>
<Tooltip
title={
isSettingsDataGridChild ? 'Scroll to top.' : 'View full settings.'
}
arrow
disableInteractive
>
<IconButton
component={Link}
to={`/server/${encodeURIComponent(server.url)}`}
@ -336,7 +310,11 @@ export default function ServerCard({ server }: { server: ServerData }) {
className="p-0"
disableRipple
>
<Info className="p-1" />
{isSettingsDataGridChild ? (
<ArrowCircleUp className="p-1" />
) : (
<Info className="p-1" />
)}
</IconButton>
</Tooltip>
<Typography variant="caption">

View File

@ -1,79 +0,0 @@
import { Info } from '@mui/icons-material';
import { Card, IconButton, Tooltip, Typography } from '@mui/material';
import Modal from '@mui/material/Modal';
import { useState } from 'react';
import { fetchDataFromBackend } from '../apiUtil';
import { ServerData } from '../data/ServerData';
import ServerSettingsDataGrid from './ServerSettingsDataGrid';
export default function ServerDetailsModal({ id }: { id: number }) {
const [server, setServer] = useState<ServerData>();
const [error, setError] = useState<string>('');
const [open, setOpen] = useState(false);
const handleOpen = () => {
let data: ServerData;
const fetchServerData = async () => {
try {
data = await fetchDataFromBackend(`server/${id}`);
} catch (err) {
if (err instanceof Error) {
setError(err.message);
} else {
setError('An unknown error occurred.');
}
}
setServer(data);
};
fetchServerData();
setOpen(true);
};
const handleClose = () => setOpen(false);
return (
<div>
<Tooltip title="View full settings." arrow disableInteractive>
<IconButton className="p-0" disableRipple onClick={handleOpen}>
<Info className="p-1" />
</IconButton>
</Tooltip>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Card
sx={{
position: 'absolute' as const,
top: '10%',
left: '10%',
width: '80%',
height: '80%',
overflow: 'hidden',
overflowX: 'hidden',
boxShadow: 24,
}}
>
<Card
sx={{
position: 'absolute' as const,
width: '100%',
height: '100%',
overflow: 'scroll',
overflowX: 'hidden',
}}
>
{!server || !server.settings ? (
<Typography align="center" variant="subtitle1">
{error}
</Typography>
) : (
<ServerSettingsDataGrid serverSettings={server.settings} />
)}
</Card>
</Card>
</Modal>
</div>
);
}

View File

@ -1,5 +1,6 @@
import { Box } from '@mui/material';
import ErrorCard from '../components/ErrorCard';
import SettingsChipCloud from '../components/Server/SettingsChipCloud';
import ServerCard from '../components/ServerCard';
import { SearchState } from '../data/SearchState';
import { ServerData } from '../data/ServerData';
@ -129,7 +130,9 @@ export default function Home({
<ErrorCard error="" />
) : (
filteredServers.map((server: ServerData) => (
<ServerCard key={server.url} server={server} />
<ServerCard key={server.url} server={server}>
<SettingsChipCloud server={server} />
</ServerCard>
))
)}
</Box>

View File

@ -1,29 +1,12 @@
import {
ContentCopy,
Launch,
Public,
PublicOff,
Warning,
} from '@mui/icons-material';
import {
Box,
Card,
CardContent,
Divider,
IconButton,
Tooltip,
Typography,
alpha,
} from '@mui/material';
import { Box } from '@mui/material';
import { useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { fetchDataFromBackend, fetchDemo } from '../apiUtil';
import { AlertResponse } from '../components/Alert';
import ErrorCard from '../components/ErrorCard';
import ExpansionBar from '../components/ExpansionsBar';
import ServerSettingsDataGrid from '../components/ServerSettingsDataGrid';
import SettingsDataGrid from '../components/Server/SettingsDataGrid';
import ServerCard from '../components/ServerCard';
import { ServerData } from '../data/ServerData';
import CopyImageIcon from '../images/copy-image.png';
export default function ServerDetails({
setAlertInfo,
@ -60,229 +43,16 @@ export default function ServerDetails({
fetchServerData();
}, [url, 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(serverUrl: string): string {
// prepend 'https://' to the URL if it's not already there
if (!/^https?:\/\//i.test(serverUrl)) {
return `https://${serverUrl}`;
}
return serverUrl;
}
return (
<Box>
{error || !server ? (
<ErrorCard error={error} />
) : (
<Card className="mb-2">
<Box className="flex px-4">
<Box className="flex flex-col items-center justify-center">
<Box className="flex content-center">
{server.customizations['LOGIN.MAINT_MODE'] === 1 ? (
<Tooltip
arrow
disableInteractive
title="Server is undergoing maintenance."
>
<Warning color="warning" />
</Tooltip>
) : (
<div>
{!server.up ? (
<Tooltip
arrow
disableInteractive
title="Server is offline."
>
<PublicOff color="error" />
</Tooltip>
) : (
<Tooltip
arrow
disableInteractive
title="Server is online."
>
<Public color="success" />
</Tooltip>
)}
</div>
)}
</Box>
<Box className="flex content-center">
<Tooltip
arrow
disableInteractive
title="Estimated server geolocation provided by MaxMind"
>
<Typography
variant="caption"
component={Link}
to="https://www.maxmind.com/"
target="_blank"
onClick={(event) => {
event.stopPropagation();
}}
sx={{
textDecoration: 'none',
}}
color={(theme) => alpha(theme.palette.text.primary, 0.5)}
>
{server.location}
</Typography>
</Tooltip>
</Box>
</Box>
<CardContent className="grow py-1">
<Box className="flex content-center">
<Typography
variant="h5"
color={(theme) => alpha(theme.palette.text.primary, 0.87)}
sx={{ lineHeight: 1.0 }}
>
{server.name}
</Typography>
{typeof server.customizations['API.WEBSITE'] === 'string' &&
server.customizations['API.WEBSITE'] !== '' && (
<Tooltip arrow disableInteractive title="Visit website.">
<IconButton
component={Link}
to={formatExternalUrl(
server.customizations['API.WEBSITE']
)}
target="_blank"
rel="noopener"
size="small"
className="py-0"
onClick={(event) => {
event.stopPropagation();
}}
disableRipple
>
<Launch
sx={{
fontSize: (theme) =>
theme.typography.caption.fontSize,
}}
/>
</IconButton>
</Tooltip>
)}
</Box>
<Box className="flex content-center">
<Typography
variant="subtitle2"
color={(theme) => alpha(theme.palette.text.primary, 0.6)}
>
{server.url}
</Typography>
{window.isSecureContext && (
<Tooltip
arrow
disableInteractive
title={clipboardTooltip}
open={
clipboardTooltip === 'Copied server URL!' ||
clipboardTooltipOpen
}
onOpen={handleClipboardTooltipOpen}
onClose={handleClipboardTooltipClose}
>
<IconButton
size="small"
className="py-0"
onClick={(event) => {
event.stopPropagation();
copyServerUrlToClipboard(server.url);
}}
disableRipple
>
<ContentCopy
sx={{
fontSize: (theme) =>
theme.typography.caption.fontSize,
}}
/>
</IconButton>
</Tooltip>
)}
</Box>
<ExpansionBar server={server} />
</CardContent>
<Box className="flex items-center justify-center">
<Typography
variant="h5"
color={(theme) => theme.palette.text.secondary}
>
{`Lv.${server.max_level}`}
</Typography>
</Box>
</Box>
<Divider />
<CardContent className="flex justify-between py-1">
{server.settings && (
<ServerSettingsDataGrid serverSettings={server.settings} />
)}
</CardContent>
<Divider />
<CardContent className="flex justify-between py-1">
<Typography variant="caption">
{server.active_sessions} active sessions
{server.login_limit !== 1 && (
<Tooltip
title={`Server allows ${server.login_limit === 0 ? 'unlimited' : server.login_limit} simultaneous game sessions per IP.`}
arrow
disableInteractive
>
<img
src={CopyImageIcon}
alt=""
style={{
maxHeight: '1.5em',
marginLeft: '0.5em',
verticalAlign: 'middle',
}}
onContextMenu={(event) => event.preventDefault()}
/>
</Tooltip>
)}
</Typography>
<Typography variant="caption">
Updated: {new Date(server.updated).toLocaleString()}
</Typography>
</CardContent>
</Card>
<ServerCard server={server}>
{server.settings && (
<SettingsDataGrid serverSettings={server.settings} />
)}
</ServerCard>
)}
</Box>
);