Better feedbacks in UI
This commit is contained in:
parent
b5946ccc84
commit
f042214c92
64
cuizin/js_src/api.js
Normal file
64
cuizin/js_src/api.js
Normal 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',
|
||||
});
|
||||
}
|
33
cuizin/js_src/components/ErrorDialog.vue
Normal file
33
cuizin/js_src/components/ErrorDialog.vue
Normal 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>
|
@ -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;
|
||||
});
|
||||
},
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user