Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
9 changes: 7 additions & 2 deletions src/components/RedirectToLogin.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -11,11 +11,16 @@ interface RedirectToLoginProps {

const RedirectToLogin: React.FC<RedirectToLoginProps> = ({ 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;
};

Expand Down
2 changes: 1 addition & 1 deletion src/data/combineReducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<R[K]> } {
type keys = keyof typeof reducers;
type returnType = { [K in keys]: ReturnType<(typeof reducers)[K]> };
const combinedReducer = (state: any, action: any) => {
Expand Down
18 changes: 13 additions & 5 deletions src/data/dataApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ 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';

const HAS_LOGGED_IN = 'hasLoggedIn';
const HAS_SEEN_TUTORIAL = 'hasSeenTutorial';
const USERNAME = 'username';

export const getConfData = async () => {
export const getConfData = async (): Promise<
Pick<ConfState, 'schedule' | 'sessions' | 'locations' | 'speakers' | 'allTracks' | 'filteredTracks'>
> => {
const response = await Promise.all([fetch(dataUrl), fetch(locationsUrl)]);
const responseData = await response[0].json();
const schedule = responseData.schedule[0] as Schedule;
Expand All @@ -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 }),
Expand All @@ -51,18 +59,18 @@ export const getUserData = async () => {
return data;
};

export const setIsLoggedInData = async (isLoggedIn: boolean) => {
export const setIsLoggedInData = async (isLoggedIn: boolean): Promise<void> => {
await Storage.set({ key: HAS_LOGGED_IN, value: JSON.stringify(isLoggedIn) });
};

export const setHasSeenTutorialData = async (hasSeenTutorial: boolean) => {
export const setHasSeenTutorialData = async (hasSeenTutorial: boolean): Promise<void> => {
await Storage.set({
key: HAS_SEEN_TUTORIAL,
value: JSON.stringify(hasSeenTutorial),
});
};

export const setUsernameData = async (username?: string) => {
export const setUsernameData = async (username?: string): Promise<void> => {
if (!username) {
await Storage.remove({ key: USERNAME });
} else {
Expand Down
44 changes: 23 additions & 21 deletions src/data/locations/locations.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<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,
},
]),
);
}
};
4 changes: 2 additions & 2 deletions src/data/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
14 changes: 8 additions & 6 deletions src/data/sessions/sessions.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { getConfData } from '../dataApi';

import type { ConfState } from './conf.state';

export const loadConfData = () => async (dispatch: React.Dispatch<any>) => {
dispatch(setLoading(true));
const data = await getConfData();
dispatch(setData(data));
dispatch(setLoading(false));
};
export const loadConfData =
() =>
async (dispatch: React.Dispatch<any>): Promise<void> => {
dispatch(setLoading(true));
const data = await getConfData();
dispatch(setData(data));
dispatch(setLoading(false));
};

export const setLoading = (isLoading: boolean) =>
({
Expand Down
76 changes: 45 additions & 31 deletions src/data/user/user.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { getUserData, setIsLoggedInData, setUsernameData, setHasSeenTutorialData

import type { UserState } from './user.state';

export const loadUserData = () => async (dispatch: React.Dispatch<any>) => {
dispatch(setLoading(true));
const data = await getUserData();
dispatch(setData(data));
dispatch(setLoading(false));
};
export const loadUserData =
() =>
async (dispatch: React.Dispatch<any>): Promise<void> => {
dispatch(setLoading(true));
const data = await getUserData();
dispatch(setData(data));
dispatch(setLoading(false));
};

export const setLoading = (isLoading: boolean) =>
({
Expand All @@ -22,34 +24,46 @@ export const setData = (data: Partial<UserState>) =>
data,
}) as const;

export const logoutUser = () => async (dispatch: React.Dispatch<any>) => {
await setIsLoggedInData(false);
dispatch(setUsername());
};
export const logoutUser =
() =>
async (dispatch: React.Dispatch<any>): Promise<void> => {
await setIsLoggedInData(false);
dispatch(setUsername());
};

export const setIsLoggedIn = (loggedIn: boolean) => async (_dispatch: React.Dispatch<any>) => {
await setIsLoggedInData(loggedIn);
return {
type: 'set-is-loggedin',
loggedIn,
} as const;
};
export const setIsLoggedIn =
(loggedIn: boolean) =>
async (_dispatch: React.Dispatch<any>): 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<any>) => {
await setUsernameData(username);
return {
type: 'set-username',
username,
} as const;
};
export const setUsername =
(username?: string) =>
async (
_dispatch: React.Dispatch<any>,
): 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<any>) => {
await setHasSeenTutorialData(hasSeenTutorial);
return {
type: 'set-has-seen-tutorial',
hasSeenTutorial,
} as const;
};
export const setHasSeenTutorial =
(hasSeenTutorial: boolean) =>
async (
_dispatch: React.Dispatch<any>,
): 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) =>
({
Expand Down
60 changes: 31 additions & 29 deletions src/pages/MapView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,34 +33,7 @@ const MapView: React.FC<StateProps & DispatchProps> = ({ 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) {
Expand All @@ -70,10 +43,39 @@ const MapView: React.FC<StateProps & DispatchProps> = ({ 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;
}
Expand Down
4 changes: 2 additions & 2 deletions src/serviceWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
Loading