parent
673cb4cfdb
commit
34a731b70a
|
@ -283,6 +283,7 @@ public enum HtmlLang implements Lang {
|
|||
LABEL_TABLE_VISIBLE_COLUMNS("html.label.table.visibleColumns", "Visible columns"),
|
||||
LABEL_TABLE_SHOW_N_OF_M("html.label.table.showNofM", "Showing {{n}} of {{m}} entries"),
|
||||
LABEL_TABLE_SHOW_PER_PAGE("html.label.table.showPerPage", "Show per page"),
|
||||
LABEL_EXPORT("html.label.export", "Export"),
|
||||
|
||||
LOGIN_LOGIN("html.login.login", "Login"),
|
||||
LOGIN_LOGOUT("html.login.logout", "Logout"),
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
"@testing-library/user-event": "^14.5.2",
|
||||
"axios": "^1.6.5",
|
||||
"bootstrap": "^5.3.2",
|
||||
"export-to-csv": "^1.2.2",
|
||||
"highcharts": "^10.3.3",
|
||||
"i18next": "^23.7.16",
|
||||
"i18next-chained-backend": "^4.6.2",
|
||||
|
|
|
@ -7,12 +7,13 @@ import DataTablesTable from "../../table/DataTablesTable";
|
|||
import {CardLoader} from "../../navigation/Loader";
|
||||
import {Link} from "react-router-dom";
|
||||
import {faCalendarCheck, faCalendarPlus, faClock} from "@fortawesome/free-regular-svg-icons";
|
||||
import FormattedDate from "../../text/FormattedDate";
|
||||
import FormattedTime from "../../text/FormattedTime";
|
||||
import {formatDate, useDatePreferences} from "../../text/FormattedDate";
|
||||
import {useTimePreferences} from "../../text/FormattedTime";
|
||||
import ExtensionIcon from "../../extensions/ExtensionIcon";
|
||||
import {ExtensionValueTableCell} from "../../extensions/ExtensionCard";
|
||||
import {usePreferences} from "../../../hooks/preferencesHook";
|
||||
import {formatDecimals} from "../../../util/formatters";
|
||||
import {formatTimeAmount} from "../../../util/format/TimeAmountFormat.js";
|
||||
|
||||
const getActivityGroup = value => {
|
||||
const VERY_ACTIVE = 3.75;
|
||||
|
@ -38,6 +39,9 @@ const PlayerListCard = ({data, title, justList, orderBy}) => {
|
|||
|
||||
const [options, setOptions] = useState(undefined);
|
||||
|
||||
const timePreferences = useTimePreferences();
|
||||
const datePreferences = useDatePreferences();
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
|
@ -80,20 +84,25 @@ const PlayerListCard = ({data, title, justList, orderBy}) => {
|
|||
}
|
||||
}));
|
||||
|
||||
const formatDateEasy = date => {
|
||||
return formatDate(date, datePreferences.offset, datePreferences.pattern, false, datePreferences.recentDaysPattern, t);
|
||||
}
|
||||
|
||||
const rows = data.players.map(player => {
|
||||
const row = {
|
||||
name: player.playerName,
|
||||
uuid: player.playerUUID,
|
||||
link: <Link to={"/player/" + player.playerUUID}>{player.playerName}</Link>,
|
||||
activityIndex: player.activityIndex,
|
||||
activityGroup: t(getActivityGroup(player.activityIndex)),
|
||||
activityIndexAndGroup: formatDecimals(player.activityIndex, decimalFormat) + " (" + t(getActivityGroup(player.activityIndex)) + ")",
|
||||
activePlaytime: player.playtimeActive,
|
||||
activePlaytimeFormatted: <FormattedTime timeMs={player.playtimeActive}/>,
|
||||
activePlaytimeFormatted: formatTimeAmount(timePreferences, player.playtimeActive),
|
||||
sessions: player.sessionCount,
|
||||
registered: player.registered,
|
||||
registeredFormatted: <FormattedDate date={player.registered}/>,
|
||||
registeredFormatted: formatDateEasy(player.registered),
|
||||
lastSeen: player.lastSeen,
|
||||
lastSeenFormatted: <FormattedDate date={player.lastSeen}/>,
|
||||
lastSeenFormatted: formatDateEasy(player.lastSeen),
|
||||
country: player.country,
|
||||
pingAverage: player.pingAverage,
|
||||
pingAverageFormatted: formatDecimals(player.pingAverage, decimalFormat) + "ms",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import {Card, InputGroup} from "react-bootstrap";
|
||||
import {Card, Dropdown, InputGroup} from "react-bootstrap";
|
||||
import Select from "../input/Select";
|
||||
import SearchField from "../input/SearchField";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
|
@ -17,6 +17,7 @@ import Toggle from "../input/Toggle";
|
|||
import CollapseWithButton from "../layout/CollapseWithButton";
|
||||
import {faMinusSquare, faPlusSquare} from "@fortawesome/free-regular-svg-icons";
|
||||
import {Trans, useTranslation} from "react-i18next";
|
||||
import {download, generateCsv, mkConfig} from "export-to-csv";
|
||||
|
||||
const PaginationOption = ({onClick, children, selected}) => (
|
||||
<li>
|
||||
|
@ -79,6 +80,51 @@ const VisibleColumnsSelector = ({columns, visibleColumnIndexes, toggleColumn}) =
|
|||
)
|
||||
}
|
||||
|
||||
const ExportMenu = ({matchingData}) => {
|
||||
const {t} = useTranslation();
|
||||
const [generating, setGenerating] = useState(false);
|
||||
|
||||
const hasData = matchingData.length > 0;
|
||||
|
||||
const exportCSV = useCallback(async () => {
|
||||
setGenerating(true);
|
||||
|
||||
const rows = matchingData.map(row => {
|
||||
const mapped = {};
|
||||
for (let entry of Object.entries(row)) {
|
||||
if (entry[1] === undefined || entry[1]["$$typeof"] === undefined) {
|
||||
mapped[entry[0]] = entry[1];
|
||||
}
|
||||
}
|
||||
return mapped;
|
||||
})
|
||||
|
||||
const csvConfig = mkConfig({
|
||||
useKeysAsHeaders: true,
|
||||
filename: "data-" + new Date().toISOString().replaceAll(":", '').substring(0, 17)
|
||||
});
|
||||
const csvOutput = generateCsv(csvConfig)(rows);
|
||||
await download(csvConfig)(csvOutput);
|
||||
setGenerating(false)
|
||||
}, [matchingData, setGenerating])
|
||||
|
||||
return (
|
||||
<>{hasData &&
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle variant={""} id="dropdown-basic">
|
||||
{generating &&
|
||||
<FontAwesomeIcon icon={"gear"} className={"fa-spin"} title={t('html.label.export')}/>}
|
||||
{!generating && <FontAwesomeIcon icon={"file-export"} title={t('html.label.export')}/>}
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item onClick={exportCSV}><FontAwesomeIcon
|
||||
icon={"file-export"}/> {t('html.label.export')} CSV</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
}</>
|
||||
)
|
||||
}
|
||||
|
||||
const DataTablesTable = ({id, rowKeyFunction, options, colorClass}) => {
|
||||
const {t} = useTranslation();
|
||||
const {nightModeEnabled} = useTheme();
|
||||
|
@ -205,6 +251,9 @@ const DataTablesTable = ({id, rowKeyFunction, options, colorClass}) => {
|
|||
<VisibleColumnsSelector columns={columns} visibleColumnIndexes={visibleColumnIndexes}
|
||||
toggleColumn={toggleColumn}/>
|
||||
</div>}
|
||||
<div className={"float-end dataTables_columns"}>
|
||||
<ExportMenu matchingData={matchingData} columns={columns}/>
|
||||
</div>
|
||||
<table id={id}
|
||||
className={"datatable table table-bordered table-striped" + (nightModeEnabled ? " table-dark" : '')}
|
||||
style={{width: "100%"}}>
|
||||
|
|
|
@ -5,13 +5,11 @@ import {useMetadata} from "../../hooks/metadataHook";
|
|||
import {useTranslation} from "react-i18next";
|
||||
import {isNumber} from "../../util/isNumber.js";
|
||||
|
||||
const FormattedDate = ({date}) => {
|
||||
const {t} = useTranslation();
|
||||
export const useDatePreferences = () => {
|
||||
const {timeZoneOffsetHours} = useMetadata();
|
||||
const {preferencesLoaded, dateFormatNoSeconds, recentDaysInDateFormat} = usePreferences();
|
||||
|
||||
if (!preferencesLoaded || date === undefined || date === null) return <></>;
|
||||
if (!isNumber(date)) return date;
|
||||
if (!preferencesLoaded) return {};
|
||||
|
||||
const pattern = dateFormatNoSeconds;
|
||||
const recentDays = recentDaysInDateFormat;
|
||||
|
@ -19,6 +17,10 @@ const FormattedDate = ({date}) => {
|
|||
|
||||
const offset = timeZoneOffsetHours * 60 * 60 * 1000;
|
||||
|
||||
return {pattern, recentDays, recentDaysPattern, offset};
|
||||
}
|
||||
|
||||
export function formatDate(date, offset, pattern, recentDays, recentDaysPattern, t) {
|
||||
const dayMs = 24 * 60 * 60 * 1000;
|
||||
const timestamp = date - offset;
|
||||
const now = Date.now();
|
||||
|
@ -35,11 +37,18 @@ const FormattedDate = ({date}) => {
|
|||
}
|
||||
}
|
||||
|
||||
const formatted = date !== 0 ? new SimpleDateFormat(format).format(timestamp) : '-';
|
||||
return date !== 0 ? new SimpleDateFormat(format).format(timestamp) : '-'
|
||||
}
|
||||
|
||||
return (
|
||||
<>{formatted}</>
|
||||
)
|
||||
const FormattedDate = ({date}) => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
const {pattern, recentDays, recentDaysPattern, offset} = useDatePreferences();
|
||||
|
||||
if (!pattern || date === undefined || date === null) return <></>;
|
||||
if (!isNumber(date)) return date;
|
||||
|
||||
return formatDate(date, offset, pattern, recentDays, recentDaysPattern, t);
|
||||
};
|
||||
|
||||
export default FormattedDate
|
|
@ -3,13 +3,12 @@ import {usePreferences} from "../../hooks/preferencesHook";
|
|||
import {formatTimeAmount} from "../../util/format/TimeAmountFormat";
|
||||
import {isNumber} from "../../util/isNumber.js";
|
||||
|
||||
const FormattedTime = ({timeMs}) => {
|
||||
export const useTimePreferences = () => {
|
||||
const {preferencesLoaded, timeFormat} = usePreferences();
|
||||
|
||||
if (!preferencesLoaded) return <></>;
|
||||
if (!isNumber(timeMs)) return timeMs;
|
||||
if (!preferencesLoaded) return undefined;
|
||||
|
||||
const options = {
|
||||
return {
|
||||
YEAR: timeFormat.year,
|
||||
YEARS: timeFormat.years,
|
||||
MONTH: timeFormat.month,
|
||||
|
@ -21,11 +20,14 @@ const FormattedTime = ({timeMs}) => {
|
|||
SECONDS: timeFormat.seconds,
|
||||
ZERO: timeFormat.zero
|
||||
}
|
||||
const formatted = formatTimeAmount(options, timeMs);
|
||||
}
|
||||
|
||||
return (
|
||||
<>{formatted}</>
|
||||
)
|
||||
const FormattedTime = ({timeMs}) => {
|
||||
const options = useTimePreferences()
|
||||
|
||||
if (!options) return <></>;
|
||||
if (!isNumber(timeMs)) return timeMs;
|
||||
return formatTimeAmount(options, timeMs);
|
||||
};
|
||||
|
||||
export const formatTimeFunction = time => {
|
||||
|
|
|
@ -1782,6 +1782,11 @@ expand-template@^2.0.3:
|
|||
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
|
||||
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
|
||||
|
||||
export-to-csv@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/export-to-csv/-/export-to-csv-1.2.2.tgz#24f09a2dba34aff65f8f97402a869e88232b8f58"
|
||||
integrity sha512-d9BxkLlyxeyFU1yhQKGjnnEQj2fh6PjyDVGyiI/uj7PD/oe7Lo1551B1lTaQeXHGPRX2DHuk6i3JMZB6ncJApQ==
|
||||
|
||||
fast-json-patch@^3.0.0-1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-3.1.1.tgz#85064ea1b1ebf97a3f7ad01e23f9337e72c66947"
|
||||
|
|
Loading…
Reference in New Issue