From 102e7b5af7fe553a4debdabca612fa280b588c2f Mon Sep 17 00:00:00 2001 From: AndreasV Date: Mon, 19 Jan 2026 21:27:14 +0100 Subject: [PATCH] feat: Establish core application structure, routing, API routes, and i18n, with Vite configured for relative asset paths. --- index.html | 12 ++-- public/site.webmanifest | 6 +- src/App.tsx | 30 ++++----- src/DefaultLayout.tsx | 20 +++--- src/apiRoutes.tsx | 134 ++++++++++++++++++++-------------------- src/i18n.js | 4 +- src/pages/Map/Map.tsx | 107 ++++++++++++++++---------------- vite.config.mjs | 1 + 8 files changed, 158 insertions(+), 156 deletions(-) diff --git a/index.html b/index.html index e624e358..a54377b3 100644 --- a/index.html +++ b/index.html @@ -2,11 +2,11 @@ - - - - - + + + + + @@ -19,6 +19,6 @@
- + diff --git a/public/site.webmanifest b/public/site.webmanifest index 408e7998..4e0d1684 100644 --- a/public/site.webmanifest +++ b/public/site.webmanifest @@ -3,12 +3,12 @@ "short_name": "OpenTAKServer", "icons": [ { - "src": "/android-chrome-192x192.png", + "src": "android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { - "src": "/android-chrome-512x512.png", + "src": "android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } @@ -16,4 +16,4 @@ "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" -} +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index d132fcad..3470af90 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import '@mantine/core/styles.css'; import { MantineProvider } from '@mantine/core'; import { Notifications } from '@mantine/notifications'; -import { BrowserRouter, Route, Routes } from 'react-router'; +import { HashRouter, Route, Routes } from 'react-router'; import React from 'react'; import { theme } from './theme'; import '@mantine/notifications/styles.css'; @@ -11,7 +11,7 @@ import 'react-resizable/css/styles.css' import '@mantine/dates/styles.css'; import 'mantine-datatable/styles.css'; import './i18n'; -import {I18nextProvider, useTranslation} from "react-i18next"; +import { I18nextProvider, useTranslation } from "react-i18next"; const Login = React.lazy(() => import('./pages/Login/Login')); const Error404 = React.lazy(() => import('./pages/Errors/Error404')); @@ -19,23 +19,23 @@ const DefaultLayout = React.lazy(() => import('./DefaultLayout')); const PasswordReset = React.lazy(() => import('./pages/PasswordReset')); export default function App() { - const { t, i18n } = useTranslation(); + const { t, i18n } = useTranslation(); return ( - - - - - } /> - } /> - } /> - {/*} /> + + + + + } /> + } /> + } /> + {/*} /> } />*/} - } /> - - - + } /> + + + ); } diff --git a/src/DefaultLayout.tsx b/src/DefaultLayout.tsx index 45e3ccc7..5b5014df 100644 --- a/src/DefaultLayout.tsx +++ b/src/DefaultLayout.tsx @@ -24,7 +24,7 @@ import axios from './axios_config'; import { apiRoutes } from './apiRoutes'; import Navbar from './components/Navbar/Navbar'; import { socket } from './socketio'; -import {t} from "i18next"; +import { t } from "i18next"; export function DefaultLayout() { const [mobileOpened, { toggle: toggleMobile }] = useDisclosure(); @@ -44,11 +44,11 @@ export function DefaultLayout() { setSocketConnected(false); } - function onAlert(alert:any) { + function onAlert(alert: any) { let message = `${alert.alert_type} from ${alert.callsign}`; let color = 'red'; let icon = ; - const alert_sound = new Audio('/alert.mp3'); + const alert_sound = new Audio('alert.mp3'); alert_sound.play(); if (alert.cancel_time !== null) { @@ -93,13 +93,13 @@ export function DefaultLayout() { return ( @@ -120,13 +120,13 @@ export function DefaultLayout() { OpenTAKServer } onClick={() => {navigate('/profile')}}> + leftSection={} onClick={() => { navigate('/profile') }}> {t("Profile")} } - onClick={() => { + disabled={localStorage.getItem('loggedIn') !== 'true'} + leftSection={} + onClick={() => { logout(); }} > diff --git a/src/apiRoutes.tsx b/src/apiRoutes.tsx index 91fd93a9..e6065b8d 100644 --- a/src/apiRoutes.tsx +++ b/src/apiRoutes.tsx @@ -1,69 +1,69 @@ export const apiRoutes = { - login: '/api/login?include_auth_token', - logout: '/api/logout', - eud: '/api/eud', - users: '/api/users', - alerts: '/api/alerts', - me: '/api/me', - create_user: '/api/user/create', - video_streams: '/api/video_streams', - generate_certificate: '/api/certificate', - data_packages: '/api/data_packages', - download_data_packages: '/api/data_packages/download', - assign_eud_to_user: '/api/user/assign_eud', - status: '/api/status', - casevac: '/api/casevac', - deleteDataPackage: '/api/data_packages', - addVideoStream: '/api/mediamtx/stream/add', - deleteVideoStream: '/api/mediamtx/stream/delete', - updateVideoStream: '/api/mediamtx/stream/update', - addUser: '/api/user/add', - changeRole: '/api/user/role', - deactivateUser: '/api/user/deactivate', - activateUser: '/api/user/activate', - deleteUser: '/api/user/delete', - adminResetPassword: '/api/user/password/reset', //Allows admins to change any user's password - register: '/api/register', - tfValidate: '/api/tf-validate', - tfSetup: '/api/tf-setup', - resetPassword: '/api/password/reset', //Allows users to reset their own password if they forgot it - mapState: '/api/map_state', - getScheduledJobs: '/api/scheduler/jobs', - runJob: '/api/scheduler/job/run', - resumeJob: '/api/scheduler/job/resume', - pauseJob: '/api/scheduler/job/pause', - getRecording: '/api/videos/recording', - getRecordings: '/api/videos/recordings', - deleteRecording: '/api/videos/recording', - adminSettings: '/api/config', - modifyJob: '/api/scheduler/job/modify', - startSSL: '/api/ssl/start', - stopSSL: '/api/ssl/stop', - startTCP: '/api/tcp/start', - stopTCP: '/api/tcp/stop', - itakQrString: '/api/itak_qr_string', - meshtasticChannels: '/api/meshtastic/channel', - generateMeshtasticPsk: '/api/meshtastic/generate_psk', - pluginPackage: '/api/packages', - deviceProfiles: '/api/profiles', - truststore: '/api/truststore', - missions: '/api/missions', - groups: '/api/groups', - mission_invite: '/api/missions/invite', - eud_stats: '/api/eud_stats', - plugins: '/api/plugins', - atakQrString: '/api/atak_qr_string', - pluginRepo: '/api/plugins/repo', - ldapLogin: '/api/ldap_login', - allGroups: '/api/groups/all', - allUsers: '/api/users/all', - groupMembers: '/api/groups/members', - userGroups: '/api/users/groups', - takgov: '/api/takgov', - takgovLink: '/api/takgov/link', - takgovToken: '/api/takgov/token', - takgovPlugins: '/api/takgov/plugins', - takgovIcon: '/api/takgov/icon', - takgovPlugin: '/api/takgov/plugin', - language: '/api/language', + login: 'api/login?include_auth_token', + logout: 'api/logout', + eud: 'api/eud', + users: 'api/users', + alerts: 'api/alerts', + me: 'api/me', + create_user: 'api/user/create', + video_streams: 'api/video_streams', + generate_certificate: 'api/certificate', + data_packages: 'api/data_packages', + download_data_packages: 'api/data_packages/download', + assign_eud_to_user: 'api/user/assign_eud', + status: 'api/status', + casevac: 'api/casevac', + deleteDataPackage: 'api/data_packages', + addVideoStream: 'api/mediamtx/stream/add', + deleteVideoStream: 'api/mediamtx/stream/delete', + updateVideoStream: 'api/mediamtx/stream/update', + addUser: 'api/user/add', + changeRole: 'api/user/role', + deactivateUser: 'api/user/deactivate', + activateUser: 'api/user/activate', + deleteUser: 'api/user/delete', + adminResetPassword: 'api/user/password/reset', //Allows admins to change any user's password + register: 'api/register', + tfValidate: 'api/tf-validate', + tfSetup: 'api/tf-setup', + resetPassword: 'api/password/reset', //Allows users to reset their own password if they forgot it + mapState: 'api/map_state', + getScheduledJobs: 'api/scheduler/jobs', + runJob: 'api/scheduler/job/run', + resumeJob: 'api/scheduler/job/resume', + pauseJob: 'api/scheduler/job/pause', + getRecording: 'api/videos/recording', + getRecordings: 'api/videos/recordings', + deleteRecording: 'api/videos/recording', + adminSettings: 'api/config', + modifyJob: 'api/scheduler/job/modify', + startSSL: 'api/ssl/start', + stopSSL: 'api/ssl/stop', + startTCP: 'api/tcp/start', + stopTCP: 'api/tcp/stop', + itakQrString: 'api/itak_qr_string', + meshtasticChannels: 'api/meshtastic/channel', + generateMeshtasticPsk: 'api/meshtastic/generate_psk', + pluginPackage: 'api/packages', + deviceProfiles: 'api/profiles', + truststore: 'api/truststore', + missions: 'api/missions', + groups: 'api/groups', + mission_invite: 'api/missions/invite', + eud_stats: 'api/eud_stats', + plugins: 'api/plugins', + atakQrString: 'api/atak_qr_string', + pluginRepo: 'api/plugins/repo', + ldapLogin: 'api/ldap_login', + allGroups: 'api/groups/all', + allUsers: 'api/users/all', + groupMembers: 'api/groups/members', + userGroups: 'api/users/groups', + takgov: 'api/takgov', + takgovLink: 'api/takgov/link', + takgovToken: 'api/takgov/token', + takgovPlugins: 'api/takgov/plugins', + takgovIcon: 'api/takgov/icon', + takgovPlugin: 'api/takgov/plugin', + language: 'api/language', }; diff --git a/src/i18n.js b/src/i18n.js index befad660..0a7a3b3e 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -1,5 +1,5 @@ import i18n from "i18next"; -import {initReactI18next} from "react-i18next"; +import { initReactI18next } from "react-i18next"; import HttpApi from "i18next-http-backend"; import LanguageDetector from "i18next-browser-languagedetector"; @@ -13,7 +13,7 @@ i18n returnEmptyString: false, debug: false, backend: { - loadPath: "/locales/{{lng}}/{{ns}}.json", + loadPath: "locales/{{lng}}/{{ns}}.json", } }); diff --git a/src/pages/Map/Map.tsx b/src/pages/Map/Map.tsx index 2ebced20..09c074b8 100644 --- a/src/pages/Map/Map.tsx +++ b/src/pages/Map/Map.tsx @@ -38,9 +38,9 @@ export default function Map() { const markersLayer = new L.LayerGroup(); const fovsLayer = new L.LayerGroup(); - function formatDrawer(eud:any, point:any) { - const detail_rows:ReactElement[] = []; - const position_rows:ReactElement[] = []; + function formatDrawer(eud: any, point: any) { + const detail_rows: ReactElement[] = []; + const position_rows: ReactElement[] = []; if (eud !== null) { Object.keys(eud).map((key, index) => { @@ -109,7 +109,7 @@ export default function Map() { return null; } - function handleFov(point:any) { + function handleFov(point: any) { if (!point) return; const uid = point.device_uid; @@ -139,12 +139,12 @@ export default function Map() { setFovs(fovs); } else { fovs[uid].setLatLngs([[point.latitude, point.longitude], - [p1.LAT, p1.LON], [p2.LAT, p2.LON]]); + [p1.LAT, p1.LON], [p2.LAT, p2.LON]]); } } } - function addEud(eud:any) { + function addEud(eud: any) { let className = classes.disconnected; if (eud.last_status === 'Connected') { className = classes.connected; @@ -309,26 +309,26 @@ export default function Map() { if (Object.hasOwn(value, 'iconset_path') && value.iconset_path !== null && value.iconset_path.includes('COT_MAPPING_SPOTMAP')) { - if (Object.hasOwn(circles, uid)) { - map.removeLayer(circles[uid]); - } - const circle = L.circle( - [value.point.latitude, value.point.longitude], - { radius: 5, color: `#${value.color_hex.slice(2)}` } - ); - circle.bindTooltip(value.callsign, { - opacity: 0.7, - permanent: true, - direction: 'bottom', - }); - circle.on('click', (e) => { - setDrawerTitle(value.callsign); - formatDrawer(value, null); - open(); - }); - circles[uid] = circle; - setCircles(circles); - circle.addTo(map); + if (Object.hasOwn(circles, uid)) { + map.removeLayer(circles[uid]); + } + const circle = L.circle( + [value.point.latitude, value.point.longitude], + { radius: 5, color: `#${value.color_hex.slice(2)}` } + ); + circle.bindTooltip(value.callsign, { + opacity: 0.7, + permanent: true, + direction: 'bottom', + }); + circle.on('click', (e) => { + setDrawerTitle(value.callsign); + formatDrawer(value, null); + open(); + }); + circles[uid] = circle; + setCircles(circles); + circle.addTo(map); } else { let marker = L.marker([value.point.latitude, value.point.longitude]); if (Object.hasOwn(markers, uid)) { @@ -372,8 +372,8 @@ export default function Map() { })); } else { marker.setIcon(L.icon({ - iconUrl: '/map_icons/marker-icon.png', - shadowUrl: '/map_icons/marker-shadow.png', + iconUrl: 'map_icons/marker-icon.png', + shadowUrl: 'map_icons/marker-shadow.png', iconAnchor: [12, 24], popupAnchor: [7, -20], tooltipAnchor: [-4, -10], @@ -457,13 +457,13 @@ export default function Map() { return ( <> @@ -476,21 +476,21 @@ export default function Map() { @@ -512,19 +512,20 @@ export default function Map() { diff --git a/vite.config.mjs b/vite.config.mjs index 8c572128..48998370 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react'; import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ + base: './', plugins: [react(), tsconfigPaths()], test: { globals: true,