Better feedbacks in UI

This commit is contained in:
Lucas Verney 2018-03-04 23:37:10 +01:00
parent b5946ccc84
commit f042214c92
6 changed files with 188 additions and 48 deletions

64
cuizin/js_src/api.js Normal file
View File

@ -0,0 +1,64 @@
import * as constants from '@/constants';
function loadJSON(response) {
return response.json();
}
function _postProcessRecipes(response) {
return loadJSON(response)
.then((jsonResponse) => {
const parsed = jsonResponse;
if (parsed.recipes) {
// Customize instructions
parsed.recipes = parsed.recipes.map(item => Object.assign(
item,
{
instructions: item.instructions.split(/\r\n/).map(
line => line.trim(),
),
},
));
}
return parsed;
});
}
export function loadRecipes() {
return fetch(`${constants.API_URL}api/v1/recipes`)
.then(_postProcessRecipes);
}
export function loadRecipe(id) {
return fetch(`${constants.API_URL}api/v1/recipe/${id}`)
.then(_postProcessRecipes);
}
export function refetchRecipe(id) {
return fetch(`${constants.API_URL}api/v1/recipe/${id}/refetch`, {
method: 'GET',
})
.then(_postProcessRecipes);
}
export function postRecipe(recipe) {
return fetch(`${constants.API_URL}api/v1/recipes`, {
method: 'POST',
body: JSON.stringify(recipe),
})
.then(_postProcessRecipes);
}
export function deleteRecipe(id) {
return fetch(`${constants.API_URL}api/v1/recipe/${id}`, {
method: 'DELETE',
});
}

View File

@ -0,0 +1,33 @@
<template>
<v-dialog v-if="error" v-model="error" max-width="500px">
<v-card>
<v-card-title class="headline">Error</v-card-title>
<v-card-text>
{{ description }} {{ value.message }}.
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="secondary" flat @click.stop="error=null">Cancel</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
props: {
description: String,
value: [Error, Boolean],
},
computed: {
error: {
get() {
return this.value;
},
set(val) {
this.$emit('input', val);
},
},
},
};
</script>

View File

