diff --git a/eslint.config.mjs b/eslint.config.mjs index 05bc2bd..2a6f8dc 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -17,8 +17,6 @@ export default defineConfig([ }, }, rules: { - // Ionic's base level; `recommended` makes it an error, which is meant for libraries. - '@typescript-eslint/explicit-module-boundary-types': 'warn', // Every page exports both the component and a `connect()`-wrapped default, // which fast refresh can't follow. Revisit if the custom connect HOC in // data/connect.tsx is replaced with hooks. diff --git a/package.json b/package.json index 162e2b8..7fcf8d9 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "preview": "vite preview", "lint": "npm run eslint && npm run prettier -- --check", "lint:fix": "npm run eslint -- --fix && npm run prettier -- --write", - "eslint": "eslint", + "eslint": "eslint --max-warnings 0", "prettier": "prettier \"**/*.{ts,tsx}\"" }, "browserslist": [ diff --git a/src/components/RedirectToLogin.tsx b/src/components/RedirectToLogin.tsx index 68d8624..d97c66e 100644 --- a/src/components/RedirectToLogin.tsx +++ b/src/components/RedirectToLogin.tsx @@ -1,6 +1,6 @@ import { IonRouterContext } from '@ionic/react'; import type React from 'react'; -import { useEffect, useContext } from 'react'; +import { useEffect, useContext, useRef } from 'react'; import type { setIsLoggedIn, setUsername } from '../data/user/user.actions'; @@ -11,11 +11,16 @@ interface RedirectToLoginProps { const RedirectToLogin: React.FC = ({ setIsLoggedIn, setUsername }) => { const ionRouterContext = useContext(IonRouterContext); + const hasRedirected = useRef(false); useEffect(() => { + if (hasRedirected.current) { + return; + } + hasRedirected.current = true; setIsLoggedIn(false); setUsername(undefined); ionRouterContext.push('/tabs/schedule'); - }, [setIsLoggedIn, setUsername]); + }, [ionRouterContext, setIsLoggedIn, setUsername]); return null; }; diff --git a/src/data/combineReducers.ts b/src/data/combineReducers.ts index 44769b0..eb4ee6e 100644 --- a/src/data/combineReducers.ts +++ b/src/data/combineReducers.ts @@ -2,7 +2,7 @@ interface R { [key: string]: (...args: any) => any; } -export function combineReducers(reducers: R) { +export function combineReducers(reducers: R): (state: any, action: any) => { [K in keyof R]: ReturnType } { type keys = keyof typeof reducers; type returnType = { [K in keys]: ReturnType<(typeof reducers)[K]> }; const combinedReducer = (state: any, action: any) => { diff --git a/src/data/dataApi.ts b/src/data/dataApi.ts index 6ceeb4c..a38c823 100644 --- a/src/data/dataApi.ts +++ b/src/data/dataApi.ts @@ -4,6 +4,8 @@ import type { Location } from '../models/Location'; import type { Schedule, Session } from '../models/Schedule'; import type { Speaker } from '../models/Speaker'; +import type { ConfState } from './sessions/conf.state'; + const dataUrl = '/assets/data/data.json'; const locationsUrl = '/assets/data/locations.json'; @@ -11,7 +13,9 @@ const HAS_LOGGED_IN = 'hasLoggedIn'; const HAS_SEEN_TUTORIAL = 'hasSeenTutorial'; const USERNAME = 'username'; -export const getConfData = async () => { +export const getConfData = async (): Promise< + Pick +> => { const response = await Promise.all([fetch(dataUrl), fetch(locationsUrl)]); const responseData = await response[0].json(); const schedule = responseData.schedule[0] as Schedule; @@ -34,7 +38,11 @@ export const getConfData = async () => { return data; }; -export const getUserData = async () => { +export const getUserData = async (): Promise<{ + isLoggedin: boolean; + hasSeenTutorial: boolean; + username: string | undefined; +}> => { const response = await Promise.all([ Storage.get({ key: HAS_LOGGED_IN }), Storage.get({ key: HAS_SEEN_TUTORIAL }), @@ -51,18 +59,18 @@ export const getUserData = async () => { return data; }; -export const setIsLoggedInData = async (isLoggedIn: boolean) => { +export const setIsLoggedInData = async (isLoggedIn: boolean): Promise => { await Storage.set({ key: HAS_LOGGED_IN, value: JSON.stringify(isLoggedIn) }); }; -export const setHasSeenTutorialData = async (hasSeenTutorial: boolean) => { +export const setHasSeenTutorialData = async (hasSeenTutorial: boolean): Promise => { await Storage.set({ key: HAS_SEEN_TUTORIAL, value: JSON.stringify(hasSeenTutorial), }); }; -export const setUsernameData = async (username?: string) => { +export const setUsernameData = async (username?: string): Promise => { if (!username) { await Storage.remove({ key: USERNAME }); } else { diff --git a/src/data/locations/locations.actions.ts b/src/data/locations/locations.actions.ts index 81b0212..77eb75c 100644 --- a/src/data/locations/locations.actions.ts +++ b/src/data/locations/locations.actions.ts @@ -8,24 +8,26 @@ export const setLocations = (locations: Location[]): DispatchObject => ({ payload: locations, }); -export const loadLocations = () => async (dispatch: (action: DispatchObject) => void) => { - try { - const response = await fetch('/assets/data/locations.json'); - const locations = await response.json(); - dispatch(setLocations(locations)); - } catch (error) { - console.error('Error loading locations:', error); - // Set default locations if data cannot be loaded - dispatch( - setLocations([ - { - id: 1, - name: 'Monona Terrace Convention Center', - lat: 43.071584, - lng: -89.38012, - center: true, - }, - ]), - ); - } -}; +export const loadLocations = + () => + async (dispatch: (action: DispatchObject) => void): Promise => { + try { + const response = await fetch('/assets/data/locations.json'); + const locations = await response.json(); + dispatch(setLocations(locations)); + } catch (error) { + console.error('Error loading locations:', error); + // Set default locations if data cannot be loaded + dispatch( + setLocations([ + { + id: 1, + name: 'Monona Terrace Convention Center', + lat: 43.071584, + lng: -89.38012, + center: true, + }, + ]), + ); + } + }; diff --git a/src/data/selectors.ts b/src/data/selectors.ts index 903209d..9c8bbfd 100644 --- a/src/data/selectors.ts +++ b/src/data/selectors.ts @@ -9,7 +9,7 @@ import type { AppState } from './state'; const getSchedule = (state: AppState) => { return state.data.schedule; }; -export const getSpeakers = (state: AppState) => state.data.speakers; +export const getSpeakers = (state: AppState): Speaker[] => state.data.speakers; const getSessions = (state: AppState) => state.data.sessions; const getFilteredTracks = (state: AppState) => state.data.filteredTracks; const getFavoriteIds = (state: AppState) => state.data.favorites; @@ -141,7 +141,7 @@ export const getSpeakerSessions = createSelector(getSessions, (sessions) => { return speakerSessions; }); -export const mapCenter = (state: AppState) => { +export const mapCenter = (state: AppState): Location => { const item = state.data.locations.find((l: Location) => l.id === state.data.mapCenterId); if (item == null) { return { diff --git a/src/data/sessions/sessions.actions.ts b/src/data/sessions/sessions.actions.ts index ccba0c3..03cde7f 100644 --- a/src/data/sessions/sessions.actions.ts +++ b/src/data/sessions/sessions.actions.ts @@ -3,12 +3,14 @@ import { getConfData } from '../dataApi'; import type { ConfState } from './conf.state'; -export const loadConfData = () => async (dispatch: React.Dispatch) => { - dispatch(setLoading(true)); - const data = await getConfData(); - dispatch(setData(data)); - dispatch(setLoading(false)); -}; +export const loadConfData = + () => + async (dispatch: React.Dispatch): Promise => { + dispatch(setLoading(true)); + const data = await getConfData(); + dispatch(setData(data)); + dispatch(setLoading(false)); + }; export const setLoading = (isLoading: boolean) => ({ diff --git a/src/data/user/user.actions.ts b/src/data/user/user.actions.ts index 5101a18..3267ebd 100644 --- a/src/data/user/user.actions.ts +++ b/src/data/user/user.actions.ts @@ -3,12 +3,14 @@ import { getUserData, setIsLoggedInData, setUsernameData, setHasSeenTutorialData import type { UserState } from './user.state'; -export const loadUserData = () => async (dispatch: React.Dispatch) => { - dispatch(setLoading(true)); - const data = await getUserData(); - dispatch(setData(data)); - dispatch(setLoading(false)); -}; +export const loadUserData = + () => + async (dispatch: React.Dispatch): Promise => { + dispatch(setLoading(true)); + const data = await getUserData(); + dispatch(setData(data)); + dispatch(setLoading(false)); + }; export const setLoading = (isLoading: boolean) => ({ @@ -22,34 +24,46 @@ export const setData = (data: Partial) => data, }) as const; -export const logoutUser = () => async (dispatch: React.Dispatch) => { - await setIsLoggedInData(false); - dispatch(setUsername()); -}; +export const logoutUser = + () => + async (dispatch: React.Dispatch): Promise => { + await setIsLoggedInData(false); + dispatch(setUsername()); + }; -export const setIsLoggedIn = (loggedIn: boolean) => async (_dispatch: React.Dispatch) => { - await setIsLoggedInData(loggedIn); - return { - type: 'set-is-loggedin', - loggedIn, - } as const; -}; +export const setIsLoggedIn = + (loggedIn: boolean) => + async (_dispatch: React.Dispatch): Promise<{ readonly type: 'set-is-loggedin'; readonly loggedIn: boolean }> => { + await setIsLoggedInData(loggedIn); + return { + type: 'set-is-loggedin', + loggedIn, + } as const; + }; -export const setUsername = (username?: string) => async (_dispatch: React.Dispatch) => { - await setUsernameData(username); - return { - type: 'set-username', - username, - } as const; -}; +export const setUsername = + (username?: string) => + async ( + _dispatch: React.Dispatch, + ): Promise<{ readonly type: 'set-username'; readonly username: string | undefined }> => { + await setUsernameData(username); + return { + type: 'set-username', + username, + } as const; + }; -export const setHasSeenTutorial = (hasSeenTutorial: boolean) => async (_dispatch: React.Dispatch) => { - await setHasSeenTutorialData(hasSeenTutorial); - return { - type: 'set-has-seen-tutorial', - hasSeenTutorial, - } as const; -}; +export const setHasSeenTutorial = + (hasSeenTutorial: boolean) => + async ( + _dispatch: React.Dispatch, + ): Promise<{ readonly type: 'set-has-seen-tutorial'; readonly hasSeenTutorial: boolean }> => { + await setHasSeenTutorialData(hasSeenTutorial); + return { + type: 'set-has-seen-tutorial', + hasSeenTutorial, + } as const; + }; export const setDarkMode = (darkMode: boolean) => ({ diff --git a/src/pages/MapView.tsx b/src/pages/MapView.tsx index f0c2c90..294ccae 100644 --- a/src/pages/MapView.tsx +++ b/src/pages/MapView.tsx @@ -33,34 +33,7 @@ const MapView: React.FC = ({ locations, loadLocation // Add useEffect to load locations when component mounts useEffect(() => { loadLocations(); - }, []); - - const initMap = () => { - if (!locations?.length || !mapCanvas.current || map.current) return; - - map.current = L.map(mapCanvas.current, { - zoomControl: true, - attributionControl: true, - }); - - // Get the center location (first item marked as center, or first item if none marked) - const centerLocation = locations.find((loc) => loc.center) || locations[0]; - map.current.setView([centerLocation.lat, centerLocation.lng], 16); - - // Add tile layer - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors', - }).addTo(map.current); - - // Add markers for all locations - locations.forEach((location: Location) => { - const marker = L.marker([location.lat, location.lng]).addTo(map.current!).bindPopup(`${location.name}`); - markers.current.push(marker); - }); - - // Show map - mapCanvas.current.classList.add('show-map'); - }; + }, [loadLocations]); const resizeMap = () => { if (map.current) { @@ -70,10 +43,39 @@ const MapView: React.FC = ({ locations, loadLocation // Initialize map useEffect(() => { + const currentMarkers = markers.current; + + const initMap = () => { + if (!locations?.length || !mapCanvas.current || map.current) return; + + map.current = L.map(mapCanvas.current, { + zoomControl: true, + attributionControl: true, + }); + + // Get the center location (first item marked as center, or first item if none marked) + const centerLocation = locations.find((loc) => loc.center) || locations[0]; + map.current.setView([centerLocation.lat, centerLocation.lng], 16); + + // Add tile layer + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', + }).addTo(map.current); + + // Add markers for all locations + locations.forEach((location: Location) => { + const marker = L.marker([location.lat, location.lng]).addTo(map.current!).bindPopup(`${location.name}`); + markers.current.push(marker); + }); + + // Show map + mapCanvas.current.classList.add('show-map'); + }; + initMap(); return () => { if (map.current) { - markers.current.forEach((marker) => marker.remove()); + currentMarkers.forEach((marker) => marker.remove()); map.current.remove(); map.current = null; } diff --git a/src/serviceWorker.ts b/src/serviceWorker.ts index 515519a..042bc07 100644 --- a/src/serviceWorker.ts +++ b/src/serviceWorker.ts @@ -23,7 +23,7 @@ type Config = { onUpdate?: (registration: ServiceWorkerRegistration) => void; }; -export function register(config?: Config) { +export function register(config?: Config): void { if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { // The URL constructor is available in all browsers that support SW. const publicUrl = new URL((process as { env: { [key: string]: string } }).env.PUBLIC_URL, window.location.href); @@ -124,7 +124,7 @@ function checkValidServiceWorker(swUrl: string, config?: Config) { }); } -export function unregister() { +export function unregister(): void { if ('serviceWorker' in navigator) { navigator.serviceWorker.ready.then((registration) => { registration.unregister();