From 166a07ae023428c3962e5b75c0328438e24734d6 Mon Sep 17 00:00:00 2001 From: alex-cannon Date: Wed, 20 Nov 2019 12:47:56 -0700 Subject: [PATCH 1/9] chore: installed react-redux, redux-thunk --- package.json | 2 ++ yarn.lock | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/package.json b/package.json index 443b2c1..f109bb4 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,9 @@ "qs": "^6.7.0", "react": "^16.8.6", "react-dom": "^16.8.6", + "react-redux": "^7.1.3", "react-router-dom": "^5.0.0", + "redux-thunk": "^2.3.0", "styled-components": "^4.2.0", "styled-normalize": "^8.0.6", "ts-jest": "^24.1.0" diff --git a/yarn.lock b/yarn.lock index 0bf78fd..4dfd38f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -885,6 +885,13 @@ dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.5.5": + version "7.7.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.2.tgz#111a78002a5c25fc8e3361bedc9529c696b85a6a" + integrity sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw== + dependencies: + regenerator-runtime "^0.13.2" + "@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.2.2", "@babel/template@^7.4.0": version "7.4.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.0.tgz#12474e9c077bae585c5d835a95c0b0b790c25c8b" @@ -3810,6 +3817,13 @@ hoist-non-react-statics@^3.1.0: dependencies: react-is "^16.7.0" +hoist-non-react-statics@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#101685d3aff3b23ea213163f6e8e12f4f111e19f" + integrity sha512-wbg3bpgA/ZqWrZuMOeJi8+SKMhr7X9TesL/rXMjTzh0p0JUBo3II8DHboYbuIXWRlttrUFxwcu/5kygrCw8fJw== + dependencies: + react-is "^16.7.0" + hosted-git-info@^2.1.4: version "2.7.1" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" @@ -6660,6 +6674,23 @@ react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA== +react-is@^16.9.0: + version "16.12.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" + integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== + +react-redux@^7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.3.tgz#717a3d7bbe3a1b2d535c94885ce04cdc5a33fc79" + integrity sha512-uI1wca+ECG9RoVkWQFF4jDMqmaw0/qnvaSvOoL/GA4dNxf6LoV8sUAcNDvE5NWKs4hFpn0t6wswNQnY3f7HT3w== + dependencies: + "@babel/runtime" "^7.5.5" + hoist-non-react-statics "^3.3.0" + invariant "^2.2.4" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.9.0" + react-router-dom@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.0.0.tgz#542a9b86af269a37f0b87218c4c25ea8dcf0c073" @@ -6764,6 +6795,11 @@ realpath-native@^1.1.0: dependencies: util.promisify "^1.0.0" +redux-thunk@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" + integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== + regenerate-unicode-properties@^8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.0.2.tgz#7b38faa296252376d363558cfbda90c9ce709662" From 085976b7184f5729928a75bbeb6a37af3e5800ed Mon Sep 17 00:00:00 2001 From: alex-cannon Date: Wed, 20 Nov 2019 13:18:49 -0700 Subject: [PATCH 2/9] chore: hacked temp reducer so it'll work --- package.json | 3 ++- src/core/store.ts | 13 +++++++++++++ src/{index.jsx => index.tsx} | 12 +++++++++++- tsconfig.json | 13 +++++++++++++ yarn.lock | 10 +++++++++- 5 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 src/core/store.ts rename src/{index.jsx => index.tsx} (87%) create mode 100644 tsconfig.json diff --git a/package.json b/package.json index f109bb4..7e66b0d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "fd-servicedirectory", "version": "1.0.0", "description": "PFA Service Directory", - "main": "src/index.jsx", + "main": "src/index.tsx", "repository": "git@github.com:CodeForFoco/fd-servicedirectory.git", "author": "Code for Fort Collins", "license": "MIT", @@ -58,6 +58,7 @@ "react-dom": "^16.8.6", "react-redux": "^7.1.3", "react-router-dom": "^5.0.0", + "redux": "^4.0.4", "redux-thunk": "^2.3.0", "styled-components": "^4.2.0", "styled-normalize": "^8.0.6", diff --git a/src/core/store.ts b/src/core/store.ts new file mode 100644 index 0000000..de127c2 --- /dev/null +++ b/src/core/store.ts @@ -0,0 +1,13 @@ +import { createStore, applyMiddleware } from "redux"; +import thunk from "redux-thunk"; + +// Temporary Reducer. Remove this. +const defaultReducer = (state, action) => { + let newAction = action; + Object.keys(newAction); + return state; +}; + +export const configureStore = initialState => { + return createStore(defaultReducer, initialState, applyMiddleware(thunk)); +}; diff --git a/src/index.jsx b/src/index.tsx similarity index 87% rename from src/index.jsx rename to src/index.tsx index 14b243e..8c141ff 100644 --- a/src/index.jsx +++ b/src/index.tsx @@ -1,8 +1,10 @@ import React from "react"; import { render } from "react-dom"; import { BrowserRouter, Redirect, Route, Switch } from "react-router-dom"; +import { Provider } from "react-redux"; import styled, { createGlobalStyle, ThemeProvider } from "styled-components"; import { Normalize } from "styled-normalize"; +import { configureStore } from "~/core/store"; import Nav from "~/components/nav"; import theme from "~/core/theme"; import Categories from "~/pages/categories"; @@ -23,6 +25,9 @@ const PageContainer = styled.div({ marginBottom: "96px", }); +// Initialize Redux store +const store = configureStore({}); + const App = () => ( @@ -52,4 +57,9 @@ const App = () => ( ); -render(, document.getElementById("app")); +render( + + + , + document.getElementById("app") +); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..487708a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +// References: +// https://parceljs.org/typeScript.html +// https://www.typescriptlang.org/docs/handbook/tsconfig-json.html +{ + "compilerOptions": { + "baseUrl": "./src", + "paths": { + "~*": ["./*"] + }, + "jsx": "react", + }, + "include": ["src/**/*"] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 4dfd38f..d53fbf9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6800,6 +6800,14 @@ redux-thunk@^2.3.0: resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== +redux@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796" + integrity sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q== + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + regenerate-unicode-properties@^8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.0.2.tgz#7b38faa296252376d363558cfbda90c9ce709662" @@ -7704,7 +7712,7 @@ svgo@^1.0.0, svgo@^1.0.5: unquote "~1.1.1" util.promisify "~1.0.0" -symbol-observable@^1.1.0: +symbol-observable@^1.1.0, symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== From 61b4dc7dd388e79ad1e60f7c6f278e5c226eb0e4 Mon Sep 17 00:00:00 2001 From: alex-cannon Date: Thu, 21 Nov 2019 11:01:05 -0700 Subject: [PATCH 3/9] chore: updated index.jsx to index.tsx in index.html --- src/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.html b/src/index.html index 2bad8af..7c9b227 100644 --- a/src/index.html +++ b/src/index.html @@ -14,6 +14,6 @@
- + From 54bb23e3f54486305363dcdea522f177d2da6c62 Mon Sep 17 00:00:00 2001 From: alex-cannon Date: Thu, 21 Nov 2019 12:28:55 -0700 Subject: [PATCH 4/9] Milestone: Moving api logic to ~/core/store/services --- src/core/store/services/APIFetchReducer.ts | 25 ++++++++ src/core/store/services/actions.ts | 72 ++++++++++++++++++++++ src/core/{ => store}/store.ts | 10 +++ src/index.tsx | 4 +- 4 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 src/core/store/services/APIFetchReducer.ts create mode 100644 src/core/store/services/actions.ts rename src/core/{ => store}/store.ts (69%) diff --git a/src/core/store/services/APIFetchReducer.ts b/src/core/store/services/APIFetchReducer.ts new file mode 100644 index 0000000..53f9f63 --- /dev/null +++ b/src/core/store/services/APIFetchReducer.ts @@ -0,0 +1,25 @@ +// Reducer that handles state for the useAPI hook +interface iAction { + type: string; + payload: any; + errorMessage: string; +} + +interface iState { + loading: boolean; + errorMessage: string; + data: any; +} + +const APIFetchReducer = (state: iState, action: iAction) => { + switch (action.type) { + case "SUCCESS": + return { ...state, loading: false, data: action.payload }; + case "FAILURE": + return { ...state, loading: false, errorMessage: action.errorMessage }; + default: + throw new Error(); + } +}; + +export default APIFetchReducer; diff --git a/src/core/store/services/actions.ts b/src/core/store/services/actions.ts new file mode 100644 index 0000000..2598528 --- /dev/null +++ b/src/core/store/services/actions.ts @@ -0,0 +1,72 @@ +import axios from "axios"; +import { getSheetData } from "./utils"; +import { stringify } from "qs"; + +const SHEET_ID = + process.env.SHEET_ID || "1ZPRRR8T51Tk-Co8h_GBh3G_7P2F7ZrYxPQDSYycpCUg"; +const API_KEY = process.env.GOOGLE_API_KEY; + +const DEFAULT_ERROR_MESSAGE = "Something went wrong!"; + +// Create our API client and inject the API key into every request +const client = axios.create({ + baseURL: `https://sheets.googleapis.com/v4/spreadsheets/${SHEET_ID}/`, + params: { key: API_KEY }, +}); + +// Utility for fetching metadata about the sheets in the spreadsheet +const getSheetTitles = async () => { + // sheetMetadata will be a list of sheets: { "sheets": [ { "properties": { "title": "Index" } }, ... ] } + const sheetMetadata = await client.get("", { + params: { + fields: "sheets.properties.title", + }, + }); + return sheetMetadata.data.sheets + .map(sheet => sheet.properties.title) + .filter(title => title !== "Index"); +}; + +// Utility for fetching a single sheet from a spreadsheet by its title +const getSheetByTitle = async title => + await client.get("values:batchGet", { + params: { + majorDimension: "ROWS", + ranges: title, + }, + }); +/** + * Handlers for the various types of data we want from the Sheets API + * They should return parsed sheet data, rather than the raw response + * from the API. + */ +export default { + // Returns *all* services as a single array—for use in search + getAllServices: async () => { + const types = await getSheetTitles(); + const allServicesRes = await client.get("values:batchGet", { + params: { + majorDimension: "ROWS", + ranges: types, + }, + paramsSerializer: params => { + return stringify(params, { indices: false }); + }, + }); + const allServices = allServicesRes.data.valueRanges.reduce((list, type) => { + return [...list, ...type.values]; + }, []); + return allServices; + }, + // Returns the spreadsheet's index sheet + getIndex: async () => { + const res = await getSheetByTitle("Index"); + return getSheetData(res.data); + }, + // Returns a list of services for a given type + getServicesByType: async type => { + const res = await getSheetByTitle(type); + return getSheetData(res.data); + }, + DEFAULT_ERROR_MESSAGE, +}; diff --git a/src/core/store.ts b/src/core/store/store.ts similarity index 69% rename from src/core/store.ts rename to src/core/store/store.ts index de127c2..514aa8b 100644 --- a/src/core/store.ts +++ b/src/core/store/store.ts @@ -11,3 +11,13 @@ const defaultReducer = (state, action) => { export const configureStore = initialState => { return createStore(defaultReducer, initialState, applyMiddleware(thunk)); }; + +export const initializeStore = () => { + return configureStore({ + services: { + loading: false, + errorMessage: null, + data: null, + }, + }); +}; diff --git a/src/index.tsx b/src/index.tsx index 8c141ff..b155582 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,7 +4,7 @@ import { BrowserRouter, Redirect, Route, Switch } from "react-router-dom"; import { Provider } from "react-redux"; import styled, { createGlobalStyle, ThemeProvider } from "styled-components"; import { Normalize } from "styled-normalize"; -import { configureStore } from "~/core/store"; +import { initializeStore } from "~core/store/store"; import Nav from "~/components/nav"; import theme from "~/core/theme"; import Categories from "~/pages/categories"; @@ -26,7 +26,7 @@ const PageContainer = styled.div({ }); // Initialize Redux store -const store = configureStore({}); +const store = initializeStore(); const App = () => ( From 8b06a93494f15c70a3e3215d988d64edad8c03d2 Mon Sep 17 00:00:00 2001 From: alex-cannon Date: Thu, 21 Nov 2019 12:30:04 -0700 Subject: [PATCH 5/9] chore: installed @types/node --- package.json | 1 + yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/package.json b/package.json index 7e66b0d..4dcf276 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ }, "dependencies": { "@types/jest": "^24.0.23", + "@types/node": "^12.12.11", "axios": "0.18.1", "husky": "^2.4.0", "lodash": "^4.17.13", diff --git a/yarn.lock b/yarn.lock index d53fbf9..2186396 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1360,6 +1360,11 @@ dependencies: jest-diff "^24.3.0" +"@types/node@^12.12.11": + version "12.12.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.11.tgz#bec2961975888d964196bf0016a2f984d793d3ce" + integrity sha512-O+x6uIpa6oMNTkPuHDa9MhMMehlxLAd5QcOvKRjAFsBVpeFWTOPnXbDvILvFgFFZfQ1xh1EZi1FbXxUix+zpsQ== + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" From 7a98ff63d07d2b4f3aab2362fddc851d9080b4cb Mon Sep 17 00:00:00 2001 From: alex-cannon Date: Thu, 21 Nov 2019 14:59:57 -0700 Subject: [PATCH 6/9] milestone: more work done on refactoring api.js --- src/core/store/services/APIFetchReducer.ts | 25 --------------- src/core/store/services/actions.ts | 37 ++++++++++++++++++---- src/core/store/services/reducers.ts | 37 ++++++++++++++++++++++ src/core/store/store.ts | 14 ++++---- 4 files changed, 74 insertions(+), 39 deletions(-) delete mode 100644 src/core/store/services/APIFetchReducer.ts create mode 100644 src/core/store/services/reducers.ts diff --git a/src/core/store/services/APIFetchReducer.ts b/src/core/store/services/APIFetchReducer.ts deleted file mode 100644 index 53f9f63..0000000 --- a/src/core/store/services/APIFetchReducer.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Reducer that handles state for the useAPI hook -interface iAction { - type: string; - payload: any; - errorMessage: string; -} - -interface iState { - loading: boolean; - errorMessage: string; - data: any; -} - -const APIFetchReducer = (state: iState, action: iAction) => { - switch (action.type) { - case "SUCCESS": - return { ...state, loading: false, data: action.payload }; - case "FAILURE": - return { ...state, loading: false, errorMessage: action.errorMessage }; - default: - throw new Error(); - } -}; - -export default APIFetchReducer; diff --git a/src/core/store/services/actions.ts b/src/core/store/services/actions.ts index 2598528..02f3412 100644 --- a/src/core/store/services/actions.ts +++ b/src/core/store/services/actions.ts @@ -1,5 +1,5 @@ import axios from "axios"; -import { getSheetData } from "./utils"; +import { getSheetData } from "~/core/utils"; import { stringify } from "qs"; const SHEET_ID = @@ -35,14 +35,39 @@ const getSheetByTitle = async title => ranges: title, }, }); + /** - * Handlers for the various types of data we want from the Sheets API - * They should return parsed sheet data, rather than the raw response - * from the API. + * Actions relating to fetching the google spreadsheet. */ +export const getServicesSuccess = (payload: any) => ({ + type: "GET_SERVICES_SUCCESS", + payload, + errorMessage: null, +}); + +export const getServicesError = (errorMessage: string) => ({ + type: "GET_SERVICES_ERROR", + payload: null, + errorMessage, +}); + +export const getServicesLoading = () => ({ + type: "GET_SERVICES_LOADING", + payload: null, + errorMessage: null, +}); + export default { + /** + * Handlers for the various types of data we want from the Sheets API + * They should return parsed sheet data, rather than the raw response + * from the API. + */ // Returns *all* services as a single array—for use in search - getAllServices: async () => { + getAllServices: () => async ({ services }, dispatch) => { + // Check if we have the google sheet + if (services) return; + const types = await getSheetTitles(); const allServicesRes = await client.get("values:batchGet", { params: { @@ -56,7 +81,7 @@ export default { const allServices = allServicesRes.data.valueRanges.reduce((list, type) => { return [...list, ...type.values]; }, []); - return allServices; + dispatch(getServicesSuccess(allServices)); }, // Returns the spreadsheet's index sheet getIndex: async () => { diff --git a/src/core/store/services/reducers.ts b/src/core/store/services/reducers.ts new file mode 100644 index 0000000..1a5a61e --- /dev/null +++ b/src/core/store/services/reducers.ts @@ -0,0 +1,37 @@ +// Reducer that handles state for the useAPI hook +interface iAction { + type: string; + payload: any; + errorMessage: string; +} + +interface iState { + loading: boolean; + errorMessage: string; + data: any; +} + +const getServicesReducer = (state: iState, action: iAction) => { + switch (action.type) { + case "GET_SERVICES_LOADING": + return { ...state, loading: false, data: null, errorMessage: null }; + case "GET_SERVICES_ERROR": + return { + ...state, + loading: false, + data: null, + errorMessage: action.errorMessage, + }; + case "GET_SERVICES_SUCCESS": + return { + ...state, + loading: false, + data: action.payload, + errorMessage: null, + }; + default: + throw new Error(); + } +}; + +export default getServicesReducer; diff --git a/src/core/store/store.ts b/src/core/store/store.ts index 514aa8b..f72f02e 100644 --- a/src/core/store/store.ts +++ b/src/core/store/store.ts @@ -1,15 +1,13 @@ -import { createStore, applyMiddleware } from "redux"; +import { createStore, combineReducers, applyMiddleware } from "redux"; import thunk from "redux-thunk"; +import services from "~/core/store/services/reducers"; -// Temporary Reducer. Remove this. -const defaultReducer = (state, action) => { - let newAction = action; - Object.keys(newAction); - return state; -}; +const reducers = combineReducers({ + services, +}); export const configureStore = initialState => { - return createStore(defaultReducer, initialState, applyMiddleware(thunk)); + return createStore(reducers, initialState, applyMiddleware(thunk)); }; export const initializeStore = () => { From 7344f023ff0a303f56c0281439db148f9c7446c1 Mon Sep 17 00:00:00 2001 From: alex-cannon Date: Thu, 21 Nov 2019 16:10:45 -0700 Subject: [PATCH 7/9] milestone: have broken categories page WIP --- src/core/interfaces/asyncAction.ts | 11 ++++++++ src/core/store/services/actions.ts | 38 +++++++++++++++++--------- src/core/store/services/reducers.ts | 21 +++++--------- src/core/store/services/useServices.ts | 22 +++++++++++++++ src/core/store/store.ts | 9 ++---- src/pages/categories/index.jsx | 8 ++++-- src/pages/search/index.jsx | 6 ++-- 7 files changed, 75 insertions(+), 40 deletions(-) create mode 100644 src/core/interfaces/asyncAction.ts create mode 100644 src/core/store/services/useServices.ts diff --git a/src/core/interfaces/asyncAction.ts b/src/core/interfaces/asyncAction.ts new file mode 100644 index 0000000..176204d --- /dev/null +++ b/src/core/interfaces/asyncAction.ts @@ -0,0 +1,11 @@ +export interface asyncAction { + type: string; + payload: any; + errorMessage: string; +} + +export interface asyncState { + loading: boolean; + errorMessage: string; + data: any; +} diff --git a/src/core/store/services/actions.ts b/src/core/store/services/actions.ts index 02f3412..b791c0d 100644 --- a/src/core/store/services/actions.ts +++ b/src/core/store/services/actions.ts @@ -57,17 +57,22 @@ export const getServicesLoading = () => ({ errorMessage: null, }); -export default { - /** - * Handlers for the various types of data we want from the Sheets API - * They should return parsed sheet data, rather than the raw response - * from the API. - */ - // Returns *all* services as a single array—for use in search - getAllServices: () => async ({ services }, dispatch) => { - // Check if we have the google sheet - if (services) return; +/** + * Handlers for the various types of data we want from the Sheets API + * They should return parsed sheet data, rather than the raw response + * from the API. + */ +// Fetches all services and updates global state. "redux-thunk" action. +export const getAllServices = () => async (dispatch, getState) => { + // Check if we have the google sheet + const services = getState().services; + if (services.data) + return dispatch(getServicesError("Error: all services already loaded.")); + let allServices = []; + + try { + dispatch(getServicesLoading()); const types = await getSheetTitles(); const allServicesRes = await client.get("values:batchGet", { params: { @@ -78,11 +83,18 @@ export default { return stringify(params, { indices: false }); }, }); - const allServices = allServicesRes.data.valueRanges.reduce((list, type) => { + allServices = allServicesRes.data.valueRanges.reduce((list, type) => { return [...list, ...type.values]; }, []); - dispatch(getServicesSuccess(allServices)); - }, + } catch (e) { + // Dispatch a 'failure' action if the request failed + return dispatch(getServicesError(DEFAULT_ERROR_MESSAGE)); + } + + dispatch(getServicesSuccess(allServices)); +}; + +export default { // Returns the spreadsheet's index sheet getIndex: async () => { const res = await getSheetByTitle("Index"); diff --git a/src/core/store/services/reducers.ts b/src/core/store/services/reducers.ts index 1a5a61e..a560966 100644 --- a/src/core/store/services/reducers.ts +++ b/src/core/store/services/reducers.ts @@ -1,17 +1,10 @@ -// Reducer that handles state for the useAPI hook -interface iAction { - type: string; - payload: any; - errorMessage: string; -} - -interface iState { - loading: boolean; - errorMessage: string; - data: any; -} +import { asyncState, asyncAction } from "~/core/interfaces/asyncAction"; -const getServicesReducer = (state: iState, action: iAction) => { +// Reducer that handles state for the useAPI hook +const getServicesReducer = ( + state: asyncState = { loading: false, errorMessage: null, data: null }, + action: asyncAction +) => { switch (action.type) { case "GET_SERVICES_LOADING": return { ...state, loading: false, data: null, errorMessage: null }; @@ -30,7 +23,7 @@ const getServicesReducer = (state: iState, action: iAction) => { errorMessage: null, }; default: - throw new Error(); + return state; } }; diff --git a/src/core/store/services/useServices.ts b/src/core/store/services/useServices.ts new file mode 100644 index 0000000..0a32f45 --- /dev/null +++ b/src/core/store/services/useServices.ts @@ -0,0 +1,22 @@ +import { useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { getAllServices } from "./actions"; + +/** + * Hook to use all services data. Service data is parsed into an object. + * Services object: { loading, errorMessage, data } + */ +const useServices = () => { + const services = useSelector(state => state.services); + const dispatch = useDispatch(); + + // Equivilent to componentDidMount + useEffect(() => { + if (services.data) return; + dispatch(getAllServices()); + }, []); + + return services; +}; + +export default useServices; diff --git a/src/core/store/store.ts b/src/core/store/store.ts index f72f02e..ec79bfa 100644 --- a/src/core/store/store.ts +++ b/src/core/store/store.ts @@ -11,11 +11,6 @@ export const configureStore = initialState => { }; export const initializeStore = () => { - return configureStore({ - services: { - loading: false, - errorMessage: null, - data: null, - }, - }); + // State is intialized per reducer + return configureStore({}); }; diff --git a/src/pages/categories/index.jsx b/src/pages/categories/index.jsx index 7f954a0..75e7e64 100644 --- a/src/pages/categories/index.jsx +++ b/src/pages/categories/index.jsx @@ -1,10 +1,10 @@ import React, { Fragment } from "react"; +import useServices from "~/core/store/services/useServices"; import styled from "styled-components"; import Logo from "~/components/logo"; import Loader from "~/components/loader"; import Error from "~/components/error"; import { H1 } from "~/components/typography"; -import api, { useAPI } from "~/core/api"; import CategoryCard from "./category-card"; const StyledLogo = styled(Logo)({ @@ -39,7 +39,7 @@ const getCategories = data => ); const Categories = () => { - const { loading, errorMessage, data } = useAPI(api.getIndex); + const { loading, errorMessage, data } = useServices(); if (errorMessage) { return ; @@ -51,12 +51,14 @@ const Categories = () => { Pick a category to see services in your area. {loading ? ( - ) : ( + ) : data ? ( {getCategories(data).map(c => ( ))} + ) : ( + "Oops, something went wrong!" )} ); diff --git a/src/pages/search/index.jsx b/src/pages/search/index.jsx index 5c5fb5c..33672b9 100644 --- a/src/pages/search/index.jsx +++ b/src/pages/search/index.jsx @@ -4,7 +4,7 @@ import Logo from "~/components/logo"; import Loader from "~/components/loader"; import InputAndSubmit from "~/components/inputAndSubmit"; import { H1 } from "~/components/typography"; -import api, { useAPI } from "~/core/api"; +import { useSelector } from "react-redux"; import ServiceCard from "~/pages/services/service-card"; import { formatService } from "~/core/utils"; @@ -58,8 +58,8 @@ const queryServices = (data, query) => { }; const Search = () => { - const { loading, error, data } = useAPI(api.getAllServices); - const index = useAPI(api.getIndex); + const { loading, error, data } = useSelector(state => state.services); + const index = { error: null }; const urlQuery = new URLSearchParams(location.search); const [searchValue, setSearchValue] = useState(urlQuery.get("s") || ""); From 82db9ea574d433299de679f70629e2dfa99d312a Mon Sep 17 00:00:00 2001 From: alex-cannon Date: Sun, 24 Nov 2019 13:07:43 -0700 Subject: [PATCH 8/9] get categories works --- src/core/store/services/actions.ts | 104 +++++++++++++++---------- src/core/store/services/reducers.ts | 28 ++++++- src/core/store/services/useServices.ts | 26 ++++++- src/core/store/store.ts | 6 +- src/pages/categories/index.jsx | 10 +-- 5 files changed, 122 insertions(+), 52 deletions(-) diff --git a/src/core/store/services/actions.ts b/src/core/store/services/actions.ts index b791c0d..e6529fa 100644 --- a/src/core/store/services/actions.ts +++ b/src/core/store/services/actions.ts @@ -36,43 +36,19 @@ const getSheetByTitle = async title => }, }); -/** - * Actions relating to fetching the google spreadsheet. - */ -export const getServicesSuccess = (payload: any) => ({ - type: "GET_SERVICES_SUCCESS", - payload, - errorMessage: null, -}); - -export const getServicesError = (errorMessage: string) => ({ - type: "GET_SERVICES_ERROR", - payload: null, - errorMessage, -}); - -export const getServicesLoading = () => ({ - type: "GET_SERVICES_LOADING", - payload: null, - errorMessage: null, -}); - /** * Handlers for the various types of data we want from the Sheets API * They should return parsed sheet data, rather than the raw response * from the API. */ // Fetches all services and updates global state. "redux-thunk" action. -export const getAllServices = () => async (dispatch, getState) => { - // Check if we have the google sheet - const services = getState().services; - if (services.data) - return dispatch(getServicesError("Error: all services already loaded.")); - +export const getAllServices = () => async (dispatch: Function) => { let allServices = []; + // Dispatch Loading action + dispatch(getServicesLoading()); + try { - dispatch(getServicesLoading()); const types = await getSheetTitles(); const allServicesRes = await client.get("values:batchGet", { params: { @@ -83,27 +59,73 @@ export const getAllServices = () => async (dispatch, getState) => { return stringify(params, { indices: false }); }, }); - allServices = allServicesRes.data.valueRanges.reduce((list, type) => { + /*allServices = allServicesRes.data.valueRanges.reduce((list, type) => { return [...list, ...type.values]; - }, []); + }, []);*/ + allServices = getSheetData(allServicesRes.data); } catch (e) { // Dispatch a 'failure' action if the request failed return dispatch(getServicesError(DEFAULT_ERROR_MESSAGE)); } + // Dispatch services data dispatch(getServicesSuccess(allServices)); }; -export default { - // Returns the spreadsheet's index sheet - getIndex: async () => { +// Returns the spreadsheet's index sheet +export const getServicesIndex = () => async (dispatch: Function) => { + dispatch(getServicesIndexLoading()); + try { const res = await getSheetByTitle("Index"); - return getSheetData(res.data); - }, - // Returns a list of services for a given type - getServicesByType: async type => { - const res = await getSheetByTitle(type); - return getSheetData(res.data); - }, - DEFAULT_ERROR_MESSAGE, + return dispatch(getServicesIndexSuccess(getSheetData(res.data))); + } catch (e) { + return dispatch(getServicesIndexError(e)); + } }; + +// Returns the spreadsheet's services by type +export const getServicesByType = () => async type => { + const res = await getSheetByTitle(type); + return getSheetData(res.data); +}; + +// Default error message +export { DEFAULT_ERROR_MESSAGE }; + +// getAllServices actions +export const getServicesSuccess = (payload: any) => ({ + type: "GET_SERVICES_SUCCESS", + payload, + errorMessage: null, +}); + +export const getServicesError = (errorMessage: string) => ({ + type: "GET_SERVICES_ERROR", + payload: null, + errorMessage, +}); + +export const getServicesLoading = () => ({ + type: "GET_SERVICES_LOADING", + payload: null, + errorMessage: null, +}); + +// getServicesIndex actions +export const getServicesIndexSuccess = (payload: any) => ({ + type: "GET_SERVICES_INDEX_SUCCESS", + payload, + errorMessage: null, +}); + +export const getServicesIndexError = (errorMessage: string) => ({ + type: "GET_SERVICES_INDEX_ERROR", + payload: null, + errorMessage, +}); + +export const getServicesIndexLoading = () => ({ + type: "GET_SERVICES_INDEX_LOADING", + payload: null, + errorMessage: null, +}); diff --git a/src/core/store/services/reducers.ts b/src/core/store/services/reducers.ts index a560966..a5d686f 100644 --- a/src/core/store/services/reducers.ts +++ b/src/core/store/services/reducers.ts @@ -1,7 +1,7 @@ import { asyncState, asyncAction } from "~/core/interfaces/asyncAction"; // Reducer that handles state for the useAPI hook -const getServicesReducer = ( +export const getServicesReducer = ( state: asyncState = { loading: false, errorMessage: null, data: null }, action: asyncAction ) => { @@ -27,4 +27,30 @@ const getServicesReducer = ( } }; +export const getServicesIndex = ( + state: asyncState = { loading: false, errorMessage: null, data: null }, + action: asyncAction +) => { + switch (action.type) { + case "GET_SERVICES_INDEX_LOADING": + return { ...state, loading: false, data: null, errorMessage: null }; + case "GET_SERVICES_INDEX_ERROR": + return { + ...state, + loading: false, + data: null, + errorMessage: action.errorMessage, + }; + case "GET_SERVICES_INDEX_SUCCESS": + return { + ...state, + loading: false, + data: action.payload, + errorMessage: null, + }; + default: + return state; + } +}; + export default getServicesReducer; diff --git a/src/core/store/services/useServices.ts b/src/core/store/services/useServices.ts index 0a32f45..311cb60 100644 --- a/src/core/store/services/useServices.ts +++ b/src/core/store/services/useServices.ts @@ -1,17 +1,19 @@ import { useEffect } from "react"; import { useSelector, useDispatch } from "react-redux"; -import { getAllServices } from "./actions"; +import { getAllServices, getServicesIndex } from "./actions"; /** * Hook to use all services data. Service data is parsed into an object. - * Services object: { loading, errorMessage, data } + * For example, { loading, errorMessage, data: {...} } + * See ~/core/interfaces/formattedService.ts for more info. */ -const useServices = () => { +export const useServices = () => { const services = useSelector(state => state.services); const dispatch = useDispatch(); // Equivilent to componentDidMount useEffect(() => { + // Do not make additional requests for services. if (services.data) return; dispatch(getAllServices()); }, []); @@ -19,4 +21,22 @@ const useServices = () => { return services; }; +/** + * Hook to fetch index sheet data. Used to define categories. + */ +export const useServicesIndex = () => { + const { loading = false, errorMessage = null, data = null } = useSelector( + state => state.servicesIndex + ); + const dispatch = useDispatch(); + + useEffect(() => { + // Don't make additional requests. + if (data) return; + dispatch(getServicesIndex()); + }, []); + + return { loading, errorMessage, data }; +}; + export default useServices; diff --git a/src/core/store/store.ts b/src/core/store/store.ts index ec79bfa..4dc74d5 100644 --- a/src/core/store/store.ts +++ b/src/core/store/store.ts @@ -1,9 +1,13 @@ import { createStore, combineReducers, applyMiddleware } from "redux"; import thunk from "redux-thunk"; -import services from "~/core/store/services/reducers"; +import { + getServicesReducer as services, + getServicesIndex as servicesIndex, +} from "~/core/store/services/reducers"; const reducers = combineReducers({ services, + servicesIndex, }); export const configureStore = initialState => { diff --git a/src/pages/categories/index.jsx b/src/pages/categories/index.jsx index 75e7e64..d8f4237 100644 --- a/src/pages/categories/index.jsx +++ b/src/pages/categories/index.jsx @@ -1,5 +1,5 @@ import React, { Fragment } from "react"; -import useServices from "~/core/store/services/useServices"; +import { useServicesIndex } from "~/core/store/services/useServices"; import styled from "styled-components"; import Logo from "~/components/logo"; import Loader from "~/components/loader"; @@ -39,7 +39,7 @@ const getCategories = data => ); const Categories = () => { - const { loading, errorMessage, data } = useServices(); + const { loading, errorMessage, data } = useServicesIndex(); if (errorMessage) { return ; @@ -49,16 +49,14 @@ const Categories = () => { Pick a category to see services in your area. - {loading ? ( + {loading || !data ? ( - ) : data ? ( + ) : ( {getCategories(data).map(c => ( ))} - ) : ( - "Oops, something went wrong!" )} ); From 2e608ed0fa7769d84ab6949cc2cfc5cdf9cc38af Mon Sep 17 00:00:00 2001 From: alex-cannon Date: Sun, 1 Dec 2019 16:34:16 -0700 Subject: [PATCH 9/9] minor changes --- src/core/store/services/useServices.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/core/store/services/useServices.ts b/src/core/store/services/useServices.ts index 311cb60..4a2fbf9 100644 --- a/src/core/store/services/useServices.ts +++ b/src/core/store/services/useServices.ts @@ -11,10 +11,10 @@ export const useServices = () => { const services = useSelector(state => state.services); const dispatch = useDispatch(); - // Equivilent to componentDidMount + // FETCH DATA 3 ATTEMPTS WHENEVER data = null && loading = false useEffect(() => { // Do not make additional requests for services. - if (services.data) return; + if (services && services.data) return; dispatch(getAllServices()); }, []); @@ -39,4 +39,21 @@ export const useServicesIndex = () => { return { loading, errorMessage, data }; }; +/** + * Hooks for Categories, Types, and Services pages. + * Lifecycle: + * 1. GET all google sheets + * 2. Store globally in array + * 3. Hooks pull their data from the array + */ + +// Returns a list of categories +export const useCategories = () => {}; + +// Returns a list of subtypes for a category +//export const useSubTypes = category => {}; + +// Returns a list of services for a subtype +//export const useSubTypeServices = subtype => {}; + export default useServices;