• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

code4recovery / tsml-ui / 15031744320

14 May 2025 09:45PM UTC coverage: 60.345% (-0.7%) from 61.057%
15031744320

Pull #447

github

web-flow
Merge dc162bc55 into 21f114f7e
Pull Request #447: Switch to OSM for maps

509 of 927 branches covered (54.91%)

Branch coverage included in aggregate %.

20 of 29 new or added lines in 4 files covered. (68.97%)

1 existing line in 1 file now uncovered.

646 of 987 relevant lines covered (65.45%)

9.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.55
/src/components/Map.tsx
1
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';
2

3
import { formatDirectionsUrl, useSettings } from '../helpers';
4
import { mapCss, mapPopupMeetingsCss } from '../styles';
5

6
import Button from './Button';
7
import Link from './Link';
8

9
import type { MapLocation, State } from '../types';
10

11
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
12
import L from 'leaflet';
13
import 'leaflet/dist/leaflet.css';
14

15
export default function Map({
16
  filteredSlugs,
17
  listMeetingsInPopup = true,
×
18
  setState,
19
  state,
20
}: {
21
  filteredSlugs: string[];
22
  listMeetingsInPopup: boolean;
23
  setState: Dispatch<SetStateAction<State>>;
24
  state: State;
25
}) {
26
  const [locations, setLocations] = useState<MapLocation[]>([]);
18✔
27
  const { settings } = useSettings();
18✔
28

29
  // reset locations when filteredSlugs changes
30
  useEffect(() => {
18✔
31
    const locations: { [index: string]: MapLocation } = {};
9✔
32
    filteredSlugs.forEach(slug => {
9✔
33
      const meeting = state.meetings[slug];
8✔
34

35
      if (meeting?.latitude && meeting?.longitude && meeting?.isInPerson) {
8✔
36
        const coords = meeting.latitude + ',' + meeting.longitude;
1✔
37

38
        // create a new pin
39
        if (!locations[coords]) {
1!
40
          locations[coords] = {
1✔
41
            directions_url: formatDirectionsUrl(meeting),
42
            formatted_address: meeting.formatted_address,
43
            key: coords,
44
            latitude: meeting.latitude,
45
            longitude: meeting.longitude,
46
            meetings: [],
47
            name: meeting.location,
48
          };
49
        }
50

51
        // add meeting to pin
52
        locations[coords].meetings.push(meeting);
1✔
53
      }
54
    });
55

56
    // quick reference array (sort so southern pins appear in front)
57
    setLocations(
9✔
NEW
58
      Object.values(locations).sort((a, b) => a.latitude - b.latitude)
×
59
    );
60
  }, [filteredSlugs]);
61

62
  return (
18✔
63
    <div aria-hidden={true} css={mapCss}>
64
      {!!locations.length && (
19✔
65
        <MapContainer
66
          style={{ height: '100%', width: '100%' }}
67
          zoomControl={!('ontouchstart' in window || !!window.TouchEvent)}
2✔
68
        >
69
          <TileLayer {...settings.map.tiles} />
70
          <Markers
71
            listMeetingsInPopup={listMeetingsInPopup}
72
            locations={locations}
73
            state={state}
74
            setState={setState}
75
          />
76
        </MapContainer>
77
      )}
78
    </div>
79
  );
80
}
81

82
const Markers = ({
2✔
83
  listMeetingsInPopup,
84
  locations,
85
  state,
86
  setState,
87
}: {
88
  listMeetingsInPopup: boolean;
89
  locations: MapLocation[];
90
  setState: Dispatch<SetStateAction<State>>;
91
  state: State;
92
}) => {
93
  const map = useMap();
1✔
94
  const { settings, strings } = useSettings();
1✔
95
  const markerRef = useRef<L.Marker>(null);
1✔
96
  const markerIcon = L.divIcon({
1✔
97
    className: 'tsml-ui-marker',
98
    html: settings.map.markers.location.html,
99
    iconAnchor: [
100
      settings.map.markers.location.width / 2,
101
      settings.map.markers.location.height / 2,
102
    ],
103
    iconSize: new L.Point(
104
      settings.map.markers.location.width,
105
      settings.map.markers.location.height
106
    ),
107
  });
108

109
  useEffect(() => {
1✔
110
    if (locations.length === 1) {
1!
111
      map.setView([locations[0].latitude, locations[0].longitude], 16);
1✔
112
      markerRef.current?.openPopup();
1✔
113
    } else {
NEW
114
      const latitudes = locations.map(({ latitude }) => latitude);
×
NEW
115
      const longitudes = locations.map(({ longitude }) => longitude);
×
NEW
116
      map.fitBounds(
×
117
        [
118
          [Math.max(...latitudes), Math.min(...longitudes)],
119
          [Math.min(...latitudes), Math.max(...longitudes)],
120
        ],
121
        { padding: [10, 10] }
122
      );
123
    }
124
  }, [locations]);
125

126
  return locations.map(location => (
1✔
127
    <Marker
1✔
128
      key={location.key}
129
      position={[location.latitude, location.longitude]}
130
      ref={locations.length === 1 ? markerRef : null}
1!
131
      icon={markerIcon}
132
    >
133
      <Popup>
134
        <h2>{location.name}</h2>
135
        <p className="notranslate">{location.formatted_address}</p>
136
        {listMeetingsInPopup && (
2✔
137
          <div css={mapPopupMeetingsCss}>
138
            {location.meetings
NEW
139
              .sort((a, b) => (a.start && b.start && a.start > b.start ? 1 : 0))
×
140
              .map((meeting, index) => (
141
                <div key={index}>
1✔
142
                  <time>
143
                    {meeting.start?.toFormat('t')}
144
                    <span>{meeting.start?.toFormat('cccc')}</span>
145
                  </time>
146
                  <Link meeting={meeting} setState={setState} state={state} />
147
                </div>
148
              ))}
149
          </div>
150
        )}
151
        {location.directions_url && (
2✔
152
          <Button
153
            href={location.directions_url}
154
            icon="geo"
155
            text={strings.get_directions}
156
            type="in-person"
157
          />
158
        )}
159
      </Popup>
160
    </Marker>
161
  ));
162
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc