shopping list and import view ux

This commit is contained in:
Kaibu
2022-04-23 14:47:10 +02:00
parent 161ae9879a
commit 8149192455
5 changed files with 375 additions and 132 deletions

View File

@@ -1,42 +1,156 @@
<template> <template>
<div id="app"> <div id="app">
<br/>
<template v-if="import_info !== undefined"> <template v-if="import_info !== undefined">
<template v-if="import_info.running"> <template v-if="import_info.running">
<h5 style="text-align: center">{{ $t('Importing') }}...</h5> <h5 style="text-align: center">{{ $t('Importing') }}...</h5>
<b-progress :max="import_info.total_recipes"> <b-progress :max="import_info.total_recipes" height="2rem">
<b-progress-bar :value="import_info.imported_recipes" :label="`${import_info.imported_recipes}/${import_info.total_recipes}`"></b-progress-bar> <b-progress-bar :value="import_info.imported_recipes"
:label="`${import_info.imported_recipes}/${import_info.total_recipes}`"></b-progress-bar>
</b-progress> </b-progress>
<loading-spinner :size="25"></loading-spinner> <loading-spinner :size="25"></loading-spinner>
</template> </template>
<b-row>
<b-col>
<b-card no-body>
<b-card-header>
<h4>{{ $t('Information') }}
<b-badge variant="success" class="float-right" v-if="!import_info.running">{{
$t('Import_finished')
}}!
</b-badge>
<b-badge variant="primary" v-else class="float-right">
{{ $t('Import_running') }}
<b-spinner small class="d-inline-block"></b-spinner>
</b-badge>
</h4>
<b-alert variant="danger" show v-if="imported_recipes.error">{{
$t('Import_Error')
}}
</b-alert>
<b-alert variant="warning" show v-if="imported_recipes.duplicates_total > 0">{{
$t("Import_Result_Info", {
imported: imported_recipes.imported_total,
total: imported_recipes.recipes.length
})
}}
</b-alert>
<b-alert variant="success" show v-if="imported_recipes.duplicates_total === 0">{{
$t("Import_Result_Info", {
imported: imported_recipes.imported_total,
total: imported_recipes.recipes.length
})
}}
</b-alert>
<b-row>
<b-col cols="12" class="text-center">
<h5><a
:href="`${resolveDjangoUrl('view_search') }?keyword=${import_info.keyword.id}`"
v-if="import_info.keyword !== null">{{ $t('View_Recipes') }}</a></h5>
</b-col>
</b-row>
</b-card-header>
<b-card-body>
<transition-group name="slide" tag="div" class="row">
<b-media v-for="(recipe, index) in imported_recipes.recipes"
v-bind:key="recipe.id"
class="p-3 mt-2 col-md-6 col-12 shadow-sm" v-hover>
<template #aside>
<b-avatar target="_blank"
:href="resolveDjangoUrl('view_recipe', recipe.id)"
v-if="recipe.imported !== undefined && recipe.imported"
class="mr-3"><i :class="recipe.icon"></i></b-avatar>
<b-avatar v-else class="mr-3"><i :class="recipe.icon"></i></b-avatar>
</template>
<h5 class="mt-0 mb-1">
<a :href="resolveDjangoUrl('view_recipe', recipe.id)"
v-if="recipe.imported !== undefined && recipe.imported"
target="_blank">{{
recipe.recipe_name
}}</a> <span v-else>{{ recipe.recipe_name }}</span>
<b-badge class="float-right text-white">{{ index + 1 }}</b-badge>
</h5>
<p class="mb-0">
<b-badge v-if="recipe.imported !== undefined && recipe.imported"
variant="success">{{ $t('Imported') }}
</b-badge>
<b-badge v-if="recipe.imported !== undefined && !recipe.imported"
variant="warning">{{ $t('Duplicate') }}
</b-badge>
<b-badge v-if="recipe.imported === undefined">
<b-spinner small class="d-inline-block"></b-spinner>
</b-badge>
</p>
</b-media>
</transition-group>
</b-card-body>
<b-card-footer>
<h4>{{ $t('Information') }}
<b-badge variant="success" class="float-right" v-if="!import_info.running">{{
$t('Import_finished')
}}!
</b-badge>
<b-badge variant="primary" v-else class="float-right">
{{ $t('Import_running') }}
<b-spinner small class="d-inline-block"></b-spinner>
</b-badge>
</h4>
<b-alert variant="danger" show v-if="imported_recipes.error">{{
$t('Import_Error')
}}
</b-alert>
<b-alert variant="warning" show v-if="imported_recipes.duplicates_total > 0">{{
$t("Import_Result_Info", {
imported: imported_recipes.imported_total,
total: imported_recipes.recipes.length
})
}}
</b-alert>
<b-alert variant="success" show v-if="imported_recipes.duplicates_total === 0">{{
$t("Import_Result_Info", {
imported: imported_recipes.imported_total,
total: imported_recipes.recipes.length
})
}}
</b-alert>
<b-row>
<b-col cols="12" class="text-center">
<h5><a
:href="`${resolveDjangoUrl('view_search') }?keyword=${import_info.keyword.id}`"
v-if="import_info.keyword !== null">{{ $t('View_Recipes') }}</a></h5>
</b-col>
</b-row>
</b-card-footer>
</b-card>
</b-col>
</b-row>
<div class="row"> <b-row>
<div class="col col-md-12" v-if="!import_info.running"> <b-col cols="12">
<span>{{ $t('Import_finished') }}! </span> <b-card no-body>
<a :href="`${resolveDjangoUrl('view_search') }?keyword=${import_info.keyword.id}`" <b-card-header>
v-if="import_info.keyword !== null">{{ $t('View_Recipes') }}</a> <h4>{{ $t('Details') }}
<b-button v-b-toggle.collapse-details variant="primary" class="float-right">
{{ $t('Toggle') }}
</b-button>
</h4>
</b-card-header>
<b-card-body>
</div> <b-collapse id="collapse-details" class="mt-2">
</div> <b-card>
<textarea id="id_textarea" ref="output_text" class="form-control"
<br/> style="height: 50vh"
<div class="row">
<div class="col col-md-12">
<label for="id_textarea">{{ $t('Information') }}</label>
<textarea id="id_textarea" ref="output_text" class="form-control" style="height: 50vh"
v-html="import_info.msg" v-html="import_info.msg"
disabled></textarea> disabled></textarea>
</b-card>
</div> </b-collapse>
</div> </b-card-body>
<br/> </b-card>
<br/> </b-col>
</b-row>
</template> </template>
</div> </div>
@@ -49,7 +163,7 @@ import {BootstrapVue} from 'bootstrap-vue'
import 'bootstrap-vue/dist/bootstrap-vue.css' import 'bootstrap-vue/dist/bootstrap-vue.css'
import {ResolveUrlMixin, ToastMixin} from "@/utils/utils"; import {ResolveUrlMixin, ToastMixin, RandomIconMixin} from "@/utils/utils";
import LoadingSpinner from "@/components/LoadingSpinner"; import LoadingSpinner from "@/components/LoadingSpinner";
@@ -62,10 +176,61 @@ export default {
mixins: [ mixins: [
ResolveUrlMixin, ResolveUrlMixin,
ToastMixin, ToastMixin,
RandomIconMixin
], ],
components: { components: {
LoadingSpinner LoadingSpinner
}, },
computed: {
imported_recipes: function () {
let msg = this.import_info.msg.split(/\r?\n/)
let out = {recipes: [], imported_total: 0, duplicates_total: 0, info: '', error: false}
if (this.import_info.msg.includes("--------------------")) {
out.error = true
}
msg.forEach((cur, i) => {
let line = cur.trim()
if (line === '') {
return
}
if (this.isNumber(line.charAt(0))) {
let recipe = line.split(/-(.*)/s)
out.recipes.push({
id: recipe[0].trim(),
recipe_name: recipe[1].trim(),
icon: this.getRandomFoodIcon()
})
} else {
if (i === msg.length - 4) {
out.info = line
}
if (i === msg.length - 2) {
out.imported_total = parseInt(line.match(/\d+/)[0])
}
}
if (out.info !== '') {
let items = out.info.split(/:(.*)/s)[1]
items = items.split(",")
out.duplicates_total = items.length
out.recipes.forEach((recipe) => {
recipe.imported = true
items.forEach((item) => {
if (recipe.recipe_name === item.trim()) {
recipe.imported = false
}
})
})
} else {
if (out.imported_total > 0) {
out.recipes.forEach((recipe) => {
recipe.imported = true
})
}
}
})
return out
}
},
data() { data() {
return { return {
import_id: window.IMPORT_ID, import_id: window.IMPORT_ID,
@@ -90,15 +255,48 @@ export default {
apiClient.retrieveImportLog(this.import_id).then(result => { apiClient.retrieveImportLog(this.import_id).then(result => {
this.import_info = result.data this.import_info = result.data
}) })
},
isNumber: function (char) {
if (typeof char !== 'string') {
return false;
} }
if (char.trim() === '') {
return false;
} }
return !isNaN(char);
}
}, directives: {
hover: {
inserted: function (el) {
el.addEventListener("mouseenter", () => {
el.classList.add("shadow")
})
el.addEventListener("mouseleave", () => {
el.classList.remove("shadow")
})
},
},
},
} }
</script> </script>
<style> <style>
.slide-leave-active,
.slide-enter-active {
transition: 1s;
}
.slide-enter {
transform: translate(100%, 0);
}
.slide-leave-to {
transform: translate(-100%, 0);
}
</style> </style>

