Add API helpers

This commit is contained in:
Maciej Ziarkowski 2020-01-02 10:59:13 +00:00
parent 82a50d77d6
commit dbd6487b0b
11 changed files with 111 additions and 171 deletions

View File

@ -0,0 +1,44 @@
type JsonReviver = (name: string, value: any) => any;
export function apiGet(path: string, options?: {
jsonReviver?: JsonReviver
}): Promise<any> {
return apiRequest(path, 'GET', null, options);
}
export function apiPost(path: string, data?: object, options?: {
jsonReviver?: JsonReviver
}): Promise<any> {
return apiRequest(path, 'POST', data, options);
}
export function apiDelete(path: string, options?: {
jsonReviver?: JsonReviver
}): Promise<any> {
return apiRequest(path, 'DELETE', null, options);
}
async function apiRequest(
path: string,
method: 'GET' | 'POST' | 'DELETE',
data?: object,
options?: {
jsonReviver?: JsonReviver
}
): Promise<any> {
const res = await fetch(path, {
method: method,
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
body: data == undefined ? null : JSON.stringify(data),
});
const reviver = options?.jsonReviver;
if (reviver != undefined) {
return JSON.parse(await res.text(), reviver);
} else {
return await res.json();
}
}

View File

@ -1,6 +1,7 @@
import React, { Fragment } from 'react';
import { NavLink, Redirect } from 'react-router-dom';
import { apiPost } from '../apiHelpers';
import ErrorBox from '../components/error-box';
import InfoBox from '../components/info-box';
import { compareObjects } from '../helpers';
@ -162,15 +163,10 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
*/
async handleLike(like: boolean) {
try {
const res = await fetch(`/api/buildings/${this.props.building.building_id}/like.json`, {
method: 'POST',
headers:{
'Content-Type': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({like: like})
});
const data = await res.json();
const data = await apiPost(
`/api/buildings/${this.props.building.building_id}/like.json`,
{like: like}
);
if (data.error) {
this.setState({error: data.error});
@ -188,15 +184,10 @@ const withCopyEdit = (WrappedComponent: React.ComponentType<CategoryViewProps>)
this.setState({error: undefined});
try {
const res = await fetch(`/api/buildings/${this.props.building.building_id}.json`, {
method: 'POST',
body: JSON.stringify(this.state.buildingEdits),
headers:{
'Content-Type': 'application/json'
},
credentials: 'same-origin'
});
const data = await res.json();
const data = await apiPost(
`/api/buildings/${this.props.building.building_id}.json`,
this.state.buildingEdits
);
if (data.error) {
this.setState({error: data.error});

View File

@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import './edit-history.css';
import { apiGet } from '../../apiHelpers';
import { Building } from '../../models/building';
import { EditHistoryEntry } from '../../models/edit-history-entry';
import ContainerHeader from '../container-header';
@ -17,10 +18,9 @@ const EditHistory: React.FunctionComponent<EditHistoryProps> = (props) => {
useEffect(() => {
const fetchData = async () => {
const res = await fetch(`/api/buildings/${props.building.building_id}/history.json`);
const data = await res.json();
const {history} = await apiGet(`/api/buildings/${props.building.building_id}/history.json`);
setHistory(data.history);
setHistory(history);
};
if (props.building != undefined) { // only call fn if there is a building provided

View File

@ -5,6 +5,7 @@ import { Redirect, Route, RouteComponentProps, Switch } from 'react-router-dom';
import { parseJsonOrDefault } from '../helpers';
import { strictParseInt } from '../parse';
import { apiGet, apiPost } from './apiHelpers';
import BuildingView from './building/building-view';
import Categories from './building/categories';
import { EditHistory } from './building/edit-history/edit-history';
@ -64,16 +65,9 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
async fetchLatestRevision() {
try {
const res = await fetch(`/api/buildings/revision`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin'
});
const data = await res.json();
const {latestRevisionId} = await apiGet(`/api/buildings/revision`);
this.increaseRevision(data.latestRevisionId);
this.increaseRevision(latestRevisionId);
} catch(error) {
console.error(error);
}
@ -89,27 +83,9 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
// TODO: simplify API calls, create helpers for fetching data
const buildingId = strictParseInt(this.props.match.params.building);
let [building, building_uprns, building_like] = await Promise.all([
fetch(`/api/buildings/${buildingId}.json`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin'
}).then(res => res.json()),
fetch(`/api/buildings/${buildingId}/uprns.json`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin'
}).then(res => res.json()),
fetch(`/api/buildings/${buildingId}/like.json`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin'
}).then(res => res.json())
apiGet(`/api/buildings/${buildingId}.json`),
apiGet(`/api/buildings/${buildingId}/uprns.json`),
apiGet(`/api/buildings/${buildingId}/like.json`)
]);
building.uprns = building_uprns.uprns;
@ -158,15 +134,8 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
this.increaseRevision(building.revision_id);
// get UPRNs and update
fetch(`/api/buildings/${building.building_id}/uprns.json`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin'
}).then(
res => res.json()
).then((res) => {
apiGet(`/api/buildings/${building.building_id}/uprns.json`)
.then((res) => {
if (res.error) {
console.error(res);
} else {
@ -179,15 +148,8 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
});
// get if liked and update
fetch(`/api/buildings/${building.building_id}/like.json`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin'
}).then(
res => res.json()
).then((res) => {
apiGet(`/api/buildings/${building.building_id}/like.json`)
.then((res) => {
if (res.error) {
console.error(res);
} else {
@ -225,37 +187,21 @@ class MapApp extends React.Component<MapAppProps, MapAppState> {
}
likeBuilding(buildingId) {
fetch(`/api/buildings/${buildingId}/like.json`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({ like: true })
}).then(
res => res.json()
).then(function (res) {
apiPost(`/api/buildings/${buildingId}/like.json`, { like: true })
.then(res => {
if (res.error) {
console.error({ error: res.error });
} else {
this.increaseRevision(res.revision_id);
}
}.bind(this)).catch(
}).catch(
(err) => console.error({ error: err })
);
}
updateBuilding(buildingId, data) {
fetch(`/api/buildings/${buildingId}.json`, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin'
}).then(
res => res.json()
).then(res => {
apiPost(`/api/buildings/${buildingId}.json`, data)
.then(res => {
if (res.error) {
console.error({ error: res.error });
} else {

View File

@ -5,6 +5,7 @@ import { AttributionControl, GeoJSON, Map, TileLayer, ZoomControl } from 'react-
import 'leaflet/dist/leaflet.css';
import './map.css';
import { apiGet } from '../apiHelpers';
import { HelpIcon } from '../components/icons';
import { Building } from '../models/building';
@ -58,13 +59,9 @@ class ColouringMap extends Component<ColouringMapProps, ColouringMapState> {
handleClick(e) {
const mode = this.props.mode;
const lat = e.latlng.lat;
const lng = e.latlng.lng;
fetch(
'/api/buildings/locate?lat='+lat+'&lng='+lng
).then(
(res) => res.json()
).then(function(data){
const { lat, lng } = e.latlng;
apiGet(`/api/buildings/locate?lat=${lat}&lng=${lng}`)
.then(data => {
if (data && data.length){
const building = data[0];
if (mode === 'multi-edit') {
@ -82,7 +79,7 @@ class ColouringMap extends Component<ColouringMapProps, ColouringMapState> {
this.props.selectBuilding(undefined);
}
}
}.bind(this)).catch(
}).catch(
(err) => console.error(err)
);
}
@ -94,8 +91,8 @@ class ColouringMap extends Component<ColouringMapProps, ColouringMapState> {
}
async getBoundary() {
const res = await fetch('/geometries/boundary-detailed.geojson');
const data = await res.json() as GeoJsonObject;
const data = await apiGet('/geometries/boundary-detailed.geojson') as GeoJsonObject;
this.setState({
boundary: data
});

View File

@ -3,6 +3,7 @@ import React, { Component } from 'react';
import './search-box.css';
import { apiGet } from '../apiHelpers';
import { SearchIcon } from '../components/icons';
interface SearchResult {
@ -96,11 +97,8 @@ class SearchBox extends Component<SearchBoxProps, SearchBoxState> {
fetching: true
});
fetch(
'/api/search?q='+this.state.q
).then(
(res) => res.json()
).then((data) => {
apiGet(`/api/search?q=${this.state.q}`)
.then((data) => {
if (data && data.results){
this.setState({
results: data.results,

View File

@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import { apiGet } from '../apiHelpers';
import { BuildingEditSummary } from '../building/edit-history/building-edit-summary';
import InfoBox from '../components/info-box';
import { EditHistoryEntry } from '../models/edit-history-entry';
@ -9,10 +10,9 @@ const ChangesPage = () => {
useEffect(() => {
const fetchData = async () => {
const res = await fetch(`/api/history`);
const data = await res.json();
const {history} = await apiGet(`/api/history`);
setHistory(data.history);
setHistory(history);
};
fetchData();

View File

@ -2,6 +2,7 @@ import React, { FunctionComponent } from 'react';
import { Link } from 'react-router-dom';
import { dateReviver } from '../../helpers';
import { apiGet } from '../apiHelpers';
interface ExtractViewModel {
@ -28,11 +29,9 @@ export default class DataExtracts extends React.Component<{}, DataExtractsState>
}
async componentDidMount() {
const res = await fetch('/api/extracts');
const data = JSON.parse(await res.text(), dateReviver);
let data = await apiGet('/api/extracts', { jsonReviver: dateReviver});
const extracts = (data.extracts as ExtractViewModel[])
.sort((a, b) => a.extracted_on.valueOf() - b.extracted_on.valueOf())
.reverse();
.sort((a, b) => b.extracted_on.valueOf() - a.extracted_on.valueOf());

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react';
import { Link, Redirect } from 'react-router-dom';
import { apiGet, apiPost } from '../apiHelpers';
import ErrorBox from '../components/error-box';
import InfoBox from '../components/info-box';
import SupporterLogos from '../components/supporter-logos';
@ -39,24 +40,13 @@ class Login extends Component<LoginProps, any> {
event.preventDefault();
this.setState({error: undefined});
fetch('/api/login', {
method: 'POST',
body: JSON.stringify(this.state),
headers:{
'Content-Type': 'application/json'
},
credentials: 'same-origin'
}).then(
res => res.json()
).then(function(res){
apiPost('/api/login', this.state)
.then(res => {
if (res.error) {
this.setState({error: res.error});
} else {
fetch('/api/users/me', {
credentials: 'same-origin'
}).then(
(res) => res.json()
).then(user => {
apiGet('/api/users/me')
.then(user => {
if (user.error) {
this.setState({error: user.error});
} else {
@ -66,7 +56,7 @@ class Login extends Component<LoginProps, any> {
(err) => this.setState({error: err})
);
}
}.bind(this)).catch(
}).catch(
(err) => this.setState({error: err})
);
}

View File

@ -1,6 +1,7 @@
import React, { Component, FormEvent } from 'react';
import { Link, Redirect } from 'react-router-dom';
import { apiDelete, apiPost } from '../apiHelpers';
import ConfirmationModal from '../components/confirmation-modal';
import ErrorBox from '../components/error-box';
import { User } from '../models/user';
@ -32,12 +33,8 @@ class MyAccountPage extends Component<MyAccountPageProps, MyAccountPageState> {
event.preventDefault();
this.setState({error: undefined});
fetch('/api/logout', {
method: 'POST',
credentials: 'same-origin'
}).then(
res => res.json()
).then(function(res){
apiPost('/api/logout')
.then(function(res){
if (res.error) {
this.setState({error: res.error});
} else {
@ -52,18 +49,14 @@ class MyAccountPage extends Component<MyAccountPageProps, MyAccountPageState> {
event.preventDefault();
this.setState({error: undefined});
fetch('/api/api/key', {
method: 'POST',
credentials: 'same-origin'
}).then(
res => res.json()
).then(function(res){
apiPost('/api/api/key')
.then(res => {
if (res.error) {
this.setState({error: res.error});
} else {
this.props.updateUser(res);
}
}.bind(this)).catch(
}).catch(
(err) => this.setState({error: err})
);
}
@ -81,11 +74,7 @@ class MyAccountPage extends Component<MyAccountPageProps, MyAccountPageState> {
this.setState({ error: undefined });
try {
const res = await fetch('/api/users/me', {
method: 'DELETE',
credentials: 'same-origin'
});
const data = await res.json();
const data = await apiDelete('/api/users/me');
if(data.error) {
this.setState({ error: data.error });

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react';
import { Link, Redirect } from 'react-router-dom';
import { apiGet, apiPost } from '../apiHelpers';
import ErrorBox from '../components/error-box';
import InfoBox from '../components/info-box';
import SupporterLogos from '../components/supporter-logos';
@ -48,36 +49,21 @@ class SignUp extends Component<SignUpProps, SignUpState> {
} as Pick<SignUpState, keyof SignUpState>);
}
handleSubmit(event) {
async handleSubmit(event) {
event.preventDefault();
this.setState({error: undefined});
fetch('/api/users', {
method: 'POST',
body: JSON.stringify(this.state),
headers:{
'Content-Type': 'application/json'
},
credentials: 'same-origin'
}).then(
res => res.json()
).then(function(res){
try {
const res = await apiPost('/api/users', this.state);
if(res.error) {
this.setState({ error: res.error });
} else {
fetch('/api/users/me', {
credentials: 'same-origin'
}).then(
(res) => res.json()
).then(
(user) => this.props.login(user)
).catch(
(err) => this.setState({error: err})
);
const user = await apiGet('/api/users/me');
this.props.login(user);
}
} catch(err) {
this.setState({error: err});
}
}.bind(this)).catch(
(err) => this.setState({error: err})
);
}
render() {