summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.css44
-rw-r--r--src/App.js119
-rw-r--r--src/SearchFilters.css22
-rw-r--r--src/SearchFilters.js64
-rw-r--r--src/SearchResults.css54
-rw-r--r--src/SearchResults.js77
-rw-r--r--src/foursquare.pngbin0 -> 679 bytes
-rw-r--r--src/index.css5
-rw-r--r--src/index.js8
-rw-r--r--src/registerServiceWorker.js117
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
new file mode 100644
index 0000000..0825497
--- /dev/null
+++ b/src/foursquare.png
Binary files differ
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();
+ });
+ }
+}