diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/App.css | 44 | ||||
-rw-r--r-- | src/App.js | 119 | ||||
-rw-r--r-- | src/SearchFilters.css | 22 | ||||
-rw-r--r-- | src/SearchFilters.js | 64 | ||||
-rw-r--r-- | src/SearchResults.css | 54 | ||||
-rw-r--r-- | src/SearchResults.js | 77 | ||||
-rw-r--r-- | src/foursquare.png | bin | 0 -> 679 bytes | |||
-rw-r--r-- | src/index.css | 5 | ||||
-rw-r--r-- | src/index.js | 8 | ||||
-rw-r--r-- | src/registerServiceWorker.js | 117 |
10 files changed, 510 insertions, 0 deletions
diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..986f62e --- /dev/null +++ b/src/App.css @@ -0,0 +1,44 @@ +.App { + box-sizing: border-box; + padding-left: 2em; + padding-right: 2em; +} + +.left { + width: 200px; + float: left; +} + +.right { + margin-left: 220px; +} + +.clear { + clear: both; +} + +.loading { + margin: 2rem auto 2rem auto; + text-align: center; + font-weight: bold; +} + +.loading .spinner { + animation: spinner-spin infinite 10s linear; + margin: 26px 0 26px 0; +} + +@keyframes spinner-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.error { + width: 75%; + margin: 1rem auto 1rem auto; + padding: 1rem; + border: solid 1px #ebccd1; + border-radius: 4px; + color: #a94442; + background-color: #f2dede; +} diff --git a/src/App.js b/src/App.js new file mode 100644 index 0000000..e5d3e40 --- /dev/null +++ b/src/App.js @@ -0,0 +1,119 @@ +import React, { Component } from 'react'; +import update from 'immutability-helper'; +import 'whatwg-fetch'; +import 'promise-polyfill/src/polyfill'; +import './App.css'; +import logo from './foursquare.png'; +import SearchFilters from './SearchFilters.js'; +import SearchResults from './SearchResults.js'; + +class App extends Component { + + constructor(props) { + super(props); + + this.handleFilterChange = this.handleFilterChange.bind(this); + this.handleReset = this.handleReset.bind(this); + + this.data = []; + this.state = { + 'error': null, + 'isLoading': true, + 'filters': {} + }; + } + + componentDidMount() { + fetch('https://d22kvuv0c7qr4n.cloudfront.net/data/groups-data.json') + .then(response => response.json()) + .then(data => { + this.data = data; + this.originalFilters = data['search-fields'].reduce((a, b) => { + b['value'] = ''; + a[b.id] = b; + return a; + }, {}); + this.setState({ 'isLoading': false, 'filters': this.originalFilters }); + }) + .catch(error => this.setState({ error: error, isLoading: false })); + } + + handleFilterChange(filter, newValue) { + // Update the value on the filter + this.setState(update(this.state, { + 'filters': { + [filter.id]: { + value: { $set: newValue } + } + } + })); + } + + handleReset() { + this.setState({'filters': this.originalFilters}); + } + + matchFilters(entry) { + for (const filterId in this.state.filters) { + const filter = this.state.filters[filterId]; + const field = (filterId.startsWith('udf') ? entry.udf : entry)[filter.id]; + + // Unset filters always match. + if (!filter.value && filter.value !== false) { + continue; + } + + // Undefined fields never match. + if (field === undefined) { + return false; + + // Boolean field. + } else if (typeof(field) === "boolean") { + if (String(field) !== filter.value) { + return false; + } + + // All other fields are expected to be objects with an `id`. + } else if (field.id !== filter.value) { + return false; + } + } + + return true; + } + + render() { + if (this.state.isLoading) { + return ( + <div className="loading"> + <img src={logo} alt="" className="spinner" /> + <p>LOADING</p> + </div> + ); + } else if (this.state.error) { + return (<div className="error">Error: {this.state.error.message}</div>); + } + + // Filter the data. + const results = this.data.groups.filter(entry => this.matchFilters(entry)); + + return ( + <div className="App"> + <h2>Community Group Search</h2> + <div className="left"> + <SearchFilters + filters={this.state.filters} + onChange={this.handleFilterChange} + onReset={this.handleReset} + ></SearchFilters> + </div> + <div className="right"> + <SearchResults results={results}></SearchResults> + </div> + <div className="clear"></div> + </div> + ); + } +} + +export default App; diff --git a/src/SearchFilters.css b/src/SearchFilters.css new file mode 100644 index 0000000..d757bf6 --- /dev/null +++ b/src/SearchFilters.css @@ -0,0 +1,22 @@ + +.Filter { + text-align: left; + margin-bottom: 1rem; +} + +.Filter label { + font-weight: bold; + display: block; + margin-bottom: 5px; +} + +.Filter select { + width: 100%; +} + +.SearchFilters { +} + +.search-btn input { + width: 100%; +} diff --git a/src/SearchFilters.js b/src/SearchFilters.js new file mode 100644 index 0000000..d805225 --- /dev/null +++ b/src/SearchFilters.js @@ -0,0 +1,64 @@ +import React, { Component } from 'react'; +import './SearchFilters.css'; + +class Filter extends Component { + + constructor(props) { + super(props); + this.handleChange = this.handleChange.bind(this); + } + + handleChange(event) { + this.props.onChange(this.props.filter, event.target.value); + } + + render() { + let options = this.props.filter.values.map((option) => { + return ( + <option key={option.key} value={option.key}>{option.value}</option> + ); + }); + + return ( + <div className="Filter"> + <label>{this.props.filter.label}</label> + <select + value={this.props.filter.value} + onChange={this.handleChange} + > + <option value="">Any</option> + {options} + </select> + </div> + ); + } + +} + +class SearchFilters extends Component { + + render() { + let filters = Object.keys(this.props.filters).map((filterId) => { + const filter = this.props.filters[filterId]; + return ( + <Filter + key={filterId} + filter={filter} + onChange={this.props.onChange} + ></Filter> + ); + }); + + return ( + <div className="SearchFilters"> + {filters} + <div> + <button onClick={this.props.onReset}>Reset</button> + </div> + </div> + ); + } + +} + +export default SearchFilters; diff --git a/src/SearchResults.css b/src/SearchResults.css new file mode 100644 index 0000000..194c731 --- /dev/null +++ b/src/SearchResults.css @@ -0,0 +1,54 @@ +.result-count::before, .result-count::after { + content: "\2014"; +} + +.result-count { + font-size: 12px; + text-align: center; + font-style: italic; +} + +.Result { + box-sizing: border-box; + border-bottom: 1px solid #dbeaf1; + overflow: auto; + padding-bottom: 1rem; +} + +.Result p { + margin-bottom: 0; +} + +.Result h3 { + clear: both; +} + +.Result .image { + float: left; + width: 180px; +} + +.Result .image img { + width: 100%; +} + +.Result hr { + width: 75%; +} + +.Result .details, .Result h3 { + margin-left: 200px; +} + +.badge { + background: #aed3e5; + font-size: 10pt; + border-radius: 4px; + display: inline-block; + margin: 0 0.5rem 5px 0; + padding: 2px 10px 2px 10px; +} + +.label { + font-weight: bold; +} diff --git a/src/SearchResults.js b/src/SearchResults.js new file mode 100644 index 0000000..030c448 --- /dev/null +++ b/src/SearchResults.js @@ -0,0 +1,77 @@ +import React, { Component } from 'react'; +import './SearchResults.css'; + +class SearchResult extends Component { + + badge(f) { + if (this.props.data.udf[f]) { + return ( + <span className="badge"> + {this.props.data.udf[f].label}{' '} + </span> + ); + } else { + return null; + } + } + + image() { + const src = this.props.data['image-url']; + if (src) { + return (<img src={src} alt="" />); + } else { + return null; + } + } + + render() { + + return ( + <div className="Result"> + <h3>{this.props.data.name}</h3> + + <div className="image"> + {this.image()} + </div> + + <div className="details"> + {this.badge('udf_3')} + {this.badge('udf_2')} + {this.badge('udf_1')} + {this.props.data.childcare ? (<span className="badge">Childcare</span>) : null} + + <p>{this.props.data.description}</p> + + <p> + <span className="label">Day</span>: {this.props.data.meetingDay.label}{" "} + <span className="label">Time</span>: {this.props.data.meetingTime.label}{" "} + <span className="label">Location</span>: {this.props.data.area.label} + </p> + </div> + </div> + ); + } + +} + +class SearchResults extends Component { + + render() { + let results = this.props.results.map((result) => { + return ( + <SearchResult key={result.id} data={result}></SearchResult> + ); + }); + + return ( + <div className="SearchResults"> + <p className="result-count">Found {results.length} groups</p> + {results} + </div> + ); + } + +} + +export default SearchResults; + diff --git a/src/foursquare.png b/src/foursquare.png Binary files differnew file mode 100644 index 0000000..0825497 --- /dev/null +++ b/src/foursquare.png diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..b4cc725 --- /dev/null +++ b/src/index.css @@ -0,0 +1,5 @@ +body { + margin: 0; + padding: 0; + font-family: sans-serif; +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..fae3e35 --- /dev/null +++ b/src/index.js @@ -0,0 +1,8 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import './index.css'; +import App from './App'; +import registerServiceWorker from './registerServiceWorker'; + +ReactDOM.render(<App />, document.getElementById('root')); +registerServiceWorker(); diff --git a/src/registerServiceWorker.js b/src/registerServiceWorker.js new file mode 100644 index 0000000..a3e6c0c --- /dev/null +++ b/src/registerServiceWorker.js @@ -0,0 +1,117 @@ +// In production, we register a service worker to serve assets from local cache. + +// This lets the app load faster on subsequent visits in production, and gives +// it offline capabilities. However, it also means that developers (and users) +// will only see deployed updates on the "N+1" visit to a page, since previously +// cached resources are updated in the background. + +// To learn more about the benefits of this model, read https://goo.gl/KwvDNy. +// This link also includes instructions on opting out of this behavior. + +const isLocalhost = Boolean( + window.location.hostname === 'localhost' || + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.1/8 is considered localhost for IPv4. + window.location.hostname.match( + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ + ) +); + +export default function register() { + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(process.env.PUBLIC_URL, window.location); + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 + return; + } + + window.addEventListener('load', () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + + if (isLocalhost) { + // This is running on localhost. Lets check if a service worker still exists or not. + checkValidServiceWorker(swUrl); + + // Add some additional logging to localhost, pointing developers to the + // service worker/PWA documentation. + navigator.serviceWorker.ready.then(() => { + console.log( + 'This web app is being served cache-first by a service ' + + 'worker. To learn more, visit https://goo.gl/SC7cgQ' + ); + }); + } else { + // Is not local host. Just register service worker + registerValidSW(swUrl); + } + }); + } +} + +function registerValidSW(swUrl) { + navigator.serviceWorker + .register(swUrl) + .then(registration => { + registration.onupdatefound = () => { + const installingWorker = registration.installing; + installingWorker.onstatechange = () => { + if (installingWorker.state === 'installed') { + if (navigator.serviceWorker.controller) { + // At this point, the old content will have been purged and + // the fresh content will have been added to the cache. + // It's the perfect time to display a "New content is + // available; please refresh." message in your web app. + console.log('New content is available; please refresh.'); + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log('Content is cached for offline use.'); + } + } + }; + }; + }) + .catch(error => { + console.error('Error during service worker registration:', error); + }); +} + +function checkValidServiceWorker(swUrl) { + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl) + .then(response => { + // Ensure service worker exists, and that we really are getting a JS file. + if ( + response.status === 404 || + response.headers.get('content-type').indexOf('javascript') === -1 + ) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then(registration => { + registration.unregister().then(() => { + window.location.reload(); + }); + }); + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl); + } + }) + .catch(() => { + console.log( + 'No internet connection found. App is running in offline mode.' + ); + }); +} + +export function unregister() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready.then(registration => { + registration.unregister(); + }); + } +} |