Skip to content

Commit

Permalink
Merge pull request #68 from CodeForFoco/issue-64-redux
Browse files Browse the repository at this point in the history
I completed the requested changes, so I'll go ahead and merge.
  • Loading branch information
Alex Cannon authored Jan 18, 2020
2 parents 3bb1a6d + 072a9f8 commit 55105a1
Show file tree
Hide file tree
Showing 15 changed files with 367 additions and 61 deletions.
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "fd-servicedirectory",
"version": "1.0.0",
"description": "PFA Service Directory",
"main": "src/index.jsx",
"main": "src/index.tsx",
"repository": "[email protected]:CodeForFoco/fd-servicedirectory.git",
"author": "Code for Fort Collins",
"license": "MIT",
Expand Down Expand Up @@ -49,14 +49,18 @@
},
"dependencies": {
"@types/jest": "^24.0.23",
"@types/node": "^12.12.11",
"axios": "0.18.1",
"husky": "^2.4.0",
"lodash": "^4.17.13",
"polished": "^3.3.0",
"qs": "^6.7.0",
"react": "^16.8.6",
"react": "^16.8.3",
"react-dom": "^16.8.6",
"react-redux": "^7.1.3",
"react-router-dom": "^5.0.0",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"styled-components": "^4.2.0",
"styled-normalize": "^8.0.6",
"ts-jest": "^24.1.0"
Expand Down
107 changes: 107 additions & 0 deletions src/core/api/services/useServices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { stringify } from "qs";
import { formatService } from "~core/utils";

import { ActionRequest, Request, FormattedService } from "~/types/services";
import { axiosClient as client, DEFAULT_ERROR_MESSAGE } from "~/core/constants";

export const useServices = (): Request => {
const services = useSelector(state => state.services);
const dispatch = useDispatch();

useEffect(() => {
if (!services || !services.data) {
dispatch(getAllServices);
}
}, []);

return services;
};

const getAllServices = async (dispatch: Function) => {
dispatch(getServicesLoading());

try {
const types = await getSheetTitles();
const allServicesRes = await client.get("values:batchGet", {
params: {
majorDimension: "ROWS",
ranges: types,
},
paramsSerializer: params => {
return stringify(params, { indices: false });
},
});

dispatch(
getServicesSuccess(
allServicesRes.data.valueRanges.reduce((list, type) => {
// Remove "Types, Id, etc..." row from service
type.values.shift();

return [...list, type.values.map(service => formatService(service))];
}, [])
)
);
} catch (e) {
return dispatch(getServicesError(DEFAULT_ERROR_MESSAGE));
}
};

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");
};

// Reducer that handles state for the useAPI hook
export const getServicesReducer = (
state: Request = { loading: true, errorMessage: null, data: null },
action: ActionRequest
) => {
switch (action.type) {
case "GET_SERVICES_LOADING":
return { ...state, loading: true, data: null, errorMessage: null };
case "GET_SERVICES_ERROR":
return {
loading: false,
data: null,
errorMessage: action.errorMessage,
};
case "GET_SERVICES_SUCCESS":
return {
loading: false,
data: action.payload,
errorMessage: null,
};
default:
return state;
}
};

