Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Redux to the app #68

Merged
merged 20 commits into from
Jan 18, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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": "^2.0.0 || ^3.0.0 || ^4.0.0-0",
Alex-Cannon marked this conversation as resolved.
Show resolved Hide resolved
"redux-thunk": "^2.3.0",
"styled-components": "^4.2.0",
"styled-normalize": "^8.0.6",
"ts-jest": "^24.1.0"
Expand Down
106 changes: 106 additions & 0 deletions src/core/api/services/useServices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { stringify } from "qs";
import { formatService } from "~core/utils";

import { ActionRequest, Request } 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) return;
Alex-Cannon marked this conversation as resolved.
Show resolved Hide resolved
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: any) => ({
Alex-Cannon marked this conversation as resolved.
Show resolved Hide resolved
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({});
};
6 changes: 3 additions & 3 deletions src/core/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import formattedService from "./interfaces/formattedService";
import formattedService from "../types/formattedService";
Alex-Cannon marked this conversation as resolved.
Show resolved Hide resolved

interface iSheet {
valueRanges: Array<any>;
valueRanges: any[];
Alex-Cannon marked this conversation as resolved.
Show resolved Hide resolved
}

// Returns the rows of data from a sheet (excluding the header row)
export const getSheetData = (sheet: iSheet): Array<any> => {
export const getSheetData = (sheet: iSheet): formattedService[] => {
const items = sheet.valueRanges[0].values;
// Remove the header row
items.shift();
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";
Alex-Cannon marked this conversation as resolved.
Show resolved Hide resolved
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")
);
10 changes: 5 additions & 5 deletions src/pages/categories/index.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { Fragment } from "react";
import useServicesIndex from "~/core/api/services/useServicesIndex";
Alex-Cannon marked this conversation as resolved.
Show resolved Hide resolved
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)({
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 @@ -49,12 +49,12 @@ const Categories = () => {
<Fragment>
<StyledLogo />
<IntroText>Pick a category to see services in your area.</IntroText>
{loading ? (
{loading || !data ? (
Alex-Cannon marked this conversation as resolved.
Show resolved Hide resolved
<Loader />
) : (
<CategoryList>
{getCategories(data).map(c => (
<CategoryCard key={c.slug} {...c} />
{getCategories(data).map(category => (
<CategoryCard key={category.slug} {...category} />
))}
</CategoryList>
)}
Expand Down
15 changes: 6 additions & 9 deletions src/pages/service-detail/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import PhysicalInfo from "~/components/physical-info";
import Requirements from "~/components/requirements";
import TitleBar from "~/components/title-bar";
import { P1, P2 } from "~/components/typography";
import api, { useAPI } from "~/core/api";
import { formatPhoneNumber, formatService } from "~/core/utils";
import useServices from "~/core/api/services/useServices";
import { formatPhoneNumber } from "~/core/utils";

const ServiceCard = styled(Box)({
margin: "72px 16px 0 16px",
Expand All @@ -32,8 +32,7 @@ const PhoneLink = styled.a({

const ServiceDetail = ({ match }) => {
const { categoryId, serviceId, typeId } = match.params;

const { loading, errorMessage, data } = useAPI(api.getServicesByType, typeId);
const { loading, errorMessage, data } = useServices();

if (loading) {
return <Loader />;
Expand All @@ -43,16 +42,14 @@ const ServiceDetail = ({ match }) => {
return <Error {...{ errorMessage }} />;
}

const serviceRow = data.find(d => d[1] === serviceId);
const typeList = data.find(typeList => typeList[1].type === typeId);
const service = typeList.find(service => service.id === serviceId);

// If no service is found, show an empty state
if (!serviceRow) {
if (!service) {
Alex-Cannon marked this conversation as resolved.
Show resolved Hide resolved
return <p>No service was found that matches this id!</p>;
}

// Format the service into a useful object
const service = formatService(serviceRow);

return (
<Fragment>
<TitleBar
Expand Down
Loading