diff --git a/README.md b/README.md index a089a59d..24917810 100644 --- a/README.md +++ b/README.md @@ -5,30 +5,38 @@ Citizen Services Capstone Project Team B (Wayfinder) ## Project Information [![Lifecycle:Experimental](https://img.shields.io/badge/Lifecycle-Experimental-339999)]() ## Project Description -The "Wayfinder" application is a mobile application that directs citizens and employees to government services. The Wayfinder proof of concept provides an extensible platform that allows new services and locations to be added as they become available. Another use case for the Wayfinder app is the ability to process application generated analytics data allowing the product team to analyze valuable usage data that will inform the creation of new services that can be delivered using the Wayfinder app. +The "Wayfinder" application is a Progressive Web Application that directs citizens to government services. The Wayfinder proof-of-concept provides an extensible platform that allows new services and locations to be added as they become available. Another use case for the Wayfinder app is the ability to process application generated analytics data allowing the product team to analyze valuable usage data that will inform the creation of new services that can be delivered using the Wayfinder app. ## Introduction -This repository is a mono-repo containing all relevant documentation, code, and infrastructure that is required for the Wayfinder application and container(s). -There will be additional supplementary README's for the back-end and front-end. +This repository is a mono-repo containing all relevant documentation, code, and infrastructure that is required for the Wayfinder application. ## Project Status -Project is currently under development via the Capstone team. +Project was completed by the Capstone Team. + +### Frontend: +* [Node.js](https://nodejs.org/en) +* [Vite](https://vitejs.dev/) +* [React](https://react.dev/) +* [TypeScript](https://devdocs.io/typescript/) +* [emotion.js](https://emotion.sh/docs/introduction) +* [leaflet.js](https://leafletjs.com/) + [react-leaflet](https://react-leaflet.js.org/) +* [Cypress](https://www.cypress.io/) + +### Backend: +* [Node.js](https://nodejs.org/en) +* [Express.js](https://expressjs.com/) +* [TypeScript](https://devdocs.io/typescript/) +* [Mongoose](https://mongoosejs.com/docs/) +* [Jest](https://jestjs.io/) + +### Database: +* [MongoDB](https://www.mongodb.com/docs/) + +### Data Visualization: +* [PowerBI](https://learn.microsoft.com/en-us/power-bi/) -## Stack - -### Front-End -* React PWA - * Typescript - * Node - -### Back-End -* Express -* MongoDB -* Typescript -* Node -* Swagger ### Mongo Dev Key Examples diff --git a/src/frontend-pwa/package.json b/src/frontend-pwa/package.json index dd8a4c83..adaec78f 100644 --- a/src/frontend-pwa/package.json +++ b/src/frontend-pwa/package.json @@ -1,7 +1,7 @@ { "name": "testing-vite", "private": true, - "version": "0.9.0", + "version": "1.0.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src/frontend-pwa/src/components/common/Button/Button.tsx b/src/frontend-pwa/src/components/common/Button/Button.tsx index 25bd4f50..c3f419f4 100644 --- a/src/frontend-pwa/src/components/common/Button/Button.tsx +++ b/src/frontend-pwa/src/components/common/Button/Button.tsx @@ -12,7 +12,7 @@ import StyledButton from './button.styles'; -export type ButtonVariants = 'default' | 'primary' | 'secondary'; +export type ButtonVariants = 'default' | 'primary' | 'secondary' | 'tertiary'; export type ButtonSizes = 'sm' | 'md' | 'lg'; export type ButtonProps = { diff --git a/src/frontend-pwa/src/components/common/Button/button.styles.ts b/src/frontend-pwa/src/components/common/Button/button.styles.ts index 61de53ab..1e5b27e4 100644 --- a/src/frontend-pwa/src/components/common/Button/button.styles.ts +++ b/src/frontend-pwa/src/components/common/Button/button.styles.ts @@ -25,7 +25,8 @@ const StyledButton = styled.button` font-weight: 500; letter-spacing: 1px; cursor: pointer; - background-color: ${(props) => (props.variant === 'primary' ? '#003366' : props.variant === 'secondary' ? '#FFFFFF' : '#000000')}; + background-color: ${(props) => (props.variant === 'primary' ? '#003366' : props.variant === 'secondary' ? '#DC3545' : '#000000')}; + background-color: ${(props) => (props.variant === 'primary' ? '#003366' : props.variant === 'secondary' ? '#DC3545' : props.variant === 'tertiary' ? '#198754' : '#000000')}; color: #FFFFFF; &:hover { transform: scale(0.98); diff --git a/src/frontend-pwa/src/components/utility/Mapping/Mapping.tsx b/src/frontend-pwa/src/components/utility/Mapping/Mapping.tsx index 7bda5e00..d11c6d68 100644 --- a/src/frontend-pwa/src/components/utility/Mapping/Mapping.tsx +++ b/src/frontend-pwa/src/components/utility/Mapping/Mapping.tsx @@ -34,6 +34,7 @@ import { StyledPopup, StyledMapContainer, PopupInfo, + MapTilesNotFoundDiv, } from './mapping.styles'; import SingleLocation from '../../../Type/SingleLocation'; import LocationsArray from '../../../Type/LocationsArray'; @@ -86,6 +87,16 @@ export default function Mapping({ locations, currentLocation }: MappingProps) { ? 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' : '/mapTiles/{z}/{x}/{y}.png'; + if (!onlineMode && !state.mapsCached) { + return ( + +

