init
This commit is contained in:
657
frontend/package-lock.json
generated
657
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,12 +10,13 @@
|
||||
"dependencies": {
|
||||
"core-js": "^2.6.5",
|
||||
"vue": "^2.6.6",
|
||||
"vue-router": "^3.0.2",
|
||||
"vuetify": "^1.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.5.0",
|
||||
"@vue/cli-plugin-eslint": "^3.5.0",
|
||||
"@vue/cli-service": "^3.5.0",
|
||||
"@vue/cli-service": "^3.6.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"eslint": "^5.8.0",
|
||||
"eslint-plugin-vue": "^5.0.0",
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<title>frontend</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
|
||||
@@ -1,27 +1,119 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<ToolBar/>
|
||||
<v-toolbar app tabs>
|
||||
<v-toolbar-side-icon
|
||||
@click.stop="showNav = !showNav"
|
||||
></v-toolbar-side-icon>
|
||||
<v-toolbar-title>{{ title }}</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn icon
|
||||
v-for="(tool, idx) in tools"
|
||||
:key="`tool-${idx}`"
|
||||
>
|
||||
<v-icon
|
||||
:color="tool.color ? tool.color : 'dark'"
|
||||
v-html="tool.icon"
|
||||
></v-icon>
|
||||
</v-btn>
|
||||
|
||||
<template v-slot:extension>
|
||||
<router-view name="extension"></router-view>
|
||||
</template>
|
||||
</v-toolbar>
|
||||
<v-navigation-drawer
|
||||
fixed
|
||||
app
|
||||
clipped
|
||||
v-model="showNav"
|
||||
>
|
||||
<v-list dense>
|
||||
<v-list-tile
|
||||
v-for="link in links"
|
||||
:key="link.path"
|
||||
:to="link.path"
|
||||
>
|
||||
<v-list-tile-avatar>
|
||||
<v-icon v-html="link.icon"
|
||||
></v-icon>
|
||||
</v-list-tile-avatar>
|
||||
<v-list-tile-title v-html="link.text"
|
||||
></v-list-tile-title>
|
||||
</v-list-tile>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
<v-content>
|
||||
<HelloWorld/>
|
||||
<router-view></router-view>
|
||||
</v-content>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HelloWorld from './components/HelloWorld'
|
||||
import ToolBar from './components/ToolBar'
|
||||
|
||||
import NotFound from './routes/NotFound'
|
||||
|
||||
export const RecipeCategories = {
|
||||
0: "Petit-déjeuner",
|
||||
1: "Entrée",
|
||||
2: "Plat",
|
||||
3: "Dessert"
|
||||
}
|
||||
// *** TOOLS ***
|
||||
//
|
||||
// * create_recipe: go to creation page
|
||||
// * import_recipe: go to import page
|
||||
// ---- Recipe list
|
||||
// * category_tabs: navigate through categories
|
||||
// * search: show input inside extension
|
||||
// ---- Recipe details
|
||||
// * go_back: go back to previous page
|
||||
// * edit: edit fields of model details
|
||||
// * add_to_menu: show menu slot picker in bottom sheet
|
||||
// ---- Planning
|
||||
// * complete_menu: ask for automatic completion of menu
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
HelloWorld,
|
||||
ToolBar
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
//
|
||||
links: [],
|
||||
categories: RecipeCategories,
|
||||
showNav: null,//
|
||||
}
|
||||
},
|
||||
created () {
|
||||
// Build navigation links from router.js config
|
||||
for (var idx in this.$router.options.routes) {
|
||||
var route = this.$router.options.routes[idx];
|
||||
this.links.push({
|
||||
path: route.path,
|
||||
text: route.meta.title,
|
||||
icon: route.meta.icon,
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tools: function() {
|
||||
return [{icon: 'add'},]
|
||||
},
|
||||
// TODO: move this inside routing events
|
||||
title: function() {
|
||||
var title = "CookAssistant"
|
||||
for (var key in this.$route.matched) {
|
||||
var route = this.$route.matched[key];
|
||||
if (route.hasOwnProperty('meta')) {
|
||||
if (route.meta.title != undefined) {
|
||||
title = route.meta.title
|
||||
}
|
||||
}
|
||||
}
|
||||
return title;
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html { overflow-y: auto }
|
||||
</style>
|
||||
|
||||
153
frontend/src/api.js
Normal file
153
frontend/src/api.js
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
|
||||
TODO:
|
||||
Implement 'create'/'edit'/'read' modes with generic
|
||||
configuration retrieved from OPTIONS request (using api.js)
|
||||
|
||||
'read' would use GET methods
|
||||
'edit' would use PUT actions
|
||||
'create' would use POST action
|
||||
|
||||
*/
|
||||
const baseUrl = "http://localhost:8000/api"
|
||||
|
||||
/*
|
||||
Check that the response status is what's expected.
|
||||
Returns an object with :
|
||||
* success: bool
|
||||
* data: response data
|
||||
*/
|
||||
const processStatusCode = function(expected_code) {
|
||||
return function(response) {
|
||||
const status_code = response.status;
|
||||
let data;
|
||||
if (expected_code != 204) {
|
||||
data = response.json();
|
||||
} else {
|
||||
data = null
|
||||
}
|
||||
return Promise.all([status_code, data])
|
||||
.then(ret => ({
|
||||
success: ret[0] == expected_code,
|
||||
data: ret[1]
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Returns data on success or throw an error with data.
|
||||
*/
|
||||
const processSuccess = function(result) {
|
||||
if (!result.success) {
|
||||
throw result.data;
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
const _headers = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
const _request = {
|
||||
get (url) {
|
||||
return fetch(
|
||||
url,
|
||||
{ method: 'GET',
|
||||
})
|
||||
.then(processStatusCode(200))
|
||||
.then(processSuccess);
|
||||
},
|
||||
post (url, data) {
|
||||
return fetch(
|
||||
url,
|
||||
{ method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: _headers,
|
||||
})
|
||||
.then(processStatusCode(201))
|
||||
.then(processSuccess)
|
||||
},
|
||||
put (url, data) {
|
||||
return fetch(
|
||||
url,
|
||||
{ method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
headers: _headers,
|
||||
})
|
||||
.then(processStatusCode(200))
|
||||
.then(processSuccess)
|
||||
},
|
||||
delete(url) {
|
||||
return fetch(
|
||||
url,
|
||||
{ method: 'DELETE',
|
||||
headers: _headers, })
|
||||
.then(processStatusCode(204))
|
||||
.then(processSuccess)
|
||||
},
|
||||
options (url) {
|
||||
return fetch(
|
||||
url,
|
||||
{ method: 'OPTIONS',
|
||||
})
|
||||
.then(processStatusCode(200))
|
||||
.then(processSuccess)
|
||||
}
|
||||
}
|
||||
|
||||
const mountEndPoint = function(url) {
|
||||
return {
|
||||
read (id) {
|
||||
return _request.get(url + id + '/')
|
||||
},
|
||||
create (data) {
|
||||
return _request.post(url, data)
|
||||
},
|
||||
edit (id, data) {
|
||||
return _request.put(url + id + '/', data)
|
||||
},
|
||||
delete (id) {
|
||||
return _request.delete(url + id + '/')
|
||||
},
|
||||
options() { return _request.options(url) }
|
||||
}
|
||||
}
|
||||
|
||||
const mountNestedEndPoint = function(url) {
|
||||
const getUrl = function(parent_id) {
|
||||
// TODO: Make it safe against url injection !
|
||||
return url.replace('*', parent_id)
|
||||
}
|
||||
return {
|
||||
read (parent_id, id) {
|
||||
return _request.get(getUrl(parent_id) + id + '/')
|
||||
},
|
||||
create (parent_id, data) {
|
||||
return _request.post(
|
||||
getUrl(parent_id),
|
||||
data,
|
||||
)
|
||||
},
|
||||
edit (parent_id, id, data) {
|
||||
return _request.put(
|
||||
getUrl(parent_id) + id + '/',
|
||||
data,
|
||||
)
|
||||
},
|
||||
delete (parent_id, id) {
|
||||
return _request.delete( getUrl(parent_id) + id + '/' )
|
||||
},
|
||||
options () {
|
||||
return _request.options( getUrl(1) )
|
||||
},
|
||||
list (parent_id) {
|
||||
return _request.get( getUrl(parent_id) )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
recipes: mountEndPoint(`${baseUrl}/recips/`),
|
||||
ingredients: mountNestedEndPoint(
|
||||
`${baseUrl}/recips/*/amounts/`
|
||||
),
|
||||
}
|
||||
40
frontend/src/components/CategoryTabs.vue
Normal file
40
frontend/src/components/CategoryTabs.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<!-- Categories list -->
|
||||
<span>
|
||||
<v-tabs v-if="showTabs" :mandatory="false">
|
||||
<v-tab v-for="key in Object.keys(categories)"
|
||||
:key="`cat-${key}`"
|
||||
:to="`/recipes/category/${key}`">
|
||||
{{ categories[key] }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<template v-else>
|
||||
<v-toolbar-title>
|
||||
Détails
|
||||
</v-toolbar-title>
|
||||
<v-btn v-show="!showTabs"
|
||||
icon
|
||||
@click="$router.go(-1)"
|
||||
><v-icon>arrow_back</v-icon></v-btn>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { RecipeCategories } from '../App'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
categories: RecipeCategories,
|
||||
}),
|
||||
computed: {
|
||||
showTabs: function() {
|
||||
return !(this.$route.path.startsWith("/recipes/id"))
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -1,147 +0,0 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-layout
|
||||
text-xs-center
|
||||
wrap
|
||||
>
|
||||
<v-flex xs12>
|
||||
<v-img
|
||||
:src="require('../assets/logo.svg')"
|
||||
class="my-3"
|
||||
contain
|
||||
height="200"
|
||||
></v-img>
|
||||
</v-flex>
|
||||
|
||||
<v-flex mb-4>
|
||||
<h1 class="display-2 font-weight-bold mb-3">
|
||||
Welcome to Vuetify
|
||||
</h1>
|
||||
<p class="subheading font-weight-regular">
|
||||
For help and collaboration with other Vuetify developers,
|
||||
<br>please join our online
|
||||
<a href="https://community.vuetifyjs.com" target="_blank">Discord Community</a>
|
||||
</p>
|
||||
</v-flex>
|
||||
|
||||
<v-flex
|
||||
mb-5
|
||||
xs12
|
||||
>
|
||||
<h2 class="headline font-weight-bold mb-3">What's next?</h2>
|
||||
|
||||
<v-layout justify-center>
|
||||
<a
|
||||
v-for="(next, i) in whatsNext"
|
||||
:key="i"
|
||||
:href="next.href"
|
||||
class="subheading mx-3"
|
||||
target="_blank"
|
||||
>
|
||||
{{ next.text }}
|
||||
</a>
|
||||
</v-layout>
|
||||
</v-flex>
|
||||
|
||||
<v-flex
|
||||
xs12
|
||||
mb-5
|
||||
>
|
||||
<h2 class="headline font-weight-bold mb-3">Important Links</h2>
|
||||
|
||||
<v-layout justify-center>
|
||||
<a
|
||||
v-for="(link, i) in importantLinks"
|
||||
:key="i"
|
||||
:href="link.href"
|
||||
class="subheading mx-3"
|
||||
target="_blank"
|
||||
>
|
||||
{{ link.text }}
|
||||
</a>
|
||||
</v-layout>
|
||||
</v-flex>
|
||||
|
||||
<v-flex
|
||||
xs12
|
||||
mb-5
|
||||
>
|
||||
<h2 class="headline font-weight-bold mb-3">Ecosystem</h2>
|
||||
|
||||
<v-layout justify-center>
|
||||
<a
|
||||
v-for="(eco, i) in ecosystem"
|
||||
:key="i"
|
||||
:href="eco.href"
|
||||
class="subheading mx-3"
|
||||
target="_blank"
|
||||
>
|
||||
{{ eco.text }}
|
||||
</a>
|
||||
</v-layout>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({
|
||||
ecosystem: [
|
||||
{
|
||||
text: 'vuetify-loader',
|
||||
href: 'https://github.com/vuetifyjs/vuetify-loader'
|
||||
},
|
||||
{
|
||||
text: 'github',
|
||||
href: 'https://github.com/vuetifyjs/vuetify'
|
||||
},
|
||||
{
|
||||
text: 'awesome-vuetify',
|
||||
href: 'https://github.com/vuetifyjs/awesome-vuetify'
|
||||
}
|
||||
],
|
||||
importantLinks: [
|
||||
{
|
||||
text: 'Documentation',
|
||||
href: 'https://vuetifyjs.com'
|
||||
},
|
||||
{
|
||||
text: 'Chat',
|
||||
href: 'https://community.vuetifyjs.com'
|
||||
},
|
||||
{
|
||||
text: 'Made with Vuetify',
|
||||
href: 'https://madewithvuetifyjs.com'
|
||||
},
|
||||
{
|
||||
text: 'Twitter',
|
||||
href: 'https://twitter.com/vuetifyjs'
|
||||
},
|
||||
{
|
||||
text: 'Articles',
|
||||
href: 'https://medium.com/vuetify'
|
||||
}
|
||||
],
|
||||
whatsNext: [
|
||||
{
|
||||
text: 'Explore components',
|
||||
href: 'https://vuetifyjs.com/components/api-explorer'
|
||||
},
|
||||
{
|
||||
text: 'Select a layout',
|
||||
href: 'https://vuetifyjs.com/layout/pre-defined'
|
||||
},
|
||||
{
|
||||
text: 'Frequently Asked Questions',
|
||||
href: 'https://vuetifyjs.com/getting-started/frequently-asked-questions'
|
||||
}
|
||||
|
||||
]
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
26
frontend/src/components/RessourceField.vue
Normal file
26
frontend/src/components/RessourceField.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<v-select v-if="type == 'choice'"
|
||||
:label="label"
|
||||
placeholder="----"
|
||||
:items="choices"
|
||||
item-text="display_name"
|
||||
:value="value"
|
||||
@change="val => $emit('input', val)"
|
||||
/>
|
||||
<v-text-field v-else
|
||||
:label="label"
|
||||
:type="type"
|
||||
:value="value"
|
||||
@input="val => $emit('input', val)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//TODO: work with nested object fields...
|
||||
export default {
|
||||
props: ['value', 'label', 'read_only', 'required', 'type', 'choices'],
|
||||
data () {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,37 +0,0 @@
|
||||
<template>
|
||||
<v-toolbar dark color="primary">
|
||||
<!-- <v-toolbar-side-icon></v-toolbar-side-icon> -->
|
||||
|
||||
<v-toolbar-title class="white--text">
|
||||
Cook Assistant
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn icon>
|
||||
<v-icon>search</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon>
|
||||
<v-icon>apps</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon>
|
||||
<v-icon>refresh</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon>
|
||||
<v-icon>more_vert</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
87
frontend/src/components/recipes/Ingredient.vue
Normal file
87
frontend/src/components/recipes/Ingredient.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<v-card-text class="py-1" v-if="!editing">
|
||||
{{ item.display }}
|
||||
</v-card-text>
|
||||
<v-input v-else
|
||||
:append-icon="append_icon"
|
||||
@click:append="appendIconAction"
|
||||
:messages="errors"
|
||||
>
|
||||
<v-container fluid class="pa-0 ma-0">
|
||||
<v-layout>
|
||||
<v-flex xs3>
|
||||
<RField
|
||||
v-bind="fields.amount"
|
||||
v-model="item.amount"
|
||||
@input="markUpdated"
|
||||
/>
|
||||
</v-flex>
|
||||
<v-flex xs3>
|
||||
<RField
|
||||
v-bind="fields.unit"
|
||||
v-model="item.unit"
|
||||
@input="markUpdated"
|
||||
/>
|
||||
</v-flex>
|
||||
<v-flex xs6>
|
||||
<RField
|
||||
v-bind="fields.ingredient.children.name"
|
||||
v-model="item.ingredient.name"
|
||||
@input="markUpdated"
|
||||
/>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-input>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '../../api.js'
|
||||
import RField from '../RessourceField'
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
components: { RField },
|
||||
props: ["item", "fields", "editing"],
|
||||
data() {
|
||||
return {
|
||||
errors: [],
|
||||
updated: false,
|
||||
append_icon: "delete"
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isNew () {
|
||||
return !this.item.ingredient.id
|
||||
},
|
||||
},
|
||||
created () {
|
||||
if (this.isNew) {
|
||||
this.append_icon = "add"
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
appendIconAction () {
|
||||
if (this.isNew) {
|
||||
this.$emit('create', this)
|
||||
} else {
|
||||
// TODO: should ask for confirmation
|
||||
this.$emit('delete', this)
|
||||
}
|
||||
},
|
||||
markUpdated () {
|
||||
this.updated = true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
editing(newValue) {
|
||||
if (newValue == false && this.updated) {
|
||||
// TODO: check if there has been updates
|
||||
this.$emit('save', this)
|
||||
this.updated = false
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
103
frontend/src/components/recipes/IngredientList.vue
Normal file
103
frontend/src/components/recipes/IngredientList.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<v-card class="pa-1">
|
||||
<v-card-title>
|
||||
<strong>Ingrédients</strong>
|
||||
</v-card-title>
|
||||
<ingredient-details
|
||||
v-for="ingdt in edited_ingdts"
|
||||
:item="ingdt"
|
||||
:fields="fields"
|
||||
:editing="editing"
|
||||
@delete="deleteItem"
|
||||
@save="saveItem"
|
||||
/>
|
||||
<!-- New ingredient in editing mode -->
|
||||
<p v-if="editing">
|
||||
(+) <ingredient-details
|
||||
:item="newItem()"
|
||||
:fields="fields"
|
||||
:editing="editing"
|
||||
@create="createItem"
|
||||
/>
|
||||
</p>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '../../api.js'
|
||||
import Ingredient from './Ingredient'
|
||||
function NullIngredient (recipe_id) {
|
||||
this.recipe = recipe_id
|
||||
this.ingredient = { name: "" }
|
||||
this.amount = 0
|
||||
this.unit = ""
|
||||
this.display = ""
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'ingredient-details': Ingredient,
|
||||
},
|
||||
props: ['ingredients', 'editing', 'recipe'],
|
||||
data () {
|
||||
return {
|
||||
fields: {},
|
||||
edited_ingdts: [],
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// Copy the property to mutate it
|
||||
api.ingredients.options()
|
||||
.then(opts => {
|
||||
this.fields = opts.actions.POST
|
||||
})
|
||||
console.log("get list:", this.recipe)
|
||||
api.ingredients.list(this.recipe)
|
||||
.then(data => this.edited_ingdts = data)
|
||||
.catch(err => console.error("error reading ingdt:", err))
|
||||
},
|
||||
methods: {
|
||||
newItem () {
|
||||
return new NullIngredient(this.recipe)
|
||||
},
|
||||
createItem (comp) {
|
||||
let item = comp.item
|
||||
item.recipe = this.recipe
|
||||
api.ingredients.create(
|
||||
this.recipe,
|
||||
item
|
||||
)
|
||||
.then(data => {
|
||||
this.edited_ingdts.push(data)
|
||||
//TODO: reset additionnal Ingredient
|
||||
})
|
||||
.catch(err => comp.errors.push(JSON.stringify(err)))
|
||||
},
|
||||
saveItem(comp) {
|
||||
let item = comp.item
|
||||
item.recipe = this.recipe // Inject recipe's id into data
|
||||
api.ingredients.edit(
|
||||
item.recipe,
|
||||
item.ingredient.id,
|
||||
item,
|
||||
)
|
||||
.then(data => {
|
||||
// TODO: better identification for ingredients
|
||||
// BUG: will not work if ingredient changes!!
|
||||
let idx = this.edited_ingdts.indexOf(it => it.ingredient == data.ingredient)
|
||||
this.edited_ingdts[idx] = data
|
||||
})
|
||||
.catch(err => comp.errors.push(JSON.stringify(err)))
|
||||
},
|
||||
deleteItem(comp) {
|
||||
let item = comp.item
|
||||
api.ingredients.delete(this.recipe, item.ingredient.id)
|
||||
.then(() => {
|
||||
var idx = this.edited_ingdts.indexOf(item)
|
||||
this.edited_ingdts.splice(idx, 1)
|
||||
})
|
||||
.catch(err => comp.errors.push(JSON.stringify(err)))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
194
frontend/src/components/recipes/RecipeDetails.vue
Normal file
194
frontend/src/components/recipes/RecipeDetails.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<!-- Details view of a Recipe
|
||||
|
||||
Has read/edit modes
|
||||
|
||||
-->
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-layout wrap>
|
||||
<v-flex xs10> <!-- Heading -->
|
||||
<v-alert v-for="(error,idx) in errors"
|
||||
:key="`alert-${idx}`"
|
||||
:value="true">
|
||||
Error: {{ error }}
|
||||
</v-alert>
|
||||
<v-card-text v-if="!editing">
|
||||
<h6 class="title pb-3">
|
||||
{{item.name}}
|
||||
</h6>
|
||||
<p class="subheading pb-3">
|
||||
{{categories[item.category]}}
|
||||
</p>
|
||||
</v-card-text>
|
||||
<template v-else>
|
||||
<RField
|
||||
v-bind="fields.name"
|
||||
v-model="item.name"
|
||||
/>
|
||||
<RField
|
||||
v-bind="fields.category"
|
||||
v-model="item.category"
|
||||
/>
|
||||
</template>
|
||||
</v-flex>
|
||||
<v-flex xs2> <!-- Buttons -->
|
||||
<!-- Add to planning -->
|
||||
<v-bottom-sheet v-show="!editing"
|
||||
v-model="sheetSelectSlot"
|
||||
>
|
||||
<template v-slot:activator>
|
||||
<v-btn
|
||||
fab dark
|
||||
color="success"
|
||||
><v-icon>add</v-icon></v-btn>
|
||||
</template>
|
||||
<!-- Put a meal picker here... -->
|
||||
<v-card elevation="10" style="min-height: 300px;">
|
||||
<v-card-text>
|
||||
Content of bottom sheet
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-bottom-sheet>
|
||||
<!-- Edit mode -->
|
||||
<v-fab-transition>
|
||||
<v-btn
|
||||
fab dark
|
||||
:small="!editing"
|
||||
color="error"
|
||||
@click="switchEdit"
|
||||
>
|
||||
<v-icon v-if="editing">save</v-icon>
|
||||
<v-icon v-else>edit</v-icon>
|
||||
</v-btn>
|
||||
</v-fab-transition>
|
||||
</v-flex>
|
||||
<v-flex xs12 md4>
|
||||
<IngredientList v-if="!isLoading"
|
||||
:editing="editing"
|
||||
:recipe="item.id"
|
||||
:ingredients="item.ingredients"
|
||||
/>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RField from '../RessourceField'
|
||||
import { RecipeCategories } from '../../App'
|
||||
import api from '../../api.js'
|
||||
import NotFound from '../../routes/NotFound'
|
||||
const IngredientList = import('./IngredientList')
|
||||
|
||||
function NullRecipe() {
|
||||
this.name = String();
|
||||
this.category = Number();
|
||||
this.ingredients = [];
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IngredientList: () => ({
|
||||
component: IngredientList,
|
||||
loading: NotFound, //TODO: actual loading view
|
||||
error: NotFound,
|
||||
delay: 100,
|
||||
timeout: 3000,
|
||||
}),
|
||||
RField,
|
||||
},
|
||||
data() {
|
||||
// Transform categories for v-select component
|
||||
var select_items = []
|
||||
Object.keys(RecipeCategories)
|
||||
.map(function(key) {
|
||||
select_items.push({
|
||||
text: RecipeCategories[key],
|
||||
value: key,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
isLoading: true,
|
||||
sheetSelectSlot: false,
|
||||
editing: false,
|
||||
select_items,
|
||||
categories: RecipeCategories,
|
||||
errors: [],
|
||||
// Will be populated by initRecipeItem method
|
||||
item: new NullRecipe,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
switchEdit: function() {
|
||||
if (this.editing) {
|
||||
var data = {
|
||||
name: this.item.name,
|
||||
category: this.item.category,
|
||||
}
|
||||
if (this.item.id) {
|
||||
// Edit recipe
|
||||
api.recipes.edit(this.item.id, data)
|
||||
.then(data => {
|
||||
this.item = data
|
||||
this.editing = false
|
||||
})
|
||||
.catch(err => this.errors.push(err))
|
||||
} else {
|
||||
// Create recipe
|
||||
api.recipes.create(data)
|
||||
.then(data => {
|
||||
this.item = data
|
||||
this.editing = false
|
||||
})
|
||||
.catch(err => this.errors.push(err))
|
||||
}
|
||||
} else {
|
||||
this.editing = true
|
||||
}
|
||||
},
|
||||
initRecipeItem: function(id) {
|
||||
// An id of 0 means we want a creation page
|
||||
if (id == 0) {
|
||||
this.item = new NullRecipe
|
||||
this.editing = true
|
||||
} else {
|
||||
api.recipes.read(id)
|
||||
.then(data => {
|
||||
this.errors = []
|
||||
this.item = data
|
||||
})
|
||||
.catch(err => {
|
||||
this.errors.push(err)
|
||||
this.item = new NullRecipe
|
||||
})
|
||||
.then(() => this.isLoading = false)
|
||||
}
|
||||
},
|
||||
updateRecip: function(form_data) {
|
||||
console.log(form_data);
|
||||
var messages = [];
|
||||
return messages;
|
||||
}
|
||||
},
|
||||
created () {
|
||||
api.recipes.options()
|
||||
.then(data => this.fields = data.actions.POST)
|
||||
.then(() => console.log(this.fields))
|
||||
},
|
||||
beforeRouteEnter (to, from, next) {
|
||||
next(vm => {
|
||||
const id = to.params.id;
|
||||
vm.initRecipeItem(id);
|
||||
})
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
const id = to.params.id;
|
||||
this.initRecipeItem(id);
|
||||
next()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
48
frontend/src/components/recipes/RecipeList.vue
Normal file
48
frontend/src/components/recipes/RecipeList.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
/* List view of recipes */
|
||||
<template>
|
||||
<v-list>
|
||||
<template v-for="(item, idx) in items">
|
||||
<v-list-tile
|
||||
:key="item.title"
|
||||
:to="`/recipes/id/${item.id}`">
|
||||
<v-list-tile-content>
|
||||
{{item.name}}
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
<v-divider v-if="idx < (items.length - 1)"
|
||||
:key="idx"></v-divider>
|
||||
</template>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
// Recipes from db grouped by categories id
|
||||
content: {
|
||||
0: [], 1: [], 2: [], 3: []
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
items: function() {
|
||||
return this.content[this.$route.params.cat];
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
const vm = this;
|
||||
fetch('http://localhost:8000/api/recips/')
|
||||
.then((response) => response.json())
|
||||
.then(function(data) {
|
||||
for (var idx in data) {
|
||||
var obj = data[idx];
|
||||
vm.content[parseInt(obj.category)].push(obj);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -1,9 +1,11 @@
|
||||
import Vue from 'vue'
|
||||
import './plugins/vuetify'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
render: h => h(App),
|
||||
}).$mount('#app')
|
||||
|
||||
46
frontend/src/router.js
Normal file
46
frontend/src/router.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
|
||||
import Index from './routes/Index'
|
||||
import NotFound from './routes/NotFound'
|
||||
import Recipes from './routes/Recipes'
|
||||
import Planning from './routes/Planning'
|
||||
|
||||
import RecipeDetails from './components/recipes/RecipeDetails'
|
||||
import RecipeList from './components/recipes/RecipeList'
|
||||
import CategoryTabs from './components/CategoryTabs'
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
const router = new Router({
|
||||
routes: [
|
||||
{ path: '/',
|
||||
component: Index,
|
||||
meta: { title: "Home", icon: "home" }
|
||||
},
|
||||
|
||||
{ path: '/recipes',
|
||||
components: { default: Recipes,
|
||||
extension: CategoryTabs, },
|
||||
meta: { title: "Recettes",
|
||||
icon:"book" },
|
||||
children: [
|
||||
{ path: 'id/:id',
|
||||
component: RecipeDetails,
|
||||
meta: { subtitle: "Détails", } },
|
||||
{ path: 'category/:cat',
|
||||
component: RecipeList,
|
||||
meta: { subtitle: "Liste", } },
|
||||
{ path: '*',
|
||||
component: NotFound }
|
||||
]
|
||||
},
|
||||
|
||||
{ path: '/planning',
|
||||
component: Planning,
|
||||
meta: { title: "Menu", icon: "calendar_today" },
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
||||
21
frontend/src/routes/Index.vue
Normal file
21
frontend/src/routes/Index.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<section>
|
||||
<v-img
|
||||
:src="require('../assets/logo.svg')"
|
||||
class="ma-3"
|
||||
contain
|
||||
height="200"
|
||||
></v-img>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
9
frontend/src/routes/NotFound.vue
Normal file
9
frontend/src/routes/NotFound.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<v-alert value="true">Not found</v-alert>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({})
|
||||
}
|
||||
</script>
|
||||
91
frontend/src/routes/Planning.vue
Normal file
91
frontend/src/routes/Planning.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<v-list two-line overflow-y>
|
||||
<template v-for="(item, index) in items">
|
||||
<v-subheader v-if="item.header"
|
||||
:key="item.header"
|
||||
>{{item.header}}</v-subheader>
|
||||
<v-divider v-else-if="item.divider"></v-divider>
|
||||
<v-list-tile v-else
|
||||
:key="index">
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-sub-title
|
||||
>{{ item.subtitle }}</v-list-tile-sub-title>
|
||||
<v-list-tile-title
|
||||
>{{ item.title }}</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
<v-list-tile-action v-show="item.title">
|
||||
<v-btn icon color="primary" small
|
||||
:to="`/recipes/id/${item.id}`"
|
||||
><v-icon>remove_red_eye</v-icon></v-btn>
|
||||
</v-list-tile-action>
|
||||
<v-list-tile-action>
|
||||
<v-btn icon color="warning" small>
|
||||
<v-icon>
|
||||
{{ item.title ? 'delete' : 'add' }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-list-tile-action>
|
||||
</v-list-tile>
|
||||
</template>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
const MockPlanning = {
|
||||
"Lundi": {
|
||||
"Midi": "Raclette",
|
||||
"Soir": "Soupe",
|
||||
},
|
||||
"Mardi": {
|
||||
"Midi": null,
|
||||
"Soir": null,
|
||||
},
|
||||
"Mercredi": {
|
||||
"Midi": null,
|
||||
"Soir": null,
|
||||
},
|
||||
"Jeudi": {
|
||||
"Midi": null,
|
||||
"Soir": null,
|
||||
},
|
||||
"Vendredi": {
|
||||
"Midi": null,
|
||||
"Soir": null,
|
||||
},
|
||||
"Samedi": {
|
||||
"Midi": null,
|
||||
"Soir": null,
|
||||
},
|
||||
"Dimanche": {
|
||||
"Midi": null,
|
||||
"Soir": null,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
data() {
|
||||
var items = [];
|
||||
|
||||
for (var day in MockPlanning) {
|
||||
items.push({ divider: true })
|
||||
items.push({ header: day });
|
||||
for (var meal in MockPlanning[day]) {
|
||||
items.push({ id: 0, subtitle: meal, title: MockPlanning[day][meal] });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
toolActions: [
|
||||
{ icon: "find_replace", color: "success" },
|
||||
],
|
||||
planning: MockPlanning,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
14
frontend/src/routes/Recipes.vue
Normal file
14
frontend/src/routes/Recipes.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
}),
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
13
frontend/src/views/Home.vue
Normal file
13
frontend/src/views/Home.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<HelloWorld />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HelloWorld from '../components/HelloWorld'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HelloWorld
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
const BundleTracker = require("webpack-bundle-tracker");
|
||||
|
||||
module.exports = {
|
||||
baseUrl: "http://127.0.0.1:8080/",
|
||||
publicPath: "http://localhost:8080/",
|
||||
outputDir: './dist/',
|
||||
|
||||
chainWebpack: config => {
|
||||
@@ -17,8 +17,8 @@ module.exports = {
|
||||
.set('__STATIC__', 'static')
|
||||
|
||||
config.devServer
|
||||
.public('http://0.0.0.0:8080')
|
||||
.host('0.0.0.0')
|
||||
.public('http://localhost:8080')
|
||||
.host('localhost')
|
||||
.port(8080)
|
||||
.hotOnly(true)
|
||||
.watchOptions({poll: 1000})
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"status":"done","publicPath":"http://127.0.0.1:8080/","chunks":{"app":[{"name":"app.js","publicPath":"http://127.0.0.1:8080/app.js","path":"C:\\Users\\lecor\\Documents\\Dev\\Python\\cookAssistant\\frontend\\dist\\app.js"},{"name":"app.a2bf8f46c2ce5a8648c5.hot-update.js","publicPath":"http://127.0.0.1:8080/app.a2bf8f46c2ce5a8648c5.hot-update.js","path":"C:\\Users\\lecor\\Documents\\Dev\\Python\\cookAssistant\\frontend\\dist\\app.a2bf8f46c2ce5a8648c5.hot-update.js"}]},"error":"ModuleError","message":"Module Error (from ./node_modules/vue-loader/lib/loaders/templateLoader.js):\n(Emitted value instead of an instance of Error) \n\n Errors compiling template:\n\n tag <v-toolbar-items> has no matching end tag.\n\n 7 | </v-toolbar-title>\n 8 | <v-spacer></v-spacer>\n 9 | <v-toolbar-items>\n | ^^^^^^^^^^^^^^^^^\n 10 | <v-btn\n 11 | flat\n"}
|
||||
{"status":"done","publicPath":"http://localhost:8080/","chunks":{"null":[{"name":"0.js","publicPath":"http://localhost:8080/0.js","path":"/home/artus/Documents/cookAssistant/frontend/dist/0.js"},{"name":"0.880af34e5f0e1c2d5443.hot-update.js","publicPath":"http://localhost:8080/0.880af34e5f0e1c2d5443.hot-update.js","path":"/home/artus/Documents/cookAssistant/frontend/dist/0.880af34e5f0e1c2d5443.hot-update.js"}],"app":[{"name":"app.js","publicPath":"http://localhost:8080/app.js","path":"/home/artus/Documents/cookAssistant/frontend/dist/app.js"}]},"error":"ModuleBuildError","message":"Module build failed (from ./node_modules/babel-loader/lib/index.js):\nSyntaxError: /home/artus/Documents/cookAssistant/frontend/src/components/recipes/IngredientList.vue: Unexpected token (85:70)\n\n 83 | )\n 84 | .then(data => {\n> 85 | var idx = this.edited_ingdts.indexOf(i => i.ingredient.id ==)\n | ^\n 86 | })\n 87 | .catch(err => comp.errors.push(JSON.stringify(err)))\n 88 | },\n at Object.raise (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:3851:17)\n at Object.unexpected (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:5167:16)\n at Object.parseExprAtom (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:6328:20)\n at Object.parseExprAtom (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:3570:20)\n at Object.parseExprSubscripts (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:5914:23)\n at Object.parseMaybeUnary (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:5894:21)\n at Object.parseExprOpBaseRightExpr (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:5854:34)\n at Object.parseExprOpRightExpr (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:5847:21)\n at Object.parseExprOp (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:5826:27)\n at Object.parseExprOps (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:5791:17)\n at Object.parseMaybeConditional (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:5754:23)\n at Object.parseMaybeAssign (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:5701:21)\n at Object.parseFunctionBody (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:6891:24)\n at Object.parseArrowExpression (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:6851:10)\n at Object.parseExprAtom (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:6213:18)\n at Object.parseExprAtom (/home/artus/Documents/cookAssistant/frontend/node_modules/@babel/parser/lib/index.js:3570:20)"}
|
||||
Reference in New Issue
Block a user