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"