+ {mappingContent.offlineMapTilesNotFoundMessage[lang]} +

+
+ ); + } + return ( { !isNaN(lat) diff --git a/src/frontend-pwa/src/components/utility/Mapping/mapping.styles.ts b/src/frontend-pwa/src/components/utility/Mapping/mapping.styles.ts index d3cc6db7..333cf084 100644 --- a/src/frontend-pwa/src/components/utility/Mapping/mapping.styles.ts +++ b/src/frontend-pwa/src/components/utility/Mapping/mapping.styles.ts @@ -44,3 +44,10 @@ export const StyledPopupDiv = styled.div` max-height: 10em; overflow-y: auto; `; + +export const MapTilesNotFoundDiv = styled.div` + color: darkred; + align-items: center; + justify-content: center; + padding: 3em; +`; diff --git a/src/frontend-pwa/src/constants/Constants.ts b/src/frontend-pwa/src/constants/Constants.ts index b9289e93..6693004d 100644 --- a/src/frontend-pwa/src/constants/Constants.ts +++ b/src/frontend-pwa/src/constants/Constants.ts @@ -9,6 +9,7 @@ const constants = { REPORTS_KEY: 'reports', UNSENT_REPORTS_KEY: 'unsentReports', APP_INSTALL_KEY: 'appInstalled', + MAPS_CACHED_KEY: 'mapsCached', UPDATE_ARRAY_KEY: 'updateArr', }; diff --git a/src/frontend-pwa/src/content/changelogContent.ts b/src/frontend-pwa/src/content/changelogContent.ts index 3c18531a..b7fd65b7 100644 --- a/src/frontend-pwa/src/content/changelogContent.ts +++ b/src/frontend-pwa/src/content/changelogContent.ts @@ -7,6 +7,16 @@ export interface ContentMap { } const ChangeLog: ContentMap = { + '1.0.0': { + eng: [ + 'Added ability to delete and recache map tiles at will', + 'Changed the secondary button color to a softer shade of red', + ], + fr: [ + 'Ajout de la possibilité de supprimer et de remettre en cache des tuiles de carte à volonté', + 'Modification de la couleur du bouton secondaire en une nuance de rouge plus douce', + ], + }, '0.9.0': { eng: [ 'Settings view revamped to new modern design', diff --git a/src/frontend-pwa/src/content/content.ts b/src/frontend-pwa/src/content/content.ts index d54c3692..ce164b4f 100644 --- a/src/frontend-pwa/src/content/content.ts +++ b/src/frontend-pwa/src/content/content.ts @@ -68,6 +68,62 @@ export const SettingsContent: ContentMap = { eng: 'Pull in new location data from the server', fr: 'Récupérer de nouvelles données de localisation à partir du serveur', }, + refreshDataButtonTextRefresh: { + eng: 'Refresh', + fr: 'Rafraîchir', + }, + refreshDataButtonTextOffline: { + eng: 'Offline', + fr: 'Hors ligne', + }, + refreshDataTextConfirm: { + eng: 'App data updated', + fr: 'Données d\'application mises à jour', + }, + refreshDataModalButton: { + eng: 'OK', + fr: 'D\'ACCORD', + }, + offlineMapTilesTitle: { + eng: 'Offline Map Tiles', + fr: 'Tuiles de carte hors ligne', + }, + offlineMapTilesToolTip: { + eng: 'To decrease app size, you can delete the saved tiles. This will prevent offline map functionality.', + fr: 'Pour réduire la taille de l\'application, vous pouvez supprimer les vignettes enregistrées. Cela empêchera la fonctionnalité de carte hors ligne.', + }, + installMapTilesButtonTextOffline: { + eng: 'Offline', + fr: 'Hors ligne', + }, + installMapTilesButtonTextOnline: { + eng: 'Download', + fr: 'Télécharger', + }, + installMapTilesModalWarning: { + eng: 'The size of the mapTile package is ~35MB. do you wish to proceed with the download?', + fr: 'La taille du package mapTile est d\'environ 35 Mo. Souhaitez-vous poursuivre le téléchargement ?', + }, + clearCache: { + eng: 'Delete Data', + fr: 'Suprimmer les données', + }, + clearCacheToolTip: { + eng: 'Deletes the cached map data.', + fr: 'Supprime les données de site mises en cache.', + }, + clearCacheConfirmText: { + eng: 'Are you sure you want to clear the cache data? Offline map will be unavailable.', + fr: 'Voulez-vous vraiment effacer les données du cache ? La carte hors ligne ne sera pas disponible.', + }, + clearCacheButtonConfirm: { + eng: 'Confirm', + fr: 'Confirmer', + }, + clearCacheButtonCancel: { + eng: 'Cancel', + fr: 'Annuler', + }, languages: { eng: [ 'English', @@ -512,6 +568,10 @@ export const mappingContent: ContentMap = { eng: 'Phone Number: ', fr: 'Numéro de téléphone: ', }, + offlineMapTilesNotFoundMessage: { + eng: 'The map data was not found and the device appears to be offline. The offline map is unable to be rendered.', + fr: 'Les données cartographiques sont introuvables et l\'appareil semble être hors ligne. La carte hors ligne ne peut pas être rendue.', + }, }; export const reportHistoryContent: ContentMap = { diff --git a/src/frontend-pwa/src/services/app/AppActions.ts b/src/frontend-pwa/src/services/app/AppActions.ts index 71243c1d..949420f3 100644 --- a/src/frontend-pwa/src/services/app/AppActions.ts +++ b/src/frontend-pwa/src/services/app/AppActions.ts @@ -8,6 +8,7 @@ enum AppActionType { SET_REPORTS = 'SET_REPORTS', SET_TOOL_TIP_TEXT = 'SET_TOOL_TIP_TEXT', SET_ONLINE = 'SET_ONLINE', + SET_MAP_CACHED = 'SET_MAP_CACHED', } export default AppActionType; diff --git a/src/frontend-pwa/src/services/app/AppReducer.ts b/src/frontend-pwa/src/services/app/AppReducer.ts index b21340fa..920f7948 100644 --- a/src/frontend-pwa/src/services/app/AppReducer.ts +++ b/src/frontend-pwa/src/services/app/AppReducer.ts @@ -9,6 +9,7 @@ const { SET_REPORTS, SET_TOOL_TIP_TEXT, SET_ONLINE, + SET_MAP_CACHED, } = AppActionType; export type AppAction = { @@ -26,6 +27,7 @@ export const initialState = { reports: {}, toolTipText: '', isOnline: false, + mapsCached: true, }; /** @@ -53,6 +55,8 @@ export const reducer = (state: object, action: AppAction): object => { return { ...state, toolTipText: action.payload }; case SET_ONLINE: return { ...state, isOnline: action.payload }; + case SET_MAP_CACHED: + return { ...state, mapsCached: action.payload }; default: throw new Error(); } diff --git a/src/frontend-pwa/src/services/app/useAppService.ts b/src/frontend-pwa/src/services/app/useAppService.ts index bd431553..2250bc3f 100644 --- a/src/frontend-pwa/src/services/app/useAppService.ts +++ b/src/frontend-pwa/src/services/app/useAppService.ts @@ -20,6 +20,7 @@ const { SET_REPORTS, SET_TOOL_TIP_TEXT, SET_ONLINE, + SET_MAP_CACHED, } = AppActionType; /** @@ -288,6 +289,17 @@ const useAppService = () => { }); }; + /** + * @summary Sets the state for determining whether the map tiles are present or not + * @param cached is a boolean that indicates whether the app has cached map tiles or not + * @type {( cached: boolean )} + * @author Dallas Richmond + */ + const setMapsCache = (cached: boolean) => { + saveDataToLocalStorage(constants.MAPS_CACHED_KEY, cached); + dispatch({ type: SET_MAP_CACHED, payload: cached }); + }; + return { setAppData, setCurrentLocation, @@ -302,6 +314,7 @@ const useAppService = () => { setOfflineReports, setOnline, setAppInstall, + setMapsCache, state, }; }, [state, dispatch]); diff --git a/src/frontend-pwa/src/views/Eula/Eula.tsx b/src/frontend-pwa/src/views/Eula/Eula.tsx index 8dfcb4a0..b9b54736 100644 --- a/src/frontend-pwa/src/views/Eula/Eula.tsx +++ b/src/frontend-pwa/src/views/Eula/Eula.tsx @@ -16,7 +16,12 @@ import OnlineCheck from '../../utils/OnlineCheck'; export default function Eula() { const [termAgreement, setTermAgreement] = useState(false); - const { state, setEulaState, setAnalytics } = useAppService(); + const { + state, + setEulaState, + setAnalytics, + setMapsCache, + } = useAppService(); const { lang } = state.settings; const geolocationKnown = localStorageKeyExists(constants.CURRENT_LOCATION_KEY); const latitude = state.currentLocation ? state.currentLocation.lat : 49.2827; @@ -27,6 +32,7 @@ export default function Eula() { }; const handleButtonClick = () => { + setMapsCache(true); setEulaState(); if (geolocationKnown) { const analytics = { diff --git a/src/frontend-pwa/src/views/Settings/Settings.tsx b/src/frontend-pwa/src/views/Settings/Settings.tsx index 0c057ed8..450c28d7 100644 --- a/src/frontend-pwa/src/views/Settings/Settings.tsx +++ b/src/frontend-pwa/src/views/Settings/Settings.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ /* eslint-disable react/jsx-indent */ /* eslint-disable max-len */ /** @@ -21,6 +22,10 @@ import { SliderWrapper, SettingsContainer, ContentContainer, + ModalWrapper, + ModalPopup, + AccordionButtonDiv, + ModalBackground, } from './settings.styles'; import useAppService from '../../services/app/useAppService'; import MoreInfoButton from '../../components/common/MoreInfoButton/MoreInfoButton'; @@ -36,12 +41,16 @@ export default function Settings() { setSettings, updateSettings, setAnalytics, + setMapsCache, state, } = useAppService(); const [locationRangeValue, setLocationRangeValue] = useState(state.settings.location_range); const [offlineToggleValue, setOfflineToggleValue] = useState(state.settings.offline_mode); const [analyticsToggleValue, setAnalyticsToggleValue] = useState(state.settings.analytics_opt_in); const [lang, setLang] = useState(state.settings.lang || 'eng'); + const [isClearCacheModalOpen, setIsClearCacheModalOpen] = useState(false); + const [isInstallMapTilesModalOpen, setIsInstallMapTilesModalOpen] = useState(false); + const [refreshDataComplete, setRefreshDataComplete] = useState(false); const onlineCheck = state.isOnline && !state.settings.offline_mode; const geolocationKnown = localStorageKeyExists(constants.CURRENT_LOCATION_KEY); const latitude = state.currentLocation ? state.currentLocation.lat : 49.2827; @@ -164,12 +173,11 @@ export default function Settings() { }; /** - * @summary Pulls in new app data if user hit the refresh button + * @summary Refreshes app data, builds analytics for usage of "refresh data button" * @author Dallas Richmond */ const handleRefresh = () => { setAppData(onlineCheck); - if (state.settings.analytics_opt_in && geolocationKnown) { const analytics = { latitude, @@ -183,28 +191,118 @@ export default function Settings() { }; sendAnalytics(analytics); } + setRefreshDataComplete(true); + }; + + /** + * @summary Opens the map tile confirmation modal. + * @author Tyler Maloney + */ + const handleInstallMapTilesConfirm = () => { + setIsInstallMapTilesModalOpen(true); + }; + + /** + * @summary Pulls in new map tile data if user hits the refresh button. + * Forces the page to unregister the service-worker and reload + * the window. This forces an updated service-worker to initialize + * and download, triggering assets to be downloaded anew. + * + * + * @author Dallas Richmond, Tyler Maloney + */ + const handleInstallMapTilesModalConfirm = () => { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.getRegistrations().then((registrations) => { + registrations.forEach((registration) => { + registration.unregister(); + }); + }); + navigator.serviceWorker.register('/sw.js') + .then(() => { + setMapsCache(true); + window.location.reload(); + }) + .catch((error) => { + console.error('Error registering service worker:', error); + }); + } + setIsInstallMapTilesModalOpen(false); + }; + + /** + * @summary Closes the map tile confirmation modal. + * @author Tyler Maloney + */ + const handleInstallMapTilesModalCancel = () => { + setIsInstallMapTilesModalOpen(false); + }; + + /** + * @summary Show the map tile confirmation modal to the user. + * @author Tyler Maloney + */ + const handleClearCacheConfirm = () => { + setIsClearCacheModalOpen(true); + }; + + /** + * @summary Clears all mapTile data from browser cache, + * sets the mapsCached state to false, + * and closes the confirmation modal. + * + * @author Tyler Maloney, Dallas Richmond + */ + const handleCacheModalConfirm = () => { + if ('serviceWorker' in navigator) { + // get all cache keys + caches.keys() + .then((cacheNames) => { + // find cache matching workbox precache + const precacheName = cacheNames.filter((cacheName) => cacheName.startsWith('workbox-precache-v2')); + caches.open(precacheName[0]) + .then((cache) => { + // get the keys for all assets in cache, check assets, if asset includes /mapTiles it is deleted + cache.keys() + .then((keys) => { + keys.forEach((key) => { + if (key.url.includes('/mapTiles')) { + cache.delete(key.url); + } + }); + }); + }); + }); + } + setMapsCache(false); + setIsClearCacheModalOpen(false); + }; + + /** + * @summary Closes all modals. + * @author Tyler Maloney + */ + const handleCacheModalCancel = () => { + setIsClearCacheModalOpen(false); + setRefreshDataComplete(false); }; return ( -
- {SettingsContent.settingsTitle[lang]} -
+
{SettingsContent.settingsTitle[lang]}
{SettingsContent.languages[lang].map((data: string, index: number) => ( - + ))} )} text={SettingsContent.language[lang]} - tooltip={( - - )} + tooltip={} /> )} text={SettingsContent.locationRange[lang]} - tooltip={( - - )} + tooltip={} handleClick={handleLocationRangeAnalytics} />
{SettingsContent.offlineMode[lang]} - + {SettingsContent.analytics[lang]} - + +
+ +
)} - text={(SettingsContent.refreshData[lang])} - tooltip={( - + text={SettingsContent.refreshData[lang]} + tooltip={} + /> + + + {state.mapsCached ? ( +