mirror of
https://github.com/gnmyt/myspeed.git
synced 2026-02-10 07:39:08 -06:00
Add ExportButton component for downloading speedtest data in CSV/JSON formats
This commit is contained in:
@@ -245,6 +245,9 @@
|
||||
"invalid_view": "Ungültige Ansicht"
|
||||
},
|
||||
"statistics": {
|
||||
"export": {
|
||||
"button": "Exportieren"
|
||||
},
|
||||
"overview": {
|
||||
"total_title": "Insgesamte Tests",
|
||||
"total_description": "Die Anzahl der ausgeführten Tests",
|
||||
|
||||
@@ -279,6 +279,9 @@
|
||||
},
|
||||
"statistics": {
|
||||
"average": "Average",
|
||||
"export": {
|
||||
"button": "Export"
|
||||
},
|
||||
"overview": {
|
||||
"total_title": "Total tests",
|
||||
"total_description": "The number of executed tests",
|
||||
|
||||
87
client/src/common/components/ExportButton/ExportButton.jsx
Normal file
87
client/src/common/components/ExportButton/ExportButton.jsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faDownload, faChevronDown, faFileLines, faCode } from "@fortawesome/free-solid-svg-icons";
|
||||
import { t } from "i18next";
|
||||
import "./styles.sass";
|
||||
|
||||
export const ExportButton = ({ dateRange }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const dropdownRef = useRef(null);
|
||||
const buttonRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target) &&
|
||||
buttonRef.current && !buttonRef.current.contains(event.target)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const formatDateParam = (date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
const handleExport = async (format) => {
|
||||
setExporting(true);
|
||||
setIsOpen(false);
|
||||
|
||||
try {
|
||||
const fromParam = formatDateParam(dateRange.from);
|
||||
const toParam = formatDateParam(dateRange.to);
|
||||
const url = `/api/speedtests/export?from=${fromParam}&to=${toParam}&format=${format}`;
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Export failed');
|
||||
|
||||
const blob = await response.blob();
|
||||
const downloadUrl = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = downloadUrl;
|
||||
a.download = `myspeed-export-${fromParam}-to-${toParam}.${format}`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(downloadUrl);
|
||||
document.body.removeChild(a);
|
||||
} catch (error) {
|
||||
console.error('Export failed:', error);
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="export-button-container">
|
||||
<button
|
||||
className="export-button"
|
||||
ref={buttonRef}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
disabled={exporting}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} className="export-icon" />
|
||||
<span className="export-text">{t("statistics.export.button")}</span>
|
||||
<FontAwesomeIcon icon={faChevronDown} className={`chevron-icon ${isOpen ? 'open' : ''}`} />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="export-dropdown" ref={dropdownRef}>
|
||||
<div className="export-option" onClick={() => handleExport('csv')}>
|
||||
<FontAwesomeIcon icon={faFileLines} />
|
||||
<span>{t("storage.csv")}</span>
|
||||
</div>
|
||||
<div className="export-option" onClick={() => handleExport('json')}>
|
||||
<FontAwesomeIcon icon={faCode} />
|
||||
<span>{t("storage.json")}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
client/src/common/components/ExportButton/index.js
Normal file
1
client/src/common/components/ExportButton/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export {ExportButton as default} from "./ExportButton.jsx";
|
||||
109
client/src/common/components/ExportButton/styles.sass
Normal file
109
client/src/common/components/ExportButton/styles.sass
Normal file
@@ -0,0 +1,109 @@
|
||||
@use "@/common/styles/colors" as *
|
||||
|
||||
.export-button-container
|
||||
position: relative
|
||||
display: inline-block
|
||||
|
||||
.export-button
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 0.5rem
|
||||
padding: 0.5rem 0.875rem
|
||||
background-color: $dark-gray
|
||||
border: 1px solid $light-gray
|
||||
border-radius: 0.5rem
|
||||
cursor: pointer
|
||||
transition: all 0.2s ease-in-out
|
||||
color: $white
|
||||
font-family: inherit
|
||||
font-size: 0.8rem
|
||||
font-weight: 600
|
||||
|
||||
&:hover:not(:disabled)
|
||||
border-color: $accent-primary
|
||||
|
||||
&:disabled
|
||||
opacity: 0.6
|
||||
cursor: not-allowed
|
||||
|
||||
.export-icon
|
||||
color: $subtext
|
||||
font-size: 0.875rem
|
||||
|
||||
.export-text
|
||||
white-space: nowrap
|
||||
|
||||
.chevron-icon
|
||||
color: $subtext
|
||||
font-size: 0.625rem
|
||||
transition: transform 0.2s ease
|
||||
|
||||
&.open
|
||||
transform: rotate(180deg)
|
||||
|
||||
.export-dropdown
|
||||
position: absolute
|
||||
top: calc(100% + 0.375rem)
|
||||
right: 0
|
||||
left: 0
|
||||
z-index: 1000
|
||||
background-color: $dark-gray
|
||||
border: 1px solid $light-gray
|
||||
border-radius: 0.6rem
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3)
|
||||
overflow: hidden
|
||||
padding: 6px
|
||||
animation: dropdownFadeIn 0.15s ease-out
|
||||
|
||||
@keyframes dropdownFadeIn
|
||||
from
|
||||
opacity: 0
|
||||
transform: translateY(-8px) scale(0.98)
|
||||
to
|
||||
opacity: 1
|
||||
transform: translateY(0) scale(1)
|
||||
|
||||
.export-option
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 10px
|
||||
padding: 8px 12px
|
||||
border-radius: 6px
|
||||
color: $subtext
|
||||
cursor: pointer
|
||||
transition: all 0.15s ease
|
||||
|
||||
&:hover
|
||||
background-color: $darker-gray
|
||||
color: $white
|
||||
|
||||
svg
|
||||
color: $white
|
||||
|
||||
svg
|
||||
width: 14px
|
||||
height: 14px
|
||||
flex-shrink: 0
|
||||
|
||||
span
|
||||
font-size: 0.875rem
|
||||
font-weight: 500
|
||||
|
||||
@media screen and (max-width: 600px)
|
||||
.export-text
|
||||
display: none
|
||||
|
||||
.export-button
|
||||
padding: 0.5rem 0.625rem
|
||||
|
||||
.chevron-icon
|
||||
display: none
|
||||
|
||||
.export-dropdown
|
||||
position: fixed
|
||||
top: auto
|
||||
bottom: 1rem
|
||||
left: 50%
|
||||
right: auto
|
||||
transform: translateX(-50%)
|
||||
max-width: calc(100vw - 2rem)
|
||||
@@ -22,6 +22,7 @@ import OverviewChart from "@/pages/Statistics/charts/OverviewChart";
|
||||
import AverageChart from "@/pages/Statistics/charts/AverageChart";
|
||||
import HourlyChart from "@/pages/Statistics/charts/HourlyChart.jsx";
|
||||
import ConsistencyChart from "@/pages/Statistics/charts/ConsistencyChart";
|
||||
import ExportButton from "@/common/components/ExportButton";
|
||||
import i18n, {t} from "i18next";
|
||||
import "./styles.sass";
|
||||
|
||||
@@ -161,6 +162,7 @@ export const Statistics = () => {
|
||||
to={dateRange.to}
|
||||
onChange={handleDateRangeChange}
|
||||
/>
|
||||
<ExportButton dateRange={dateRange} />
|
||||
</div>
|
||||
|
||||
<OverviewChart tests={statistics.tests} time={statistics.time} dateRange={dateRange} onClick={() => setExpandedChart('overview')}/>
|
||||
|
||||
Reference in New Issue
Block a user