@ -2,6 +2,8 @@
<v-container fluid grid-list-md> <v-container fluid grid-list-md>
<Loader v-if="isLoading"></Loader> <Loader v-if="isLoading"></Loader>
<v-layout row wrap v-else> <v-layout row wrap v-else>
<ErrorDialog :v-model="error" description="Unable to load recipes: " />
<v-flex xs12 v-if="!recipes.length" class="text-xs-center"> <v-flex xs12 v-if="!recipes.length" class="text-xs-center">
<p>Start by adding a recipe with the "+" button on the top right corner!</p> <p>Start by adding a recipe with the "+" button on the top right corner!</p>
</v-flex> </v-flex>
@ -32,16 +34,20 @@
</template> </template>
<script> <script>
import * as constants from '@/constants'; import * as api from '@/api';
import ErrorDialog from '@/components/ErrorDialog';
import Loader from '@/components/Loader'; import Loader from '@/components/Loader';
export default { export default {
components: { components: {
ErrorDialog,
Loader, Loader,
}, },
data() { data() {
return { return {
isLoading: false, isLoading: false,
error: null,
recipes: [], recipes: [],
}; };
}, },
@ -56,11 +62,14 @@ export default {
fetchRecipes() { fetchRecipes() {
this.isLoading = true; this.isLoading = true;
fetch(`${constants.API_URL}api/v1/recipes`) api.loadRecipes()
.then(response => response.json())
.then((response) => { .then((response) => {
this.recipes = response.recipes; this.recipes = response.recipes;
this.isLoading = false; this.isLoading = false;
})
.catch((error) => {
this.error = error;
this.isLoading = false;
}); });
}, },
}, },

View File

@ -1,6 +1,17 @@
<template> <template>
<v-container text-xs-center> <v-container text-xs-center v-if="isImporting">
<v-layout row wrap> <v-layout row wrap>
<Loader></Loader>
<v-flex xs12>
<p>Importing...</p>
</v-flex>
</v-layout>
</v-container>
<v-container text-xs-center v-else>
<v-layout row wrap>
<ErrorDialog v-model="error" description="Unable to import recipe: " />
<v-flex xs12> <v-flex xs12>
<h2>Import from URL</h2> <h2>Import from URL</h2>
<v-form v-model="validImport"> <v-form v-model="validImport">
@ -12,7 +23,7 @@
></v-text-field> ></v-text-field>
<v-btn <v-btn
@click="submitImport" @click="submitImport"
:disabled="!validImport || disabledImport" :disabled="!validImport || isImporting"
> >
Import Import
</v-btn> </v-btn>
@ -87,15 +98,23 @@
</template> </template>
<script> <script>
import * as constants from '@/constants'; import * as api from '@/api';
import ErrorDialog from '@/components/ErrorDialog';
import Loader from '@/components/Loader';
export default { export default {
components: {
ErrorDialog,
Loader,
},
data() { data() {
return { return {
error: null,
url: null, url: null,
validImport: false, validImport: false,
disabledImport: false, isImporting: false,
validAdd: false, validAdd: false,
title: null, title: null,
picture: null, picture: null,
@ -121,21 +140,18 @@ export default {
}, },
methods: { methods: {
submitImport() { submitImport() {
this.disabledImport = true; this.isImporting = true;
fetch(`${constants.API_URL}api/v1/recipes`, { api.postRecipe({ url: this.url })
method: 'POST', .then(response => this.$router.push({
body: JSON.stringify({
url: this.url,
}),
})
.then(response => response.json())
.then(response => response.recipes[0])
.then(recipe => this.$router.push({
name: 'Recipe', name: 'Recipe',
params: { params: {
recipeId: recipe.id, recipeId: response.recipes[0].id,
}, },
})); }))
.catch((error) => {
this.isImporting = false;
this.error = error;
});
}, },
submitAdd() { submitAdd() {
// TODO // TODO

View File

@ -2,6 +2,10 @@
<v-container grid-list-md class="panel"> <v-container grid-list-md class="panel">
<Loader v-if="isLoading"></Loader> <Loader v-if="isLoading"></Loader>
<v-layout row v-else> <v-layout row v-else>
<ErrorDialog :v-model="errorDelete" description="Unable to delete recipe: " />
<ErrorDialog :v-model="errorFetch" description="Unable to fetch recipe: " />
<ErrorDialog :v-model="errorRefetch" description="Unable to refetch recipe: " />
<v-dialog v-model="refetchConfirm" max-width="500px"> <v-dialog v-model="refetchConfirm" max-width="500px">
<v-card> <v-card>
<v-card-title class="headline">Refetch recipe</v-card-title> <v-card-title class="headline">Refetch recipe</v-card-title>
@ -74,58 +78,74 @@
</template> </template>
<script> <script>
import * as constants from '@/constants'; import * as api from '@/api';
import ErrorDialog from '@/components/ErrorDialog';
import Loader from '@/components/Loader'; import Loader from '@/components/Loader';
export default { export default {
components: { components: {
ErrorDialog,
Loader, Loader,
}, },
data() { data() {
return { return {
isLoading: false, isLoading: false,
recipe: null, recipe: null,
errorFetch: null,
errorDelete: null,
errorRefetch: null,
deleteConfirm: false, deleteConfirm: false,
refetchConfirm: false, refetchConfirm: false,
}; };
}, },
created() { created() {
this.fetchRecipe(); this.loadRecipe();
}, },
watch: { watch: {
// call again the method if the route changes // call again the method if the route changes
$route: 'fetchRecipe', $route: 'loadRecipe',
}, },
methods: { methods: {
handleRecipesResponse(response) { handleRecipesResponse(response) {
if (response.recipes.length < 1) {
this.$router.replace({
name: 'Home',
});
}
this.recipe = response.recipes[0]; this.recipe = response.recipes[0];
this.recipe.instructions = this.recipe.instructions.split(/\r\n/).map(item => item.trim());
this.isLoading = false; this.isLoading = false;
}, },
fetchRecipe() { loadRecipe() {
this.isLoading = true; this.isLoading = true;
fetch(`${constants.API_URL}api/v1/recipe/${this.$route.params.recipeId}`) api.loadRecipe(this.$route.params.recipeId)
.then(response => response.json()) .then(this.handleRecipesResponse)
.then(this.handleRecipesResponse); .catch((error) => {
this.isLoading = false;
this.errorFetch = error;
});
}, },
handleDelete() { handleDelete() {
this.isLoading = true; this.isLoading = true;
this.deleteConfirm = false; this.deleteConfirm = false;
fetch(`${constants.API_URL}api/v1/recipe/${this.$route.params.recipeId}`, { api.deleteRecipe(this.$route.params.recipeId)
method: 'DELETE', .then(() => this.$router.replace('/'))
}) .catch((error) => {
.then(() => this.$router.replace('/')); this.isLoading = false;
this.errorDelete = error;
});
}, },
handleRefetch() { handleRefetch() {
this.isLoading = true; this.isLoading = true;
this.refetchConfirm = false; this.refetchConfirm = false;
fetch(`${constants.API_URL}api/v1/recipe/${this.$route.params.recipeId}/refetch`, { api.refetchRecipe(this.$route.params.recipeId)
method: 'GET', .then(this.handleRecipesResponse)
}) .catch((error) => {
.then(response => response.json()) this.isLoading = false;
.then(this.handleRecipesResponse); this.errorRefetch = error;
});
}, },
}, },
}; };

View File

@ -2,6 +2,7 @@ import json
import os import os
import bottle import bottle
import peewee
from cuizin import db from cuizin import db
from cuizin.scraping import fetch_recipe from cuizin.scraping import fetch_recipe
@ -11,7 +12,7 @@ MODULE_DIR = os.path.dirname(os.path.realpath(__file__))
app = bottle.Bottle() app = bottle.Bottle()
@app.hook('after_request') @app.hook('before_request')
def enable_cors(): def enable_cors():
""" """
Add CORS headers at each request. Add CORS headers at each request.
@ -66,19 +67,17 @@ def api_v1_recipes_post():
'error': 'No URL provided' 'error': 'No URL provided'
} }
recipes = []
try: try:
# Try to add
return {
'recipes': [fetch_recipe(data['url']).to_dict()]
}
except peewee.IntegrityError:
# Redirect to pre-existing recipe if already there
recipe = db.Recipe.select().where( recipe = db.Recipe.select().where(
db.Recipe.url == data['url'] db.Recipe.url == data['url']
).first() ).first()
assert recipe bottle.redirect('/api/v1/recipe/%s' % recipe.id, 301)
recipes = [recipe.to_dict()]
except AssertionError:
recipes = [fetch_recipe(data['url']).to_dict()]
return {
'recipes': recipes
}
@app.route('/api/v1/recipe/:id', ['GET', 'OPTIONS']) @app.route('/api/v1/recipe/:id', ['GET', 'OPTIONS'])
@ -112,8 +111,7 @@ def api_v1_recipe_refetch(id):
db.Recipe.id == id db.Recipe.id == id
).first() ).first()
if not recipe: if not recipe:
# TODO: Error return bottle.abort(400, 'No recipe with id %s.' % id)
pass
recipe = fetch_recipe(recipe.url, recipe=recipe) recipe = fetch_recipe(recipe.url, recipe=recipe)