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>
|
<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;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user