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:
@@ -1,35 +1,6 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-app-bar 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 />
|
||||
<LanguageMenu />
|
||||
</v-app-bar>
|
||||
<TheAppBar />
|
||||
<v-main>
|
||||
<v-banner v-if="demo" sticky
|
||||
><div class="text-center">
|
||||
@@ -47,34 +18,25 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SiteMenu from "@/components/UI/SiteMenu";
|
||||
import SearchBar from "@/components/UI/Search/SearchBar";
|
||||
import TheAppBar from "@/components/UI/TheAppBar";
|
||||
import AddRecipeFab from "@/components/UI/AddRecipeFab";
|
||||
import LanguageMenu from "@/components/UI/LanguageMenu";
|
||||
import Vuetify from "./plugins/vuetify";
|
||||
import { user } from "@/mixins/user";
|
||||
import { api } from "./api";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
|
||||
components: {
|
||||
SiteMenu,
|
||||
TheAppBar,
|
||||
AddRecipeFab,
|
||||
SearchBar,
|
||||
LanguageMenu,
|
||||
},
|
||||
|
||||
mixins: [user],
|
||||
|
||||
watch: {
|
||||
$route() {
|
||||
this.search = false;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isMobile() {
|
||||
return this.$vuetify.breakpoint.name === "xs";
|
||||
demo() {
|
||||
const appInfo = this.$store.getters.getAppInfo;
|
||||
return appInfo.demoStatus;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -96,15 +58,9 @@ export default {
|
||||
this.$store.dispatch("requestTags");
|
||||
this.darkModeSystemCheck();
|
||||
this.darkModeAddEventListener();
|
||||
|
||||
const api_status = await api.meta.getIsDemo();
|
||||
this.demo = api_status.demoStatus;
|
||||
this.$store.dispatch("requestAppInfo");
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
search: false,
|
||||
demo: false,
|
||||
}),
|
||||
methods: {
|
||||
// For Later!
|
||||
|
||||
@@ -126,9 +82,6 @@ export default {
|
||||
this.darkModeSystemCheck();
|
||||
});
|
||||
},
|
||||
navigateFromSearch(slug) {
|
||||
this.$router.push(`/recipe/${slug}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -160,6 +113,7 @@ export default {
|
||||
|
||||
.notify-base {
|
||||
color: white !important;
|
||||
/* min-height: 50px; */
|
||||
margin-right: 60px;
|
||||
margin-bottom: -5px;
|
||||
opacity: 0.9 !important;
|
||||
@@ -176,11 +130,4 @@ export default {
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: grey;
|
||||
}
|
||||
|
||||
.notify-base {
|
||||
color: white !important;
|
||||
margin-right: 60px;
|
||||
margin-bottom: -5px;
|
||||
opacity: 0.9 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,27 +5,27 @@ import { store } from "@/store";
|
||||
const prefix = baseURL + "categories";
|
||||
|
||||
const categoryURLs = {
|
||||
get_all: `${prefix}`,
|
||||
get_category: category => `${prefix}/${category}`,
|
||||
delete_category: category => `${prefix}/${category}`,
|
||||
getAll: `${prefix}`,
|
||||
getCategory: category => `${prefix}/${category}`,
|
||||
deleteCategory: category => `${prefix}/${category}`,
|
||||
};
|
||||
|
||||
export const categoryAPI = {
|
||||
async getAll() {
|
||||
let response = await apiReq.get(categoryURLs.get_all);
|
||||
let response = await apiReq.get(categoryURLs.getAll);
|
||||
return response.data;
|
||||
},
|
||||
async create(name) {
|
||||
let response = await apiReq.post(categoryURLs.get_all, { name: name });
|
||||
let response = await apiReq.post(categoryURLs.getAll, { name: name });
|
||||
store.dispatch("requestCategories");
|
||||
return response.data;
|
||||
},
|
||||
async getRecipesInCategory(category) {
|
||||
let response = await apiReq.get(categoryURLs.get_category(category));
|
||||
let response = await apiReq.get(categoryURLs.getCategory(category));
|
||||
return response.data;
|
||||
},
|
||||
async delete(category) {
|
||||
let response = await apiReq.delete(categoryURLs.delete_category(category));
|
||||
let response = await apiReq.delete(categoryURLs.deleteCategory(category));
|
||||
store.dispatch("requestCategories");
|
||||
return response.data;
|
||||
},
|
||||
@@ -44,6 +44,11 @@ export const tagAPI = {
|
||||
let response = await apiReq.get(tagURLs.getAll);
|
||||
return response.data;
|
||||
},
|
||||
async create(name) {
|
||||
let response = await apiReq.post(tagURLs.getAll, { name: name });
|
||||
store.dispatch("requestTags");
|
||||
return response.data;
|
||||
},
|
||||
async getRecipesInTag(tag) {
|
||||
let response = await apiReq.get(tagURLs.getTag(tag));
|
||||
return response.data;
|
||||
|
||||
@@ -5,15 +5,22 @@ const prefix = baseURL + "debug";
|
||||
|
||||
const debugURLs = {
|
||||
version: `${prefix}/version`,
|
||||
debug: `${prefix}`,
|
||||
lastRecipe: `${prefix}/last-recipe-json`,
|
||||
demo: `${prefix}/is-demo`,
|
||||
};
|
||||
|
||||
export const metaAPI = {
|
||||
async get_version() {
|
||||
async getAppInfo() {
|
||||
let response = await apiReq.get(debugURLs.version);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getDebugInfo() {
|
||||
const response = await apiReq.get(debugURLs.debug);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getLastJson() {
|
||||
let response = await apiReq.get(debugURLs.lastRecipe);
|
||||
return response.data;
|
||||
@@ -21,7 +28,6 @@ export const metaAPI = {
|
||||
|
||||
async getIsDemo() {
|
||||
let response = await apiReq.get(debugURLs.demo);
|
||||
console.log(response);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ const prefix = baseURL + "recipes/";
|
||||
|
||||
const recipeURLs = {
|
||||
allRecipes: baseURL + "recipes",
|
||||
summary: baseURL + "recipes" + "/summary",
|
||||
allRecipesByCategory: prefix + "category",
|
||||
create: prefix + "create",
|
||||
createByURL: prefix + "create-url",
|
||||
@@ -56,9 +57,7 @@ export const recipeAPI = {
|
||||
const fd = new FormData();
|
||||
fd.append("image", fileObject);
|
||||
fd.append("extension", fileObject.name.split(".").pop());
|
||||
|
||||
let response = apiReq.put(recipeURLs.updateImage(recipeSlug), fd);
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
@@ -87,4 +86,21 @@ export const recipeAPI = {
|
||||
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async allSummary() {
|
||||
const response = await apiReq.get(recipeURLs.summary);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
recipeImage(recipeSlug) {
|
||||
return `/api/recipes/${recipeSlug}/image?image_type=original`;
|
||||
},
|
||||
|
||||
recipeSmallImage(recipeSlug) {
|
||||
return `/api/recipes/${recipeSlug}/image?image_type=small`;
|
||||
},
|
||||
|
||||
recipeTinyImage(recipeSlug) {
|
||||
return `/api/recipes/${recipeSlug}/image?image_type=tiny`;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
7
frontend/src/mixins/utilMixins.js
Normal file
7
frontend/src/mixins/utilMixins.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export const utilMixins = {
|
||||
commputed: {
|
||||
isMobile() {
|
||||
return this.$vuetify.breakpoint.name === "xs";
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -3,7 +3,7 @@ export const validators = {
|
||||
return {
|
||||
emailRule: v =>
|
||||
!v ||
|
||||
/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(v) ||
|
||||
/^[^@\s]+@[^@\s.]+.[^@.\s]+$/.test(v) ||
|
||||
this.$t('user.e-mail-must-be-valid'),
|
||||
|
||||
existsRule: value => !!value || this.$t('general.field-required'),
|
||||
|
||||
91
frontend/src/pages/Admin/About/index.vue
Normal file
91
frontend/src/pages/Admin/About/index.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card class="mt-3">
|
||||
<v-card-title class="headline">
|
||||
About Mealie
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-list-item-group color="primary">
|
||||
<v-list-item v-for="property in prettyInfo" :key="property.name">
|
||||
<v-list-item-icon>
|
||||
<v-icon> {{ property.icon || "mdi-account" }} </v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="pl-4 flex row justify-space-between">
|
||||
<div>{{ property.name }}</div>
|
||||
<div>{{ property.value }}</div>
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
prettyInfo: [],
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.getInfo();
|
||||
},
|
||||
methods: {
|
||||
async getInfo() {
|
||||
const debugInfo = await api.meta.getDebugInfo();
|
||||
|
||||
this.prettyInfo = [
|
||||
{
|
||||
name: "Version",
|
||||
icon: "mdi-information",
|
||||
value: debugInfo.version,
|
||||
},
|
||||
{
|
||||
name: "Application Mode",
|
||||
icon: "mdi-dev-to",
|
||||
value: debugInfo.production ? "Production" : "Development",
|
||||
},
|
||||
{
|
||||
name: "Demo Status",
|
||||
icon: "mdi-test-tube",
|
||||
value: debugInfo.demoStatus ? "Demo" : "Not Demo",
|
||||
},
|
||||
{
|
||||
name: "API Port",
|
||||
icon: "mdi-api",
|
||||
value: debugInfo.apiPort,
|
||||
},
|
||||
{
|
||||
name: "API Docs",
|
||||
icon: "mdi-file-document",
|
||||
value: debugInfo.apiDocs ? "Enabled" : "Disabled",
|
||||
},
|
||||
{
|
||||
name: "Database Type",
|
||||
icon: "mdi-database",
|
||||
value: debugInfo.dbType,
|
||||
},
|
||||
{
|
||||
name: "SQLite File",
|
||||
icon: "mdi-file-cabinet",
|
||||
value: debugInfo.sqliteFile,
|
||||
},
|
||||
{
|
||||
name: "Default Group",
|
||||
icon: "mdi-account-group",
|
||||
value: debugInfo.defaultGroup,
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
@@ -7,41 +7,19 @@
|
||||
<v-card-text>
|
||||
<h2 class="mt-1">{{ $t("recipe.categories") }}</h2>
|
||||
|
||||
<v-row>
|
||||
<v-col sm="12" md="6">
|
||||
<v-select
|
||||
outlined
|
||||
:flat="isFlat"
|
||||
elavation="0"
|
||||
v-model="groupSettings.categories"
|
||||
:items="categories"
|
||||
item-text="name"
|
||||
return-object
|
||||
multiple
|
||||
chips
|
||||
:hint="
|
||||
$t(
|
||||
'meal-plan.only-recipes-with-these-categories-will-be-used-in-meal-plans'
|
||||
)
|
||||
"
|
||||
class="mt-2"
|
||||
persistent-hint
|
||||
>
|
||||
<template v-slot:selection="data">
|
||||
<v-chip
|
||||
outlined
|
||||
:input-value="data.selected"
|
||||
close
|
||||
@click:close="removeCategory(data.index)"
|
||||
color="secondary"
|
||||
dark
|
||||
>
|
||||
{{ data.item.name }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<CategoryTagSelector
|
||||
class="mt-4"
|
||||
:solo="true"
|
||||
:dense="false"
|
||||
v-model="groupSettings.categories"
|
||||
:return-object="true"
|
||||
:show-add="true"
|
||||
:hint="
|
||||
$t(
|
||||
'meal-plan.only-recipes-with-these-categories-will-be-used-in-meal-plans'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-divider> </v-divider>
|
||||
<v-card-text>
|
||||
@@ -57,28 +35,23 @@
|
||||
<strong>{{ groupSettings.webhookTime }}</strong>
|
||||
</p>
|
||||
|
||||
<v-row dense align="center">
|
||||
<v-col cols="12" md="2" sm="5">
|
||||
<v-switch
|
||||
v-model="groupSettings.webhookEnable"
|
||||
:label="$t('general.enabled')"
|
||||
></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3" sm="5">
|
||||
<TimePickerDialog @save-time="saveTime" />
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" sm="5">
|
||||
<v-btn text color="info" @click="testWebhooks">
|
||||
<v-icon left> mdi-webhook </v-icon>
|
||||
{{ $t("settings.webhooks.test-webhooks") }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-row dense class="flex align-center">
|
||||
<v-switch
|
||||
class="mx-2"
|
||||
v-model="groupSettings.webhookEnable"
|
||||
:label="$t('general.enabled')"
|
||||
></v-switch>
|
||||
<TimePickerDialog @save-time="saveTime" class="ma-2" />
|
||||
<v-btn class="ma-2" color="info" @click="testWebhooks">
|
||||
<v-icon left> mdi-webhook </v-icon>
|
||||
{{ $t("settings.webhooks.test-webhooks") }}
|
||||
</v-btn>
|
||||
</v-row>
|
||||
|
||||
<v-row
|
||||
v-for="(url, index) in groupSettings.webhookUrls"
|
||||
:key="index"
|
||||
align="center"
|
||||
align=" center"
|
||||
dense
|
||||
>
|
||||
<v-col cols="1">
|
||||
@@ -110,9 +83,11 @@
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
import TimePickerDialog from "@/components/Admin/MealPlanner/TimePickerDialog";
|
||||
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
|
||||
export default {
|
||||
components: {
|
||||
TimePickerDialog,
|
||||
CategoryTagSelector,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -160,6 +135,7 @@ export default {
|
||||
this.groupSettings.webhookUrls.splice(index, 1);
|
||||
},
|
||||
async saveGroupSettings() {
|
||||
console.log(this.groupSettings);
|
||||
await api.groups.update(this.groupSettings);
|
||||
await this.$store.dispatch("requestCurrentGroup");
|
||||
this.getSiteSettings();
|
||||
@@ -167,9 +143,6 @@ export default {
|
||||
testWebhooks() {
|
||||
api.settings.testWebhooks();
|
||||
},
|
||||
removeCategory(index) {
|
||||
this.groupSettings.categories.splice(index, 1);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -36,7 +36,6 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
siteSettings() {
|
||||
console.log(this.$store.getters.getSiteSettings);
|
||||
return this.$store.getters.getSiteSettings;
|
||||
},
|
||||
recentRecipes() {
|
||||
@@ -54,7 +53,6 @@ export default {
|
||||
this.siteSettings.categories.forEach(async element => {
|
||||
let recipes = await this.getRecipeByCategory(element.slug);
|
||||
if (recipes.recipes.length < 0) recipes.recipes = [];
|
||||
console.log(recipes);
|
||||
this.recipeByCategory.push(recipes);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -117,7 +117,7 @@ export default {
|
||||
return utils.getDateAsTextAlt(dateObject);
|
||||
},
|
||||
getImage(image) {
|
||||
return utils.getImageURL(image);
|
||||
return api.recipes.recipeTinyImage(image);
|
||||
},
|
||||
|
||||
editPlan(id) {
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
import utils from "@/utils";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -68,7 +67,7 @@ export default {
|
||||
else return 0;
|
||||
},
|
||||
getImage(image) {
|
||||
return utils.getImageURL(image);
|
||||
return api.recipes.recipeImage(image);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<v-card v-else id="myRecipe">
|
||||
<v-img
|
||||
height="400"
|
||||
:src="getImage(recipeDetails.image)"
|
||||
:src="getImage(recipeDetails.slug)"
|
||||
class="d-print-none"
|
||||
:key="imageKey"
|
||||
>
|
||||
@@ -71,7 +71,6 @@
|
||||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
import utils from "@/utils";
|
||||
import VJsoneditor from "v-jsoneditor";
|
||||
import RecipeViewer from "@/components/Recipe/RecipeViewer";
|
||||
import RecipeEditor from "@/components/Recipe/RecipeEditor";
|
||||
@@ -160,7 +159,7 @@ export default {
|
||||
},
|
||||
getImage(image) {
|
||||
if (image) {
|
||||
return utils.getImageURL(image) + "?rnd=" + this.imageKey;
|
||||
return api.recipes.recipeImage(image) + "&rnd=" + this.imageKey;
|
||||
}
|
||||
},
|
||||
deleteRecipe() {
|
||||
|
||||
60
frontend/src/pages/Recipes/TagPage.vue
Normal file
60
frontend/src/pages/Recipes/TagPage.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<CategorySidebar />
|
||||
<CardSection
|
||||
:sortable="true"
|
||||
:title="title"
|
||||
:recipes="recipes"
|
||||
:card-limit="9999"
|
||||
@sort="sortAZ"
|
||||
@sort-recent="sortRecent"
|
||||
/>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from "@/api";
|
||||
import CardSection from "@/components/UI/CardSection";
|
||||
import CategorySidebar from "@/components/UI/CategorySidebar";
|
||||
export default {
|
||||
components: {
|
||||
CardSection,
|
||||
CategorySidebar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: "",
|
||||
recipes: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentTag() {
|
||||
return this.$route.params.tag;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
async currentTag() {
|
||||
this.getRecipes();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getRecipes();
|
||||
},
|
||||
methods: {
|
||||
async getRecipes() {
|
||||
let data = await api.tags.getRecipesInTag(this.currentTag);
|
||||
this.title = data.name;
|
||||
this.recipes = data.recipes;
|
||||
},
|
||||
sortAZ() {
|
||||
this.recipes.sort((a, b) => (a.name > b.name ? 1 : -1));
|
||||
},
|
||||
sortRecent() {
|
||||
this.recipes.sort((a, b) => (a.dateAdded > b.dateAdded ? -1 : 1));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,58 +0,0 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row justify="center">
|
||||
<v-col cols="1"> </v-col>
|
||||
<v-col>
|
||||
<SearchBar @results="updateResults" :show-results="false" />
|
||||
</v-col>
|
||||
<v-col cols="2">
|
||||
<v-btn icon>
|
||||
<v-icon large> mdi-filter </v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="searchResults">
|
||||
<v-col
|
||||
:sm="6"
|
||||
:md="6"
|
||||
:lg="4"
|
||||
:xl="3"
|
||||
v-for="item in searchResults.slice(0, 10)"
|
||||
:key="item.item.name"
|
||||
>
|
||||
<RecipeCard
|
||||
:name="item.item.name"
|
||||
:description="item.item.description"
|
||||
:slug="item.item.slug"
|
||||
:rating="item.item.rating"
|
||||
:image="item.item.image"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SearchBar from "../components/UI/Search/SearchBar";
|
||||
import RecipeCard from "../components/Recipe/RecipeCard";
|
||||
export default {
|
||||
components: {
|
||||
SearchBar,
|
||||
RecipeCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchResults: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
updateResults(results) {
|
||||
this.searchResults = results;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
69
frontend/src/pages/SearchPage/FilterSelector.vue
Normal file
69
frontend/src/pages/SearchPage/FilterSelector.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<v-toolbar dense flat>
|
||||
<v-btn-toggle
|
||||
dense
|
||||
v-model="selected"
|
||||
tile
|
||||
color="primary accent-3"
|
||||
@change="emitMulti"
|
||||
group
|
||||
mandatory
|
||||
>
|
||||
<v-btn :value="false">
|
||||
Include
|
||||
</v-btn>
|
||||
|
||||
<v-btn :value="true">
|
||||
Exclude
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn-toggle
|
||||
dense
|
||||
v-model="match"
|
||||
tile
|
||||
color="primary accent-3"
|
||||
@change="emitMulti"
|
||||
group
|
||||
mandatory
|
||||
>
|
||||
<v-btn :value="false">
|
||||
And
|
||||
</v-btn>
|
||||
<v-btn :value="true">
|
||||
Or
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
default: "include", // Optionas: "include", "exclude", "any"
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected: false,
|
||||
match: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
emitChange() {
|
||||
this.$emit("input", this.selected);
|
||||
},
|
||||
emitMulti() {
|
||||
const updateData = {
|
||||
exclude: this.selected,
|
||||
matchAny: this.match,
|
||||
};
|
||||
this.$emit("update", updateData);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
178
frontend/src/pages/SearchPage/index.vue
Normal file
178
frontend/src/pages/SearchPage/index.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<CategorySidebar />
|
||||
<v-card flat>
|
||||
<v-row dense>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="searchString"
|
||||
outlined
|
||||
color="primary accent-3"
|
||||
placeholder="Placeholder"
|
||||
append-icon="mdi-magnify"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="2" sm="12">
|
||||
<v-text-field
|
||||
class="mt-0 pt-0"
|
||||
label="Max Results"
|
||||
v-model="maxResults"
|
||||
type="number"
|
||||
outlined
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row dense class="mt-0 flex-row align-center justify-space-around">
|
||||
<v-col>
|
||||
<h3 class="pl-2 text-center headline">Category Filter</h3>
|
||||
<FilterSelector class="mb-1" @update="updateCatParams" />
|
||||
<CategoryTagSelector
|
||||
:solo="true"
|
||||
:dense="false"
|
||||
v-model="includeCategories"
|
||||
:return-object="false"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<h3 class="pl-2 text-center headline">Tag Filter</h3>
|
||||
<FilterSelector class="mb-1" @update="updateTagParams" />
|
||||
|
||||
<CategoryTagSelector
|
||||
:solo="true"
|
||||
:dense="false"
|
||||
v-model="includeTags"
|
||||
:return-object="false"
|
||||
:tag-selector="true"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="fuzzyRecipes">
|
||||
<v-col
|
||||
:sm="6"
|
||||
:md="6"
|
||||
:lg="4"
|
||||
:xl="3"
|
||||
v-for="item in fuzzyRecipes.slice(0, maxResults)"
|
||||
:key="item.name"
|
||||
>
|
||||
<RecipeCard
|
||||
:name="item.item.name"
|
||||
:description="item.item.description"
|
||||
:slug="item.item.slug"
|
||||
:rating="item.item.rating"
|
||||
:image="item.item.image"
|
||||
:tags="item.item.tags"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Fuse from "fuse.js";
|
||||
import RecipeCard from "@/components/Recipe/RecipeCard";
|
||||
import CategorySidebar from "@/components/UI/CategorySidebar";
|
||||
import CategoryTagSelector from "@/components/FormHelpers/CategoryTagSelector";
|
||||
import FilterSelector from "./FilterSelector.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RecipeCard,
|
||||
CategorySidebar,
|
||||
CategoryTagSelector,
|
||||
FilterSelector,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchString: "",
|
||||
maxResults: 21,
|
||||
searchResults: [],
|
||||
catFilter: {
|
||||
exclude: false,
|
||||
matchAny: false,
|
||||
},
|
||||
tagFilter: {
|
||||
exclude: false,
|
||||
matchAny: false,
|
||||
},
|
||||
includeCategories: [],
|
||||
includeTags: [],
|
||||
options: {
|
||||
shouldSort: true,
|
||||
threshold: 0.6,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
findAllMatches: true,
|
||||
maxPatternLength: 32,
|
||||
minMatchCharLength: 2,
|
||||
keys: ["name", "description"],
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
allRecipes() {
|
||||
return this.$store.getters.getRecentRecipes;
|
||||
},
|
||||
filteredRecipes() {
|
||||
return this.allRecipes.filter(recipe => {
|
||||
const includesTags = this.check(
|
||||
this.includeTags,
|
||||
recipe.tags,
|
||||
this.tagFilter.matchAny,
|
||||
this.tagFilter.exclude
|
||||
);
|
||||
const includesCats = this.check(
|
||||
this.includeCategories,
|
||||
recipe.recipeCategory,
|
||||
this.catFilter.matchAny,
|
||||
this.catFilter.exclude
|
||||
);
|
||||
return [includesTags, includesCats].every(x => x === true);
|
||||
});
|
||||
},
|
||||
fuse() {
|
||||
return new Fuse(this.filteredRecipes, this.options);
|
||||
},
|
||||
fuzzyRecipes() {
|
||||
if (this.searchString.trim() === "") {
|
||||
return this.filteredRecipes.map(x => ({ item: x }));
|
||||
}
|
||||
const result = this.fuse.search(this.searchString.trim());
|
||||
return result;
|
||||
},
|
||||
isSearching() {
|
||||
return this.searchString && this.searchString.length > 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
check(filterBy, recipeList, matchAny, exclude) {
|
||||
let isMatch = true;
|
||||
if (filterBy.length === 0) return isMatch;
|
||||
|
||||
if (recipeList) {
|
||||
if (matchAny) {
|
||||
isMatch = filterBy.some(t => recipeList.includes(t)); // Checks if some items are a match
|
||||
} else {
|
||||
isMatch = filterBy.every(t => recipeList.includes(t)); // Checks if every items is a match
|
||||
}
|
||||
return exclude ? !isMatch : isMatch;
|
||||
} else;
|
||||
return false;
|
||||
},
|
||||
|
||||
updateTagParams(params) {
|
||||
this.tagFilter = params;
|
||||
},
|
||||
updateCatParams(params) {
|
||||
this.catFilter = params;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -6,6 +6,7 @@ import Migration from "@/pages/Admin/Migration";
|
||||
import Profile from "@/pages/Admin/Profile";
|
||||
import ManageUsers from "@/pages/Admin/ManageUsers";
|
||||
import Settings from "@/pages/Admin/Settings";
|
||||
import About from "@/pages/Admin/About";
|
||||
import { store } from "../store";
|
||||
|
||||
export default {
|
||||
@@ -50,5 +51,9 @@ export default {
|
||||
path: "settings",
|
||||
component: Settings,
|
||||
},
|
||||
{
|
||||
path: "about",
|
||||
component: About,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import NewRecipe from "@/pages/Recipe/NewRecipe";
|
||||
import CustomPage from "@/pages/Recipes/CustomPage";
|
||||
import AllRecipes from "@/pages/Recipes/AllRecipes";
|
||||
import CategoryPage from "@/pages/Recipes/CategoryPage";
|
||||
import TagPage from "@/pages/Recipes/TagPage";
|
||||
import Planner from "@/pages/MealPlan/Planner";
|
||||
import Debug from "@/pages/Debug";
|
||||
import LoginPage from "@/pages/LoginPage";
|
||||
@@ -33,7 +34,8 @@ export const routes = [
|
||||
{ path: "/search", component: SearchPage },
|
||||
{ path: "/recipes/all", component: AllRecipes },
|
||||
{ path: "/pages/:customPage", component: CustomPage },
|
||||
{ path: "/recipes/:category", component: CategoryPage },
|
||||
{ path: "/recipes/tag/:tag", component: TagPage },
|
||||
{ path: "/recipes/category/:category", component: CategoryPage },
|
||||
{ path: "/recipe/:recipe", component: ViewRecipe },
|
||||
{ path: "/new/", component: NewRecipe },
|
||||
{ path: "/meal-plan/planner", component: Planner },
|
||||
|
||||
@@ -28,6 +28,10 @@ const store = new Vuex.Store({
|
||||
mealPlanCategories: [],
|
||||
allCategories: [],
|
||||
allTags: [],
|
||||
appInfo: {
|
||||
version: "",
|
||||
demoStatus: false,
|
||||
},
|
||||
},
|
||||
|
||||
mutations: {
|
||||
@@ -43,19 +47,22 @@ const store = new Vuex.Store({
|
||||
setAllTags(state, payload) {
|
||||
state.allTags = payload;
|
||||
},
|
||||
setAppInfo(state, payload) {
|
||||
state.appInfo = payload;
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
async requestRecentRecipes() {
|
||||
const keys = [
|
||||
"name",
|
||||
"slug",
|
||||
"image",
|
||||
"description",
|
||||
"dateAdded",
|
||||
"rating",
|
||||
];
|
||||
const payload = await api.recipes.allByKeys(keys);
|
||||
// const keys = [
|
||||
// "name",
|
||||
// "slug",
|
||||
// "image",
|
||||
// "description",
|
||||
// "dateAdded",
|
||||
// "rating",
|
||||
// ];
|
||||
const payload = await api.recipes.allSummary();
|
||||
|
||||
this.commit("setRecentRecipes", payload);
|
||||
},
|
||||
@@ -67,6 +74,11 @@ const store = new Vuex.Store({
|
||||
const tags = await api.tags.getAll();
|
||||
commit("setAllTags", tags);
|
||||
},
|
||||
|
||||
async requestAppInfo({ commit }) {
|
||||
const response = await api.meta.getAppInfo();
|
||||
commit("setAppInfo", response);
|
||||
},
|
||||
},
|
||||
|
||||
getters: {
|
||||
@@ -76,6 +88,7 @@ const store = new Vuex.Store({
|
||||
state.allCategories.sort((a, b) => (a.slug > b.slug ? 1 : -1)),
|
||||
getAllTags: state =>
|
||||
state.allTags.sort((a, b) => (a.slug > b.slug ? 1 : -1)),
|
||||
getAppInfo: state => state.appInfo,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ const actions = {
|
||||
|
||||
async refreshToken({ commit, getters }) {
|
||||
if (!getters.getIsLoggedIn) {
|
||||
commit("setIsLoggedIn", false); // This is to be here... for some reasons? ¯\_(ツ)_/¯
|
||||
commit("setIsLoggedIn", false); // This has to be here... for some reasons? ¯\_(ツ)_/¯
|
||||
console.log("Not Logged In");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ const monthsShort = [
|
||||
|
||||
export default {
|
||||
getImageURL(image) {
|
||||
return `/api/recipes/${image}/image`;
|
||||
return `/api/recipes/${image}/image?image_type=small`;
|
||||
},
|
||||
generateUniqueKey(item, index) {
|
||||
const uniqueKey = `${item}-${index}`;
|
||||
|
||||
Reference in New Issue
Block a user