diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java index 88c15a328..24c1c78aa 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java @@ -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"), diff --git a/Plan/react/dashboard/package.json b/Plan/react/dashboard/package.json index 83c019643..c6b4e4ab9 100644 --- a/Plan/react/dashboard/package.json +++ b/Plan/react/dashboard/package.json @@ -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", diff --git a/Plan/react/dashboard/src/components/cards/common/PlayerListCard.jsx b/Plan/react/dashboard/src/components/cards/common/PlayerListCard.jsx index 34eeea8e0..3c9881224 100644 --- a/Plan/react/dashboard/src/components/cards/common/PlayerListCard.jsx +++ b/Plan/react/dashboard/src/components/cards/common/PlayerListCard.jsx @@ -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: {player.playerName}, activityIndex: player.activityIndex, + activityGroup: t(getActivityGroup(player.activityIndex)), activityIndexAndGroup: formatDecimals(player.activityIndex, decimalFormat) + " (" + t(getActivityGroup(player.activityIndex)) + ")", activePlaytime: player.playtimeActive, - activePlaytimeFormatted: , + activePlaytimeFormatted: formatTimeAmount(timePreferences, player.playtimeActive), sessions: player.sessionCount, registered: player.registered, - registeredFormatted: , + registeredFormatted: formatDateEasy(player.registered), lastSeen: player.lastSeen, - lastSeenFormatted: , + lastSeenFormatted: formatDateEasy(player.lastSeen), country: player.country, pingAverage: player.pingAverage, pingAverageFormatted: formatDecimals(player.pingAverage, decimalFormat) + "ms", diff --git a/Plan/react/dashboard/src/components/table/DataTablesTable.jsx b/Plan/react/dashboard/src/components/table/DataTablesTable.jsx index e2b7fd587..b20119bd6 100644 --- a/Plan/react/dashboard/src/components/table/DataTablesTable.jsx +++ b/Plan/react/dashboard/src/components/table/DataTablesTable.jsx @@ -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}) => (
  • @@ -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 && + + + {generating && + } + {!generating && } + + + {t('html.label.export')} CSV + + + } + ) +} + const DataTablesTable = ({id, rowKeyFunction, options, colorClass}) => { const {t} = useTranslation(); const {nightModeEnabled} = useTheme(); @@ -205,6 +251,9 @@ const DataTablesTable = ({id, rowKeyFunction, options, colorClass}) => { } +
    + +
    diff --git a/Plan/react/dashboard/src/components/text/FormattedDate.jsx b/Plan/react/dashboard/src/components/text/FormattedDate.jsx index 52979b6b2..ae09a078b 100644 --- a/Plan/react/dashboard/src/components/text/FormattedDate.jsx +++ b/Plan/react/dashboard/src/components/text/FormattedDate.jsx @@ -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 \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/text/FormattedTime.jsx b/Plan/react/dashboard/src/components/text/FormattedTime.jsx index dae04c83f..8ecbaff0e 100644 --- a/Plan/react/dashboard/src/components/text/FormattedTime.jsx +++ b/Plan/react/dashboard/src/components/text/FormattedTime.jsx @@ -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 => { diff --git a/Plan/react/dashboard/yarn.lock b/Plan/react/dashboard/yarn.lock index 5cef30206..556bba641 100644 --- a/Plan/react/dashboard/yarn.lock +++ b/Plan/react/dashboard/yarn.lock @@ -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"