Skip to content

Commit

Permalink
Get offline mode fully working
Browse files Browse the repository at this point in the history
  • Loading branch information
chawes13 committed Apr 25, 2020
1 parent 2cb1b8d commit 90bf556
Show file tree
Hide file tree
Showing 10 changed files with 107 additions and 96 deletions.
17 changes: 0 additions & 17 deletions fixtures/sort-options.json

This file was deleted.

1 change: 0 additions & 1 deletion public/astronaut.svg

This file was deleted.

34 changes: 0 additions & 34 deletions public/offline.html

This file was deleted.

28 changes: 18 additions & 10 deletions public/sw.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
const CACHE_NAME = 'village-people-static-cache-v1'
const STATIC_FILES_TO_CACHE = ['/index.html', '/offline.html', '/favicon.png', 'lpl-192.png', '/astronaut.svg']
const STATIC_FILES_TO_CACHE = [
'index.html',
'env',
'main.js',
'manifest.json',
'favicon.png',
'lpl-192.png',
]
const DATA_CACHE_NAME = 'village-people-data-cache-v1'

self.addEventListener('install', (event) => {
Expand Down Expand Up @@ -40,16 +47,17 @@ self.addEventListener('fetch', (event) => {
)
} else {
event.respondWith(
caches.match(event.request, { ignoreSearch: true })
.then((response) => {
return response || fetch(event.request).catch(() => {
return caches.open(CACHE_NAME)
.then((cache) => {
return cache.match('offline.html')
caches.open(CACHE_NAME)
.then((cache) => {
// Return cached file or attempt to fetch it over the network
// "Pages" that are not cached should return the index (since it's a SPA)
return cache.match(event.request)
.then((response) => {
return response || fetch(event.request).catch(() => {
return cache.match('index.html')
})
})
}
)
})
})
)
}
})
24 changes: 11 additions & 13 deletions src/js/main/contacts/components/FacetedSearch/FacetedSearch.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React, { useCallback, useReducer, useMemo } from 'react'
import React, { useCallback, useReducer, useEffect } from 'react'
import PropTypes from 'prop-types'
import * as Types from 'types'
import { debounce, map, sortBy, startCase } from 'lodash'
import { debounce, map, startCase } from 'lodash'
import { LoadingContainer } from '@launchpadlab/lp-components'
import { useUID } from 'react-uid'
import { groupContacts } from 'utils'
import reducer from './reducer'

const propTypes = {
Expand Down Expand Up @@ -35,22 +34,21 @@ function FacetedSearch({
showLabel,
}) {
const id = 'searchable-' + useUID()
const groupedInitialResults = useMemo(() => {
return groupContacts(
sortBy(initialResults, 'lastName'),
Types.CustomSortOptions.NAME
)
}, [initialResults])

const [state, dispatch] = useReducer(reducer, {
searchableContactGroups: groupedInitialResults,
resultGroups: groupedInitialResults,
searchableContactGroups: null,
resultGroups: null,
sortOption: Types.CustomSortOptions.NAME,
filterOption: '',
searchQuery: null,
searchQuery: '',
status: Types.SearchStates.INACTIVE,
})

// Reset searchable items if results change (likely to happen with SWR caching strategy)
useEffect(() => {
dispatch({ type: 'initialize-searchable', payload: initialResults })
}, [initialResults])

const debouncedSearch = useCallback(
debounce((query) => {
dispatch({ type: 'search', payload: query })
Expand Down Expand Up @@ -92,7 +90,7 @@ function FacetedSearch({
value={state.sortOption}
onChange={(e) => dispatch({ type: 'sort', payload: e.target.value })}
>
{map(['name', 'house'], (value) => (
{map(Types.CustomSortOptions, (value) => (
<option key={value} value={value}>
{startCase(value)}
</option>
Expand Down
12 changes: 12 additions & 0 deletions src/js/main/contacts/components/FacetedSearch/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ function reducer(state, action) {
}),
}
}
case 'initialize-searchable': {
const grouped = groupContacts(action.payload, state.sortOption)
return {
...state,
searchableContactGroups: grouped,
resultGroups: performFacetedSearch({
contactGroups: grouped,
query: state.searchQuery,
filter: state.filterOption,
}),
}
}
default:
throw new Error(`Missing ${action.type} in FacetedSearch reducer`)
}
Expand Down
57 changes: 37 additions & 20 deletions src/js/main/contacts/views/Contacts.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, { useEffect, useState } from 'react'
import React, { useEffect, useState, useMemo } from 'react'
import * as Types from 'types'
// import { api } from 'api'
import { Spinner } from '@launchpadlab/lp-components'
import { ContactCard, EmptyState, FacetedSearch } from '../components'
import { isEmpty, map } from 'lodash'
import { isEmpty, map, sortBy } from 'lodash'
import { getDataFromCache } from 'utils'
import CONTACTS_RESPONSE from '../../../../../fixtures/contacts.json'
import FILTER_OPTIONS_RESPONSE from '../../../../../fixtures/filter-options.json'
import SORT_OPTIONS_RESPONSE from '../../../../../fixtures/sort-options.json'

const propTypes = {}

Expand All @@ -22,29 +22,41 @@ function fetchFilterOptions() {
return Promise.resolve(JSON.parse(JSON.stringify(FILTER_OPTIONS_RESPONSE)))
}

function fetchSortOptions() {
// return api.get('/sort-options')
return Promise.resolve(JSON.parse(JSON.stringify(SORT_OPTIONS_RESPONSE)))
}

function Contacts() {
const [state, setState] = useState(Types.LoadingStates.LOADING)
const [contacts, setContacts] = useState(null)
const [filterOptions, setFilterOptions] = useState(null)
const [sortOptions, setSortOptions] = useState(null)
const [filterOptions, setFilterOptions] = useState(
JSON.parse(JSON.stringify(FILTER_OPTIONS_RESPONSE))
)

// On mount
useEffect(() => {
Promise.all([fetchContacts(), fetchFilterOptions(), fetchSortOptions()])
.then(([contacts, filterOptions, sortOptions]) => {
setContacts(contacts)
setFilterOptions(filterOptions)
setSortOptions(sortOptions)
// SWR (stale-while-revalidate) caching strategy
getDataFromCache('/contacts').then((res) => {
if (res) {
setContacts(res)
setState(Types.LoadingStates.SUCCESS)
})
.catch(() => setState(Types.LoadingStates.FAILURE))
}
})

fetchContacts().then((res) => {
setContacts(res)
setState(Types.LoadingStates.SUCCESS)
})

getDataFromCache('/filter-options').then((res) => {
if (res) {
setFilterOptions(res)
}
})

fetchFilterOptions().then(setFilterOptions)
}, [])

const sortedContacts = useMemo(() => {
return sortBy(contacts, 'lastName')
}, [contacts])

if (state === Types.LoadingStates.LOADING) return <Spinner />
if (state === Types.LoadingStates.FAILURE)
return <EmptyState className="error" message="Oops! Something went wrong" />
Expand All @@ -53,9 +65,8 @@ function Contacts() {
<div>
<FacetedSearch
label="search"
initialResults={contacts}
initialResults={sortedContacts}
filterOptions={filterOptions}
sortOptions={sortOptions}
>
{(resultGroups) => {
if (isEmpty(resultGroups))
Expand All @@ -67,7 +78,13 @@ function Contacts() {
<h3>{groupName}</h3>
<ul>
{results.map((contact) => (
<li key={contact.phoneNumber}>
<li
key={
contact.firstName +
contact.lastName +
contact.phoneNumber
}
>
<ContactCard
name={`${contact.firstName} ${contact.lastName}`}
phoneNumber={contact.phoneNumber}
Expand Down
1 change: 1 addition & 0 deletions src/js/main/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ export const SearchStates = {

export const CustomSortOptions = {
NAME: 'name',
HOUSE: 'house',
}
2 changes: 1 addition & 1 deletion src/js/services/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function before() {

// Any transformations of successful responses can go here.
// By default, we pull out the value nested at `data.attributes`.
function onSuccess(res) {
export function onSuccess(res) {
return get(res, 'data')
}

Expand Down
27 changes: 27 additions & 0 deletions src/js/utils/getDataFromCache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { onSuccess } from 'api'

function getDataFromCache(url) {
if (!supportsCaches()) return Promise.resolve(null)

const fullUrl = process.env.API_URL + url
return caches
.match(fullUrl)
.then((response) => {
if (!response) return null
return response.json() // returns a promise
})
.then(onSuccess)
.catch((err) => {
// eslint-disable-next-line
console.error(`Error getting data from ${url} cache`, err)
return null
})
}

// ----- PRIVATE -----

function supportsCaches() {
return 'caches' in window
}

export default getDataFromCache

0 comments on commit 90bf556

Please sign in to comment.