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

code4recovery / tsml-ui / 18451009991

12 Oct 2025 11:29PM UTC coverage: 43.532% (-19.9%) from 63.458%
18451009991

Pull #475

github

web-flow
Merge 9d0374e51 into 0a0ddf96f
Pull Request #475: pretty permalinks

369 of 1009 branches covered (36.57%)

Branch coverage included in aggregate %.

15 of 37 new or added lines in 5 files covered. (40.54%)

236 existing lines in 17 files now uncovered.

553 of 1109 relevant lines covered (49.86%)

4.31 hits per line

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

3.85
/src/hooks/data.tsx
1
import {
2
  createContext,
3
  PropsWithChildren,
4
  useContext,
5
  useEffect,
6
  useState,
7
} from 'react';
8

9
import {
10
  getDistance,
11
  isGoogleSheetData,
12
  loadMeetingData,
13
  translateGoogleSheet,
14
} from '../helpers';
15
import { Index, JSONData, Meeting } from '../types';
16
import { useError } from './error';
17
import { useInput } from './input';
18
import { useSettings } from './settings';
19

20
export type Data = {
21
  capabilities: {
22
    coordinates: boolean;
23
    distance: boolean;
24
    geolocation: boolean;
25
    inactive: boolean;
26
    location: boolean;
27
    region: boolean;
28
    sharing: boolean;
29
    time: boolean;
30
    type: boolean;
31
    weekday: boolean;
32
  };
33
  indexes: {
34
    distance: Index[];
35
    region: Index[];
36
    time: Index[];
37
    type: Index[];
38
    weekday: Index[];
39
  };
40
  meetings: { [index: string]: Meeting };
41
  slugs: string[];
42
  waitingForData: boolean;
43
};
44

45
const defaultData: Data = {
8✔
46
  capabilities: {
47
    coordinates: false,
48
    distance: false,
49
    geolocation: false,
50
    inactive: false,
51
    location: false,
52
    region: false,
53
    sharing: false,
54
    time: false,
55
    type: false,
56
    weekday: false,
57
  },
58
  indexes: {
59
    distance: [],
60
    region: [],
61
    time: [],
62
    type: [],
63
    weekday: [],
64
  },
65
  meetings: {},
66
  slugs: [],
67
  waitingForData: true,
68
};
69

70
const DataContext = createContext<Data>(defaultData);
8✔
71

72
export const useData = () => useContext(DataContext);
8✔
73

74
export const DataProvider = ({
8✔
75
  children,
76
  google,
77
  src,
78
  timezone,
79
}: PropsWithChildren<{ google?: string; src?: string; timezone?: string }>) => {
UNCOV
80
  const [data, setData] = useState<Data>(defaultData);
×
UNCOV
81
  const { setError } = useError();
×
UNCOV
82
  const { input, latitude, longitude, setBounds } = useInput();
×
UNCOV
83
  const { settings, strings } = useSettings();
×
84

UNCOV
85
  useEffect(() => {
×
UNCOV
86
    if (timezone) {
×
87
      try {
×
88
        // check if timezone is valid
89
        Intl.DateTimeFormat(undefined, { timeZone: timezone });
×
90
      } catch (e) {
91
        throw new Error(
×
92
          `Timezone ${timezone} is not valid. Please use one like America/New_York.`
93
        );
94
      }
95
    }
96

UNCOV
97
    const sources = src?.split(',').filter(Boolean) || [];
×
98

UNCOV
99
    if (!sources.length) {
×
100
      throw new Error('a data source must be specified');
×
101
    }
102

UNCOV
103
    const sheets: (string | undefined)[] = [];
×
104

UNCOV
105
    Promise.all(
×
106
      sources.map(src => {
UNCOV
107
        const sheetId = src.startsWith(
×
108
          'https://docs.google.com/spreadsheets/d/'
109
        )
110
          ? src.split('/')[5]
111
          : undefined;
UNCOV
112
        sheets.push(sheetId);
×
113

114
        // google sheet
UNCOV
115
        if (sheetId) {
×
116
          if (!google) {
×
117
            throw new Error('a Google API key is required');
×
118
          }
119
          src = `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/A1:ZZ?key=${google}`;
×
120
        }
121

122
        // cache busting
UNCOV
123
        if (src.endsWith('.json') && input.meeting) {
×
124
          src = `${src}?${new Date().getTime()}`;
×
125
        }
126

UNCOV
127
        return fetch(src);
×
128
      })
129
    )
130
      .then(responses =>
UNCOV
131
        Promise.all(
×
132
          responses.map(res =>
UNCOV
133
            res.ok ? res.json() : Promise.reject(res.status)
×
134
          )
135
        )
136
      )
137
      .then((responses: JSONData[][]) => {
UNCOV
138
        const json = responses
×
139
          .map((json, index) => {
UNCOV
140
            const sheetId = sheets[index];
×
UNCOV
141
            return isGoogleSheetData(json) && sheetId
×
142
              ? translateGoogleSheet(json, sheetId, settings)
143
              : json;
144
          })
145
          .flat();
146

UNCOV
147
        if (!Array.isArray(json) || !json.length) {
×
148
          throw new Error('data is not in the correct format');
×
149
        }
150

151
        const { bounds, capabilities, indexes, meetings, slugs } =
UNCOV
152
          loadMeetingData(json, data.capabilities, settings, strings, timezone);
×
153

UNCOV
154
        if (!timezone && !slugs.length) {
×
155
          throw new Error('time zone is not set');
×
156
        }
157

UNCOV
158
        if (bounds) {
×
159
          setBounds(bounds);
×
160
        }
161

UNCOV
162
        setData({
×
163
          capabilities,
164
          indexes,
165
          meetings,
166
          slugs,
167
          waitingForData: false,
168
        });
169
      })
170
      .catch(error => {
171
        setError(`Loading error: ${error}`);
×
172
        setData(prevData => ({ ...prevData, waitingForData: false }));
×
173
      });
174
  }, []);
175

176
  // calculate distance if coordinates are available
UNCOV
177
  useEffect(() => {
×
UNCOV
178
    if (!latitude || !longitude || !data.meetings || data.waitingForData) {
×
UNCOV
179
      return;
×
180
    }
181

182
    const distances = Object.fromEntries(
×
183
      settings.distance_options.map(option => [option, []])
×
184
    );
185

186
    Object.keys(data.meetings).forEach(slug => {
×
187
      const meeting = data.meetings[slug];
×
188
      if (meeting.latitude && meeting.longitude) {
×
189
        meeting.distance = getDistance(
×
190
          { latitude, longitude },
191
          meeting,
192
          settings
193
        );
194
      }
195

196
      for (const option of settings.distance_options) {
×
197
        if (meeting.distance && meeting.distance <= option) {
×
198
          (distances[option] as string[]).push(meeting.slug);
×
199
        }
200
      }
201

202
      data.meetings[slug] = meeting;
×
203
    });
204

205
    const distance: Index[] = Object.entries(distances).map(([key, slugs]) => ({
×
206
      key,
207
      name: `${key} ${settings.distance_unit}`,
208
      slugs,
209
    }));
210

211
    setData(prevData => ({
×
212
      ...prevData,
213
      capabilities: {
214
        ...prevData.capabilities,
215
        distance: true,
216
      },
217
      indexes: {
218
        ...prevData.indexes,
219
        distance,
220
      },
221
      meetings: data.meetings,
222
    }));
223
  }, [latitude, longitude, data.meetings, settings.distance_unit]);
224

UNCOV
225
  return <DataContext.Provider value={data}>{children}</DataContext.Provider>;
×
226
};
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