export const getServicesSuccess = (payload: FormattedService[][]) => ({
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 useServices;
73 changes: 73 additions & 0 deletions src/core/api/services/useServicesIndex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getSheetData } from "~/core/utils";

import { ActionRequest, Request } from "~/types/services";
import {
axiosClient as client,
DEFAULT_ERROR_MESSAGE,
INDEX_SHEET_TITLE,
} from "~/core/constants";

const useServicesIndex = (): Request => {
const servicesIndex = useSelector(state => state.servicesIndex);
const dispatch = useDispatch();

useEffect(() => {
if (servicesIndex && servicesIndex.data) return;
dispatch(getServicesIndex);
}, []);

return servicesIndex;
};

const getServicesIndex = async (dispatch: Function) => {
dispatch(getServicesIndexLoading());

try {
const res = await getSheetByTitle(INDEX_SHEET_TITLE);
dispatch(getServicesIndexSuccess(getSheetData(res.data)));
} catch {
dispatch(getServicesIndexError(DEFAULT_ERROR_MESSAGE));
}
};

const getSheetByTitle = async title =>
await client.get("values:batchGet", {
params: {
majorDimension: "ROWS",
ranges: title,
},
});

export const getServicesIndexReducer = (
state: Request = { loading: true, errorMessage: null, data: null },
action: ActionRequest
) => {
switch (action.type) {
case "GET_SHEET_INDEX_SUCCESS":
return { loading: false, data: action.payload, errorMessage: null };
case "GET_SHEET_INDEX_ERROR":
return { loading: false, data: null, errorMessage: action.errorMessage };
case "GET_SHEET_INDEX_LOADING":
return { loading: true, data: null, errorMessage: null };
default:
return state;
}
};

export const getServicesIndexSuccess = (payload): ActionRequest => ({
type: "GET_SHEET_INDEX_SUCCESS",
payload,
});

export const getServicesIndexError = (errorMessage): ActionRequest => ({
type: "GET_SHEET_INDEX_ERROR",
errorMessage,
});

export const getServicesIndexLoading = (): ActionRequest => ({
type: "GET_SHEET_INDEX_LOADING",
});

export default useServicesIndex;
13 changes: 13 additions & 0 deletions src/core/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import axios from "axios";

export const API_KEY = process.env.GOOGLE_API_KEY;
export const DEFAULT_ERROR_MESSAGE = "Something went wrong!";
export const INDEX_SHEET_TITLE = "Index";
export const SHEET_ID =
process.env.SHEET_ID || "1ZPRRR8T51Tk-Co8h_GBh3G_7P2F7ZrYxPQDSYycpCUg";

// Create our API client and inject the API key into every request
export const axiosClient = axios.create({
baseURL: `https://sheets.googleapis.com/v4/spreadsheets/${SHEET_ID}/`,
params: { key: API_KEY },
});
18 changes: 18 additions & 0 deletions src/core/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createStore, combineReducers, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { getServicesReducer as services } from "~core/api/services/useServices";
import { getServicesIndexReducer as servicesIndex } from "~core/api/services/useServicesIndex";

const reducers = combineReducers({
services,
servicesIndex,
});

export const configureStore = initialState => {
return createStore(reducers, initialState, applyMiddleware(thunk));
};

export const initializeStore = () => {
// State is intialized per reducer
return configureStore({});
};
17 changes: 12 additions & 5 deletions src/core/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import formattedService from "./interfaces/formattedService";
import { FormattedService } from "~/types/services";

interface iSheet {
valueRanges: Array<any>;
export interface iSheet {
spreadsheetid: string;
valueRanges: valueRangeItem[];
}

export interface valueRangeItem {
range: string;
majorDimension: string;
values: string[];
}

// Returns the rows of data from a sheet (excluding the header row)
export const getSheetData = (sheet: iSheet): Array<any> => {
export const getSheetData = (sheet: iSheet): string[] => {
const items = sheet.valueRanges[0].values;
// Remove the header row
items.shift();
Expand All @@ -18,7 +25,7 @@ export const getSheetData = (sheet: iSheet): Array<any> => {
* Note: A fallback is required for the Phone # field because
* the Sheets API omits the last column if there's no value.
*/
export const formatService = (service: any): formattedService => {
export const formatService = (service: any): FormattedService => {
// Split the comma separated populations into an array
const populations = service[3] === "" ? [] : service[3].split(", ");
return {
Expand Down
2 changes: 1 addition & 1 deletion src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
</head>
<body>
<div id="app"></div>
<script src="./index.jsx"></script>
<script src="./index.tsx"></script>
</body>
</html>
12 changes: 11 additions & 1 deletion src/index.jsx → src/index.tsx
Original file line number Diff line number Diff line change
@@ -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 { initializeStore } from "~/core/store";
import Nav from "~/components/nav";
import theme from "~/core/theme";
import Categories from "~/pages/categories";
Expand All @@ -23,6 +25,9 @@ const PageContainer = styled.div({
marginBottom: "96px",
});

// Initialize Redux store
const store = initializeStore();

const App = () => (
<ThemeProvider theme={theme}>
<BrowserRouter>
Expand Down Expand Up @@ -52,4 +57,9 @@ const App = () => (
</ThemeProvider>
);

render(<App />, document.getElementById("app"));
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("app")
);
18 changes: 9 additions & 9 deletions src/pages/categories/index.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { Fragment } from "react";
import styled from "styled-components";
import Logo from "~/components/logo";
import Loader from "~/components/loader";
import CategoryCard from "./category-card";
import Error from "~/components/error";
import { H1 } from "~/components/typography";
import api, { useAPI } from "~/core/api";
import CategoryCard from "./category-card";
import Logo from "~/components/logo";
import Loader from "~/components/loader";
import React, { Fragment } from "react";
import styled from "styled-components";
import useServicesIndex from "~/core/api/services/useServicesIndex";

const StyledLogo = styled(Logo)({
margin: "48px auto",
Expand Down Expand Up @@ -39,7 +39,7 @@ const getCategories = data =>
);

const Categories = () => {
const { loading, errorMessage, data } = useAPI(api.getIndex);
const { loading, errorMessage, data } = useServicesIndex();

if (errorMessage) {
return <Error {...{ errorMessage }} />;
Expand All @@ -53,8 +53,8 @@ const Categories = () => {
<Loader />
) : (
<CategoryList>
{getCategories(data).map(c => (
<CategoryCard key={c.slug} {...c} />
{getCategories(data).map(category => (
<CategoryCard key={category.slug} {...category} />
))}
</CategoryList>
)}
Expand Down
Loading

0 comments on commit 55105a1

Please sign in to comment.