126 lines
4.8 KiB
JavaScript

import Alert from "@mui/material/Alert";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Chip from "@mui/material/Chip";
import Skeleton from "@mui/material/Skeleton";
import Stack from "@mui/material/Stack";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Typography from "@mui/material/Typography";
import useMediaQuery from "@mui/material/useMediaQuery";
import { useTheme } from "@mui/material/styles";
import { useExternalI18n } from "../i18n/ExternalI18nProvider.jsx";
import { translateExternalError } from "../i18n/errors.js";
export function ExternalPage({ actions, children, title }) {
return (
<Box className="external-page">
<Stack alignItems={{ md: "center" }} direction={{ xs: "column", md: "row" }} justifyContent="space-between" spacing={2}>
<Typography component="h1" variant="h5">
{title}
</Typography>
{actions ? <Stack direction="row" flexWrap="wrap" gap={1}>{actions}</Stack> : null}
</Stack>
{children}
</Box>
);
}
export function ExternalPageState({ error, loading, onRetry }) {
const { t } = useExternalI18n();
if (loading) {
return (
<Box aria-label={t("common.loading")} className="external-skeleton" role="status">
<Skeleton height={46} variant="rounded" />
<Skeleton height="min(52vh, 520px)" variant="rounded" />
</Box>
);
}
if (error) {
return (
<Stack spacing={2}>
<Alert severity="error">{translateExternalError(error, t, "errors.loadFailed")}</Alert>
<Button onClick={onRetry} variant="outlined">{t("common.reload")}</Button>
</Stack>
);
}
return null;
}
export function ResponsiveDataList({ columns, emptyText, items, rowKey }) {
const theme = useTheme();
const mobile = useMediaQuery(theme.breakpoints.down("sm"));
const { t } = useExternalI18n();
if (!items.length) {
return <Box className="external-empty">{emptyText || t("common.noData")}</Box>;
}
if (mobile) {
return (
<Stack spacing={1.5}>
{items.map((item, index) => (
<Card key={rowKey(item, index)} variant="outlined">
<CardContent className="external-mobile-card">
{columns.map((column) => (
<Box className="external-mobile-row" key={column.key}>
<Typography color="text.secondary" component="span" variant="body2">{column.label}</Typography>
<Box>{column.render(item)}</Box>
</Box>
))}
</CardContent>
</Card>
))}
</Stack>
);
}
return (
<TableContainer className="external-table-container">
<Table size="small">
<TableHead>
<TableRow>{columns.map((column) => <TableCell key={column.key}>{column.label}</TableCell>)}</TableRow>
</TableHead>
<TableBody>
{items.map((item, index) => (
<TableRow hover key={rowKey(item, index)}>
{columns.map((column) => <TableCell key={column.key}>{column.render(item)}</TableCell>)}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
}
export function StatusChip({ status }) {
const { t } = useExternalI18n();
const normalized = String(status || "-").toLowerCase();
const enabled = ["active", "enabled", "running", "normal", "unbanned"].includes(normalized);
const dangerous = ["banned", "disabled", "failed", "blocked"].includes(normalized);
const statusKey = `status.${normalized}`;
const translated = t(statusKey);
return <Chip color={enabled ? "success" : dangerous ? "error" : "default"} label={translated === statusKey ? status || "-" : translated} size="small" variant="outlined" />;
}
export function PagePagination({ page, pageSize, total, onChange }) {
const { t } = useExternalI18n();
const totalPages = Math.max(1, Math.ceil(total / pageSize));
return (
<Stack alignItems="center" direction="row" justifyContent="space-between" spacing={1}>
<Typography color="text.secondary" variant="body2">{t("pagination.total", { count: total })}</Typography>
<Stack direction="row" spacing={1}>
<Button disabled={page <= 1} onClick={() => onChange(page - 1)} variant="outlined">{t("pagination.previous")}</Button>
<Typography className="external-page-number" variant="body2">{page} / {totalPages}</Typography>
<Button disabled={page >= totalPages} onClick={() => onChange(page + 1)} variant="outlined">{t("pagination.next")}</Button>
</Stack>
</Stack>
);
}