mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-28 10:43:17 -05:00
v0.4.1 - Frontend/UI Improvements (#267)
* fix links * actually fix #238 * Feature/mkdocs version bump (#240) * fix links (#239) Co-authored-by: hay-kot <hay-kot@pm.me> * fix #238 * bump mkdocs version * light/dark toggle * light/dark mode css * API_DOCS defaults to True * disable build on push for master Co-authored-by: hay-kot <hay-kot@pm.me> * Feature/recipe viewer (#244) * fix dialog placement * markdown support in ingredients * fix line render issue * fix tag rendering bug * change ingredients to text area * no slug error * add tag pages * remove console.logs Co-authored-by: hay-kot <hay-kot@pm.me> * changelog v0.4.1 * bug/backup-download (#245) * fix blocked download * + download blocked Co-authored-by: hay-kot <hay-kot@pm.me> * Feature/meal planner (#246) * fixes duplicate recipes in meal-plan #221 * add quick week option * scope css * add mealplanner info Co-authored-by: hay-kot <hay-kot@pm.me> * Nextcloud Import Bugs - #248 (#250) * parses datetime properly + clean category - #248 * add default credentials to docs Co-authored-by: hay-kot <hay-kot@pm.me> * Add bulk import examples to docs. (#252) * Add bulk import examples to docs. * Update api-usage.md * Add Python example for bulk import. * Change IP address in API example. * Refactor/app settings (#251) * fix env setup bugs * remove unused import * fix layout issues * changelog Co-authored-by: hay-kot <hay-kot@pm.me> * env setup fixes * Feature/about api (#253) * fix settings * app info cleanup Co-authored-by: hay-kot <hay-kot@pm.me> * Feature/image minify (#256) * fix settings * app info cleanup * bottom-bar experiment * remove dup key * type hints * add dependency * updated image with query parameters * read image options * add image minification * add image minification step * alt image routes * add image minification * set mobile bar to top Co-authored-by: hay-kot <hay-kot@pm.me> * Feature/additional endpoints (#257) * new recipe summary route * add categories to cards * add pillow * show tags instead of categories * additional debug info * add todays meal image url * about page * fix reactive tag * changelog + docs * bump version Co-authored-by: hay-kot <hay-kot@pm.me> * add pillow dependencies (#258) Co-authored-by: hay-kot <hay-kot@pm.me> * Feature/search page (#259) * add pillow dependencies * advanced search page * advanced search apge * remove extra dependencies * add pre-run script Co-authored-by: hay-kot <hay-kot@pm.me> * no image assignment * advanced search * fix docker dev build * Do not force theme settings on login form (#260) * Fix docker dev db persistence (#264) * Fix docker dev db persistence * Make run.sh the only startup script for prod + dev Credits to @hay-kot for run.sh script logic * Restore dev backend initialization in non-docker setup * Make run.sh POSIX-friendly * Allow dev backend to auto-reload in Docker * Frontend Refactor + Bug Fixes * merge category and tag selector * unifiy category selector * add hint * spacing * fix nextcloud migration * simplify email validator #261 * formatting * cleanup * auto-gen * format * update run script * unified category/tag selector * rename component * Add advanced search link * remove old code * convert keywords to tags * add proper behavior on rename * proper image name association on rename * fix test cleanup * changelog * set docker comppand * minify on migration Co-authored-by: hay-kot <hay-kot@pm.me> * bug-fixes/category-tag-creator (#266) * fix category labels * set loader for migration * v0.4.1 Co-authored-by: hay-kot <hay-kot@pm.me> Co-authored-by: hay-kot <hay-kot@pm.me> Co-authored-by: Nat <nathanynath@yahoo.fr> Co-authored-by: sephrat <34862846+sephrat@users.noreply.github.com>
This commit is contained in:
@@ -74,7 +74,7 @@
|
||||
</v-list>
|
||||
|
||||
<v-list nav dense class="fixedBottom">
|
||||
<v-list-item href="">
|
||||
<v-list-item to="/admin/about">
|
||||
<v-list-item-icon class="mr-3 pt-1">
|
||||
<v-icon :color="newVersionAvailable ? 'red--text' : ''">
|
||||
mdi-information
|
||||
@@ -83,10 +83,11 @@
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ $t("settings.current") }}
|
||||
{{ version }}
|
||||
{{ appVersion }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<a
|
||||
@click.prevent
|
||||
href="https://github.com/hay-kot/mealie/releases/latest"
|
||||
target="_blank"
|
||||
:class="newVersionAvailable ? 'red--text' : 'green--text'"
|
||||
@@ -106,14 +107,12 @@
|
||||
import { validators } from "@/mixins/validators";
|
||||
import { initials } from "@/mixins/initials";
|
||||
import { user } from "@/mixins/user";
|
||||
import { api } from "@/api";
|
||||
import axios from "axios";
|
||||
export default {
|
||||
mixins: [validators, initials, user],
|
||||
data() {
|
||||
return {
|
||||
latestVersion: null,
|
||||
version: null,
|
||||
hideImage: false,
|
||||
showSidebar: false,
|
||||
mobile: false,
|
||||
@@ -163,8 +162,6 @@ export default {
|
||||
this.mobile = this.viewScale();
|
||||
this.showSidebar = !this.viewScale();
|
||||
this.getVersion();
|
||||
let versionData = await api.meta.get_version();
|
||||
this.version = versionData.version;
|
||||
},
|
||||
|
||||
computed: {
|
||||
@@ -172,7 +169,11 @@ export default {
|
||||
return `api/users/${this.user.id}/image`;
|
||||
},
|
||||
newVersionAvailable() {
|
||||
return this.latestVersion == this.version ? false : true;
|
||||
return this.latestVersion == this.appVersion ? false : true;
|
||||
},
|
||||
appVersion() {
|
||||
const appInfo = this.$store.getters.getAppInfo;
|
||||
return appInfo.version;
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -37,7 +37,12 @@
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn color="accent" text :href="`/api/backups/${name}/download`">
|
||||
<v-btn
|
||||
color="accent"
|
||||
text
|
||||
:loading="downloading"
|
||||
@click="downloadFile(`/api/backups/${name}/download`)"
|
||||
>
|
||||
{{ $t("general.download") }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
@@ -61,6 +66,7 @@
|
||||
|
||||
<script>
|
||||
import ImportOptions from "@/components/Admin/Backup/ImportOptions";
|
||||
import axios from "axios";
|
||||
export default {
|
||||
components: { ImportOptions },
|
||||
props: {
|
||||
@@ -83,6 +89,7 @@ export default {
|
||||
dialog: false,
|
||||
forceImport: false,
|
||||
rebaseImport: false,
|
||||
downloading: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@@ -109,6 +116,23 @@ export default {
|
||||
this.close();
|
||||
this.$emit(event, eventData);
|
||||
},
|
||||
async downloadFile(downloadURL) {
|
||||
this.downloading = true;
|
||||
const response = await axios({
|
||||
url: downloadURL,
|
||||
method: "GET",
|
||||
responseType: "blob", // important
|
||||
});
|
||||
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", `${this.name}.zip`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
this.downloading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -19,10 +19,11 @@
|
||||
v-model="page.name"
|
||||
label="Page Name"
|
||||
></v-text-field>
|
||||
<CategorySelector
|
||||
<CategoryTagSelector
|
||||
v-model="page.categories"
|
||||
ref="categoryFormSelector"
|
||||
@mounted="catMounted = true"
|
||||
tag-selector="false"
|
||||
/>
|
||||
</v-card-text>
|
||||
|
||||
@@ -43,10 +44,10 @@
|
||||
<script>
|
||||
const NEW_PAGE_EVENT = "refresh-page";
|
||||
import { api } from "@/api";
|
||||
import CategorySelector from "@/components/FormHelpers/CategorySelector";
|
||||
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
|
||||
export default {
|
||||
components: {
|
||||
CategorySelector,
|
||||
CategoryTagSelector,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<NewCategoryDialog />
|
||||
<NewCategoryTagDialog :tag-dialog="false" />
|
||||
</v-app-bar>
|
||||
<v-list height="300" dense style="overflow:auto">
|
||||
<v-list-item-group>
|
||||
@@ -133,13 +133,13 @@
|
||||
import { api } from "@/api";
|
||||
import LanguageMenu from "@/components/UI/LanguageMenu";
|
||||
import draggable from "vuedraggable";
|
||||
import NewCategoryDialog from "./NewCategoryDialog.vue";
|
||||
import NewCategoryTagDialog from "@/components/UI/Dialogs/NewCategoryTagDialog.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
draggable,
|
||||
LanguageMenu,
|
||||
NewCategoryDialog,
|
||||
NewCategoryTagDialog,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -273,7 +273,6 @@ export default {
|
||||
await this.initialize();
|
||||
},
|
||||
resetPassword() {
|
||||
console.log(this.activeId);
|
||||
api.users.resetPassword(this.editedItem.id);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -42,4 +42,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-text-field{
|
||||
max-width: 300px;
|
||||
}
|
||||
</style>
|
||||
@@ -40,7 +40,13 @@
|
||||
<v-btn color="error" text @click="deleteMigration(migration.name)">
|
||||
{{ $t("general.delete") }}
|
||||
</v-btn>
|
||||
<v-btn color="accent" text @click="importMigration(migration.name)">
|
||||
<v-btn
|
||||
color="accent"
|
||||
text
|
||||
@click="importMigration(migration.name)"
|
||||
:loading="loading"
|
||||
:disabled="loading"
|
||||
>
|
||||
{{ $t("general.import") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -82,10 +88,10 @@ export default {
|
||||
this.$emit("refresh");
|
||||
},
|
||||
async importMigration(file_name) {
|
||||
this.loading == true;
|
||||
this.loading = true;
|
||||
let response = await api.migrations.import(this.folder, file_name);
|
||||
this.$emit("imported", response.successful, response.failed);
|
||||
this.loading == false;
|
||||
this.loading = false;
|
||||
},
|
||||
readableTime(timestamp) {
|
||||
let date = new Date(timestamp);
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-select
|
||||
:items="allCategories"
|
||||
v-model="selected"
|
||||
label="Categories"
|
||||
chips
|
||||
deletable-chips
|
||||
dense
|
||||
item-text="name"
|
||||
multiple
|
||||
return-object
|
||||
@input="emitChange"
|
||||
></v-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const MOUNTED_EVENT = "mounted";
|
||||
export default {
|
||||
props: {
|
||||
value: Array,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected: [],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$emit(MOUNTED_EVENT);
|
||||
},
|
||||
|
||||
computed: {
|
||||
allCategories() {
|
||||
return this.$store.getters.getAllCategories;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
emitChange() {
|
||||
this.$emit("input", this.selected);
|
||||
},
|
||||
setInit(val) {
|
||||
this.selected = val;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
129
frontend/src/components/FormHelpers/CategoryTagSelector.vue
Normal file
129
frontend/src/components/FormHelpers/CategoryTagSelector.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<v-autocomplete
|
||||
:items="activeItems"
|
||||
v-model="selected"
|
||||
:value="value"
|
||||
:label="inputLabel"
|
||||
chips
|
||||
deletable-chips
|
||||
:dense="dense"
|
||||
item-text="name"
|
||||
persistent-hint
|
||||
multiple
|
||||
:hint="hint"
|
||||
:solo="solo"
|
||||
:return-object="returnObject"
|
||||
:flat="flat"
|
||||
@input="emitChange"
|
||||
>
|
||||
<template v-slot:selection="data">
|
||||
<v-chip
|
||||
class="ma-1"
|
||||
:input-value="data.selected"
|
||||
close
|
||||
@click:close="removeByIndex(data.index)"
|
||||
label
|
||||
color="accent"
|
||||
dark
|
||||
:key="data.index"
|
||||
>
|
||||
{{ data.item.name || data.item }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:append-outer="">
|
||||
<NewCategoryTagDialog
|
||||
v-if="showAdd"
|
||||
:tag-dialog="tagSelector"
|
||||
@created-item="pushToItem"
|
||||
/>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NewCategoryTagDialog from "@/components/UI/Dialogs/NewCategoryTagDialog";
|
||||
const MOUNTED_EVENT = "mounted";
|
||||
export default {
|
||||
components: {
|
||||
NewCategoryTagDialog,
|
||||
},
|
||||
props: {
|
||||
value: Array,
|
||||
solo: {
|
||||
default: false,
|
||||
},
|
||||
dense: {
|
||||
default: true,
|
||||
},
|
||||
returnObject: {
|
||||
default: true,
|
||||
},
|
||||
tagSelector: {
|
||||
default: false,
|
||||
},
|
||||
hint: {
|
||||
default: null,
|
||||
},
|
||||
showAdd: {
|
||||
default: false,
|
||||
},
|
||||
showLabel: {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected: [],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$emit(MOUNTED_EVENT);
|
||||
this.setInit(this.value);
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(val) {
|
||||
this.selected = val;
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
inputLabel() {
|
||||
if (!this.showLabel) return null;
|
||||
return this.tagSelector ? "Tags" : "Categories";
|
||||
},
|
||||
activeItems() {
|
||||
let ItemObjects = [];
|
||||
if (this.tagSelector) ItemObjects = this.$store.getters.getAllTags;
|
||||
else {
|
||||
ItemObjects = this.$store.getters.getAllCategories;
|
||||
}
|
||||
if (this.returnObject) return ItemObjects;
|
||||
else {
|
||||
return ItemObjects.map(x => x.name);
|
||||
}
|
||||
},
|
||||
flat() {
|
||||
return this.selected.length > 0 && this.solo;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
emitChange() {
|
||||
this.$emit("input", this.selected);
|
||||
},
|
||||
setInit(val) {
|
||||
this.selected = val;
|
||||
},
|
||||
removeByIndex(index) {
|
||||
this.selected.splice(index, 1);
|
||||
},
|
||||
pushToItem(createdItem) {
|
||||
createdItem = this.returnObject ? createdItem : createdItem.name;
|
||||
this.selected.push(createdItem);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
@@ -22,13 +22,11 @@
|
||||
<v-text-field
|
||||
v-if="!options.isLoggingIn"
|
||||
v-model="user.name"
|
||||
light="light"
|
||||
prepend-icon="person"
|
||||
:label="$t('general.name')"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="user.email"
|
||||
light="light"
|
||||
prepend-icon="mdi-email"
|
||||
validate-on-blur
|
||||
:label="$t('user.email')"
|
||||
@@ -36,7 +34,6 @@
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="user.password"
|
||||
light="light"
|
||||
class="mb-2s"
|
||||
prepend-icon="mdi-lock"
|
||||
:label="$t('user.password')"
|
||||
@@ -47,7 +44,6 @@
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
v-if="options.isLoggingIn"
|
||||
dark
|
||||
color="primary"
|
||||
block="block"
|
||||
type="submit"
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from "@/utils";
|
||||
import SearchDialog from "../UI/Search/SearchDialog";
|
||||
import { api } from "@/api";
|
||||
export default {
|
||||
components: {
|
||||
SearchDialog,
|
||||
@@ -47,7 +47,7 @@ export default {
|
||||
methods: {
|
||||
getImage(slug) {
|
||||
if (slug) {
|
||||
return utils.getImageURL(slug);
|
||||
return api.recipes.recipeSmallImage(slug);
|
||||
}
|
||||
},
|
||||
setSlug(name, slug) {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
<v-card-title class=" headline">
|
||||
{{ $t("meal-plan.create-a-new-meal-plan") }}
|
||||
<v-btn color="info" class="ml-auto" @click="setQuickWeek()">
|
||||
<v-icon left>mdi-calendar-minus</v-icon> Quick Week
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row dense>
|
||||
@@ -101,6 +105,7 @@ export default {
|
||||
endDate: null,
|
||||
menu1: false,
|
||||
menu2: false,
|
||||
usedRecipes: [1],
|
||||
};
|
||||
},
|
||||
|
||||
@@ -115,17 +120,14 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
groupSettings() {
|
||||
this.buildMealStore();
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.$store.dispatch("requestCurrentGroup");
|
||||
await this.$store.dispatch("requestCurrentGroup");
|
||||
await this.buildMealStore();
|
||||
},
|
||||
|
||||
computed: {
|
||||
groupSettings() {
|
||||
console.log(this.$store.getters.getCurrentGroup);
|
||||
return this.$store.getters.getCurrentGroup;
|
||||
},
|
||||
actualStartDate() {
|
||||
@@ -152,11 +154,15 @@ export default {
|
||||
endComputedDateFormatted() {
|
||||
return this.formatDate(this.endDate);
|
||||
},
|
||||
filteredRecipes() {
|
||||
const recipes = this.items.filter(x => !this.usedRecipes.includes(x));
|
||||
return recipes.length > 0 ? recipes : this.items;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async buildMealStore() {
|
||||
let categories = Array.from(this.groupSettings.categories, x => x.name);
|
||||
const categories = Array.from(this.groupSettings.categories, x => x.name);
|
||||
this.items = await api.recipes.getAllByCategory(categories);
|
||||
|
||||
if (this.items.length === 0) {
|
||||
@@ -171,15 +177,20 @@ export default {
|
||||
this.items = await api.recipes.allByKeys(keys);
|
||||
}
|
||||
},
|
||||
get_random(list) {
|
||||
const object = list[Math.floor(Math.random() * list.length)];
|
||||
return object;
|
||||
getRandom(list) {
|
||||
let recipe = 1;
|
||||
while (this.usedRecipes.includes(recipe)) {
|
||||
recipe = list[Math.floor(Math.random() * list.length)];
|
||||
}
|
||||
return recipe;
|
||||
},
|
||||
random() {
|
||||
this.usedRecipes = [1];
|
||||
this.meals.forEach((element, index) => {
|
||||
let recipe = this.get_random(this.items);
|
||||
let recipe = this.getRandom(this.filteredRecipes);
|
||||
this.meals[index]["slug"] = recipe.slug;
|
||||
this.meals[index]["name"] = recipe.name;
|
||||
this.usedRecipes.push(recipe);
|
||||
});
|
||||
},
|
||||
processTime(index) {
|
||||
@@ -212,7 +223,7 @@ export default {
|
||||
},
|
||||
|
||||
getImage(image) {
|
||||
return utils.getImageURL(image);
|
||||
return api.recipes.recipeSmallImage(image);
|
||||
},
|
||||
|
||||
formatDate(date) {
|
||||
@@ -227,6 +238,33 @@ export default {
|
||||
const [month, day, year] = date.split("/");
|
||||
return `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`;
|
||||
},
|
||||
getNextDayOfTheWeek(dayName, excludeToday = true, refDate = new Date()) {
|
||||
const dayOfWeek = [
|
||||
"sun",
|
||||
"mon",
|
||||
"tue",
|
||||
"wed",
|
||||
"thu",
|
||||
"fri",
|
||||
"sat",
|
||||
].indexOf(dayName.slice(0, 3).toLowerCase());
|
||||
if (dayOfWeek < 0) return;
|
||||
refDate.setHours(0, 0, 0, 0);
|
||||
refDate.setDate(
|
||||
refDate.getDate() +
|
||||
+!!excludeToday +
|
||||
((dayOfWeek + 7 - refDate.getDay() - +!!excludeToday) % 7)
|
||||
);
|
||||
return refDate;
|
||||
},
|
||||
setQuickWeek() {
|
||||
const nextMonday = this.getNextDayOfTheWeek("Monday", false);
|
||||
const nextEndDate = new Date(nextMonday);
|
||||
nextEndDate.setDate(nextEndDate.getDate() + 4);
|
||||
|
||||
this.startDate = nextMonday.toISOString().substr(0, 10);
|
||||
this.endDate = nextEndDate.toISOString().substr(0, 10);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<template>
|
||||
<v-card hover :to="`/recipe/${slug}`" max-height="125">
|
||||
<v-card
|
||||
hover
|
||||
:to="`/recipe/${slug}`"
|
||||
max-height="125"
|
||||
@click="$emit('selected')"
|
||||
>
|
||||
<v-list-item>
|
||||
<v-list-item-avatar rounded size="125" class="mt-0 ml-n4">
|
||||
<v-img :src="getImage(image)"> </v-img>
|
||||
<v-img :src="getImage(slug)"> </v-img>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content class="align-self-start">
|
||||
<v-list-item-title>
|
||||
@@ -20,7 +25,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from "@/utils";
|
||||
import { api } from "@/api";
|
||||
export default {
|
||||
props: {
|
||||
name: String,
|
||||
@@ -35,7 +40,7 @@ export default {
|
||||
|
||||
methods: {
|
||||
getImage(image) {
|
||||
return utils.getImageURL(image);
|
||||
return api.recipes.recipeSmallImage(image);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
:elevation="hover ? 12 : 2"
|
||||
:to="route ? `/recipe/${slug}` : ''"
|
||||
@click="$emit('click')"
|
||||
min-height="275"
|
||||
>
|
||||
<v-img height="200" :src="getImage(image)">
|
||||
<v-img height="200" :src="getImage(slug)">
|
||||
<v-expand-transition v-if="description">
|
||||
<div
|
||||
v-if="hover"
|
||||
@@ -25,43 +26,53 @@
|
||||
</div>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-actions class="">
|
||||
<v-row dense align="center">
|
||||
<v-col>
|
||||
<v-rating
|
||||
class="mr-2"
|
||||
color="secondary"
|
||||
background-color="secondary lighten-3"
|
||||
dense
|
||||
length="5"
|
||||
size="15"
|
||||
:value="rating"
|
||||
></v-rating>
|
||||
</v-col>
|
||||
<v-col></v-col>
|
||||
<v-col align="end"> </v-col>
|
||||
</v-row>
|
||||
<v-card-actions>
|
||||
<v-rating
|
||||
class="mr-2 my-auto"
|
||||
color="secondary"
|
||||
background-color="secondary lighten-3"
|
||||
dense
|
||||
length="5"
|
||||
size="15"
|
||||
:value="rating"
|
||||
></v-rating>
|
||||
<v-spacer></v-spacer>
|
||||
<RecipeChips
|
||||
:items="tags"
|
||||
:title="false"
|
||||
:limit="2"
|
||||
:small="true"
|
||||
:isCategory="false"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from "@/utils";
|
||||
import RecipeChips from "@/components/Recipe/RecipeViewer/RecipeChips";
|
||||
import { api } from "@/api";
|
||||
export default {
|
||||
components: {
|
||||
RecipeChips,
|
||||
},
|
||||
props: {
|
||||
name: String,
|
||||
slug: String,
|
||||
description: String,
|
||||
rating: Number,
|
||||
image: String,
|
||||
|
||||
route: {
|
||||
default: true,
|
||||
},
|
||||
tags: {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getImage(image) {
|
||||
return utils.getImageURL(image);
|
||||
return api.recipes.recipeSmallImage(image);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -83,14 +83,16 @@
|
||||
:key="generateKey('ingredient', index)"
|
||||
>
|
||||
<v-row align="center">
|
||||
<v-text-field
|
||||
<v-textarea
|
||||
class="mr-2"
|
||||
:label="$t('recipe.ingredient')"
|
||||
v-model="value.recipeIngredient[index]"
|
||||
append-outer-icon="mdi-menu"
|
||||
mdi-move-resize
|
||||
auto-grow
|
||||
solo
|
||||
dense
|
||||
rows="2"
|
||||
>
|
||||
<v-icon
|
||||
class="mr-n1"
|
||||
@@ -100,7 +102,7 @@
|
||||
>
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
</v-text-field>
|
||||
</v-textarea>
|
||||
</v-row>
|
||||
</div>
|
||||
</transition-group>
|
||||
@@ -112,60 +114,21 @@
|
||||
<BulkAdd @bulk-data="appendIngredients" />
|
||||
|
||||
<h2 class="mt-6">{{ $t("recipe.categories") }}</h2>
|
||||
<v-combobox
|
||||
dense
|
||||
multiple
|
||||
chips
|
||||
item-color="secondary"
|
||||
deletable-chips
|
||||
<CategoryTagSelector
|
||||
:return-object="false"
|
||||
v-model="value.recipeCategory"
|
||||
hide-selected
|
||||
:items="allCategories"
|
||||
text="name"
|
||||
:search-input.sync="categoriesSearchInput"
|
||||
@change="categoriesSearchInput = ''"
|
||||
>
|
||||
<template v-slot:selection="data">
|
||||
<v-chip
|
||||
class="ma-1"
|
||||
:input-value="data.selected"
|
||||
close
|
||||
@click:close="removeCategory(data.index)"
|
||||
label
|
||||
color="accent"
|
||||
dark
|
||||
>
|
||||
{{ data.item }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-combobox>
|
||||
:show-add="true"
|
||||
:show-label="false"
|
||||
/>
|
||||
|
||||
<h2 class="mt-4">{{ $t("recipe.tags") }}</h2>
|
||||
<v-combobox
|
||||
dense
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
<CategoryTagSelector
|
||||
:return-object="false"
|
||||
v-model="value.tags"
|
||||
hide-selected
|
||||
:items="allTags"
|
||||
:search-input.sync="tagsSearchInput"
|
||||
@change="tagssSearchInput = ''"
|
||||
>
|
||||
<template v-slot:selection="data">
|
||||
<v-chip
|
||||
class="ma-1"
|
||||
:input-value="data.selected"
|
||||
close
|
||||
label
|
||||
@click:close="removeTags(data.index)"
|
||||
color="accent"
|
||||
dark
|
||||
>
|
||||
{{ data.item }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-combobox>
|
||||
:show-add="true"
|
||||
:tag-selector="true"
|
||||
:show-label="false"
|
||||
/>
|
||||
|
||||
<h2 class="my-4">{{ $t("recipe.notes") }}</h2>
|
||||
<v-card
|
||||
@@ -235,6 +198,7 @@
|
||||
dense
|
||||
v-model="value.recipeInstructions[index]['text']"
|
||||
:key="generateKey('instructions', index)"
|
||||
rows="4"
|
||||
>
|
||||
</v-textarea>
|
||||
</v-card-text>
|
||||
@@ -262,11 +226,13 @@ import { api } from "@/api";
|
||||
import utils from "@/utils";
|
||||
import BulkAdd from "./BulkAdd";
|
||||
import ExtrasEditor from "./ExtrasEditor";
|
||||
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
|
||||
export default {
|
||||
components: {
|
||||
BulkAdd,
|
||||
ExtrasEditor,
|
||||
draggable,
|
||||
CategoryTagSelector,
|
||||
},
|
||||
props: {
|
||||
value: Object,
|
||||
@@ -282,26 +248,14 @@ export default {
|
||||
v.split(" ").length <= 1 ||
|
||||
this.$i18n.t("recipe.no-white-space-allowed"),
|
||||
},
|
||||
categoriesSearchInput: "",
|
||||
tagsSearchInput: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
allCategories() {
|
||||
const categories = this.$store.getters.getAllCategories;
|
||||
return categories.map(cat => cat.name);
|
||||
},
|
||||
allTags() {
|
||||
const tags = this.$store.getters.getAllTags;
|
||||
return tags.map(cat => cat.name);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
uploadImage() {
|
||||
this.$emit("upload", this.fileObject);
|
||||
},
|
||||
async updateImage() {
|
||||
let slug = this.value.slug;
|
||||
const slug = this.value.slug;
|
||||
api.recipes.updateImage(slug, this.fileObject);
|
||||
},
|
||||
toggleDisabled(stepIndex) {
|
||||
@@ -324,9 +278,6 @@ export default {
|
||||
generateKey(item, index) {
|
||||
return utils.generateUniqueKey(item, index);
|
||||
},
|
||||
deleteRecipe() {
|
||||
this.$emit("delete");
|
||||
},
|
||||
|
||||
appendIngredients(ingredients) {
|
||||
this.value.recipeIngredient.push(...ingredients);
|
||||
|
||||
@@ -162,6 +162,7 @@
|
||||
|
||||
<script>
|
||||
import utils from "@/utils";
|
||||
import { api } from "@/api";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -175,7 +176,7 @@ export default {
|
||||
methods: {
|
||||
getImage(image) {
|
||||
if (image) {
|
||||
return utils.getImageURL(image) + "?rnd=" + this.imageKey;
|
||||
return api.recipes.recipeImage(image) + "?rnd=" + this.imageKey;
|
||||
}
|
||||
},
|
||||
generateKey(item, index) {
|
||||
|
||||
@@ -1,27 +1,52 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="mb-4">{{ $t("recipe.ingredients") }}</h2>
|
||||
<div
|
||||
v-for="(ingredient, index) in ingredients"
|
||||
<v-list-item
|
||||
dense
|
||||
v-for="(ingredient, index) in displayIngredients"
|
||||
:key="generateKey('ingredient', index)"
|
||||
@click="ingredient.checked = !ingredient.checked"
|
||||
>
|
||||
<v-checkbox
|
||||
hide-details
|
||||
class="ingredients"
|
||||
:label="ingredient"
|
||||
v-model="ingredient.checked"
|
||||
class="pt-0 my-auto py-auto"
|
||||
color="secondary"
|
||||
>
|
||||
</v-checkbox>
|
||||
</div>
|
||||
|
||||
<v-list-item-content>
|
||||
<vue-markdown
|
||||
class="ma-0 pa-0 text-subtitle-1 dense-markdown"
|
||||
:source="ingredient.text"
|
||||
>
|
||||
</vue-markdown>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueMarkdown from "@adapttive/vue-markdown";
|
||||
import utils from "@/utils";
|
||||
export default {
|
||||
components: {
|
||||
VueMarkdown,
|
||||
},
|
||||
props: {
|
||||
ingredients: Array,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
displayIngredients: [],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.displayIngredients = this.ingredients.map(x => ({
|
||||
text: x,
|
||||
checked: false,
|
||||
}));
|
||||
},
|
||||
methods: {
|
||||
generateKey(item, index) {
|
||||
return utils.generateUniqueKey(item, index);
|
||||
@@ -30,5 +55,8 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style >
|
||||
.dense-markdown p {
|
||||
margin: auto !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<div v-if="items && items.length > 0">
|
||||
<h2 class="mt-4">{{ title }}</h2>
|
||||
<div v-if="items.length > 0">
|
||||
<h2 v-if="title" class="mt-4">{{ title }}</h2>
|
||||
<v-chip
|
||||
:to="`/recipes/${getSlug(category)}`"
|
||||
label
|
||||
class="ma-1"
|
||||
color="accent"
|
||||
:small="small"
|
||||
dark
|
||||
v-for="category in items"
|
||||
v-for="category in items.slice(0, limit)"
|
||||
:to="`/recipes/${urlParam}/${getSlug(category)}`"
|
||||
:key="category"
|
||||
>
|
||||
{{ category }}
|
||||
@@ -18,21 +19,43 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
items: Array,
|
||||
title: String,
|
||||
category: {
|
||||
items: {
|
||||
default: [],
|
||||
},
|
||||
title: {
|
||||
default: null,
|
||||
},
|
||||
isCategory: {
|
||||
default: true,
|
||||
},
|
||||
limit: {
|
||||
default: 999,
|
||||
},
|
||||
small: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
allCategories() {
|
||||
return this.$store.getters.getAllCategories;
|
||||
},
|
||||
allTags() {
|
||||
return this.$store.getters.getAllTags;
|
||||
},
|
||||
urlParam() {
|
||||
return this.isCategory ? "category" : "tag";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getSlug(name) {
|
||||
if (this.category) {
|
||||
return this.allCategories.filter(x => x.name == name)[0].slug;
|
||||
if (!name) return;
|
||||
|
||||
if (this.isCategory) {
|
||||
const matches = this.allCategories.filter(x => x.name == name);
|
||||
if (matches.length > 0) return matches[0].slug;
|
||||
} else {
|
||||
const matches = this.allTags.filter(x => x.name == name);
|
||||
if (matches.length > 0) return matches[0].slug;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -34,7 +34,11 @@
|
||||
<Ingredients :ingredients="ingredients" />
|
||||
<div v-if="medium">
|
||||
<RecipeChips :title="$t('recipe.categories')" :items="categories" />
|
||||
<RecipeChips :title="$t('recipe.tags')" :items="tags" />
|
||||
<RecipeChips
|
||||
:title="$t('recipe.tags')"
|
||||
:items="tags"
|
||||
:isCategory="false"
|
||||
/>
|
||||
<Notes :notes="notes" />
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<v-menu offset-y v-if="sortable">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn-toggle group>
|
||||
<v-btn text v-bind="attrs" v-on="on">{{
|
||||
$t("general.sort")
|
||||
}}</v-btn>
|
||||
<v-btn text v-bind="attrs" v-on="on">
|
||||
{{ $t("general.sort") }}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</template>
|
||||
<v-list>
|
||||
@@ -53,6 +53,7 @@
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
:tags="recipe.tags"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
@@ -51,6 +51,11 @@ export default {
|
||||
to: "/recipes/all",
|
||||
title: this.$t("page.all-recipes"),
|
||||
},
|
||||
{
|
||||
icon: "mdi-magnify",
|
||||
to: "/search",
|
||||
title: "search",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-btn icon @click="dialog = true">
|
||||
<v-icon color="white">mdi-plus</v-icon>
|
||||
<v-btn icon @click="dialog = true" class="mt-n1">
|
||||
<v-icon :color="color">mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
<v-dialog v-model="dialog" width="500">
|
||||
<v-card>
|
||||
@@ -11,7 +11,7 @@
|
||||
</v-icon>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
Create a Category
|
||||
{{ title }}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
@@ -21,8 +21,8 @@
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
dense
|
||||
label="Category Name"
|
||||
v-model="categoryName"
|
||||
:label="inputLabel"
|
||||
v-model="itemName"
|
||||
:rules="[rules.required]"
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
@@ -31,7 +31,7 @@
|
||||
<v-btn color="grey" text @click="dialog = false">
|
||||
{{ $t("general.cancel") }}
|
||||
</v-btn>
|
||||
<v-btn color="success" text type="submit" :disabled="!categoryName">
|
||||
<v-btn color="success" text type="submit" :disabled="!itemName">
|
||||
{{ $t("general.create") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
@@ -43,31 +43,55 @@
|
||||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
const CREATED_ITEM_EVENT = "created-item";
|
||||
export default {
|
||||
props: {
|
||||
buttonText: String,
|
||||
value: String,
|
||||
color: {
|
||||
default: null,
|
||||
},
|
||||
tagDialog: {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
categoryName: "",
|
||||
itemName: "",
|
||||
rules: {
|
||||
required: val =>
|
||||
!!val || this.$t("settings.theme.theme-name-is-required"),
|
||||
required: val => !!val || "A Name is Required",
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
title() {
|
||||
return this.tagDialog ? "Create a Tag" : "Create a Category";
|
||||
},
|
||||
inputLabel() {
|
||||
return this.tagDialog ? "Tag Name" : "Category Name";
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
dialog(val) {
|
||||
if (!val) this.categoryName = "";
|
||||
if (!val) this.itemName = "";
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async select() {
|
||||
await api.categories.create(this.categoryName);
|
||||
this.$emit("new-category", this.categoryName);
|
||||
const newItem = await (async () => {
|
||||
if (this.tagDialog) {
|
||||
const newItem = await api.tags.create(this.itemName);
|
||||
return newItem;
|
||||
} else {
|
||||
const newItem = await api.categories.create(this.itemName);
|
||||
return newItem;
|
||||
}
|
||||
})();
|
||||
|
||||
this.$emit(CREATED_ITEM_EVENT, newItem);
|
||||
this.dialog = false;
|
||||
},
|
||||
},
|
||||
@@ -16,18 +16,25 @@
|
||||
>
|
||||
</v-text-field>
|
||||
</template>
|
||||
<v-card v-if="showResults" max-height="500" :max-width="maxWidth">
|
||||
<v-card-text class="py-1">Results</v-card-text>
|
||||
<v-card v-if="showResults" max-height="500" :max-width="maxWidth">
|
||||
<v-card-text class="flex row mx-auto">
|
||||
<div class="mr-auto">
|
||||
Results
|
||||
</div>
|
||||
<router-link to="/search">
|
||||
Advanced Search
|
||||
</router-link>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-list scrollable>
|
||||
<v-list scrollable v-if="autoResults">
|
||||
<v-list-item
|
||||
v-for="(item, index) in autoResults"
|
||||
v-for="(item, index) in autoResults.slice(0, 15)"
|
||||
:key="index"
|
||||
:to="navOnClick ? `/recipe/${item.item.slug}` : null"
|
||||
@click="navOnClick ? null : selected(item.item.slug, item.item.name)"
|
||||
>
|
||||
<v-list-item-avatar>
|
||||
<v-img :src="getImage(item.item.image)"></v-img>
|
||||
<v-img :src="getImage(item.item.slug)"></v-img>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content
|
||||
@click="
|
||||
@@ -54,7 +61,7 @@
|
||||
|
||||
<script>
|
||||
import Fuse from "fuse.js";
|
||||
import utils from "@/utils";
|
||||
import { api } from "@/api";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -136,6 +143,7 @@ export default {
|
||||
this.fuseResults = this.result;
|
||||
}
|
||||
},
|
||||
|
||||
searchSlug() {
|
||||
this.selected(this.searchSlug);
|
||||
},
|
||||
@@ -151,10 +159,9 @@ export default {
|
||||
);
|
||||
},
|
||||
getImage(image) {
|
||||
return utils.getImageURL(image);
|
||||
return api.recipes.recipeTinyImage(image);
|
||||
},
|
||||
selected(slug, name) {
|
||||
console.log("Selected", slug, name);
|
||||
this.$emit("selected", slug, name);
|
||||
},
|
||||
async onFocus() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="text-center">
|
||||
<v-dialog v-model="dialog" width="600px" height="0">
|
||||
<div class="text-center ">
|
||||
<v-dialog v-model="dialog" width="600px" height="0" :fullscreen="isMobile">
|
||||
<v-card>
|
||||
<v-app-bar dark color="primary">
|
||||
<v-toolbar-title class="headline">Search a Recipe</v-toolbar-title>
|
||||
@@ -9,13 +9,27 @@
|
||||
<SearchBar
|
||||
@results="updateResults"
|
||||
@selected="emitSelect"
|
||||
:show-results="true"
|
||||
:show-results="!isMobile"
|
||||
max-width="550px"
|
||||
:dense="false"
|
||||
:nav-on-click="false"
|
||||
:reset-search="dialog"
|
||||
:solo="false"
|
||||
/>
|
||||
<div v-if="isMobile">
|
||||
<div v-for="recipe in searchResults.slice(0, 7)" :key="recipe.name">
|
||||
<MobileRecipeCard
|
||||
class="ma-1 px-0"
|
||||
:name="recipe.item.name"
|
||||
:description="recipe.item.description"
|
||||
:slug="recipe.item.slug"
|
||||
:rating="recipe.item.rating"
|
||||
:image="recipe.item.image"
|
||||
:route="true"
|
||||
@selected="dialog = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@@ -24,16 +38,32 @@
|
||||
|
||||
<script>
|
||||
import SearchBar from "./SearchBar";
|
||||
import MobileRecipeCard from "@/components/Recipe/MobileRecipeCard";
|
||||
export default {
|
||||
components: {
|
||||
SearchBar,
|
||||
MobileRecipeCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchResults: null,
|
||||
searchResults: [],
|
||||
dialog: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isMobile() {
|
||||
return this.$vuetify.breakpoint.name === "xs";
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"$route.hash"(newHash, oldHash) {
|
||||
if (newHash === "#mobile-search") {
|
||||
this.dialog = true;
|
||||
} else if (oldHash === "#mobile-search") {
|
||||
this.dialog = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateResults(results) {
|
||||
this.searchResults = results;
|
||||
@@ -44,15 +74,22 @@ export default {
|
||||
},
|
||||
open() {
|
||||
this.dialog = true;
|
||||
this.$router.push("#mobile-search");
|
||||
},
|
||||
toggleDialog(open) {
|
||||
if (open) {
|
||||
this.$router.push("#mobile-search");
|
||||
} else {
|
||||
this.$router.back(); // 😎 back button click
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.v-dialog__content {
|
||||
margin-top: 10%;
|
||||
<style scope>
|
||||
.mobile-dialog {
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
</style>
|
||||
114
frontend/src/components/UI/TheAppBar.vue
Normal file
114
frontend/src/components/UI/TheAppBar.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-app-bar
|
||||
v-if="!isMobile"
|
||||
clipped-left
|
||||
dense
|
||||
app
|
||||
color="primary"
|
||||
dark
|
||||
class="d-print-none"
|
||||
>
|
||||
<router-link v-if="!(isMobile && search)" to="/">
|
||||
<v-btn icon>
|
||||
<v-icon size="40"> mdi-silverware-variant </v-icon>
|
||||
</v-btn>
|
||||
</router-link>
|
||||
|
||||
<div v-if="!isMobile" btn class="pl-2">
|
||||
<v-toolbar-title style="cursor: pointer" @click="$router.push('/')"
|
||||
>Mealie
|
||||
</v-toolbar-title>
|
||||
</div>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-expand-x-transition>
|
||||
<SearchBar
|
||||
ref="mainSearchBar"
|
||||
v-if="search"
|
||||
:show-results="true"
|
||||
@selected="navigateFromSearch"
|
||||
:max-width="isMobile ? '100%' : '450px'"
|
||||
/>
|
||||
</v-expand-x-transition>
|
||||
<v-btn icon @click="search = !search">
|
||||
<v-icon>mdi-magnify</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<SiteMenu />
|
||||
</v-app-bar>
|
||||
<v-app-bar
|
||||
v-else
|
||||
bottom
|
||||
clipped-left
|
||||
dense
|
||||
app
|
||||
color="primary"
|
||||
dark
|
||||
class="d-print-none"
|
||||
>
|
||||
<router-link to="/">
|
||||
<v-btn icon>
|
||||
<v-icon size="40"> mdi-silverware-variant </v-icon>
|
||||
</v-btn>
|
||||
</router-link>
|
||||
|
||||
<div v-if="!isMobile" btn class="pl-2">
|
||||
<v-toolbar-title style="cursor: pointer" @click="$router.push('/')"
|
||||
>Mealie
|
||||
</v-toolbar-title>
|
||||
</div>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-expand-x-transition>
|
||||
<SearchDialog ref="mainSearchDialog" />
|
||||
</v-expand-x-transition>
|
||||
<v-btn icon @click="$refs.mainSearchDialog.open()">
|
||||
<v-icon>mdi-magnify</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<SiteMenu />
|
||||
</v-app-bar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SiteMenu from "@/components/UI/SiteMenu";
|
||||
import SearchBar from "@/components/UI/Search/SearchBar";
|
||||
import SearchDialog from "@/components/UI/Search/SearchDialog";
|
||||
import { user } from "@/mixins/user";
|
||||
export default {
|
||||
name: "AppBar",
|
||||
|
||||
mixins: [user],
|
||||
components: {
|
||||
SiteMenu,
|
||||
SearchBar,
|
||||
SearchDialog,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
search: false,
|
||||
isMobile: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.search = false;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
// isMobile() {
|
||||
// return this.$vuetify.breakpoint.name === "xs";
|
||||
// },
|
||||
},
|
||||
methods: {
|
||||
navigateFromSearch(slug) {
|
||||
this.$router.push(`/recipe/${slug}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
Reference in New Issue
Block a user