From 735dd7a954d611f29840a73d2e6d1b0c91754b1e Mon Sep 17 00:00:00 2001 From: Matthew Grove Date: Sat, 1 Oct 2022 20:39:51 +0100 Subject: [PATCH] Add basic front end --- firestore.rules | 4 +- src/App.js | 177 +++++++++++------- src/FirstAiderDashboard.js | 56 ++++++ src/PatientDashboard.js | 7 + src/SetupDashboard.js | 77 ++++++++ ...tPanicEvents.jsx => currentPanicEvents.js} | 0 src/index.js | 19 +- src/service-worker.js | 72 +++++++ src/serviceWorkerRegistration.js | 139 ++++++++++++++ 9 files changed, 478 insertions(+), 73 deletions(-) create mode 100644 src/FirstAiderDashboard.js create mode 100644 src/PatientDashboard.js create mode 100644 src/SetupDashboard.js rename src/{currentPanicEvents.jsx => currentPanicEvents.js} (100%) create mode 100644 src/service-worker.js create mode 100644 src/serviceWorkerRegistration.js diff --git a/firestore.rules b/firestore.rules index b32a7c5..f294088 100644 --- a/firestore.rules +++ b/firestore.rules @@ -61,6 +61,7 @@ service cloud.firestore { } allow update: if isFirstAiderPresent(); + allow read: if true; match /linkedFirstAiders/{firstAider} { function getPossibleFields() { @@ -87,8 +88,5 @@ service cloud.firestore { allow read: if request.auth.uid == patient; allow update: if request.auth.uid == patient && verifyUpdateFields(getPossibleUpdateFields()); } - match /{document=**} { - allow read, write: if false; - } } } \ No newline at end of file diff --git a/src/App.js b/src/App.js index c5808ca..7b4e756 100644 --- a/src/App.js +++ b/src/App.js @@ -1,10 +1,12 @@ import logo from './logo.svg'; import './App.css'; +import React, { useState } from 'react'; import { initializeApp } from 'firebase/app'; import { getFirestore, collection, getDocs, connectFirestoreEmulator } from 'firebase/firestore'; -import { getAuth, onAuthStateChanged, createUserWithEmailAndPassword, signInWithEmailAndPassword, signInWithRedirect, getRedirectResult, connectAuthEmulator } from "firebase/auth"; +import { createUserWithEmailAndPassword, signInWithEmailAndPassword, signInWithRedirect, getRedirectResult, getAuth, onAuthStateChanged, connectAuthEmulator, signOut } from "firebase/auth"; import { getAnalytics } from "firebase/analytics"; -import { useState } from 'react'; +import FirstAiderDashboard from './FirstAiderDashboard'; +import PatientDashboard from "./PatientDashboard"; const firebaseConfig = { apiKey: "AIzaSyCQ3FcfwlAckhZNEVz3RmGGGuscW1jWDHY", @@ -23,36 +25,59 @@ const analytics = getAnalytics(app); const db = getFirestore(app); const auth = getAuth(app); - // emulators -connectFirestoreEmulator(db, 'localhost', 8080); -connectAuthEmulator(auth, "http://localhost:9099"); +// connectFirestoreEmulator(db, 'localhost', 8080); +// connectAuthEmulator(auth, "http://localhost:9099"); +class App extends React.Component { + constructor(props) { + super(props); + + this.state = { + user: null, + email: "", + password: "", + db: db, + }; + } + componentDidMount() { + + onAuthStateChanged(auth, (userCredential) => { + console.log("state changing...") + if (userCredential) { + // User is signed in, see docs for a list of available properties + // https://firebase.google.com/docs/reference/js/firebase.User + this.setState({ + user: userCredential, + }); + console.log("signed in"); + console.log(userCredential); + console.log(this.state.user); + // console.log(this.state.user.uid === null); + // ... + } else { + // User is signed out + // ... + this.setState({ + user: null, + }); + console.log("signed out"); + console.log(this.state.user); + } + }); + } -// onAuthStateChanged(auth, (user) => { -// if (user) { -// // User is signed in, see docs for a list of available properties -// // https://firebase.google.com/docs/reference/js/firebase.User -// const uid = user.uid; -// // ... -// } else { -// // User is signed out -// // ... -// }}); - - - - - -function App() { - const signIn = (event) => { + signIn = (event) => { event.preventDefault(); - signInWithEmailAndPassword(auth, email, password) + signInWithEmailAndPassword(auth, this.state.email, this.state.password) .then((userCredential) => { // Signed in - const user = userCredential.user; + this.setState({ + user: userCredential.user + }); alert("logged in!"); + console.log(this.props.user); // ... }) .catch((error) => { @@ -61,12 +86,14 @@ function App() { }); }; - const signUp = (event) => { + signUp = (event) => { event.preventDefault(); - createUserWithEmailAndPassword(auth, email, password) + createUserWithEmailAndPassword(auth, this.state.email, this.state.password) .then((userCredential) => { // Signed in - const user = userCredential.user; + this.setState({ + user: userCredential.user + }); alert("logged in!"); // ... }) @@ -77,48 +104,60 @@ function App() { // .. }); }; - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - return ( -
-
- logo -

Tiny Town Community First Response

-
-
-
- setEmail(e.target.value)} - value={email} - /> - setPassword(e.target.value)} - value={password} - /> - -
-
- setEmail(e.target.value)} - value={email} - /> - setPassword(e.target.value)} - value={password} - /> - -
-
-
- ); + + render() { + return ( +
+
+ logo +

Tiny Town Community First Response

+
+
+ { + (typeof this.state.user !== "undefined" && this.state.user !== null) ? + + : + // user.role === "firstAider" + // ? + // : + // + <> +
+ this.setState({email: e.target.value})} + value={this.state.email} + /> + this.setState({password: e.target.value})} + value={this.state.password} + /> + +
+
+ this.setState({email: e.target.value})} + value={this.state.email} + /> + this.setState({password: e.target.value})} + value={this.state.password} + /> + +
+ + } +
+
+ ); + } } export default App; diff --git a/src/FirstAiderDashboard.js b/src/FirstAiderDashboard.js new file mode 100644 index 0000000..9d0ef04 --- /dev/null +++ b/src/FirstAiderDashboard.js @@ -0,0 +1,56 @@ +import React from 'react' +import { collection, query, where, orderBy, getDocs } from "firebase/firestore"; + +export default class FirstAiderDashboard extends React.Component { + constructor(props) { + super(props); + + this.state = { + user: props.user, + events: [], + db: props.db, + }; + } + + async componentDidMount() { + let eventsTemp = []; + // const querySnapshot = await getDocs(query(collection(this.state.db, "panic-events"), where("resolved", "==", false), orderBy("timestamp", "desc"))); + const q = query(collection(this.state.db, "panic-events"), where("resolved", "==", false)); + const querySnapshot = await getDocs(q); + querySnapshot.forEach((doc) => { + eventsTemp.push({ + ...doc.data(), + id: doc.id, + }); + }); + this.setState({ + events: eventsTemp, + }); + } + + render() { + return ( +
+ {this.state.events.length > 0 ? + + + + + + + { + this.state.events.map((event) => + + + + + ) + } +
TimePeople attendingSee more
{event.timestamp}{event.peopleAttending}Details
+ : +

No events!

+ } +
+ ); + } +} diff --git a/src/PatientDashboard.js b/src/PatientDashboard.js new file mode 100644 index 0000000..d143cab --- /dev/null +++ b/src/PatientDashboard.js @@ -0,0 +1,7 @@ +import React from 'react' + +export default function PatientDashboard() { + return ( +
patientDashboard
+ ) +} diff --git a/src/SetupDashboard.js b/src/SetupDashboard.js new file mode 100644 index 0000000..53081d4 --- /dev/null +++ b/src/SetupDashboard.js @@ -0,0 +1,77 @@ +import React from 'react' +import { collection, query, where, orderBy, getDocs } from "firebase/firestore"; + +export default class SetupDashboard extends React.Component { + constructor(props) { + super(props); + + this.state = { + user: props.user, + events: [], + db: props.db, + }; + } + + async componentDidMount() { + let eventsTemp = []; + // const querySnapshot = await getDocs(query(collection(this.state.db, "panic-events"), where("resolved", "==", false), orderBy("timestamp", "desc"))); + const q = query(collection(this.state.db, "panic-events"), where("resolved", "==", false)); + const querySnapshot = await getDocs(q); + querySnapshot.forEach((doc) => { + eventsTemp.push({ + ...doc.data(), + id: doc.id, + }); + }); + this.setState({ + events: eventsTemp, + }); + } + + render() { + return ( +
+ {this.state.events.length > 0 ? + + + + + + + { + this.state.events.map((event) => + + + + + ) + } +
TimePeople attendingSee more
{event.timestamp}{event.peopleAttending}Details
+ : +

No events!

+ } +
+ this.setState({email: e.target.value})} + value={this.state.email} + /> + this.setState({qualId: e.target.value})} + value={this.state.qualId} + /> + this.setState({phone: e.target.value})} + value={this.state.phone} + /> + +
+
+ ); + } +} diff --git a/src/currentPanicEvents.jsx b/src/currentPanicEvents.js similarity index 100% rename from src/currentPanicEvents.jsx rename to src/currentPanicEvents.js diff --git a/src/index.js b/src/index.js index d563c0f..a8ca30a 100644 --- a/src/index.js +++ b/src/index.js @@ -2,15 +2,32 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; +import * as serviceWorkerRegistration from './serviceWorkerRegistration'; import reportWebVitals from './reportWebVitals'; +// onAuthStateChanged(auth, (user) => { +// if (user) { +// // User is signed in, see docs for a list of available properties +// // https://firebase.google.com/docs/reference/js/firebase.User +// const uid = user.uid; +// // ... +// } else { +// // User is signed out +// // ... +// }}); + const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - + ); +// If you want your app to work offline and load faster, you can change +// unregister() to register() below. Note this comes with some pitfalls. +// Learn more about service workers: https://cra.link/PWA +serviceWorkerRegistration.register(); + // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals diff --git a/src/service-worker.js b/src/service-worker.js new file mode 100644 index 0000000..0f1e0ce --- /dev/null +++ b/src/service-worker.js @@ -0,0 +1,72 @@ +/* eslint-disable no-restricted-globals */ + +// This service worker can be customized! +// See https://developers.google.com/web/tools/workbox/modules +// for the list of available Workbox modules, or add any other +// code you'd like. +// You can also remove this file if you'd prefer not to use a +// service worker, and the Workbox build step will be skipped. + +import { clientsClaim } from 'workbox-core'; +import { ExpirationPlugin } from 'workbox-expiration'; +import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching'; +import { registerRoute } from 'workbox-routing'; +import { StaleWhileRevalidate } from 'workbox-strategies'; + +clientsClaim(); + +// Precache all of the assets generated by your build process. +// Their URLs are injected into the manifest variable below. +// This variable must be present somewhere in your service worker file, +// even if you decide not to use precaching. See https://cra.link/PWA +precacheAndRoute(self.__WB_MANIFEST); + +// Set up App Shell-style routing, so that all navigation requests +// are fulfilled with your index.html shell. Learn more at +// https://developers.google.com/web/fundamentals/architecture/app-shell +const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$'); +registerRoute( + // Return false to exempt requests from being fulfilled by index.html. + ({ request, url }) => { + // If this isn't a navigation, skip. + if (request.mode !== 'navigate') { + return false; + } // If this is a URL that starts with /_, skip. + + if (url.pathname.startsWith('/_')) { + return false; + } // If this looks like a URL for a resource, because it contains // a file extension, skip. + + if (url.pathname.match(fileExtensionRegexp)) { + return false; + } // Return true to signal that we want to use the handler. + + return true; + }, + createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html') +); + +// An example runtime caching route for requests that aren't handled by the +// precache, in this case same-origin .png requests like those from in public/ +registerRoute( + // Add in any other file extensions or routing criteria as needed. + ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst. + new StaleWhileRevalidate({ + cacheName: 'images', + plugins: [ + // Ensure that once this runtime cache reaches a maximum size the + // least-recently used images are removed. + new ExpirationPlugin({ maxEntries: 50 }), + ], + }) +); + +// This allows the web app to trigger skipWaiting via +// registration.waiting.postMessage({type: 'SKIP_WAITING'}) +self.addEventListener('message', (event) => { + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting(); + } +}); + +// Any other custom service worker logic can go here. diff --git a/src/serviceWorkerRegistration.js b/src/serviceWorkerRegistration.js new file mode 100644 index 0000000..19f5f8d --- /dev/null +++ b/src/serviceWorkerRegistration.js @@ -0,0 +1,139 @@ +// This optional code is used to register a service worker. +// register() is not called by default. + +// This lets the app load faster on subsequent visits in production, and gives +// it offline capabilities. However, it also means that developers (and users) +// will only see deployed updates on subsequent visits to a page, after all the +// existing tabs open on the page have been closed, since previously cached +// resources are updated in the background. + +// To learn more about the benefits of this model and instructions on how to +// opt-in, read https://cra.link/PWA + +const isLocalhost = Boolean( + window.location.hostname === 'localhost' || + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.0/8 are considered localhost for IPv4. + window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/) +); + +export function register(config) { + // if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + if ('serviceWorker' in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebook/create-react-app/issues/2374 + return; + } + + window.addEventListener('load', () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + + if (isLocalhost) { + // This is running on localhost. Let's check if a service worker still exists or not. + checkValidServiceWorker(swUrl, config); + + // Add some additional logging to localhost, pointing developers to the + // service worker/PWA documentation. + navigator.serviceWorker.ready.then(() => { + console.log( + 'This web app is being served cache-first by a service ' + + 'worker. To learn more, visit https://cra.link/PWA' + ); + }); + } else { + // Is not localhost. Just register service worker + registerValidSW(swUrl, config); + } + }); + } else { + } +} + +function registerValidSW(swUrl, config) { + navigator.serviceWorker + .register(swUrl) + .then((registration) => { + registration.onupdatefound = () => { + const installingWorker = registration.installing; + if (installingWorker == null) { + return; + } + installingWorker.onstatechange = () => { + if (installingWorker.state === 'installed') { + if (navigator.serviceWorker.controller) { + // At this point, the updated precached content has been fetched, + // but the previous service worker will still serve the older + // content until all client tabs are closed. + console.log( + 'New content is available and will be used when all ' + + 'tabs for this page are closed. See https://cra.link/PWA.' + ); + + // Execute callback + if (config && config.onUpdate) { + config.onUpdate(registration); + } + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log('Content is cached for offline use.'); + + // Execute callback + if (config && config.onSuccess) { + config.onSuccess(registration); + } + } + } + }; + }; + }) + .catch((error) => { + console.error('Error during service worker registration:', error); + }); +} + +function checkValidServiceWorker(swUrl, config) { + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl, { + headers: { 'Service-Worker': 'script' }, + }) + .then((response) => { + // Ensure service worker exists, and that we really are getting a JS file. + const contentType = response.headers.get('content-type'); + if ( + response.status === 404 || + (contentType != null && contentType.indexOf('javascript') === -1) + ) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then((registration) => { + registration.unregister().then(() => { + window.location.reload(); + }); + }); + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl, config); + } + }) + .catch(() => { + console.log('No internet connection found. App is running in offline mode.'); + }); +} + +export function unregister() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready + .then((registration) => { + registration.unregister(); + }) + .catch((error) => { + console.error(error.message); + }); + } +}