diff --git a/package.json b/package.json index 443b2c1..5ab82b8 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", @@ -28,11 +28,17 @@ "jest --bail --findRelatedTests --verbose" ] }, + "jest": { + "moduleNameMapper": { + "~(.*)$": "/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", @@ -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" diff --git a/src/core/api/services/useServices.ts b/src/core/api/services/useServices.ts new file mode 100644 index 0000000..9f1eb32 --- /dev/null +++ b/src/core/api/services/useServices.ts @@ -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; diff --git a/src/core/api/services/useServicesIndex.ts b/src/core/api/services/useServicesIndex.ts new file mode 100644 index 0000000..6a31830 --- /dev/null +++ b/src/core/api/services/useServicesIndex.ts @@ -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; diff --git a/src/core/constants.ts b/src/core/constants.ts new file mode 100644 index 0000000..0ddbdc1 --- /dev/null +++ b/src/core/constants.ts @@ -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 }, +}); diff --git a/src/core/store.ts b/src/core/store.ts new file mode 100644 index 0000000..0e97aae --- /dev/null +++ b/src/core/store.ts @@ -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({}); +}; diff --git a/src/core/test-wrapper.tsx b/src/core/test-wrapper.tsx new file mode 100644 index 0000000..f56f347 --- /dev/null +++ b/src/core/test-wrapper.tsx @@ -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 }) => ( + + {children} + +); + +export default MockApp; diff --git a/src/core/utils.ts b/src/core/utils.ts index f41f705..9bb5c73 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -1,11 +1,18 @@ -import formattedService from "./interfaces/formattedService"; +import { FormattedService } from "~/types/services"; -interface iSheet { - valueRanges: Array; +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 => { +export const getSheetData = (sheet: iSheet): string[] => { const items = sheet.valueRanges[0].values; // Remove the header row items.shift(); @@ -18,7 +25,7 @@ export const getSheetData = (sheet: iSheet): Array => { * 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 { 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 @@
- + diff --git a/src/index.jsx b/src/index.tsx similarity index 85% rename from src/index.jsx rename to src/index.tsx index 14b243e..f68e14b 100644 --- a/src/index.jsx +++ b/src/index.tsx @@ -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"; @@ -23,6 +25,9 @@ const PageContainer = styled.div({ marginBottom: "96px", }); +// Initialize Redux store +const store = initializeStore(); + const App = () => ( @@ -52,4 +57,9 @@ const App = () => ( ); -render(, document.getElementById("app")); +render( + + + , + document.getElementById("app") +); diff --git a/src/pages/categories/__tests__/category-card.test.tsx b/src/pages/categories/__tests__/category-card.test.tsx new file mode 100644 index 0000000..96adbd1 --- /dev/null +++ b/src/pages/categories/__tests__/category-card.test.tsx @@ -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( + + + + ); + + expect(getByText(props.title)).toBeTruthy(); + expect(getByText(props.description)).toBeTruthy(); +}); diff --git a/src/pages/categories/index.jsx b/src/pages/categories/index.jsx index 7f954a0..1b45eba 100644 --- a/src/pages/categories/index.jsx +++ b/src/pages/categories/index.jsx @@ -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", @@ -39,7 +39,7 @@ const getCategories = data => ); const Categories = () => { - const { loading, errorMessage, data } = useAPI(api.getIndex); + const { loading, errorMessage, data } = useServicesIndex(); if (errorMessage) { return ; @@ -53,8 +53,8 @@ const Categories = () => { ) : ( - {getCategories(data).map(c => ( - + {getCategories(data).map(category => ( + ))} )} diff --git a/src/pages/service-detail/index.jsx b/src/pages/service-detail/index.jsx index 45ae376..bc96631 100644 --- a/src/pages/service-detail/index.jsx +++ b/src/pages/service-detail/index.jsx @@ -1,16 +1,16 @@ -import React, { Fragment } from "react"; -import styled from "styled-components"; import Box from "~/components/box"; import Button from "~/components/button"; import Divider from "~/components/divider"; -import Loader from "~/components/loader"; import Error from "~/components/error"; +import { formatPhoneNumber } from "~/core/utils"; +import Loader from "~/components/loader"; import PhysicalInfo from "~/components/physical-info"; +import { P1, P2 } from "~/components/typography"; +import React, { Fragment } from "react"; import Requirements from "~/components/requirements"; +import styled from "styled-components"; 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"; const ServiceCard = styled(Box)({ margin: "72px 16px 0 16px", @@ -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 ; @@ -43,16 +42,14 @@ const ServiceDetail = ({ match }) => { return ; } - 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) { return

No service was found that matches this id!

; } - // Format the service into a useful object - const service = formatService(serviceRow); - return ( { const { categoryId, typeId } = match.params; - - const { loading, errorMessage, data } = useAPI(api.getServicesByType, typeId); + const { loading, errorMessage, data } = useServices(); if (loading) { return ; @@ -26,25 +24,21 @@ const Services = ({ match }) => { return ; } - // Format services into useful objects - const services = data.map(formatService); + // Get my services + const myServices = getMyServices(data, typeId); - // If there are no services, show an empty state - if (services.length === 0) { - return

No types were found for this category!

; + if (!myServices || myServices.length === 0) { + return

No services were found for this type!

; } - // Grab the current type from the first service - const currentType = services[0].type; - return ( - {services.map(s => ( + {myServices.map(s => ( { ); }; +const getMyServices = (allTypes, myType) => { + let myServices = []; + + allTypes.forEach(services => { + if (services[1].type === myType) { + myServices = services; + } + }); + + return myServices; +}; + export default Services; diff --git a/src/pages/types/index.jsx b/src/pages/types/index.jsx index 22e3149..1ea008b 100644 --- a/src/pages/types/index.jsx +++ b/src/pages/types/index.jsx @@ -1,10 +1,10 @@ +import Error from "~/components/error"; +import Loader from "~/components/loader"; import React, { Fragment } from "react"; import styled from "styled-components"; -import Loader from "~/components/loader"; -import Error from "~/components/error"; import TitleBar from "~/components/title-bar"; -import api, { useAPI } from "~/core/api"; import TypeCard from "./type-card"; +import useServicesIndex from "~/core/api/services/useServicesIndex"; const TypesList = styled.ul({ listStyle: "none", @@ -13,8 +13,8 @@ const TypesList = styled.ul({ }); const Types = ({ match }) => { + const { loading, errorMessage, data } = useServicesIndex(); const { categoryId } = match.params; - const { loading, errorMessage, data } = useAPI(api.getIndex); if (loading) { return ; diff --git a/src/core/interfaces/formattedService.ts b/src/types/services/index.ts similarity index 60% rename from src/core/interfaces/formattedService.ts rename to src/types/services/index.ts index f1b7023..546c74d 100644 --- a/src/core/interfaces/formattedService.ts +++ b/src/types/services/index.ts @@ -1,4 +1,16 @@ -export interface formattedService { +export interface ActionRequest { + type: string; + errorMessage?: any; + payload?: any; +} + +export interface Request { + loading: boolean; + errorMessage: any; + data: any; +} + +export interface FormattedService { address: string; description: string; hours: number; @@ -17,5 +29,3 @@ export interface formattedService { lowIncome: boolean; }; } - -export default formattedService; 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 0bf78fd..aeeabb1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -885,6 +885,20 @@ 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/runtime@^7.6.2", "@babel/runtime@^7.7.6": + version "7.7.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.7.tgz#194769ca8d6d7790ec23605af9ee3e42a0aa79cf" + integrity sha512-uCnC2JEVAu8AKB5do1WRIsvrdJ0flYx/A/9f/6chdacnEZ7LmavjdsDXr5ksYBegxtuTPR5Va9/+13QF/kFkCA== + 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" @@ -1288,6 +1302,32 @@ dependencies: any-observable "^0.3.0" +"@sheerun/mutationobserver-shim@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b" + integrity sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q== + +"@testing-library/dom@^6.11.0": + version "6.11.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-6.11.0.tgz#962a38f1a721fdb7c9e35e7579e33ff13a00eda4" + integrity sha512-Pkx9LMIGshyNbfmecjt18rrAp/ayMqGH674jYER0SXj0iG9xZc+zWRjk2Pg9JgPBDvwI//xGrI/oOQkAi4YEew== + dependencies: + "@babel/runtime" "^7.6.2" + "@sheerun/mutationobserver-shim" "^0.3.2" + "@types/testing-library__dom" "^6.0.0" + aria-query "3.0.0" + pretty-format "^24.9.0" + wait-for-expect "^3.0.0" + +"@testing-library/react@^9.4.0": + version "9.4.0" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-9.4.0.tgz#b021ac8cb987c8dc54c6841875f745bf9b2e88e5" + integrity sha512-XdhDWkI4GktUPsz0AYyeQ8M9qS/JFie06kcSnUVcpgOwFjAu9vhwR83qBl+lw9yZWkbECjL8Hd+n5hH6C0oWqg== + dependencies: + "@babel/runtime" "^7.7.6" + "@testing-library/dom" "^6.11.0" + "@types/testing-library__react" "^9.1.2" + "@types/babel__core@^7.1.0": version "7.1.1" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.1.tgz#ce9a9e5d92b7031421e1d0d74ae59f572ba48be6" @@ -1353,21 +1393,61 @@ 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" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== +"@types/prop-types@*": + version "15.7.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" + integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== + "@types/q@^1.5.1": version "1.5.2" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== +"@types/react-dom@*": + version "16.9.4" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.4.tgz#0b58df09a60961dcb77f62d4f1832427513420df" + integrity sha512-fya9xteU/n90tda0s+FtN5Ym4tbgxpq/hb/Af24dvs6uYnYn+fspaxw5USlw0R8apDNwxsqumdRoCoKitckQqw== + dependencies: + "@types/react" "*" + +"@types/react@*": + version "16.9.17" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.17.tgz#58f0cc0e9ec2425d1441dd7b623421a867aa253e" + integrity sha512-UP27In4fp4sWF5JgyV6pwVPAQM83Fj76JOcg02X5BZcpSu5Wx+fP9RMqc2v0ssBoQIFvD5JdKY41gjJJKmw6Bg== + dependencies: + "@types/prop-types" "*" + csstype "^2.2.0" + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/testing-library__dom@*", "@types/testing-library__dom@^6.0.0": + version "6.11.1" + resolved "https://registry.yarnpkg.com/@types/testing-library__dom/-/testing-library__dom-6.11.1.tgz#6058a6ac391db679f7c60dbb27b81f0620de2dd9" + integrity sha512-ImChHtQqmjwraRLqBC2sgSQFtczeFvBmBcfhTYZn/3KwXbyD07LQykEQ0xJo7QHc1GbVvf7pRyGaIe6PkCdxEw== + dependencies: + pretty-format "^24.3.0" + +"@types/testing-library__react@^9.1.2": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@types/testing-library__react/-/testing-library__react-9.1.2.tgz#e33af9124c60a010fc03a34eff8f8a34a75c4351" + integrity sha512-CYaMqrswQ+cJACy268jsLAw355DZtPZGt3Jwmmotlcu8O/tkoXBI6AeZ84oZBJsIsesozPKzWzmv/0TIU+1E9Q== + dependencies: + "@types/react-dom" "*" + "@types/testing-library__dom" "*" + "@types/yargs-parser@*": version "13.1.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.1.0.tgz#c563aa192f39350a1d18da36c5a8da382bbd8228" @@ -1510,7 +1590,7 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -aria-query@^3.0.0: +aria-query@3.0.0, aria-query@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc" integrity sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w= @@ -2646,6 +2726,11 @@ cssstyle@^1.0.0: dependencies: cssom "0.3.x" +csstype@^2.2.0: + version "2.6.8" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.8.tgz#0fb6fc2417ffd2816a418c9336da74d7f07db431" + integrity sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA== + damerau-levenshtein@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514" @@ -3810,6 +3895,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" @@ -6472,7 +6564,7 @@ prettier@^1.18.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea" integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw== -pretty-format@^24.9.0: +pretty-format@^24.3.0, pretty-format@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9" integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA== @@ -6660,6 +6752,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" @@ -6689,15 +6798,14 @@ react-router@5.0.0: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react@^16.8.6: - version "16.8.6" - resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe" - integrity sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw== +react@^16.8.3: + version "16.12.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.12.0.tgz#0c0a9c6a142429e3614834d5a778e18aa78a0b83" + integrity sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.13.6" read-pkg-up@^4.0.0: version "4.0.0" @@ -6764,6 +6872,19 @@ 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== + +redux@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" + integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== + 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" @@ -7668,7 +7789,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== @@ -8125,6 +8246,11 @@ w3c-hr-time@^1.0.1: dependencies: browser-process-hrtime "^0.1.2" +wait-for-expect@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-3.0.1.tgz#ec204a76b0038f17711e575720aaf28505ac7185" + integrity sha512-3Ha7lu+zshEG/CeHdcpmQsZnnZpPj/UsG3DuKO8FskjuDbkx3jE3845H+CuwZjA2YWYDfKMU2KhnCaXMLd3wVw== + walker@^1.0.7, walker@~1.0.5: version "1.0.7" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"