diff --git a/client/public/assets/locales/de.json b/client/public/assets/locales/de.json
index c9a0c4f3..f4fbfec1 100644
--- a/client/public/assets/locales/de.json
+++ b/client/public/assets/locales/de.json
@@ -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",
diff --git a/client/public/assets/locales/en.json b/client/public/assets/locales/en.json
index c430af0d..4cc601ee 100644
--- a/client/public/assets/locales/en.json
+++ b/client/public/assets/locales/en.json
@@ -279,6 +279,9 @@
},
"statistics": {
"average": "Average",
+ "export": {
+ "button": "Export"
+ },
"overview": {
"total_title": "Total tests",
"total_description": "The number of executed tests",
diff --git a/client/src/common/components/ExportButton/ExportButton.jsx b/client/src/common/components/ExportButton/ExportButton.jsx
new file mode 100644
index 00000000..8ad9fd2f
--- /dev/null
+++ b/client/src/common/components/ExportButton/ExportButton.jsx
@@ -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 (
+
+
+
+ {isOpen && (
+
+
handleExport('csv')}>
+
+ {t("storage.csv")}
+
+
handleExport('json')}>
+
+ {t("storage.json")}
+
+
+ )}
+
+ );
+};
diff --git a/client/src/common/components/ExportButton/index.js b/client/src/common/components/ExportButton/index.js
new file mode 100644
index 00000000..df7fb806
--- /dev/null
+++ b/client/src/common/components/ExportButton/index.js
@@ -0,0 +1 @@
+export {ExportButton as default} from "./ExportButton.jsx";
\ No newline at end of file
diff --git a/client/src/common/components/ExportButton/styles.sass b/client/src/common/components/ExportButton/styles.sass
new file mode 100644
index 00000000..899ec08d
--- /dev/null
+++ b/client/src/common/components/ExportButton/styles.sass
@@ -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)
diff --git a/client/src/pages/Statistics/Statistics.jsx b/client/src/pages/Statistics/Statistics.jsx
index 2f8065de..d374811b 100644
--- a/client/src/pages/Statistics/Statistics.jsx
+++ b/client/src/pages/Statistics/Statistics.jsx
@@ -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}
/>
+
setExpandedChart('overview')}/>