From c7543b9e96be22baa38409831bf0592e1c83fc0c Mon Sep 17 00:00:00 2001 From: Braydon Hall <40751395+nobrayner@users.noreply.github.com> Date: Tue, 23 Mar 2021 19:17:34 +1100 Subject: [PATCH 1/5] chore: move test files to be colocated --- src/{__tests__ => }/App.test.tsx | 8 +-- src/__tests__/Feedback.test.tsx | 70 ------------------- .../raids/ViewRaid.test.tsx} | 6 +- .../raids/index.test.tsx} | 6 +- 4 files changed, 10 insertions(+), 80 deletions(-) rename src/{__tests__ => }/App.test.tsx (87%) delete mode 100644 src/__tests__/Feedback.test.tsx rename src/{__tests__/Raids.test.tsx => routes/raids/ViewRaid.test.tsx} (95%) rename src/{__tests__/AllRaids.test.tsx => routes/raids/index.test.tsx} (78%) diff --git a/src/__tests__/App.test.tsx b/src/App.test.tsx similarity index 87% rename from src/__tests__/App.test.tsx rename to src/App.test.tsx index 6ec8d68..d7142bb 100644 --- a/src/__tests__/App.test.tsx +++ b/src/App.test.tsx @@ -1,14 +1,14 @@ import * as React from 'react' -import { loadingScreen, render, screen, userEvent } from '../tests/testUtils' -import App, { AppRouter } from '../App' +import { loadingScreen, render, screen, userEvent } from './tests/testUtils' +import App, { AppRouter } from './App' import { MemoryRouter } from 'react-router-dom' import { fetchedCollectionData, fetchedDocumentData, -} from '../tests/data/raidsData' +} from './tests/data/raidsData' import Header from '#components/header' import { Global } from '@emotion/react' -import { globalStyles } from '../styles/globalStyles' +import { globalStyles } from './styles/globalStyles' beforeAll(() => { jest.doMock('../utils/useDocument', () => diff --git a/src/__tests__/Feedback.test.tsx b/src/__tests__/Feedback.test.tsx deleted file mode 100644 index 592584f..0000000 --- a/src/__tests__/Feedback.test.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import * as React from 'react' -import { render, screen, userEvent } from '../tests/testUtils' -import faker from 'faker' -import App from '../App' -import { - fetchedCollectionData, - fetchedDocumentData, -} from '../tests/data/raidsData' - -const buildFeedback = { - name: faker.name.firstName(), - repoName: faker.commerce.productName(), - description: faker.lorem.paragraph(10), - rating: Math.round(faker.random.number({ min: 1, max: 5 })), -} - -beforeAll(() => { - jest.doMock('../utils/useDocument', () => - jest.fn().mockReturnValue(fetchedDocumentData), - ) - jest.doMock('../utils/useCollection', () => - jest.fn().mockReturnValue(fetchedCollectionData), - ) - jest.spyOn(console, 'log') -}) -afterAll(() => { - jest.resetAllMocks() -}) - -// Skipping for now as need to wait for UX to place feedback button on home screen. -it.skip('should renders and visits Raids then the first Link', async () => { - render() - userEvent.click(screen.getByText('Feedback')) - - expect(screen.getByLabelText(`3`)).toBeChecked() - userEvent.type(screen.getByPlaceholderText('Your name'), buildFeedback.name) - userEvent.type( - screen.getByPlaceholderText('Repo name'), - buildFeedback.repoName, - ) - // Both didn't work - // fireEvent.change( - // screen.getByLabelText(`${buildFeedback.rating}`), - // `${buildFeedback.rating}`, - // ) - - // userEvent.selectOptions( - // screen.getByRole('radiogroup'), - // `${buildFeedback.rating}`, - // ) - - userEvent.click(screen.getByText(`${buildFeedback.rating}`)) - - userEvent.type( - screen.getByLabelText('Description'), - buildFeedback.description, - ) - expect(screen.getByPlaceholderText('Your name')).toHaveDisplayValue( - buildFeedback.name, - ) - expect(screen.getByPlaceholderText('Repo name')).toHaveDisplayValue( - buildFeedback.repoName, - ) - expect(screen.getByLabelText('Description')).toHaveDisplayValue( - buildFeedback.description, - ) - expect(screen.getByLabelText(`${buildFeedback.rating}`)).toBeChecked() - - userEvent.click(screen.getByText('Submit')) -}) diff --git a/src/__tests__/Raids.test.tsx b/src/routes/raids/ViewRaid.test.tsx similarity index 95% rename from src/__tests__/Raids.test.tsx rename to src/routes/raids/ViewRaid.test.tsx index 065aab0..2811a03 100644 --- a/src/__tests__/Raids.test.tsx +++ b/src/routes/raids/ViewRaid.test.tsx @@ -1,10 +1,10 @@ import * as React from 'react' -import { loadingScreen, render, screen, userEvent } from '../tests/testUtils' -import App from '../App' +import { loadingScreen, render, screen, userEvent } from '../../tests/testUtils' +import App from '../../App' import { fetchedCollectionData, fetchedDocumentData, -} from '../tests/data/raidsData' +} from '../../tests/data/raidsData' beforeAll(() => { jest.doMock('../utils/useDocument', () => diff --git a/src/__tests__/AllRaids.test.tsx b/src/routes/raids/index.test.tsx similarity index 78% rename from src/__tests__/AllRaids.test.tsx rename to src/routes/raids/index.test.tsx index fe20e06..eaa19ca 100644 --- a/src/__tests__/AllRaids.test.tsx +++ b/src/routes/raids/index.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react' -import { loadingScreen, render, screen, userEvent } from '../tests/testUtils' -import App from '../App' -import { fetchedCollectionData } from '../tests/data/raidsData' +import { loadingScreen, render, screen, userEvent } from '../../tests/testUtils' +import App from '../../App' +import { fetchedCollectionData } from '../../tests/data/raidsData' beforeAll(() => { jest.doMock('../utils/useCollection', () => From 8fb7834730e7e5d74f913b0b7d04d004d1f3da02 Mon Sep 17 00:00:00 2001 From: Braydon Hall <40751395+nobrayner@users.noreply.github.com> Date: Tue, 23 Mar 2021 19:20:42 +1100 Subject: [PATCH 2/5] chore: move assets dir --- {src/components/assets => assets}/bg_logo.svg | 0 {src/components/assets => assets}/logo.svg | 0 jest.config.js | 1 + snowpack.config.js | 1 + src/routes/Home.tsx | 4 ++-- tsconfig.json | 3 ++- 6 files changed, 6 insertions(+), 3 deletions(-) rename {src/components/assets => assets}/bg_logo.svg (100%) rename {src/components/assets => assets}/logo.svg (100%) diff --git a/src/components/assets/bg_logo.svg b/assets/bg_logo.svg similarity index 100% rename from src/components/assets/bg_logo.svg rename to assets/bg_logo.svg diff --git a/src/components/assets/logo.svg b/assets/logo.svg similarity index 100% rename from src/components/assets/logo.svg rename to assets/logo.svg diff --git a/jest.config.js b/jest.config.js index 003e18d..55ce03a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,6 +3,7 @@ module.exports = { moduleNameMapper: { '^#components/(.*)': '/src/components/$1', '^#utils/(.*)': '/src/utils/$1', + '^#assets/(.*)': '/assets/$1', }, setupFilesAfterEnv: ['./src/setupTests.ts'], } diff --git a/snowpack.config.js b/snowpack.config.js index aa5d5a5..5e48833 100644 --- a/snowpack.config.js +++ b/snowpack.config.js @@ -20,6 +20,7 @@ module.exports = { alias: { '#components': './src/components', '#utils': './src/utils', + '#assets': './assets', }, exclude: ['**/*.stories.@(js|jsx|ts|tsx)'], } diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index 2761964..e2c5b57 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -1,7 +1,7 @@ import React from 'react' import { NavLink } from 'react-router-dom' -import backgroundLogo from '../components/assets/bg_logo.svg' -import logo from '../components/assets/logo.svg' +import backgroundLogo from '#assets/bg_logo.svg' +import logo from '#assets/logo.svg' import styled from '@emotion/styled' const Home = () => { diff --git a/tsconfig.json b/tsconfig.json index 934eda5..0681609 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,8 @@ /* paths - import rewriting/resolving */ "paths": { "#components/*": ["./src/components/*"], - "#utils/*": ["./src/utils/*"] + "#utils/*": ["./src/utils/*"], + "#assets/*": ["./assets/*"] }, /* noEmit - Snowpack builds (emits) files, not tsc. */ "noEmit": true, From d620261bd1d6b16b1efe248a12d6ebf79af7d098 Mon Sep 17 00:00:00 2001 From: Braydon Hall <40751395+nobrayner@users.noreply.github.com> Date: Tue, 23 Mar 2021 19:24:36 +1100 Subject: [PATCH 3/5] fix: tests --- package.json | 2 +- src/App.test.tsx | 4 ++-- src/routes/raids/ViewRaid.test.tsx | 4 ++-- src/routes/raids/index.test.tsx | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 118ab6a..4ca8452 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "start": "yarn firestore \"snowpack dev\"", "start:firestore": "firebase emulators:start --import=./devData --only firestore", "build": "snowpack build", - "test": "yarn firestore \"jest --watch\"", + "test": "jest --watch", "test:ci": "yarn firestore \"jest --coverage\"", "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"", "lint": "eslint --fix --ext js,jsx,ts,tsx --ignore-path .gitignore .", diff --git a/src/App.test.tsx b/src/App.test.tsx index d7142bb..754b418 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -11,10 +11,10 @@ import { Global } from '@emotion/react' import { globalStyles } from './styles/globalStyles' beforeAll(() => { - jest.doMock('../utils/useDocument', () => + jest.doMock('./utils/useDocument', () => jest.fn().mockReturnValue(fetchedDocumentData), ) - jest.doMock('../utils/useCollection', () => + jest.doMock('./utils/useCollection', () => jest.fn().mockReturnValue(fetchedCollectionData), ) }) diff --git a/src/routes/raids/ViewRaid.test.tsx b/src/routes/raids/ViewRaid.test.tsx index 2811a03..a408687 100644 --- a/src/routes/raids/ViewRaid.test.tsx +++ b/src/routes/raids/ViewRaid.test.tsx @@ -7,10 +7,10 @@ import { } from '../../tests/data/raidsData' beforeAll(() => { - jest.doMock('../utils/useDocument', () => + jest.doMock('../../utils/useDocument', () => jest.fn().mockReturnValue(fetchedDocumentData), ) - jest.doMock('../utils/useCollection', () => + jest.doMock('../../utils/useCollection', () => jest.fn().mockReturnValue(fetchedCollectionData), ) }) diff --git a/src/routes/raids/index.test.tsx b/src/routes/raids/index.test.tsx index eaa19ca..b101392 100644 --- a/src/routes/raids/index.test.tsx +++ b/src/routes/raids/index.test.tsx @@ -4,7 +4,7 @@ import App from '../../App' import { fetchedCollectionData } from '../../tests/data/raidsData' beforeAll(() => { - jest.doMock('../utils/useCollection', () => + jest.doMock('../../utils/useCollection', () => jest.fn().mockReturnValue(fetchedCollectionData), ) }) From 9fc9b4150a8c3666a173398eacb3cec1f612d8e7 Mon Sep 17 00:00:00 2001 From: Braydon Hall <40751395+nobrayner@users.noreply.github.com> Date: Fri, 26 Mar 2021 08:01:56 +1100 Subject: [PATCH 4/5] feat: #80 with nice typing --- src/routes/raids/ViewRaid.tsx | 27 ++-- src/routes/raids/index.tsx | 13 +- src/utils/firestoreCollections.ts | 1 + src/utils/useCollection/index.ts | 52 -------- src/utils/useDocument/index.ts | 51 -------- src/utils/useFirestore/index.ts | 25 ---- src/utils/useFirestoreQuery.ts | 209 ++++++++++++++++++++++++++++++ src/utils/useMemoCompare.ts | 26 ++++ types/utils/firestore.d.ts | 18 --- types/viewRaidData.d.ts | 1 + 10 files changed, 261 insertions(+), 162 deletions(-) create mode 100644 src/utils/firestoreCollections.ts delete mode 100644 src/utils/useCollection/index.ts delete mode 100644 src/utils/useDocument/index.ts delete mode 100644 src/utils/useFirestore/index.ts create mode 100644 src/utils/useFirestoreQuery.ts create mode 100644 src/utils/useMemoCompare.ts delete mode 100644 types/utils/firestore.d.ts diff --git a/src/routes/raids/ViewRaid.tsx b/src/routes/raids/ViewRaid.tsx index 58f75dd..bcdc300 100644 --- a/src/routes/raids/ViewRaid.tsx +++ b/src/routes/raids/ViewRaid.tsx @@ -6,7 +6,8 @@ import styled from '@emotion/styled' import UserStatBlock from '#components/userStatBlock' import Emoji from '#components/emoji' import LoadingSpinner from '#components/loadingSpinner' -import useDocument from '#utils/useDocument' +import { RAID_STATS } from '#utils/firestoreCollections' +import useFirestoreQuery, { to } from '#utils/useFirestoreQuery' const userStatSorts: { [key: string]: (a: UserStats, b: UserStats) => number @@ -19,12 +20,20 @@ const userStatSortNames = Object.keys(userStatSorts) const ViewRaid = () => { const { raidId } = useParams<{ raidId: string }>() - const documentData = useDocument('raid-stats', raidId) + const documentQuery = useFirestoreQuery((firestore) => + firestore.collection(RAID_STATS).withConverter(to()).doc(), + ) const [currentSort, setCurrentSort] = useState(userStatSortNames[0]) - if (documentData.state === 'success') { - const data = documentData.data + if (documentQuery.status === 'success') { + const data = documentQuery.data + + if (!data) + return ( +

Couldn`t find that Raid - did you fall into the wrong dungeon?

+ ) + return ( <$StatsView> <$Header> @@ -84,14 +93,10 @@ const ViewRaid = () => { ) - } else if (documentData.state === 'error') { - return

{JSON.stringify(documentData.error)}

+ } else if (documentQuery.status === 'error') { + return

{JSON.stringify(documentQuery.error)}

} else { - return documentData.state === 'loading' ? ( - - ) : ( -

Couldn`t find that Raid - did you fall into the wrong dungeon?

- ) + return } } diff --git a/src/routes/raids/index.tsx b/src/routes/raids/index.tsx index 8e55f72..2a7687e 100644 --- a/src/routes/raids/index.tsx +++ b/src/routes/raids/index.tsx @@ -2,13 +2,16 @@ import React from 'react' import { Link } from 'react-router-dom' import LoadingSpinner from '#components/loadingSpinner' -import useCollection from '#utils/useCollection' +import useFirestoreQuery, { to } from '#utils/useFirestoreQuery' +import { RAID_STATS } from '#utils/firestoreCollections' const AllRaids = () => { - const collectionData = useCollection('raid-stats') + const collectionQuery = useFirestoreQuery((firestore) => + firestore.collection(RAID_STATS).withConverter(to()), + ) - if (collectionData.state === 'success') { - const data = collectionData.data + if (collectionQuery.status === 'success') { + const data = collectionQuery.data return ( <>

Raids

@@ -40,7 +43,7 @@ const AllRaids = () => { ) } - return collectionData.state === 'loading' ? ( + return collectionQuery.status === 'loading' ? ( ) : ( // Theoretically this can/should never be hit... But a fun message nonetheless diff --git a/src/utils/firestoreCollections.ts b/src/utils/firestoreCollections.ts new file mode 100644 index 0000000..a6ab158 --- /dev/null +++ b/src/utils/firestoreCollections.ts @@ -0,0 +1 @@ +export const RAID_STATS = 'raid-stats' diff --git a/src/utils/useCollection/index.ts b/src/utils/useCollection/index.ts deleted file mode 100644 index 9941801..0000000 --- a/src/utils/useCollection/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { useState, useEffect } from 'react' -import firestore from '#utils/useFirestore' - -function useCollection( - collectionName: string, -): UseFirestoreData[]> { - const [collectionData, setCollectionData] = useState< - UseFirestoreData[]> - >({ - state: 'loading', - data: null, - error: null, - }) - - useEffect(() => { - firestore - .collection(collectionName) - .get() - .then((snapshot) => { - if (!snapshot.empty) { - setCollectionData({ - state: 'success', - data: snapshot.docs.map>( - (s) => - ({ - id: s.id, - ...s.data(), - } as DocumentWithId), - ), - error: null, - }) - } else { - setCollectionData({ - state: 'not-found', - data: null, - error: null, - }) - } - }) - .catch((error) => { - setCollectionData({ - state: 'error', - data: null, - error, - }) - }) - }, [collectionName]) - - return collectionData -} - -export default useCollection diff --git a/src/utils/useDocument/index.ts b/src/utils/useDocument/index.ts deleted file mode 100644 index bca187e..0000000 --- a/src/utils/useDocument/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { useState, useEffect } from 'react' -import firestore from '#utils/useFirestore' - -function useDocument( - collectionName: string, - documentId: string, -): UseFirestoreData> { - const [documentData, setDocumentData] = useState< - UseFirestoreData> - >({ - state: 'loading', - data: null, - error: null, - }) - - useEffect(() => { - firestore - .collection(collectionName) - .doc(documentId) - .get() - .then((snapshot) => { - if (snapshot.exists) { - setDocumentData({ - state: 'success', - data: { - ...snapshot.data(), - id: snapshot.id, - } as DocumentWithId, - error: null, - }) - } else { - setDocumentData({ - state: 'not-found', - data: null, - error: null, - }) - } - }) - .catch((error) => { - setDocumentData({ - state: 'error', - data: null, - error: error, - }) - }) - }, [collectionName, documentId]) - - return documentData -} - -export default useDocument diff --git a/src/utils/useFirestore/index.ts b/src/utils/useFirestore/index.ts deleted file mode 100644 index 8317c17..0000000 --- a/src/utils/useFirestore/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import firebase from 'firebase/app' -import 'firebase/firestore' - -const firebaseConfig = { - apiKey: 'AIzaSyCAgs6SNew9kKKFgQh7NLkqHK1n9Akq-GM', - authDomain: 'raid-stats-c1d5a.firebaseapp.com', - databaseURL: 'https://raid-stats-c1d5a-default-rtdb.firebaseio.com', - projectId: 'raid-stats-c1d5a', - storageBucket: 'raid-stats-c1d5a.appspot.com', - messagingSenderId: '47482470658', - appId: '1:47482470658:web:bd07aa5f9e1b0df3c2c21b', -} - -if (!firebase.apps.length) { - firebase.initializeApp(firebaseConfig) -} - -const firestore = firebase.firestore() - -// Comment out the following to pull from the live firestore DB -if (import.meta.env.NODE_ENV !== 'production') { - firestore.useEmulator('localhost', 8080) -} - -export default firestore diff --git a/src/utils/useFirestoreQuery.ts b/src/utils/useFirestoreQuery.ts new file mode 100644 index 0000000..7fa92c3 --- /dev/null +++ b/src/utils/useFirestoreQuery.ts @@ -0,0 +1,209 @@ +import { useReducer, useEffect } from 'react' +import useMemoCompare from './useMemoCompare' + +import firebase from 'firebase/app' +import 'firebase/firestore' + +const firebaseConfig = { + apiKey: 'AIzaSyCAgs6SNew9kKKFgQh7NLkqHK1n9Akq-GM', + authDomain: 'raid-stats-c1d5a.firebaseapp.com', + databaseURL: 'https://raid-stats-c1d5a-default-rtdb.firebaseio.com', + projectId: 'raid-stats-c1d5a', + storageBucket: 'raid-stats-c1d5a.appspot.com', + messagingSenderId: '47482470658', + appId: '1:47482470658:web:bd07aa5f9e1b0df3c2c21b', +} + +if (!firebase.apps.length) { + firebase.initializeApp(firebaseConfig) +} + +const firestore = firebase.firestore() + +// Comment out the following to pull from the live firestore DB +if (import.meta.env.NODE_ENV !== 'production') { + firestore.useEmulator('localhost', 8080) +} + +export function to() { + return { + toFirestore(data: TData): firebase.firestore.DocumentData { + return data + }, + fromFirestore( + snapshot: firebase.firestore.QueryDocumentSnapshot, + options: firebase.firestore.SnapshotOptions, + ): TData { + return snapshot.data(options) as TData + }, + } +} + +const reducer = < + TQueryResult extends FirestoreQueryResultUnion, + TData +>( + state: FirestoreQueryState, + action: FirestoreQueryStateAction, +): FirestoreQueryState => { + switch (action.type) { + case 'idle': + case 'loading': + return { status: action.type, data: null, error: null } + case 'success': + return { + status: 'success', + data: action.payload as FirestoreQueryStateData, + error: null, + } + case 'error': + return { status: 'error', data: null, error: action.payload } + default: + throw new Error('invalid action') + } +} + +export default function useFirestoreQuery< + TQuery extends FirestoreQuery, + TQueryResult extends ReturnType, + TData extends FirestoreQueryDataType +>(query: TQuery): FirestoreQueryState { + const [state, dispatch] = useReducer< + FirestoreQueryReducer + >(reducer, { + status: 'loading', + data: null, + error: null, + }) + + // Get cached Firestore query object with useMemoCompare (https://usehooks.com/useMemoCompare) + // Needed because firestore.collection().doc() will always be a new object reference + // causing effect to run -> state change -> rerender -> effect runs -> etc ... + // This is nicer than requiring hook consumer to always memoize query with useMemo. + const queryCached = useMemoCompare(query(firestore), (prevQuery) => { + if (prevQuery && query) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + query.isEqual(prevQuery) + } + + return false + }) + + useEffect(() => { + if (!queryCached) { + dispatch({ type: 'idle' }) + return + } + + dispatch({ type: 'loading' }) + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return queryCached.onSnapshot( + ( + snapshot: + | firebase.firestore.QuerySnapshot + | firebase.firestore.DocumentSnapshot, + ) => { + if ('docs' in snapshot) { + dispatch({ + type: 'success', + payload: getCollectionData(snapshot), + }) + } else { + const data = getDocData(snapshot) + + dispatch({ type: 'success', payload: data }) // need something better for the null case... + } + }, + (error: firebase.firestore.FirestoreError) => { + dispatch({ type: 'error', payload: error }) + }, + ) + }, [queryCached]) // Only run effect if queryCached changes + + return state +} + +function getDocData( + docSnapshot: firebase.firestore.DocumentSnapshot, +): FirestoreDocumentData | null { + return docSnapshot.exists === true + ? { id: docSnapshot.id, ...(docSnapshot.data() as TData) } + : null +} + +function getCollectionData( + collectionSnapshot: firebase.firestore.QuerySnapshot, +) { + return collectionSnapshot.docs + .map(getDocData) + .filter((d) => d) as FirestoreDocumentData[] +} + +type FirestoreDocumentData = TData & { id: string } + +type FirestoreQueryResultUnion = + | firebase.firestore.Query + | firebase.firestore.CollectionReference + | firebase.firestore.DocumentReference + +type FirestoreQuery = ( + firestore: firebase.firestore.Firestore, +) => FirestoreQueryResultUnion + +type FirestoreQueryDataType< + TQuery extends FirestoreQuery +> = TQuery extends FirestoreQuery ? TData : never + +type FirestoreQueryState< + TQueryResult extends FirestoreQueryResultUnion, + TData +> = + | { + status: 'idle' | 'loading' + data: null + error: null + } + | { + status: 'success' + data: FirestoreQueryStateData + error: null + } + | { + status: 'error' + data: null + error: Error + } + +type FirestoreQueryStateData< + TQueryResult extends FirestoreQueryResultUnion, + TData +> = TQueryResult extends firebase.firestore.DocumentReference + ? FirestoreDocumentData | null + : FirestoreDocumentData[] + +type FirestoreQueryReducer< + TQueryResult extends FirestoreQueryResultUnion, + TData +> = ( + state: FirestoreQueryState, + action: FirestoreQueryStateAction, +) => FirestoreQueryState + +type FirestoreQueryStateAction = + | { + type: 'idle' | 'loading' + } + | { + type: 'success' + payload: + | FirestoreDocumentData + | FirestoreDocumentData[] + | null + } + | { + type: 'error' + payload: Error + } diff --git a/src/utils/useMemoCompare.ts b/src/utils/useMemoCompare.ts new file mode 100644 index 0000000..b63fc8f --- /dev/null +++ b/src/utils/useMemoCompare.ts @@ -0,0 +1,26 @@ +import { useEffect, useRef } from 'react' + +export default function useMemoCompare( + next: T, + compare: (previous: T | undefined, next: T | undefined) => boolean, +) { + // Ref for storing previous value + const previousRef = useRef() + const previous = previousRef.current + + // Pass previous and next value to compare function + // to determine whether to consider them equal. + const isEqual = compare(previous, next) + + // If not equal update previousRef to next value. + // We only update if not equal so that this hook continues to return + // the same old value if compare keeps returning true. + useEffect(() => { + if (!isEqual) { + previousRef.current = next + } + }) + + // Finally, if equal then return the previous value + return isEqual ? previous : next +} diff --git a/types/utils/firestore.d.ts b/types/utils/firestore.d.ts deleted file mode 100644 index 5b71703..0000000 --- a/types/utils/firestore.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -type UseFirestoreData = - | { - state: 'loading' | 'not-found' - data: null - error: null - } - | { - state: 'success' - data: TDocument - error: null - } - | { - state: 'error' - data: null - error: Error - } - -type DocumentWithId = TDocument & { id: string } diff --git a/types/viewRaidData.d.ts b/types/viewRaidData.d.ts index ffd7178..6d5dde4 100644 --- a/types/viewRaidData.d.ts +++ b/types/viewRaidData.d.ts @@ -8,6 +8,7 @@ interface UserStats { } interface ViewRaidData { + id: string dungeon: string title: string status: 'active' | 'completed' From f218e191b8c54a52f38cffacd08ff9700dcc6d18 Mon Sep 17 00:00:00 2001 From: Braydon Hall <40751395+nobrayner@users.noreply.github.com> Date: Sat, 27 Mar 2021 08:57:16 +1100 Subject: [PATCH 5/5] Actually make stuff work --- snowpack.config.js | 2 +- {assets => src/assets}/bg_logo.svg | 0 {assets => src/assets}/logo.svg | 0 src/routes/Home.tsx | 4 +- src/routes/raids/ViewRaid.tsx | 5 +- src/routes/raids/index.tsx | 15 ++-- src/utils/useFirestoreQuery.ts | 48 +++++++------ tsconfig.json | 2 +- yarn.lock | 107 ++++++++++++++++++++++------- 9 files changed, 128 insertions(+), 55 deletions(-) rename {assets => src/assets}/bg_logo.svg (100%) rename {assets => src/assets}/logo.svg (100%) diff --git a/snowpack.config.js b/snowpack.config.js index 5e48833..9343ceb 100644 --- a/snowpack.config.js +++ b/snowpack.config.js @@ -20,7 +20,7 @@ module.exports = { alias: { '#components': './src/components', '#utils': './src/utils', - '#assets': './assets', + assets: './src/assets', }, exclude: ['**/*.stories.@(js|jsx|ts|tsx)'], } diff --git a/assets/bg_logo.svg b/src/assets/bg_logo.svg similarity index 100% rename from assets/bg_logo.svg rename to src/assets/bg_logo.svg diff --git a/assets/logo.svg b/src/assets/logo.svg similarity index 100% rename from assets/logo.svg rename to src/assets/logo.svg diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index e2c5b57..cb35a79 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -1,7 +1,7 @@ import React from 'react' import { NavLink } from 'react-router-dom' -import backgroundLogo from '#assets/bg_logo.svg' -import logo from '#assets/logo.svg' +import backgroundLogo from 'assets/bg_logo.svg' +import logo from 'assets/logo.svg' import styled from '@emotion/styled' const Home = () => { diff --git a/src/routes/raids/ViewRaid.tsx b/src/routes/raids/ViewRaid.tsx index bcdc300..31fbeae 100644 --- a/src/routes/raids/ViewRaid.tsx +++ b/src/routes/raids/ViewRaid.tsx @@ -21,7 +21,10 @@ const userStatSortNames = Object.keys(userStatSorts) const ViewRaid = () => { const { raidId } = useParams<{ raidId: string }>() const documentQuery = useFirestoreQuery((firestore) => - firestore.collection(RAID_STATS).withConverter(to()).doc(), + firestore + .collection(RAID_STATS) + .withConverter(to()) + .doc(raidId), ) const [currentSort, setCurrentSort] = useState(userStatSortNames[0]) diff --git a/src/routes/raids/index.tsx b/src/routes/raids/index.tsx index 2a7687e..14ad061 100644 --- a/src/routes/raids/index.tsx +++ b/src/routes/raids/index.tsx @@ -2,10 +2,11 @@ import React from 'react' import { Link } from 'react-router-dom' import LoadingSpinner from '#components/loadingSpinner' -import useFirestoreQuery, { to } from '#utils/useFirestoreQuery' import { RAID_STATS } from '#utils/firestoreCollections' +import useFirestoreQuery, { to } from '#utils/useFirestoreQuery' const AllRaids = () => { + console.log('Rendering page...') const collectionQuery = useFirestoreQuery((firestore) => firestore.collection(RAID_STATS).withConverter(to()), ) @@ -41,16 +42,20 @@ const AllRaids = () => { ) + } else if (collectionQuery.status === 'error') { + return ( + <> +

An error occurred

+ {JSON.stringify(collectionQuery.error)} + + ) } return collectionQuery.status === 'loading' ? ( ) : ( // Theoretically this can/should never be hit... But a fun message nonetheless -

- {`Strange... I could've sworn the Manticore was here a second ago! Seems - there aren't any Raids right now. Try again later!`} -

+

{`We ain't doing much, here`}

) } diff --git a/src/utils/useFirestoreQuery.ts b/src/utils/useFirestoreQuery.ts index 7fa92c3..c48c433 100644 --- a/src/utils/useFirestoreQuery.ts +++ b/src/utils/useFirestoreQuery.ts @@ -25,18 +25,20 @@ if (import.meta.env.NODE_ENV !== 'production') { firestore.useEmulator('localhost', 8080) } +const converter = { + toFirestore(data: unknown): firebase.firestore.DocumentData { + return data as firebase.firestore.DocumentData + }, + fromFirestore( + snapshot: firebase.firestore.QueryDocumentSnapshot, + options: firebase.firestore.SnapshotOptions, + ): unknown { + return snapshot.data(options) + }, +} + export function to() { - return { - toFirestore(data: TData): firebase.firestore.DocumentData { - return data - }, - fromFirestore( - snapshot: firebase.firestore.QueryDocumentSnapshot, - options: firebase.firestore.SnapshotOptions, - ): TData { - return snapshot.data(options) as TData - }, - } + return converter as firebase.firestore.FirestoreDataConverter } const reducer = < @@ -71,7 +73,7 @@ export default function useFirestoreQuery< const [state, dispatch] = useReducer< FirestoreQueryReducer >(reducer, { - status: 'loading', + status: 'idle', data: null, error: null, }) @@ -80,15 +82,20 @@ export default function useFirestoreQuery< // Needed because firestore.collection().doc() will always be a new object reference // causing effect to run -> state change -> rerender -> effect runs -> etc ... // This is nicer than requiring hook consumer to always memoize query with useMemo. - const queryCached = useMemoCompare(query(firestore), (prevQuery) => { - if (prevQuery && query) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - query.isEqual(prevQuery) - } + const queryCached = useMemoCompare( + query(firestore), + (prevQueryResult, nextQueryResult) => { + if (prevQueryResult && nextQueryResult) { + return nextQueryResult.isEqual( + prevQueryResult as firebase.firestore.Query & + firebase.firestore.CollectionReference & + firebase.firestore.DocumentReference, + ) + } - return false - }) + return prevQueryResult === nextQueryResult // If both are undefined, then they are the same + }, + ) useEffect(() => { if (!queryCached) { @@ -113,7 +120,6 @@ export default function useFirestoreQuery< }) } else { const data = getDocData(snapshot) - dispatch({ type: 'success', payload: data }) // need something better for the null case... } }, diff --git a/tsconfig.json b/tsconfig.json index 0681609..6b53677 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "paths": { "#components/*": ["./src/components/*"], "#utils/*": ["./src/utils/*"], - "#assets/*": ["./assets/*"] + "assets/*": ["./src/assets/*"] }, /* noEmit - Snowpack builds (emits) files, not tsc. */ "noEmit": true, diff --git a/yarn.lock b/yarn.lock index 5e7507c..68d2a16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4398,7 +4398,7 @@ better-opn@^2.0.0: dependencies: open "^7.0.3" -big-integer@^1.6.17: +big-integer@^1.6.17, big-integer@^1.6.7: version "1.6.48" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz" integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w== @@ -4507,6 +4507,13 @@ boxen@^4.1.0, boxen@^4.2.0: type-fest "^0.8.1" widest-line "^3.1.0" +bplist-parser@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.1.1.tgz#d60d5dcc20cba6dc7e1f299b35d3e1f95dafbae6" + integrity sha1-1g1dzCDLptx+HymbNdPh+V2vuuY= + dependencies: + big-integer "^1.6.7" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz" @@ -5073,6 +5080,11 @@ cli-spinners@^2.0.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.5.0.tgz" integrity sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ== +cli-spinners@^2.5.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.0.tgz#36c7dc98fb6a9a76bd6238ec3f77e2425627e939" + integrity sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q== + cli-table3@0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz" @@ -5820,6 +5832,15 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== +default-browser-id@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-2.0.0.tgz#01ecce371a71e85f15a17177e7863047e73dbe7d" + integrity sha1-AezONxpx6F8VoXF354YwR+c9vn0= + dependencies: + bplist-parser "^0.1.0" + pify "^2.3.0" + untildify "^2.0.0" + defaults@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz" @@ -6437,10 +6458,10 @@ es6-weak-map@^2.0.3: es6-iterator "^2.0.3" es6-symbol "^3.1.1" -esbuild@^0.8.7: - version "0.8.36" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.8.36.tgz" - integrity sha512-kcUQB61Tf8rLJ3mOwP2ruWi/iFufaQcEs4No+JA6e7W2kMOtFExOsbyeFpEF6zNacwk2RF5fYUz5jfZwgn/SJg== +esbuild@^0.9.3: + version "0.9.7" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.9.7.tgz#ea0d639cbe4b88ec25fbed4d6ff00c8d788ef70b" + integrity sha512-VtUf6aQ89VTmMLKrWHYG50uByMF4JQlVysb8dmg6cOgW8JnFCipmz7p+HNBl+RR3LLCuBxFGVauAe2wfnF9bLg== escalade@^3.1.1: version "3.1.1" @@ -7032,6 +7053,11 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fdir@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-5.0.0.tgz#a40b5d9adfb530daeca55558e8ad87ec14a44769" + integrity sha512-cteqwWMA43lEmgwOg5HSdvhVFD39vHjQDhZkRMlKmeoNPtSSgUw1nUypydiY2upMdGiBFBZvNBDbnoBh0yCzaQ== + fecha@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.0.tgz" @@ -7485,15 +7511,15 @@ fsevents@^1.2.7: bindings "^1.5.0" nan "^2.12.1" -fsevents@^2.1.2, fsevents@^2.2.0, fsevents@~2.3.1: +fsevents@^2.1.2, fsevents@~2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.1.tgz#b209ab14c61012636c8863507edf7fb68cc54e9f" integrity sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw== -fsevents@~2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" - integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== +fsevents@^2.2.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== fstream@^1.0.12: version "1.0.12" @@ -8634,7 +8660,7 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-core-module@^2.1.0: +is-core-module@^2.1.0, is-core-module@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz" integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== @@ -8690,7 +8716,7 @@ is-directory@^0.3.1: is-docker@^2.0.0: version "2.1.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.1.1.tgz" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.1.1.tgz#4125a88e44e450d384e09047ede71adc2d144156" integrity sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw== is-dom@^1.0.0: @@ -8944,7 +8970,7 @@ is-wsl@^1.1.0: is-wsl@^2.1.1, is-wsl@^2.2.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== dependencies: is-docker "^2.0.0" @@ -11042,7 +11068,7 @@ open@^6.3.0: dependencies: is-wsl "^1.1.0" -open@^7.0.2, open@^7.0.3, open@^7.0.4: +open@^7.0.2, open@^7.0.3: version "7.3.1" resolved "https://registry.yarnpkg.com/open/-/open-7.3.1.tgz" integrity sha512-f2wt9DCBKKjlFbjzGb8MOAW8LH8F0mrs1zc7KTjAJ9PZNQbfenzWbNP1VZJvw6ICMG9r14Ah6yfwPn7T7i646A== @@ -11050,6 +11076,14 @@ open@^7.0.2, open@^7.0.3, open@^7.0.4: is-docker "^2.0.0" is-wsl "^2.1.1" +open@^7.0.4: + version "7.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" + integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + openapi3-ts@^1.2.0: version "1.4.0" resolved "https://registry.yarnpkg.com/openapi3-ts/-/openapi3-ts-1.4.0.tgz" @@ -11101,6 +11135,11 @@ os-browserify@^0.3.0: resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz" integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz" @@ -11463,12 +11502,12 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: +picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1, picomatch@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== -pify@^2.0.0: +pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz" integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= @@ -12702,6 +12741,14 @@ resolve@^1.1.6, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.17 is-core-module "^2.1.0" path-parse "^1.0.6" +resolve@^1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + responselike@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz" @@ -12765,11 +12812,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: inherits "^2.0.1" rollup@^2.34.0: - version "2.38.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.38.1.tgz" - integrity sha512-q07T6vU/V1kqM8rGRRyCgEvIQcIAXoKIE5CpkYAlHhfiWM1Iuh4dIPWpIbqFngCK6lwAB2aYHiUVhIbSWHQWhw== + version "2.42.4" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.42.4.tgz#97c910a48bd0db6aaa4271dd48745870cbbbf970" + integrity sha512-Zqv3EvNfcllBHyyEUM754npqsZw82VIjK34cDQMwrQ1d6aqxzeYu5yFb7smGkPU4C1Bj7HupIMeT6WU7uIdnMw== optionalDependencies: - fsevents "~2.1.2" + fsevents "~2.3.1" router@^1.3.1: version "1.3.5" @@ -13169,12 +13216,17 @@ snapdragon@^0.8.1: use "^3.1.0" snowpack@^3.0.1: - version "3.0.11" - resolved "https://registry.yarnpkg.com/snowpack/-/snowpack-3.0.11.tgz" - integrity sha512-lBxgkvWTgdg0szE31JUt01wQkA9Lnmm+6lxqeV9rxDfflpx7ASnldVHFvu7Se70QJmPTQB0UJjfKI+xmYGwiiQ== + version "3.1.2" + resolved "https://registry.yarnpkg.com/snowpack/-/snowpack-3.1.2.tgz#47f376ac48b83de07093722c402c9218a6c9b542" + integrity sha512-LsYlBNjB/t/p5QP434Pa1TqjyuX8VtXiYQaAWZkOn1d1TVKEt7nigMBr8Z+EDXYn6YlLXYKHXDvv/NhUS7Ri9A== dependencies: - esbuild "^0.8.7" + cli-spinners "^2.5.0" + default-browser-id "^2.0.0" + esbuild "^0.9.3" + fdir "^5.0.0" open "^7.0.4" + picomatch "^2.2.2" + resolve "^1.20.0" rollup "^2.34.0" optionalDependencies: fsevents "^2.2.0" @@ -14361,6 +14413,13 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" +untildify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-2.1.0.tgz#17eb2807987f76952e9c0485fc311d06a826a2e0" + integrity sha1-F+soB5h/dpUunASF/DEdBqgmouA= + dependencies: + os-homedir "^1.0.0" + unzipper@^0.10.10: version "0.10.11" resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.11.tgz"