diff --git a/package-lock.json b/package-lock.json
index 5f2520e..29d8aeb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,13 @@
"react-router-dom": "^7.13.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
+ },
+ "devDependencies": {
+ "@types/node": "^25.2.0",
+ "@types/react": "^19.2.10",
+ "@types/react-dom": "^19.2.3",
+ "@types/react-router-dom": "^5.3.3",
+ "typescript": "^5.9.3"
}
},
"node_modules/@adobe/css-tools": {
@@ -3615,6 +3622,13 @@
"@types/node": "*"
}
},
+ "node_modules/@types/history": {
+ "version": "4.7.11",
+ "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
+ "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/html-minifier-terser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
@@ -3726,6 +3740,49 @@
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"license": "MIT"
},
+ "node_modules/@types/react": {
+ "version": "19.2.10",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz",
+ "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@types/react-router": {
+ "version": "5.1.20",
+ "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
+ "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/history": "^4.7.11",
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-router-dom": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
+ "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/history": "^4.7.11",
+ "@types/react": "*",
+ "@types/react-router": "*"
+ }
+ },
"node_modules/@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@@ -6341,6 +6398,13 @@
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
"license": "MIT"
},
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -16336,17 +16400,16 @@
}
},
"node_modules/typescript": {
- "version": "4.9.5",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
- "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
- "node": ">=4.2.0"
+ "node": ">=14.17"
}
},
"node_modules/unbox-primitive": {
diff --git a/package.json b/package.json
index e5e449c..67281cf 100644
--- a/package.json
+++ b/package.json
@@ -42,5 +42,12 @@
"last 1 firefox version",
"last 1 safari version"
]
+ },
+ "devDependencies": {
+ "@types/node": "^25.2.0",
+ "@types/react": "^19.2.10",
+ "@types/react-dom": "^19.2.3",
+ "@types/react-router-dom": "^5.3.3",
+ "typescript": "^5.9.3"
}
}
diff --git a/src/App.js b/src/App.tsx
similarity index 87%
rename from src/App.js
rename to src/App.tsx
index 9f5a790..c8b3f80 100644
--- a/src/App.js
+++ b/src/App.tsx
@@ -1,41 +1,54 @@
-import React, { useState, useEffect, useCallback } from 'react';
+import React, { useState, useEffect, useCallback, ReactNode } from 'react';
import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom';
import Dashboard from './components/Dashboard';
import Comparison from './components/Comparison';
import Slides from './components/Slides';
import { fetchData, getCacheStatus, refreshData } from './services/dataService';
import { useLanguage } from './contexts/LanguageContext';
+import type { MuseumRecord, CacheStatus } from './types';
import './App.css';
-function NavLink({ to, children }) {
+interface NavLinkProps {
+ to: string;
+ children: ReactNode;
+ className?: string;
+}
+
+function NavLink({ to, children, className }: NavLinkProps) {
const location = useLocation();
const isActive = location.pathname === to;
return (
-
+
{children}
);
}
+interface DataSource {
+ id: string;
+ labelKey: string;
+ enabled: boolean;
+}
+
function App() {
const { t, dir, switchLanguage } = useLanguage();
- const [data, setData] = useState([]);
- const [loading, setLoading] = useState(true);
- const [refreshing, setRefreshing] = useState(false);
- const [error, setError] = useState(null);
- const [isOffline, setIsOffline] = useState(false);
- const [cacheInfo, setCacheInfo] = useState(null);
- const [showDataLabels, setShowDataLabels] = useState(false);
- const [includeVAT, setIncludeVAT] = useState(true);
- const [dataSource, setDataSource] = useState('museums');
+ const [data, setData] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [refreshing, setRefreshing] = useState(false);
+ const [error, setError] = useState(null);
+ const [isOffline, setIsOffline] = useState(false);
+ const [cacheInfo, setCacheInfo] = useState(null);
+ const [showDataLabels, setShowDataLabels] = useState(false);
+ const [includeVAT, setIncludeVAT] = useState(true);
+ const [dataSource, setDataSource] = useState('museums');
- const dataSources = [
+ const dataSources: DataSource[] = [
{ id: 'museums', labelKey: 'dataSources.museums', enabled: true },
{ id: 'coffees', labelKey: 'dataSources.coffees', enabled: false },
{ id: 'ecommerce', labelKey: 'dataSources.ecommerce', enabled: false }
];
- const loadData = useCallback(async (forceRefresh = false) => {
+ const loadData = useCallback(async (forceRefresh: boolean = false) => {
try {
setLoading(!forceRefresh);
setRefreshing(forceRefresh);
@@ -49,7 +62,7 @@ function App() {
const status = getCacheStatus();
setCacheInfo(status);
} catch (err) {
- setError(err.message);
+ setError((err as Error).message);
console.error(err);
} finally {
setLoading(false);
@@ -59,6 +72,7 @@ function App() {
useEffect(() => {
loadData();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleRefresh = () => {
@@ -132,7 +146,7 @@ function App() {
{t('nav.comparison')}
{isOffline && (
-
+