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:
Hayden
2021-04-07 17:22:25 -08:00
committed by GitHub
parent b8cddfd6c5
commit 9c379dfb83
89 changed files with 2001 additions and 710 deletions

View File

@@ -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;
},
},

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -273,7 +273,6 @@ export default {
await this.initialize();
},
resetPassword() {
console.log(this.activeId);
api.users.resetPassword(this.editedItem.id);
},
},

View File

@@ -42,4 +42,7 @@ export default {
</script>
<style scoped>
.v-text-field{
max-width: 300px;
}
</style>

View File

@@ -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);

View File

@@ -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>

View 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>

View File

@@ -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"

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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);
},
},
};

View File

@@ -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);
},
},
};

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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;
}
},
},

View File

@@ -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>

View File

@@ -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>

View File

@@ -51,6 +51,11 @@ export default {
to: "/recipes/all",
title: this.$t("page.all-recipes"),
},
{
icon: "mdi-magnify",
to: "/search",
title: "search",
},
],
};
},

View File

@@ -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;
},
},

View File

@@ -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() {

View File

@@ -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>

View 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>