diff --git a/frontend/src/App.js b/frontend/src/App.js index dbccafd..2982bda 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -5,6 +5,8 @@ import Login from './components/Login'; import Gamenights from './components/Gamenights'; import Gamenight from './components/Gamenight'; +import { get_gamenights, get_games, unpack_api_result, login } from './api/Api'; + const localStorageUserKey = 'user'; function App() { @@ -15,24 +17,14 @@ function App() { const [games, setGames] = useState([]); const [activeGamenight, setActiveGamenight] = useState(null); + const POST_HEADER = {'Content-Type': 'application/json'}; + const AUTH_HEADER = {'Authorization': `Bearer ${user?.jwt}`}; + const handleLogin = (input) => { - const requestOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(input) - }; - fetch('api/login', requestOptions) - .then(response => response.json()) - .then(data => { - if(data.result === "Ok") { - setUser(data.user); - localStorage.setItem(localStorageUserKey, JSON.stringify(data.user)); - } else { - setFlashData({ - type: "Error", - message: data.message - }); - } + unpack_api_result(login(input), setFlashData) + .then(result => { + setUser(result.user); + localStorage.setItem(localStorageUserKey, JSON.stringify(result.user)); }); }; @@ -55,44 +47,16 @@ function App() { useEffect(() => { if (user !== null) { - const requestOptions = { - method: 'GET', - headers: { 'Authorization': `Bearer ${user.jwt}` }, - }; - fetch('api/gamenights', requestOptions) - .then(response => response.json()) - .then(data => { - if(data.result === "Ok") { - setGamenights(data.gamenights) - } else { - setFlashData({ - type: "Error", - message: data.message - }); - } - }); + unpack_api_result(get_gamenights(user.jwt), setFlashData) + .then(result => setGamenights(result.gamenights)); } }, [user]) useEffect(() => { if (user !== null) { - const requestOptions = { - method: 'GET', - headers: { 'Authorization': `Bearer ${user.jwt}` }, - }; - fetch('api/games', requestOptions) - .then(response => response.json()) - .then(data => { - if(data.result === "Ok") { - setGames(data.games) - } else { - setFlashData({ - type: "Error", - message: data.message - }); - } - }); - } + unpack_api_result(get_games(user.jwt), setFlashData) + .then(result => setGames(result.games)); + } }, [user]) useEffect(() => { diff --git a/frontend/src/api/Api.js b/frontend/src/api/Api.js new file mode 100644 index 0000000..182ea18 --- /dev/null +++ b/frontend/src/api/Api.js @@ -0,0 +1,70 @@ + +import fetchResource from './FetchResource' + +export function unpack_api_result(promise, onError) { + promise.then(result => { + if(result.result !== 'Ok') { + onError({ + type: 'Error', + message: result.message + }); + } + }) + .catch(error => { + onError({ + type: 'Error', + message: `${error.status} ${error.message}` + }); + }); + return promise; +} + +export function get_gamenights(token) { + return fetchResource('api/gamenights', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}` + }, + }); +} + +export function post_gamenight(input, token) { + return fetchResource('api/gamenights', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(input) + }); +} + +export function delete_gamenight(input, token) { + return fetchResource('api/gamenights', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(input) + }); +} + +export function get_games(token) { + return fetchResource('api/games', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}` + } + }); +} + +export function login(body) { + return fetchResource('api/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }); +} \ No newline at end of file diff --git a/frontend/src/api/FetchResource.js b/frontend/src/api/FetchResource.js new file mode 100644 index 0000000..252c79a --- /dev/null +++ b/frontend/src/api/FetchResource.js @@ -0,0 +1,75 @@ +const API_URL = ''; + +function ApiError(message, data, status) { + let response = null; + let isObject = false; + + try { + response = JSON.parse(data); + isObject = true; + } catch (e) { + response = data; + } + + this.response = response; + this.message = message; + this.status = status; + this.toString = function () { + return `${ this.message }\nResponse:\n${ isObject ? JSON.stringify(this.response, null, 2) : this.response }`; + }; +} + +const fetchResource = (path, userOptions = {}) => { + const defaultOptions = {}; + const defaultHeaders = { + 'Content-Type': 'application/json' + }; + + const options = { + ...defaultOptions, + ...userOptions, + headers: { + ...defaultHeaders, + ...userOptions.headers, + }, + }; + + const url = `${ API_URL }/${ path }`; + const isFile = options.body instanceof File; + + if (options.body && typeof options.body === 'object' && !isFile) { + options.body = JSON.stringify(options.body); + } + + let response = null; + + return fetch(url, options) + .then(responseObject => { + response = responseObject; + + if (response.status === 401) { + } + + if (response.status < 200 || response.status >= 300) { + return response.text(); + } + + return response.json(); + }) + .then(parsedResponse => { + if (response.status < 200 || response.status >= 300) { + throw parsedResponse; + } + + return parsedResponse; + }) + .catch(error => { + if (response) { + throw new ApiError(`Request failed with status ${ response.status }.`, error, response.status); + } else { + throw new ApiError(error.toString(), null, 'REQUEST_FAILED'); + } + }); +}; + +export default fetchResource; \ No newline at end of file diff --git a/frontend/src/components/AddGameNight.jsx b/frontend/src/components/AddGameNight.jsx index dc2a746..83b34a5 100644 --- a/frontend/src/components/AddGameNight.jsx +++ b/frontend/src/components/AddGameNight.jsx @@ -9,6 +9,7 @@ import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import GameAdder from './GameAdder'; +import { post_gamenight, unpack_api_result} from '../api/Api'; function AddGameNight(props) { const [expanded, setExpanded] = useState(false); @@ -44,32 +45,14 @@ function AddGameNight(props) { game_list: gameList, } - const requestOptions = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${props.user.jwt}` - }, - body: JSON.stringify(input) - }; - - fetch('api/gamenights', requestOptions) - .then(response => response.json()) - .then(data => { - if(data.result !== "Ok") { - props.setFlash({ - type: "Error", - message: data.message - }); - } else { - setExpanded(false); - setGameName(""); - setDate(null); - } + unpack_api_result(post_gamenight(input, props.user.jwt), props.setFlash) + .then(result => { + setExpanded(false); + setGameName(""); + setDate(null); }) .then(() => props.refetchGamenights()) } - }; if(expanded) { diff --git a/frontend/src/components/Gamenights.jsx b/frontend/src/components/Gamenights.jsx index f034cfa..a86903c 100644 --- a/frontend/src/components/Gamenights.jsx +++ b/frontend/src/components/Gamenights.jsx @@ -9,35 +9,15 @@ import GamesIcon from '@mui/icons-material/Games'; import DeleteIcon from '@mui/icons-material/Delete'; import AddGameNight from './AddGameNight'; +import {delete_gamenight, unpack_api_result} from '../api/Api'; function Gamenights(props) { const [dense, setDense] = React.useState(false); - const DeleteGamenight = (gameId) => { + const DeleteGamenight = (game_id) => { if (props.user !== null) { - let input = { - game_id: gameId, - } - - const requestOptions = { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${props.user.jwt}` - }, - body: JSON.stringify(input) - }; - - fetch('api/gamenights', requestOptions) - .then(response => response.json()) - .then(data => { - if(data.result !== "Ok") { - props.setFlash({ - type: "Error", - message: data.message - }); - } - }) + const input = { game_id: game_id }; + unpack_api_result(delete_gamenight(input, props.user.jwt), props.setFlash) .then(() => props.refetchGamenights()); } }