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

code4recovery / tsml-ui / 19269363735

11 Nov 2025 02:50PM UTC coverage: 44.044% (+0.4%) from 43.641%
19269363735

Pull #487

github

web-flow
Merge 1d5088de3 into d06d75f3c
Pull Request #487: WIP: vite

418 of 1100 branches covered (38.0%)

Branch coverage included in aggregate %.

573 of 1150 relevant lines covered (49.83%)

4.26 hits per line

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

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

3
import L from 'leaflet';
4
import 'leaflet/dist/leaflet.css';
5
import { MapContainer, Marker, Popup, TileLayer, useMap } from 'react-leaflet';
6

7
import { formatDirectionsUrl } from '../helpers';
8
import { useData, useError, useFilter, useInput, useLocation, useSettings } from '../hooks';
9
import { mapCss, mapPopupMeetingsCss } from '../styles';
10
import type { MapLocation } from '../types';
11
import Button from './Button';
12
import Link from './Link';
13

14
export default function Map() {
15
  const { error } = useError();
10✔
16
  const [locations, setLocations] = useState<MapLocation[]>([]);
10✔
17
  const { settings } = useSettings();
10✔
18
  const [darkMode, setDarkMode] = useState(
10✔
19
    window.matchMedia('(prefers-color-scheme: dark)').matches
20
  );
21
  const { meetings } = useData();
10✔
22
  const { filteredSlugs, meeting } = useFilter();
10✔
23
  const { input } = useInput();
10✔
24
  const { latitude, longitude } = useLocation();
10✔
25

26
  useEffect(() => {
10✔
27
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
5✔
28
    const handleChange = ({ matches }: MediaQueryListEvent) => {
5✔
29
      setDarkMode(matches);
×
30
    };
31
    mediaQuery.addEventListener('change', handleChange);
5✔
32
    return () => {
5✔
33
      mediaQuery.removeEventListener('change', handleChange);
×
34
    };
35
  }, []);
36

37
  // reset locations when filteredSlugs changes
38
  useEffect(() => {
10✔
39
    const locations: { [index: string]: MapLocation } = {};
5✔
40

41
    (meeting?.slug ? [meeting.slug] : filteredSlugs)?.forEach(slug => {
5✔
42
      const meeting = meetings[slug];
3✔
43

44
      if (meeting?.latitude && meeting?.longitude && meeting?.isInPerson) {
3!
45
        const coords = meeting.latitude + ',' + meeting.longitude;
×
46

47
        // create a new pin
48
        if (!locations[coords]) {
×
49
          locations[coords] = {
×
50
            directions_url: formatDirectionsUrl(meeting),
51
            formatted_address: meeting.formatted_address,
52
            key: coords,
53
            latitude: meeting.latitude,
54
            longitude: meeting.longitude,
55
            meetings: [],
56
            name: meeting.location,
57
          };
58
        }
59

60
        // add meeting to pin
61
        locations[coords].meetings.push(meeting);
×
62
      }
63
    });
64

65
    // quick reference array (sort so southern pins appear in front)
66
    setLocations(
5✔
67
      Object.values(locations).sort((a, b) => a.latitude - b.latitude)
×
68
    );
69
  }, [filteredSlugs]);
70

71
  if (error) {
10!
72
    return null;
×
73
  }
74

75
  return (
10✔
76
    <div aria-hidden={true} css={mapCss}>
77
      {!!locations.length && (
10!
78
        <MapContainer
79
          style={{ height: '100%', width: '100%', position: 'absolute' }}
80
          zoomControl={!('ontouchstart' in window || !!window.TouchEvent)}
×
81
        >
82
          <TileLayer
83
            {...(settings.map.tiles_dark && darkMode
×
84
              ? settings.map.tiles_dark
85
              : settings.map.tiles)}
86
          />
87
          <Markers locations={locations} />
88
          {latitude && longitude && input.mode === 'location' && (
×
89
            <Marker
90
              icon={mapMarkerIcon(settings.map.markers.geocode)}
91
              position={[latitude, longitude]}
92
            />
93
          )}
94
          {latitude && longitude && input.mode === 'me' && (
×
95
            <Marker
96
              icon={mapMarkerIcon(settings.map.markers.geolocation)}
97
              position={[latitude, longitude]}
98
            />
99
          )}
100
        </MapContainer>
101
      )}
102
    </div>
103
  );
104
}
105

106
const Markers = ({ locations }: { locations: MapLocation[] }) => {
2✔
107
  const map = useMap();
×
108
  const { meeting } = useFilter();
×
109
  const { settings, strings } = useSettings();
×
110
  const markerRef = useRef<L.Marker>(null);
×
111
  const markerIcon = mapMarkerIcon(settings.map.markers.location);
×
112

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

130
  return locations.map(location => (
×
131
    <Marker
×
132
      key={location.key}
133
      position={[location.latitude, location.longitude]}
134
      ref={locations.length === 1 ? markerRef : null}
×
135
      icon={markerIcon}
136
    >
137
      <Popup>
138
        <h2>{location.name}</h2>
139
        <p className="notranslate">{location.formatted_address}</p>
140
        {!meeting && (
×
141
          <div css={mapPopupMeetingsCss}>
142
            {location.meetings
143
              .sort((a, b) => (a.start && b.start && a.start > b.start ? 1 : 0))
×
144
              .map((meeting, index) => (
145
                <div key={index}>
×
146
                  <time>
147
                    {meeting.start?.toFormat('t')}
148
                    <span>{meeting.start?.toFormat('cccc')}</span>
149
                  </time>
150
                  <Link meeting={meeting} />
151
                </div>
152
              ))}
153
          </div>
154
        )}
155
        {location.directions_url && (
×
156
          <Button
157
            href={location.directions_url}
158
            icon="geo"
159
            text={strings.get_directions}
160
            type="in-person"
161
          />
162
        )}
163
      </Popup>
164
    </Marker>
165
  ));
166
};
167

168
const mapMarkerIcon = ({ height, html, width }: MapMarker) =>
2✔
169
  L.divIcon({
×
170
    className: 'tsml-ui-marker',
171
    html,
172
    iconAnchor: [width / 2, height / 2],
173
    iconSize: new L.Point(width, height),
174
  });
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