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>
<Loader v-if="isLoading"></Loader>
<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">
<p>Start by adding a recipe with the "+" button on the top right corner!</p>
</v-flex>
@ -32,16 +34,20 @@
</template>
<script>
import * as constants from '@/constants';
import * as api from '@/api';
import ErrorDialog from '@/components/ErrorDialog';
import Loader from '@/components/Loader';
export default {
components: {
ErrorDialog,
Loader,
},
data() {
return {
isLoading: false,
error: null,
recipes: [],
};
},
@ -56,11 +62,14 @@ export default {
fetchRecipes() {
this.isLoading = true;
fetch(`${constants.API_URL}api/v1/recipes`)
.then(response => response.json())
api.loadRecipes()
.then((response) => {
this.recipes = response.recipes;
this.isLoading = false;
})
.catch((error) => {
this.error = error;
this.isLoading = false;
});
},
},

View File

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

View File

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

View File

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