View File

@@ -1,12 +1,6 @@
<template> <template>
<div id="app"> <div id="app">
<div> <div>
<div class="row">
<div class="col col-md-12">
<h2>{{ $t('Import') }}</h2>
</div>
</div>
<div class="row"> <div class="row">
<div class="col col-md-12"> <div class="col col-md-12">
<b-tabs content-class="mt-3" v-model="tab_index"> <b-tabs content-class="mt-3" v-model="tab_index">
@@ -282,13 +276,13 @@
<b-badge variant="success" class="ml-1" v-b-tooltip:top <b-badge variant="success" class="ml-1" v-b-tooltip:top
:title="$t('Import_Supported')"><i :title="$t('Import_Supported')"><i
class="fas fa-file-import fa-fw" v-if="i.import"></i></b-badge> class="fas fa-file-import fa-fw" v-if="i.import"></i></b-badge>
<b-badge variant="warning" class="ml-1" v-b-tooltip:top <b-badge variant="warning" class="ml-1 text-muted" v-b-tooltip:top
:title="$t('Import_Not_Yet_Supported')"><i :title="$t('Import_Not_Yet_Supported')"><i
class="fas fa-file-import fa-fw" v-if="!i.import"></i></b-badge> class="fas fa-file-import fa-fw" v-if="!i.import"></i></b-badge>
<b-badge variant="success" class="ml-1" v-b-tooltip:top <b-badge variant="success" class="ml-1" v-b-tooltip:top
:title="$t('Export_Supported')"><i :title="$t('Export_Supported')"><i
class="fas fa-file-export fa-fw" v-if="i.export"></i></b-badge> class="fas fa-file-export fa-fw" v-if="i.export"></i></b-badge>
<b-badge variant="warning" class="ml-1" v-b-tooltip:top <b-badge variant="warning" class="ml-1 text-muted" v-b-tooltip:top
:title="$t('Export_Not_Yet_Supported')"><i :title="$t('Export_Not_Yet_Supported')"><i
class="fas fa-file-export fa-fw" v-if="!i.export"></i></b-badge> class="fas fa-file-export fa-fw" v-if="!i.export"></i></b-badge>
</b-list-group-item> </b-list-group-item>
@@ -311,13 +305,13 @@
<b-badge variant="success" class="ml-1" v-b-tooltip:top <b-badge variant="success" class="ml-1" v-b-tooltip:top
:title="$t('Import_Supported')"><i :title="$t('Import_Supported')"><i
class="fas fa-file-import fa-fw" v-if="i.import"></i></b-badge> class="fas fa-file-import fa-fw" v-if="i.import"></i></b-badge>
<b-badge variant="warning" class="ml-1" v-b-tooltip:top <b-badge variant="warning" class="ml-1 text-muted" v-b-tooltip:top
:title="$t('Import_Not_Yet_Supported')"><i :title="$t('Import_Not_Yet_Supported')"><i
class="fas fa-file-import fa-fw" v-if="!i.import"></i></b-badge> class="fas fa-file-import fa-fw" v-if="!i.import"></i></b-badge>
<b-badge variant="success" class="ml-1" v-b-tooltip:top <b-badge variant="success" class="ml-1" v-b-tooltip:top
:title="$t('Export_Supported')"><i :title="$t('Export_Supported')"><i
class="fas fa-file-export fa-fw" v-if="i.export"></i></b-badge> class="fas fa-file-export fa-fw" v-if="i.export"></i></b-badge>
<b-badge variant="warning" class="ml-1" v-b-tooltip:top <b-badge variant="warning" class="ml-1 text-muted" v-b-tooltip:top
:title="$t('Export_Not_Yet_Supported')"><i :title="$t('Export_Not_Yet_Supported')"><i
class="fas fa-file-export fa-fw" v-if="!i.export"></i></b-badge> class="fas fa-file-export fa-fw" v-if="!i.export"></i></b-badge>
</b-list-group-item> </b-list-group-item>
@@ -361,7 +355,7 @@
:placeholder="$t('paste_json')" style="font-size: 12px"> :placeholder="$t('paste_json')" style="font-size: 12px">
</b-textarea> </b-textarea>
</div> </div>
<b-button @click="loadRecipe('',false, undefined)" variant="primary"><i <b-button class="mt-2" @click="loadRecipe('',false, undefined)" variant="primary"><i
class="fas fa-code"></i> class="fas fa-code"></i>
{{ $t('Import') }} {{ $t('Import') }}
</b-button> </b-button>
@@ -369,16 +363,23 @@
</b-tab> </b-tab>
<!-- Bookmarklet Tab --> <!-- Bookmarklet Tab -->
<b-tab v-bind:title="$t('Bookmarklet')"> <b-tab v-bind:title="$t('Bookmarklet')">
<b-container>
<b-row class="mt-4">
<b-col cols="12">
<!-- TODO localize --> <!-- TODO localize -->
Some pages cannot be imported from their URL, the Bookmarklet can be used to import from Some pages cannot be imported from their URL, the Bookmarklet can be used to
import from
some of them anyway.<br/> some of them anyway.<br/>
1. Drag the following button to your bookmarks bar <a class="btn btn-outline-info btn-sm" 1. Drag the following button to your bookmarks bar: <br/> <a
class="btn btn-outline-info btn-sm"
:href="makeBookmarklet()">Import into :href="makeBookmarklet()">Import into
Tandoor</a> <br/> Tandoor</a> <br/>
2. Open the page you want to import from <br/> 2. Open the page you want to import from <br/>
3. Click on the bookmark to perform the import <br/> 3. Click on the bookmark to perform the import <br/>
</b-col>
</b-row>
</b-container>
</b-tab> </b-tab>
</b-tabs> </b-tabs>
@@ -446,7 +447,7 @@ export default {
}, },
data() { data() {
return { return {
tab_index: 1, tab_index: 0,
collapse_visible: { collapse_visible: {
url: true, url: true,
options: false, options: false,

View File

@@ -22,7 +22,7 @@
:label="$t('copy_markdown_table')" icon="fab fa-markdown"/> :label="$t('copy_markdown_table')" icon="fab fa-markdown"/>
</div> </div>
</b-button> </b-button>
<i id="id_filters_button" class="fas fa-filter fa-fw mt-1" style="font-size: 16px" <i id="id_filters_button" class="fas fa-filter fa-fw mt-1" style="font-size: 16px; cursor: pointer"
:class="filterApplied ? 'text-danger' : 'text-primary'"/> :class="filterApplied ? 'text-danger' : 'text-primary'"/>
</div> </div>
</div> </div>
@@ -35,7 +35,7 @@
<i v-if="!loading" class="fas fa-shopping-cart fa-fw d-inline-block d-md-none"></i> <i v-if="!loading" class="fas fa-shopping-cart fa-fw d-inline-block d-md-none"></i>
<span class="d-none d-md-block">{{ $t('Shopping_list') }}</span> <span class="d-none d-md-block">{{ $t('Shopping_list') }}</span>
</template> </template>
<div class="container p-0" id="shoppinglist"> <div class="container p-0 p-md-3" id="shoppinglist">
<div class="row"> <div class="row">
<div class="col col-md-12 p-0 p-lg-3"> <div class="col col-md-12 p-0 p-lg-3">
<div role="tablist"> <div role="tablist">
@@ -52,6 +52,7 @@
:description="$t('Amount')" :description="$t('Amount')"
v-model="new_item.amount" v-model="new_item.amount"
style="font-size: 16px; border-radius: 5px !important; border: 1px solid #e8e8e8 !important" style="font-size: 16px; border-radius: 5px !important; border: 1px solid #e8e8e8 !important"
ref="amount_input_complex"
></b-form-input> ></b-form-input>
</b-col> </b-col>
<b-col cols="12" md="4" v-if="!ui.entry_mode_simple" class="mt-1"> <b-col cols="12" md="4" v-if="!ui.entry_mode_simple" class="mt-1">
@@ -67,10 +68,11 @@
<b-col cols="12" md="11" v-if="ui.entry_mode_simple" class="mt-1"> <b-col cols="12" md="11" v-if="ui.entry_mode_simple" class="mt-1">
<b-form-input size="lg" type="text" :placeholder="$t('QuickEntry')" <b-form-input size="lg" type="text" :placeholder="$t('QuickEntry')"
v-model="new_item.ingredient" v-model="new_item.ingredient"
@keyup.enter="addItem"></b-form-input> @keyup.enter="addItem"
ref="amount_input_simple"></b-form-input>
</b-col> </b-col>
<b-col cols="12" md="1" class="d-none d-md-block mt-1"> <b-col cols="12" md="1" class="d-none d-md-block mt-1">
<b-button variant="link" class="px-0"> <b-button variant="link" class="px-0" type="submit">
<i class="btn fas fa-cart-plus fa-lg px-0 text-success" <i class="btn fas fa-cart-plus fa-lg px-0 text-success"
@click="addItem"/> @click="addItem"/>
</b-button> </b-button>
@@ -861,7 +863,7 @@ export default {
delay: 0, delay: 0,
clear: Math.random(), clear: Math.random(),
ui: { ui: {
entry_mode_simple: false, entry_mode_simple: true,
selected_supermarket: undefined, selected_supermarket: undefined,
}, },
settings: { settings: {
@@ -1019,14 +1021,23 @@ export default {
watch: { watch: {
ui: { ui: {
handler() { handler() {
this.$cookies.set(SETTINGS_COOKIE_NAME, this.ui) this.$cookies.set(SETTINGS_COOKIE_NAME, {ui: this.ui, settings: {entrymode: this.entrymode}})
if (this.entrymode) {
this.$nextTick(function () {
this.setFocus()
})
}
}, },
deep: true, deep: true,
}, },
entrymode: { entrymode: {
handler() { handler() {
this.$cookies.set(SETTINGS_COOKIE_NAME, {ui: this.ui, settings: {entrymode: this.entrymode}})
if (this.entrymode) { if (this.entrymode) {
document.getElementById('shoppinglist').scrollTop = 0 document.getElementById('shoppinglist').scrollTop = 0
this.$nextTick(function () {
this.setFocus()
})
} }
} }
}, },
@@ -1078,13 +1089,23 @@ export default {
} }
this.$nextTick(function () { this.$nextTick(function () {
if (this.$cookies.isKey(SETTINGS_COOKIE_NAME)) { if (this.$cookies.isKey(SETTINGS_COOKIE_NAME)) {
this.ui = Object.assign({}, this.ui, this.$cookies.get(SETTINGS_COOKIE_NAME)) this.ui = Object.assign({}, this.ui, this.$cookies.get(SETTINGS_COOKIE_NAME).ui)
this.entrymode = this.$cookies.get(SETTINGS_COOKIE_NAME).settings.entrymode
} }
}) })
this.$i18n.locale = window.CUSTOM_LOCALE this.$i18n.locale = window.CUSTOM_LOCALE
console.log(window.CUSTOM_LOCALE) console.log(window.CUSTOM_LOCALE)
}, },
methods: { methods: {
setFocus() {
if (this.ui.entry_mode_simple) {
this.$refs['amount_input_simple'].focus()
} else {
if (this.$refs['amount_input_complex']) {
this.$refs['amount_input_complex'].focus()
}
}
},
// this.genericAPI inherited from ApiMixin // this.genericAPI inherited from ApiMixin
addItem: function () { addItem: function () {
if (this.ui.entry_mode_simple) { if (this.ui.entry_mode_simple) {
@@ -1106,6 +1127,7 @@ export default {
} else { } else {
this.addEntry() this.addEntry()
} }
this.setFocus()
}, },
addEntry: function (x) { addEntry: function (x) {
let api = new ApiApiFactory() let api = new ApiApiFactory()
@@ -1646,7 +1668,13 @@ export default {
padding-left: 5px; padding-left: 5px;
} }
@media (min-width: 768px) { @media (max-width: 991.9px) {
#shoppinglist {
max-width: none;
}
}
@media (min-width: 992px) {
#shoppinglist { #shoppinglist {
overflow-y: auto; overflow-y: auto;
overflow-x: auto; overflow-x: auto;

View File

@@ -20,7 +20,7 @@
aria-expanded="false" aria-expanded="false"
type="button" type="button"
:class="settings.left_handed ? 'dropdown-spacing' : ''" :class="settings.left_handed ? 'dropdown-spacing' : ''"
class="btn dropdown-toggle btn-link text-decoration-none text-body pr-0 pl-1 dropdown-toggle-no-caret"> class="btn dropdown-toggle btn-link text-decoration-none text-body pr-0 pl-1 pr-md-3 pl-md-3 dropdown-toggle-no-caret">
<i class="fas fa-ellipsis-v"></i> <i class="fas fa-ellipsis-v"></i>
</button> </button>
</div> </div>
@@ -53,7 +53,7 @@
class="p-0 mr-0 mr-md-2 p-md-2 text-decoration-none" variant="link"> class="p-0 mr-0 mr-md-2 p-md-2 text-decoration-none" variant="link">
<div class="text-nowrap"> <div class="text-nowrap">
<i class="fa fa-chevron-right rotate" :class="showDetails ? 'rotated' : ''"></i> <i class="fa fa-chevron-right rotate" :class="showDetails ? 'rotated' : ''"></i>
<span class="d-none d-md-inline-block">{{ $t("Details") }}</span> <span class="d-none d-md-inline-block"><span class="ml-2">{{ $t("Details") }}</span></span>
</div> </div>
</b-button> </b-button>
</b-col> </b-col>
@@ -112,7 +112,7 @@
aria-expanded="false" aria-expanded="false"
type="button" type="button"
:class="settings.left_handed ? 'dropdown-spacing' : ''" :class="settings.left_handed ? 'dropdown-spacing' : ''"
class="btn dropdown-toggle btn-link text-decoration-none text-body pr-1 dropdown-toggle-no-caret" class="btn dropdown-toggle btn-link text-decoration-none text-body pr-1 pr-md-3 pl-md-3 dropdown-toggle-no-caret"
> >
<i class="fas fa-ellipsis-v fa-lg"></i> <i class="fas fa-ellipsis-v fa-lg"></i>
</button> </button>
@@ -453,4 +453,16 @@ export default {
.invis-border { .invis-border {
border: 1px solid transparent; border: 1px solid transparent;
} }
@media (min-width: 992px) {
.fa-ellipsis-v {
font-size: 20px;
}
}
@media (min-width: 576px) {
.fa-ellipsis-v {
font-size: 16px;
}
}
</style> </style>

View File

@@ -400,5 +400,9 @@
"Import_Supported": "Import supported", "Import_Supported": "Import supported",
"Export_Supported": "Export supported", "Export_Supported": "Export supported",
"Import_Not_Yet_Supported": "Import not yet supported", "Import_Not_Yet_Supported": "Import not yet supported",
"Export_Not_Yet_Supported": "Export not yet supported" "Export_Not_Yet_Supported": "Export not yet supported",
"Import_Result_Info": "{imported} of {total} recipes were imported",
"Recipes_In_Import": "Recipes in your import file",
"Toggle": "Toggle",
"Import_Error": "An Error occurred during your import. Please expand the Details at the bottom of the page to view it."
} }