From bc778bfd976c026e2011277369e1d937be8f8e2f Mon Sep 17 00:00:00 2001 From: vjrj Date: Tue, 21 Jul 2026 18:45:15 +0200 Subject: [PATCH 01/10] =?UTF-8?q?deps:=20react-leaflet=201.8=20->=204.2=20?= =?UTF-8?q?(+=20leaflet=201.9)=20=E2=80=94=20the=20last=20legacy=20react-*?= =?UTF-8?q?=20lib?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ground-up hooks rewrite of the app's core map. Removes the final batch of React-19-blocking warnings (legacy context on Map/LayersControl/TileLayer/ Marker/CircleMarker/Circle/Tooltip + ReactDOM.render from the old controls). Core: -> ; .leafletElement refs (6 files) -> the map/ layer instances directly, via a child (useMap) + plain refs on Marker/Circle/GeoJSON; controlled viewport (onViewportChanged + state.center/ zoom) -> (useMapEvents moveend/zoomend) + imperative setView; onClick -> eventHandlers={{click}}; Path styling -> pathOptions/style; subsUnion takes the L.Map directly. The 4 v1-only plugins reimplemented (new in-repo helpers under Maps/): - react-leaflet-control -> MapControl (L.Control + createPortal) - react-leaflet-google -> GoogleMutantLayer (createLayerComponent + leaflet.gridlayer.googlemutant; Google Maps API already loaded by Gkeys) - react-leaflet-fullscreen -> createControlComponent + leaflet.fullscreen - leaflet-sleep / leaflet-graphicscale kept as vanilla (work on leaflet 1.9) Browser-verified: /fires (tiles, OSM+Google layer switch, custom control, fullscreen, graphic scale, pan/zoom -> re-fetch, no NaN), fire detail (GeoJSON rect + fitBounds), home (3 maps coexist; SelectionMap draggable marker -> updatePosition + distance circle). Console now shows 0 React warnings on home/fires. REST smoke byte-identical. --- PENDIENTE.md | 9 +- UPGRADE.md | 4 +- imports/ui/components/Maps/DefMapLayers.js | 8 +- imports/ui/components/Maps/FireCircleMark.js | 2 +- imports/ui/components/Maps/FireIconMark.js | 15 +-- imports/ui/components/Maps/FirePixel.js | 5 +- imports/ui/components/Maps/FirePolygonMark.js | 2 +- imports/ui/components/Maps/FullScreenMap.js | 34 +++--- .../ui/components/Maps/GoogleMutantLayer.js | 25 ++++ imports/ui/components/Maps/MapBridge.js | 22 ++++ imports/ui/components/Maps/MapControl.js | 32 +++++ .../ui/components/Maps/SubsUnion/SubsUnion.js | 7 +- .../components/SelectionMap/SelectionMap.js | 67 +++++------ imports/ui/pages/Fires/Fires.js | 46 ++++---- imports/ui/pages/FiresMap/FiresMap.js | 60 ++++++---- .../pages/Subscriptions/SubscriptionsMap.js | 38 +++--- package-lock.json | 111 ++++++------------ package.json | 9 +- 18 files changed, 276 insertions(+), 220 deletions(-) create mode 100644 imports/ui/components/Maps/GoogleMutantLayer.js create mode 100644 imports/ui/components/Maps/MapBridge.js create mode 100644 imports/ui/components/Maps/MapControl.js diff --git a/PENDIENTE.md b/PENDIENTE.md index 86ca6a5..8dc4a60 100644 --- a/PENDIENTE.md +++ b/PENDIENTE.md @@ -18,7 +18,7 @@ desarrollo (no en producción) y **bloquean el salto a React 19**. Estado: | react-bootstrap | 0.31 → 2 | ✅ hecho (CSS sigue en Bootstrap 4) | | reactstrap + 3 deps muertas | — | ✅ eliminadas (0 usos) | | react-router-dom | 4.2 → 6.30 | ✅ hecho (history 5, HOC `withRouterCompat`) | -| **react-leaflet + leaflet** | 1.8/1.3 → 4/1.9 | ⏸️ **diferida** (mapa = feature central; 4 plugins v1-only sin equivalente v4; ver UPGRADE.md) | +| react-leaflet + leaflet | 1.8/1.3 → 4.2/1.9 | ✅ hecho (Map→MapContainer, refs directas, useMap/useMapEvents; 4 plugins reimplementados: MapControl portal, GoogleMutantLayer, fullscreen, sleep/graphicscale vanilla; ver UPGRADE.md) | | **Bootstrap CSS/JS** | 4.1 → 5 | ⏸️ **diferida** (carrusel/navbar jQuery BS4; ver UPGRADE.md) | Lo verificado en navegador tras cada lib: `/`, `/fires`, `/fire/archive/`, @@ -28,9 +28,10 @@ parámetros por defecto; `UNSAFE_componentWillReceiveProps` (FromNow, FireStats, Fires, SelectionMap) → `getDerivedStateFromProps`/`componentDidUpdate`; Reconnect (banner Blaze `meteorStatus` con `findDOMNode`) → React nativo con `useTracker(Meteor.status)`; react-share 2→5 y react-progress-bar.js → progressbar.js -(ambos tiraban `findDOMNode`/`defaultProps`). **Tras esto, la ÚNICA fuente de -warnings en consola es react-leaflet + sus 4 plugins (diferida).** (Queda un -`findDOMNode` menor solo en `/status`: el `` de Status.js, página admin.) +(ambos tiraban `findDOMNode`/`defaultProps`); y react-leaflet 1.8→4.2 (mapa central, +4 plugins reimplementados). **Tras esto la consola tiene 0 warnings de React en las +rutas principales** (verificado forzando re-render en / y /fires). Único resto para +React 19: el `` de Status.js (`findDOMNode`), solo en `/status` (admin). --- diff --git a/UPGRADE.md b/UPGRADE.md index baa7e63..f71fdbb 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -308,7 +308,7 @@ MONGO_CONTAINER=tcef-mongo7 MONGO_SHELL=mongosh MONGO_PORT=27019 ./smoke/smoke.s | Dep | From | Target | Status | |---|---|---|---| -| React / react-dom | 16.0 | 18 | ✅ done — 18.3.1 + createRoot. The peripheral react-* libs that only worked on 18 via legacy context are now all modernized (rows below) **except react-leaflet** (deferred). Our own React-19 blockers are also cleared: `defaultProps` on function components → default params; `UNSAFE_componentWillReceiveProps` → `getDerivedStateFromProps`/`componentDidUpdate`; the Blaze/findDOMNode `Reconnect` → native React; react-share 2→5 and react-progress-bar.js → progressbar.js (both dropped their findDOMNode/defaultProps). **Sole remaining console warnings are react-leaflet + its 4 plugins** — nothing else. | +| React / react-dom | 16.0 | 18 | ✅ done — 18.3.1 + createRoot. The peripheral react-* libs that only worked on 18 via legacy context are now all modernized (rows below) **except react-leaflet** (deferred). Our own React-19 blockers are also cleared: `defaultProps` on function components → default params; `UNSAFE_componentWillReceiveProps` → `getDerivedStateFromProps`/`componentDidUpdate`; the Blaze/findDOMNode `Reconnect` → native React; react-share 2→5 and react-progress-bar.js → progressbar.js (both dropped their findDOMNode/defaultProps). react-leaflet 1.8→4.2 done too (row below). **Dev console is now 0 React warnings** on the main routes. The only React-19 leftover is the Blaze `serverFacts` on the /status admin page (findDOMNode via gadicc:blaze-react-component). | | react-share | 2.0 | 5.3 | ✅ done — v2 shipped function-component `defaultProps` (React-19 blocker). v5 is clean. Dropped the dead GooglePlus button (removed upstream in v4+); the other 6 share buttons are API-compatible. | | react-progress-bar.js | 0.2.3 | — (progressbar.js 1.1) | ✅ removed — the wrapper used the deprecated `findDOMNode`. `LoadingBar` now drives `progressbar.js` (its own underlying dep, promoted to direct) through a ref. | | gadicc:blaze-react-component (in Reconnect) | — | native React | ✅ Reconnect's Blaze `meteorStatus` bridge used `findDOMNode` and was always mounted → rewritten with `useTracker(Meteor.status)` + countdown effect, reusing 255kb:meteor-status's CSS. (Status.js's Blaze `serverFacts` kept — /status admin page only.) | @@ -320,7 +320,7 @@ MONGO_CONTAINER=tcef-mongo7 MONGO_SHELL=mongosh MONGO_PORT=27019 ./smoke/smoke.s | react-meteor-data | 0.2.16 | 3.0.6 | ✅ done — 0.2.16 (React-15/16-era) looped `withTracker` on fire-detail pages under React 18 (never-ready → blank). `meteor add react-meteor-data@3.0.6`; no call-site changes (HOC API kept). Also dropped `tmeasday:check-npm-versions` (the constraint pinning 0.2.16). See section above. | | i18next | 10.5 | 23.16 | ✅ done — with react-i18next 14. `whitelist`→`supportedLngs`, added `compatibilityJSON: 'v3'` (our locale JSON uses natural-language keys + v3 `_plural` layout, not the v4 `_one`/`_other` suffixes), custom separators `ß`/`ð`/`đ` kept. `sendMissing`→`saveMissing`, missingKeyHandler signature is `(lngs, ns, key, fallbackValue)` now (array first). See react-i18next row. | | react-i18next | 7.4 | 14.1 | ✅ done — 48 `translate([], {wait:true})`/`translate()` HOCs → `withTranslation()`; `react.wait:true`→`react.useSuspense:false`. `` unchanged (locale JSON already stores the indexed-tag format `<1><0>{{x}}`). ReSendEmail: removed `` (deleted in v10) → ``, dropped the removed bare `t` export. This is what silenced the per-component `Translate`/`I18n` legacy-context console spam. Backends: xhr→`i18next-http-backend` (client), sync-fs→`i18next-fs-backend` (server); `i18next-localstorage-cache` dropped (was `enabled:false`); languagedetector 2→8. Browser-verified: es/en switch, `` interpolation (`{{countTotal}}`+``) renders on /fires. | -| react-leaflet + leaflet | 1.8 / 1.3.1 | 4.x / 1.9 | ⏸️ **deferred as debt** (the one lib not modernized this pass). react-leaflet v4 is a ground-up hooks rewrite of the app's core feature (the fire map), and the migration is deeply entangled: (a) **4 unmaintained v1-only plugins with no v4 equivalent** — `react-leaflet-control` (custom map buttons, 3 maps), `react-leaflet-fullscreen`, `react-leaflet-google` (`GoogleLayer` base layers in DefMapLayers), `leaflet-sleep` (`sleep`/`wakeTime`… props on ``) — each needs a bespoke `createControlComponent`/`createLayerComponent`/googlemutant reimplementation; (b) the **controlled-viewport** pattern (`onViewportChanged` + `state.center/zoom` in FiresMap/SelectionMap) is gone in v4 (`MapContainer` center/zoom are initial-only; you drive it via a `useMap()` child); (c) `.leafletElement` refs in 6 files/13 sites (FiresMap, SelectionMap, SubscriptionsMap, Fires, SubsUnion) — v4 hands you the `L.Map`/layer directly with different timing; (d) `Leaflet.control.graphicScale().addTo(map)` + `subsUnion` reach into the raw map via the ref. **v1.8 works on React 18** (verified: all maps render), so this only blocks the eventual React 19 jump. Do it as its own focused project with heavy in-browser verification (drag markers, viewport-driven fire reload, fitBounds, layer switch, fullscreen, sleep, custom controls). Stays pinned at 1.8 until then. **After the rest of the front-end modernization + warning cleanup, react-leaflet + these 4 plugins are the SOLE remaining source of dev-console warnings** (legacy context on Map/LayersControl/TileLayer/Marker/CircleMarker/Circle/Tooltip and the plugins, plus `ReactDOM.render` from react-leaflet-control). | +| react-leaflet + leaflet | 1.8 / 1.3.1 | 4.2.1 / 1.9.4 | ✅ done — the last and hardest lib; a ground-up hooks rewrite of the app's core map. ``→``; `.leafletElement` refs (6 files/13 sites) → the map/layer instances directly (via a `` child using `useMap`, and plain refs on Marker/Circle/GeoJSON); the controlled-viewport pattern (`onViewportChanged`+`state.center/zoom`) → `` (`useMapEvents` moveend/zoomend) + imperative `setView`; `onClick`→`eventHandlers={{click}}`, path styling → `pathOptions`/`style`; `subsUnion` takes the `L.Map` directly. **The 4 v1-only plugins were reimplemented** with react-leaflet v4's factories / vanilla Leaflet plugins: `react-leaflet-control`→ in-repo `MapControl` (L.Control + createPortal), `react-leaflet-google`→ `GoogleMutantLayer` (`createLayerComponent` + `leaflet.gridlayer.googlemutant`; Google Maps API already loaded by Gkeys), `react-leaflet-fullscreen`→ `createControlComponent` + `leaflet.fullscreen`, `leaflet-sleep`/`leaflet-graphicscale` kept as vanilla (work on leaflet 1.9). Browser-verified: /fires (tiles, OSM+Google layer switch, custom control, fullscreen, graphic scale, pan/zoom→re-fetch pipeline, no NaN), fire detail (GeoJSON rect + fitBounds), home (all 3 maps coexist; SelectionMap draggable marker→updatePosition + distance circle). **Console now 0 warnings** across home/`/fires` (forced full re-render). REST smoke byte-identical. | | arkham:comments-ui | 1.4.x | — | ✅ replaced with in-repo React feature | | nimble:restivus | Atmosphere | vendored | ✅ local precompiled package, accounts-password 2.x | | maximum:server-transform, meteorhacks:zones, less, markdown, test stack | — | — | ✅ removed (dead/unused) | diff --git a/imports/ui/components/Maps/DefMapLayers.js b/imports/ui/components/Maps/DefMapLayers.js index 0355394..022f298 100644 --- a/imports/ui/components/Maps/DefMapLayers.js +++ b/imports/ui/components/Maps/DefMapLayers.js @@ -5,7 +5,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { withTranslation } from 'react-i18next'; -import { GoogleLayer } from 'react-leaflet-google/lib/'; +import GoogleMutantLayer from '/imports/ui/components/Maps/GoogleMutantLayer'; import Gkeys from '/imports/startup/client/Gkeys'; import { TileLayer, LayersControl } from 'react-leaflet'; @@ -54,15 +54,15 @@ class DefMapLayers extends Component { {/* React.Fragment does not work here */} { this.state.gkey && - + } { this.state.gkey && - + } { this.state.gkey && - + } ); diff --git a/imports/ui/components/Maps/FireCircleMark.js b/imports/ui/components/Maps/FireCircleMark.js index 80b9826..7d0d6e0 100644 --- a/imports/ui/components/Maps/FireCircleMark.js +++ b/imports/ui/components/Maps/FireCircleMark.js @@ -31,7 +31,7 @@ class FireCircleMark extends Component { } = this.props; const rect = rectangleAround({ lat, lon }, track, track); return ( - + ); diff --git a/imports/ui/components/Maps/FireIconMark.js b/imports/ui/components/Maps/FireIconMark.js index d63b1e7..034c1fe 100644 --- a/imports/ui/components/Maps/FireIconMark.js +++ b/imports/ui/components/Maps/FireIconMark.js @@ -33,9 +33,9 @@ class FireIconMark extends Component { lat, lon, scan, track, nasa, id, history, falsePositives, industries, neighbour, when, t } = this.props; return ( -
+ { !falsePositives && !industries && - + } { industries && @@ -43,23 +43,20 @@ class FireIconMark extends Component { } { /* disabled */ industries && false && } { falsePositives && !industries && - + {t('Es una industria (fuente: nuestros usuarios/as)')} { /* disabled because was a past fire (and can be marked multiple times) */ false && } } { !falsePositives && !industries && - + } -
+ ); } } diff --git a/imports/ui/components/Maps/FirePixel.js b/imports/ui/components/Maps/FirePixel.js index da78185..d09213a 100644 --- a/imports/ui/components/Maps/FirePixel.js +++ b/imports/ui/components/Maps/FirePixel.js @@ -12,11 +12,8 @@ const FirePixel = ({ }) => ( diff --git a/imports/ui/components/Maps/FirePolygonMark.js b/imports/ui/components/Maps/FirePolygonMark.js index 92e3640..1b76bde 100644 --- a/imports/ui/components/Maps/FirePolygonMark.js +++ b/imports/ui/components/Maps/FirePolygonMark.js @@ -29,7 +29,7 @@ class FirePolygonMark extends Component { const lon = centerid.coordinates[0]; const lat = centerid.coordinates[1]; return ( - + ); /* */ } diff --git a/imports/ui/components/Maps/FullScreenMap.js b/imports/ui/components/Maps/FullScreenMap.js index 8738d19..543fd2c 100644 --- a/imports/ui/components/Maps/FullScreenMap.js +++ b/imports/ui/components/Maps/FullScreenMap.js @@ -1,25 +1,27 @@ /* eslint-disable react/jsx-indent-props */ /* eslint-disable import/no-absolute-path */ -/* eslint-disable import/no-absolute-path */ -import React, { Component } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { withTranslation } from 'react-i18next'; -import FullscreenControl from 'react-leaflet-fullscreen'; -import 'react-leaflet-fullscreen/dist/styles.css'; +import { createControlComponent } from '@react-leaflet/core'; +import L from 'leaflet'; +import 'leaflet.fullscreen'; +import 'leaflet.fullscreen/Control.FullScreen.css'; -class FullScreenMap extends Component { - render() { - const { t } = this.props; - return ( - - ); - } -} +// Replacement for react-leaflet-fullscreen (v1-only): wrap the vanilla +// leaflet.fullscreen control with react-leaflet v4's control factory. +const FullscreenControl = createControlComponent( + ({ position = 'topleft', title, titleCancel }) => L.control.fullscreen({ position, title, titleCancel }) +); + +const FullScreenMap = ({ t }) => ( + +); FullScreenMap.propTypes = { t: PropTypes.func.isRequired diff --git a/imports/ui/components/Maps/GoogleMutantLayer.js b/imports/ui/components/Maps/GoogleMutantLayer.js new file mode 100644 index 0000000..d2b07e7 --- /dev/null +++ b/imports/ui/components/Maps/GoogleMutantLayer.js @@ -0,0 +1,25 @@ +import { createLayerComponent } from '@react-leaflet/core'; +import L from 'leaflet'; +import 'leaflet.gridlayer.googlemutant'; + +// Replacement for react-leaflet-google's GoogleLayer (v1-only). The Google Maps +// JS API is already loaded (with the places library) by Gkeys before any Google +// BaseLayer renders, so googlemutant can use window.google.maps directly. +function createGoogleMutant({ + maptype = 'roadmap', opacity, googlekey, ...options +}, ctx) { + const instance = L.gridLayer.googleMutant({ + type: String(maptype).toLowerCase(), + ...(opacity != null ? { opacity } : {}), + ...options + }); + return { instance, context: { ...ctx } }; +} + +function updateGoogleMutant(instance, props, prevProps) { + if (props.opacity != null && props.opacity !== prevProps.opacity) { + instance.setOpacity(props.opacity); + } +} + +export default createLayerComponent(createGoogleMutant, updateGoogleMutant); diff --git a/imports/ui/components/Maps/MapBridge.js b/imports/ui/components/Maps/MapBridge.js new file mode 100644 index 0000000..ea44ea2 --- /dev/null +++ b/imports/ui/components/Maps/MapBridge.js @@ -0,0 +1,22 @@ +import { useEffect } from 'react'; +import { useMap, useMapEvents } from 'react-leaflet'; + +// v4 replaces the v1 `ref`/`.leafletElement` + handleLeafletLoad pattern: this +// child runs inside and hands the ready Leaflet map to a class +// parent once, so the parent can drive it imperatively (setView, fitBounds, +// graphicScale, subsUnion, …). +export const MapReady = ({ onReady }) => { + const map = useMap(); + useEffect(() => { + if (onReady) onReady(map); + // run once per map instance + }, [map]); // eslint-disable-line react-hooks/exhaustive-deps + return null; +}; + +// Bridges Leaflet map events to callbacks (replaces the v1 onMoveend / +// onViewportChanged / onZoomend props on ). +export const MapEvents = ({ handlers }) => { + useMapEvents(handlers || {}); + return null; +}; diff --git a/imports/ui/components/Maps/MapControl.js b/imports/ui/components/Maps/MapControl.js new file mode 100644 index 0000000..ce69571 --- /dev/null +++ b/imports/ui/components/Maps/MapControl.js @@ -0,0 +1,32 @@ +import { useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { useMap } from 'react-leaflet'; +import L from 'leaflet'; + +// Replacement for react-leaflet-control (v1-only): mounts a Leaflet control at +// the given corner and portals arbitrary React children into it. Click/scroll +// on the control no longer pans/zooms the map underneath. +const MapControl = ({ position = 'topright', children }) => { + const map = useMap(); + const [container, setContainer] = useState(null); + + useEffect(() => { + const ReactControl = L.Control.extend({ + onAdd: () => { + const div = L.DomUtil.create('div', 'leaflet-control leaflet-control-react'); + L.DomEvent.disableClickPropagation(div); + L.DomEvent.disableScrollPropagation(div); + setContainer(div); + return div; + }, + onRemove: () => setContainer(null) + }); + const control = new ReactControl({ position }); + control.addTo(map); + return () => control.remove(); + }, [map, position]); + + return container ? createPortal(children, container) : null; +}; + +export default MapControl; diff --git a/imports/ui/components/Maps/SubsUnion/SubsUnion.js b/imports/ui/components/Maps/SubsUnion/SubsUnion.js index 41a4d81..c2f4216 100644 --- a/imports/ui/components/Maps/SubsUnion/SubsUnion.js +++ b/imports/ui/components/Maps/SubsUnion/SubsUnion.js @@ -12,7 +12,8 @@ const subsUnion = (union, options) => { const interactive = options.interactive || false; if (options.subs) { - const lmap = options.map.leafletElement; + // v4: options.map is the Leaflet map instance directly (no .leafletElement) + const lmap = options.map; if (union) { lmap.removeLayer(union); } @@ -30,7 +31,7 @@ const subsUnion = (union, options) => { if (options.fit && options.bounds) { // console.log(options.bounds); const bounds = JSON.parse(options.bounds); - options.map.leafletElement.fitBounds(L.latLngBounds(bounds._northEast, bounds._southWest)); + lmap.fitBounds(L.latLngBounds(bounds._northEast, bounds._southWest)); } } else if (options.subs.length > 0) { const result = calcUnion(L, options.subs, sub => sub, true); @@ -43,7 +44,7 @@ const subsUnion = (union, options) => { }); union.addTo(lmap); if (options.fit) { - options.map.leafletElement.fitBounds(bounds); + lmap.fitBounds(bounds); } } } diff --git a/imports/ui/components/SelectionMap/SelectionMap.js b/imports/ui/components/SelectionMap/SelectionMap.js index 9726a31..7be7a98 100644 --- a/imports/ui/components/SelectionMap/SelectionMap.js +++ b/imports/ui/components/SelectionMap/SelectionMap.js @@ -7,7 +7,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { Meteor } from 'meteor/meteor'; -import { Map, Marker, CircleMarker, Circle, Tooltip } from 'react-leaflet'; +import { MapContainer, Marker, CircleMarker, Circle, Tooltip } from 'react-leaflet'; import Leaflet from 'leaflet'; import { withTranslation } from 'react-i18next'; import { withTracker } from 'meteor/react-meteor-data'; @@ -18,7 +18,8 @@ import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers'; import 'leaflet-graphicscale/dist/Leaflet.GraphicScale.min.css'; import 'leaflet-graphicscale/dist/Leaflet.GraphicScale.min.js'; import 'leaflet-sleep/Leaflet.Sleep.js'; -import Control from 'react-leaflet-control'; +import MapControl from '/imports/ui/components/Maps/MapControl'; +import { MapReady, MapEvents } from '/imports/ui/components/Maps/MapBridge'; import { Row, Col, Button, ButtonGroup } from 'react-bootstrap'; import subsUnion from '/imports/ui/components/Maps/SubsUnion/SubsUnion'; import UserSubsToFiresCollection from '/imports/api/Subscriptions/Subscriptions'; @@ -51,13 +52,16 @@ class SelectionMap extends Component { this.fit = this.fit.bind(this); this.addScale = this.addScale.bind(this); this.onFstBtn = this.onFstBtn.bind(this); - this.onViewportChanged = this.onViewportChanged.bind(this); + this.onMapMove = this.onMapMove.bind(this); + this.handleMapReady = this.handleMapReady.bind(this); } - componentDidMount() { - if (this.isValidState()) { - this.addScale(); - } + // v4: MapReady hands us the ready Leaflet map (replaces the componentDidMount + // + ref/.leafletElement wiring) + handleMapReady(map) { + this.map = map; + if (this.isValidState()) this.addScale(); + this.handleLeafletLoad(map); } // Was UNSAFE_componentWillReceiveProps: pull center/distance from props into @@ -75,6 +79,8 @@ class SelectionMap extends Component { marker: center[0] ? center : this.state.marker, distance: distance || this.state.distance }); + // v4 map is uncontrolled: move it imperatively on a real center change + if (centerChanged && this.map) this.map.setView(center, this.state.zoom); } this.fit(); } @@ -90,7 +96,11 @@ class SelectionMap extends Component { this.props.onSndBtn(); } - onViewportChanged(viewport) { + // v4: fed by on moveend/zoomend (was the v1 onViewportChanged prop) + onMapMove() { + if (!this.map) return; + const c = this.map.getCenter(); + const viewport = { center: [c.lat, c.lng], zoom: this.map.getZoom() }; if (this.props.onViewportChanged) { this.props.onViewportChanged(viewport); } @@ -100,7 +110,7 @@ class SelectionMap extends Component { } getMap() { - return this.selectionMap.leafletElement; + return this.map; } toggleDraggable() { @@ -108,7 +118,7 @@ class SelectionMap extends Component { } updatePosition() { - const { lat, lng } = this.marker.leafletElement.getLatLng(); + const { lat, lng } = this.marker.getLatLng(); // console.log(`New marker lat ${lat} and lng ${lng}`); const currentDistance = this.state.distance; this.setState(update(this.state, { $merge: { marker: [lat, lng] } })); @@ -122,10 +132,10 @@ class SelectionMap extends Component { // console.log("fit!"); if (this.props.currentSubs.length > 0 && this.state.subsFit && this.props.action !== action.add) { // has autofit, do nothing - } else if (this.selectionMap && this.distanceCircle) { - if (!this.getMap().getBounds().contains(this.distanceCircle.leafletElement.getBounds())) { + } else if (this.map && this.distanceCircle) { + if (!this.getMap().getBounds().contains(this.distanceCircle.getBounds())) { // console.log('New area circle not visible'); - this.getMap().fitBounds(this.distanceCircle.leafletElement.getBounds()); // padding , [70, 70]); + this.getMap().fitBounds(this.distanceCircle.getBounds()); // padding , [70, 70]); } else { // console.log('New area circle visible'); } @@ -133,7 +143,7 @@ class SelectionMap extends Component { } addScale() { - if (this.selectionMap) { + if (this.map) { // https://www.npmjs.com/package/leaflet-graphicscale const map = this.getMap(); const options = { @@ -170,16 +180,10 @@ class SelectionMap extends Component { this.isValidState() ? - { - this.selectionMap = map; - this.handleLeafletLoad(map); - }} + + + {this.props.action === action.edit && this.props.currentSubs.map((subs, index) => ( @@ -198,7 +204,7 @@ class SelectionMap extends Component { position={[subs.location.lat, subs.location.lon]} icon={removeIcon} title={t('Pulsa para borrar')} - onClick={() => { onRemove(subs._id); }} + eventHandlers={{ click: () => { onRemove(subs._id); } }} > {index === 0 && { this.distanceCircle = ref; }} - color="#145A32" - fillColor="green" - fillOpacity={0.1} + pathOptions={{ color: '#145A32', fillColor: 'green', fillOpacity: 0.1 }} radius={this.state.distance * 1000} /> } - + { this.props.sndBtn && this.props.onSndBtn && - - {/* */} - - - -); + + ); +}; Navigation.propTypes = { t: PropTypes.func.isRequired, From f775d4ac26c88c8d154ff241d9ff43dc4053007a Mon Sep 17 00:00:00 2001 From: vjrj Date: Wed, 22 Jul 2026 00:13:10 +0200 Subject: [PATCH 04/10] bootstrap: home carousel jQuery plugin -> react-bootstrap Second (last) jQuery/BS4 JS blocker to the BS5 CSS swap. Replaces the `bootstrap-carousel-swipe` jQuery plugin + `$(...).carousel()` init with react-bootstrap's (native swipe, no jQuery) for both home carousels. - Preserves the progressive `.lazy` background mechanism (blur -> full image on slide-in): the old `slide.bs.carousel` handler becomes , which marks the incoming index in component state. - Keeps all per-slide class hooks (carousel-item-N, carousel-snd-item-N). - CSS ports in Index-custom.scss for react-bootstrap's BS5 markup: indicators render as -
+ + {Object.keys(FalsePositiveTypes).map(key => ( - + this.onTypeSelect(key)}> + {FalsePositiveTypes[key]} + )) } -
- + + } diff --git a/imports/ui/pages/Profile/Profile.js b/imports/ui/pages/Profile/Profile.js index e381bda..02faada 100644 --- a/imports/ui/pages/Profile/Profile.js +++ b/imports/ui/pages/Profile/Profile.js @@ -3,7 +3,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Row, Form, Button } from 'react-bootstrap'; +import { Row, Form, Button, Dropdown } from 'react-bootstrap'; import _ from 'lodash'; import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; @@ -200,24 +200,19 @@ class Profile extends React.Component { {this.t('Idioma')} -
- -
+ + {enabledLangs.map(lang => ( - + )) } -
-
+ +
{this.t('Puedes participar en las traducciones')} From bddfb392c14e93349b0a276ba13d37f3196e08ca Mon Sep 17 00:00:00 2001 From: vjrj Date: Wed, 22 Jul 2026 05:55:34 +0200 Subject: [PATCH 06/10] bootstrap: swap BS4 (alexwine) CSS for bootstrap@5 npm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the Bootstrap 4->5 migration now that every jQuery/BS4 widget is React (navbar, carousel, dropdowns) — plus the feedback toggle here (Feedback.js: global `$('#feedback-form').toggle()` -> React state). - Load Bootstrap 5 CSS from the `bootstrap` npm package in client/index.js (imported first so app + component styles and react-bootstrap override it). - Remove the `alexwine:bootstrap-4` meteor package (BS4 CSS + jQuery + BS4 JS). jQuery for jquery-validation still comes from the npm `jquery` dep. - Utility renames to BS5: ml-auto->ms-auto, float-right->float-end, btn-block->w-100, data-toggle->data-bs-toggle (FromNow tooltip). - forms.scss `.form-label` is no longer a shim (BS5 ships it); comment updated. Full-app build boots clean; server suite 36 passing. Needs a visual staging pass across all pages (BS4->5 shifts grid gutters/typography); forms should improve since react-bootstrap v2 already emitted BS5 markup. --- .meteor/packages | 5 ++- .meteor/versions | 1 - PENDIENTE.md | 40 ++++++++++--------- imports/startup/client/index.js | 4 ++ imports/ui/components/Feedback/Feedback.js | 9 +++-- imports/ui/components/FromNow/FromNow.js | 2 +- .../ui/components/Navigation/Navigation.js | 2 +- .../OAuthLoginButton/OAuthLoginButton.js | 2 +- imports/ui/pages/Signup/Signup.js | 2 +- imports/ui/stylesheets/forms.scss | 9 ++--- package-lock.json | 24 ++++++++--- package.json | 1 + 12 files changed, 63 insertions(+), 38 deletions(-) diff --git a/.meteor/packages b/.meteor/packages index ad5927c..b651f13 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -30,7 +30,6 @@ audit-argument-checks@1.0.8 ddp-rate-limiter@1.2.2 dynamic-import@0.7.4 static-html@1.4.0 -alexwine:bootstrap-4 gadicc:blaze-react-component # Add braze to react 255kb:meteor-status # Connect status @@ -45,6 +44,10 @@ ostrio:meteor-root aldeed:schema-deny dburles:collection-helpers reywood:publish-composite +# Bootstrap CSS/JS now comes from the `bootstrap` npm package (BS5), imported in +# imports/startup/client/index.js. The old alexwine:bootstrap-4 package (BS4 CSS + +# jQuery + BS4 JS) was removed after the navbar/carousel/dropdown/feedback widgets +# were migrated off jQuery. facts-base@1.0.2 nspangler:autoreconnect quave:synced-cron diff --git a/.meteor/versions b/.meteor/versions index 276b578..a70d819 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -8,7 +8,6 @@ accounts-password@3.0.3 aldeed:collection2@4.2.0 aldeed:schema-deny@4.0.2 aldeed:simple-schema@1.13.1 -alexwine:bootstrap-4@4.1.0 allow-deny@2.0.0 audit-argument-checks@1.0.8 autoupdate@2.0.0 diff --git a/PENDIENTE.md b/PENDIENTE.md index 7d5f445..144e6a7 100644 --- a/PENDIENTE.md +++ b/PENDIENTE.md @@ -65,25 +65,27 @@ React 19: el `` de Status.js (`findDOMNode`), solo en `/statu - ✅ **`meteor-accounts-t9n` actualizado** a `^2.6.0` (es/en OK). El paquete sigue sin build gallego (`gl`), así que se mantiene el fallback gl→es documentado en `i18n.js`. Traducción gallega de la app (`gl/common.json`): 0 claves faltantes. -- **Salto Bootstrap 4→5 (CSS/JS) pendiente:** react-bootstrap ya está en v2 y - corre sobre el CSS Bootstrap 4 actual (`alexwine:bootstrap-4`) con un único - shim `.form-label` en `forms.scss`. **Ya no quedan dependencias del JS jQuery de - BS4** — los dos blockers se migraron a React: - - ✅ **Collapse del navbar** (`Navigation.js` + `NavItem.js`): `data-toggle= - "collapse"` jQuery → estado React (`useState` que alterna `.show`). - *Verificar en staging el menú móvil (` de react-bootstrap con swipe nativo; se - conserva el mecanismo `.lazy` de fondos (blur→full) vía `onSlide`. Dep - `bootstrap-carousel-swipe` eliminada. Ports de CSS en `Index-custom.scss`: - indicadores `li`→`button` (markup BS5) y shim `.visually-hidden`. - *Verificar en staging el home: transiciones, swipe, indicadores, prev/next y - carga progresiva de las imágenes de fondo.* - - Falta (el salto de CSS en sí, ya sin bloqueos de JS): importar `bootstrap@5` - SCSS+JS, `ml-auto`→`ms-auto` (y demás utilidades direccionales), revisar - `new-age.scss`/`custom.scss`/`bootstrap-overrides.scss` y retirar - `alexwine:bootstrap-4`. +- ✅ **Salto Bootstrap 4→5 (CSS/JS) hecho** — react-bootstrap v2 ahora corre sobre + su CSS nativo (BS5). **Compila y arranca; falta SOLO verificación visual en + staging** (BS4→5 cambia sutilezas de grid/gutters/tipografía en todas las páginas). + - Widgets jQuery/BS4 migrados a React (los blockers): navbar collapse + (`Navigation.js`/`NavItem.js`), carrusel del home (`Index.js` → ``, + conserva `.lazy` vía `onSlide`), dropdowns de idioma/tipo (`Profile.js`/ + `Fires.js` → ``), y toggle del feedback (`Feedback.js` → estado React). + Deps `bootstrap-carousel-swipe` y `alexwine:bootstrap-4` eliminadas; jQuery + global ya no se usa (solo `jquery`+`jquery-validation` vía npm en `validate.js`). + - CSS: `bootstrap@5` npm importado en `imports/startup/client/index.js` (antes + que `app.scss`, para que los overrides ganen). Renombres de utilidades: + `ml-auto`→`ms-auto`, `float-right`→`float-end`, `btn-block`→`w-100`, + `data-toggle`→`data-bs-toggle`, `sr-only`→`.visually-hidden` (shim). + - **QA visual pendiente en staging (todas las páginas):** formularios (react- + bootstrap ya emite markup BS5 → deberían mejorar), navbar, botones, modales, + cards, grid/gutters, y los widgets migrados (menú móvil, carrusel, dropdowns, + feedback). Alertas de `themeteorchef:bert` (comprobar que siguen bien sin el + jQuery de alexwine). + - Menor: `popper.js@1` en `package.json` es legacy sin uso (react-bootstrap trae + `@popperjs/core@2`) → se puede quitar. El bloque `.form-label` de `forms.scss` + ya es redundante (BS5 lo trae) salvo por el `display:block` explícito. - **Limpieza menor:** migración 217 `// TODO remove falsepositives lowercase collection`; `prerender.js` gating comentado; sección per-fire del sitemap eliminada (era código muerto tras `firesMapEnabled=false`). diff --git a/imports/startup/client/index.js b/imports/startup/client/index.js index f5dd1e2..4b474be 100644 --- a/imports/startup/client/index.js +++ b/imports/startup/client/index.js @@ -1,4 +1,8 @@ /* global */ +// Bootstrap 5 CSS (npm) replaces the old `alexwine:bootstrap-4` meteor package. +// Import it first so the app + component stylesheets below (and react-bootstrap, +// which targets BS5) override it. +import 'bootstrap/dist/css/bootstrap.css'; import React from 'react'; import { createRoot } from 'react-dom/client'; import { HelmetProvider } from 'react-helmet-async'; diff --git a/imports/ui/components/Feedback/Feedback.js b/imports/ui/components/Feedback/Feedback.js index 8e47f71..d4da8e2 100644 --- a/imports/ui/components/Feedback/Feedback.js +++ b/imports/ui/components/Feedback/Feedback.js @@ -18,6 +18,7 @@ import './Feedback.scss'; class Feedback extends Component { constructor(props) { super(props); + this.state = { open: false }; this.handleSubmit = this.handleSubmit.bind(this); this.onTabClick = this.onTabClick.bind(this); } @@ -50,7 +51,9 @@ class Feedback extends Component { } onTabClick() { - $('#feedback-form').toggle('slide'); + // Was `$('#feedback-form').toggle('slide')` — global jQuery from the BS4 + // meteor package. React state instead, so nothing depends on that global. + this.setState(s => ({ open: !s.open })); } handleSubmit() { @@ -76,7 +79,7 @@ class Feedback extends Component {
{ !this.props.isHome &&
-
(this.formdiv = formdiv)} style={{ display: 'none' }} className="card"> +
(this.formdiv = formdiv)} style={{ display: this.state.open ? 'block' : 'none' }} className="card">
(this.form = form)} className="form card-body" @@ -109,7 +112,7 @@ class Feedback extends Component { /> - diff --git a/imports/ui/components/FromNow/FromNow.js b/imports/ui/components/FromNow/FromNow.js index 68afa23..dd385e2 100644 --- a/imports/ui/components/FromNow/FromNow.js +++ b/imports/ui/components/FromNow/FromNow.js @@ -33,7 +33,7 @@ class FromNow extends Component { render() { return ( - {this.state.when} + {this.state.when} ); } } diff --git a/imports/ui/components/Navigation/Navigation.js b/imports/ui/components/Navigation/Navigation.js index 599b92c..606f74a 100644 --- a/imports/ui/components/Navigation/Navigation.js +++ b/imports/ui/components/Navigation/Navigation.js @@ -37,7 +37,7 @@ const Navigation = ({ name = '', ...props }) => {