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

Fix TypeScript Errors and add Redux #64

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 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
6 changes: 5 additions & 1 deletion 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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious, what do we need Node types for?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was an issue to do with making TypeScript recognize process.env as a global. I should look into it as it's been a few weeks.

"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-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",
"ts-jest": "^24.1.0"
Expand Down
11 changes: 11 additions & 0 deletions src/core/interfaces/asyncAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface asyncAction {
type: string;
payload: any;
errorMessage: string;
}

export interface asyncState {
loading: boolean;
errorMessage: string;
data: any;
}
131 changes: 131 additions & 0 deletions src/core/store/services/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import axios from "axios";
import { getSheetData } from "~/core/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.
*/
// Fetches all services and updates global state. "redux-thunk" action.
export const getAllServices = () => async (dispatch: Function) => {
let allServices = [];

// Dispatch Loading action
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 });
},
});
/*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));
};

// Returns the spreadsheet's index sheet
export const getServicesIndex = () => async (dispatch: Function) => {
dispatch(getServicesIndexLoading());
try {
const res = await getSheetByTitle("Index");
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,
});
56 changes: 56 additions & 0 deletions src/core/store/services/reducers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { asyncState, asyncAction } from "~/core/interfaces/asyncAction";

// Reducer that handles state for the useAPI hook
export 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 };
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:
return state;
}
};

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;
59 changes: 59 additions & 0 deletions src/core/store/services/useServices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getAllServices, getServicesIndex } from "./actions";

/**
* Hook to use all services data. Service data is parsed into an object.
* For example, { loading, errorMessage, data: {...} }
* See ~/core/interfaces/formattedService.ts for more info.
*/
export const useServices = () => {
const services = useSelector(state => state.services);
const dispatch = useDispatch();

// FETCH DATA 3 ATTEMPTS WHENEVER data = null && loading = false
useEffect(() => {
// Do not make additional requests for services.
if (services && services.data) return;
dispatch(getAllServices());
}, []);

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

/**
* 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 => {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these being used? If not could we get rid of them for now?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah for sure. I think for now, I should remove any code like this or that isn't being used. The app doesn't use redux yet, but I think it'd be good to merge in TypeScript for now.


// Returns a list of services for a subtype
//export const useSubTypeServices = subtype => {};

export default useServices;
20 changes: 20 additions & 0 deletions src/core/store/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createStore, combineReducers, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import {
getServicesReducer as services,
getServicesIndex as servicesIndex,
} from "~/core/store/services/reducers";

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({});
};
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/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")
);
6 changes: 3 additions & 3 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/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)({
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,7 +49,7 @@ const Categories = () => {
<Fragment>
<StyledLogo />
<IntroText>Pick a category to see services in your area.</IntroText>
{loading ? (
{loading || !data ? (
<Loader />
) : (
<CategoryList>
Expand Down
6 changes: 3 additions & 3 deletions src/pages/search/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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") || "");
Expand Down
Loading