Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
14 changes: 12 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": "git@github.com:CodeForFoco/fd-servicedirectory.git",
"author": "Code for Fort Collins",
"license": "MIT",
Expand All @@ -28,11 +28,17 @@
"jest --bail --findRelatedTests --verbose"
]
},
"jest": {
"moduleNameMapper": {
"~(.*)$": "<rootDir>/src/$1"
}
},
"devDependencies": {
"@babel/core": "^7.4.3",
"@babel/preset-env": "^7.4.3",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.7.2",
"@testing-library/react": "^9.4.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^24.7.1",
"eslint": "^5.16.0",
Expand All @@ -49,14 +55,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({});
};
12 changes: 12 additions & 0 deletions src/core/test-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as React from "react";
import { BrowserRouter } from "react-router-dom";
import { ThemeProvider } from "styled-components";
import theme from "~/core/theme";

const MockApp = ({ children }) => (
<ThemeProvider theme={theme}>
<BrowserRouter>{children}</BrowserRouter>
</ThemeProvider>
);

export default MockApp;
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>
14 changes: 12 additions & 2 deletions 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 * as 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")
);
24 changes: 24 additions & 0 deletions src/pages/categories/__tests__/category-card.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from "react";
import { render } from "@testing-library/react";

import MockApp from "~/core/test-wrapper";
import CategoryCard from "~/pages/categories/category-card";

const props = {
color: "blue",
description: "The world says hello!",
icon: "home",
slug: "what-the-heck",
title: "Hello World",
};

test("Category Card - Displays title & description", async () => {
const { getByText } = render(
<MockApp>
<CategoryCard {...props} />
</MockApp>
);

expect(getByText(props.title)).toBeTruthy();
expect(getByText(props.description)).toBeTruthy();
});
Loading