models, messages and multiselects

This commit is contained in:
vabene1111
2024-05-01 10:04:19 +02:00
parent 32b75250dc
commit 569b7e78fe
13 changed files with 607 additions and 226 deletions

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectTasksOptions">
<TaskOptions isEnabled="true">
<TaskOptions isEnabled="false">
<option name="arguments" value="-m flake8 $FilePath$ --config $ContentRoot$\.flake8" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
@@ -27,7 +27,7 @@
<option name="workingDir" value="" />
<envs />
</TaskOptions>
<TaskOptions isEnabled="true">
<TaskOptions isEnabled="false">
<option name="arguments" value="-m isort $FilePath$" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
@@ -47,7 +47,7 @@
<option name="workingDir" value="" />
<envs />
</TaskOptions>
<TaskOptions isEnabled="true">
<TaskOptions isEnabled="false">
<option name="arguments" value="-m yapf -i $FilePath$" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
@@ -67,7 +67,7 @@
<option name="workingDir" value="" />
<envs />
</TaskOptions>
<TaskOptions isEnabled="true">
<TaskOptions isEnabled="false">
<option name="arguments" value="--cwd $ProjectFileDir$\vue prettier -w --config $ProjectFileDir$\.prettierrc $FilePath$" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />

View File

@@ -23,7 +23,7 @@
"vue-router": "4",
"vue-simple-calendar": "^7.1.0",
"vuedraggable": "^4.1.0",
"vuetify": "^3.5.16"
"vuetify": "^3.6.1"
},
"devDependencies": {
"@fortawesome/fontawesome-free": "^6.5.1",

View File

@@ -1,12 +1,13 @@
<template>
<v-app>
<v-app-bar color="tandoor" flat density="comfortable">
<router-link :to="{name: 'view_home', params: {}}">
<router-link :to="{ name: 'view_home', params: {} }">
<v-img src="../../assets/brand_logo.svg" width="140px" class="ms-2"></v-img>
</router-link>
<v-spacer></v-spacer>
<global-search-dialog></global-search-dialog>
<v-btn>DEBUG <message-list-dialog></message-list-dialog></v-btn>
<v-avatar color="cyan" class="me-2">V</v-avatar>
</v-app-bar>
@@ -17,14 +18,15 @@
<v-navigation-drawer v-if="lgAndUp">
<v-list-item title="My Application" subtitle="Vuetify"></v-list-item>
<v-divider></v-divider>
<v-list-item prepend-icon="fas fa-book" title="Home" :to="{name: 'view_home', params: {}}"></v-list-item>
<v-list-item prepend-icon="fas fa-calendar-alt" title="Mealplan" :to="{name: 'view_mealplan', params: {}}"></v-list-item>
<v-list-item prepend-icon="fas fa-shopping-cart" title="Shopping" :to="{name: 'view_shopping', params: {}}"></v-list-item>
<v-list-item prepend-icon="fas fa-bars" title="More" :to="{name: 'view_books', params: {}}"></v-list-item> <!-- TODO link -->
<v-list-item prepend-icon="fas fa-book" title="Home" :to="{ name: 'view_home', params: {} }"></v-list-item>
<v-list-item prepend-icon="fas fa-calendar-alt" title="Mealplan" :to="{ name: 'view_mealplan', params: {} }"></v-list-item>
<v-list-item prepend-icon="fas fa-shopping-cart" title="Shopping" :to="{ name: 'view_shopping', params: {} }"></v-list-item>
<v-list-item prepend-icon="fas fa-bars" title="More" :to="{ name: 'view_books', params: {} }"></v-list-item>
<!-- TODO link -->
</v-navigation-drawer>
<v-bottom-navigation grow v-if="!lgAndUp">
<v-btn value="recent" :to="{name: 'view_home', params: {}}">
<v-btn value="recent" :to="{ name: 'view_home', params: {} }">
<v-icon icon="fa-fw fas fa-book "/>
<span>Recipes</span>
</v-btn>
@@ -40,33 +42,31 @@
<span>Shopping</span>
</v-btn>
<v-btn value="nearby" to="/books"> <!-- TODO link -->
<v-btn value="nearby" to="/books">
<!-- TODO link -->
<v-icon icon="fa-fw fas fa-bars"></v-icon>
<span>More</span>
</v-btn>
</v-bottom-navigation>
<v-snackbar-queued
:vertical="true"
location="top center"
></v-snackbar-queued>
</v-app>
</template>
<script lang="ts" setup>
import GlobalSearchDialog from "@/components/inputs/GlobalSearchDialog.vue"
import GlobalSearchDialog from "@/components/inputs/GlobalSearchDialog.vue";
import {ref} from "vue";
import {useDisplay} from "vuetify";
import {useDisplay} from "vuetify"
import VSnackbarQueued from "@/components/display/VSnackbarQueued.vue";
import MessageListDialog from "@/components/dialogs/MessageListDialog.vue";
const {lgAndUp} = useDisplay()
const drawer = ref(true)
const rail = ref(true)
const overlay = ref(false)
</script>
<style scoped>
</style>
<style scoped></style>

View File

@@ -2,39 +2,35 @@
<v-dialog activator="parent" v-model="dialog">
<template v-slot:default="{ isActive }">
<v-card style="overflow: auto">
<v-card-title>Meal Plan Edit <i class="mt-2 float-right fas fa-times" @click="isActive.value = false"></i></v-card-title>
<v-card-title>Meal Plan Edit
<v-btn icon="fas fa-times" variant="flat" size="x-small" class="mt-2 float-right " @click="isActive.value = false"></v-btn>
</v-card-title>
<v-divider></v-divider>
<v-card-text>
<v-form>
<v-row>
<v-col cols="12" md="6">
<v-text-field label="Title"></v-text-field>
<v-text-field label="From Date" type="date"></v-text-field>
<v-text-field label="Meal Type"></v-text-field>
<v-number-input control-variant="split" :min="0"></v-number-input>
<v-text-field label="Share"></v-text-field>
<v-text-field label="Title" v-model="mutableMealPlan.title"></v-text-field>
<v-date-input
v-model="dateRangeValue"
label="Plan Date"
multiple="range"
prepend-icon=""
prepend-inner-icon="$calendar"
></v-date-input>
<ModelSelect model="MealType" v-model="mutableMealPlan.mealType"></ModelSelect>
<v-number-input control-variant="split" :min="0" v-model="mutableMealPlan.servings"></v-number-input>
<v-text-field label="Share" v-model="mutableMealPlan.shared"></v-text-field>
</v-col>
<v-col cols="12" md="6">
<Multiselect
name="recipe"
:columns="{ sm: 12, md : 6}"
label="Recipe"
label-prop="name"
value-prop="id"
:object="true"
:strict="false"
:search="true"
:items="recipeSearch"
:delay="300"
rules="required"
></Multiselect>
<v-text-field label="To Date" type="date"></v-text-field>
<ModelSelect model="recipe" v-model="mutableMealPlan.recipe"></ModelSelect>
<!-- <v-number-input label="Days" control-variant="split" :min="1"></v-number-input>--> <!--TODO create days input with +/- snyced to date -->
<recipe-card :recipe="mutableMealPlan.recipe" v-if="mutableMealPlan && mutableMealPlan.recipe"></recipe-card>
</v-col>
</v-row>
<v-row>
<v-col>
<v-textarea label="Note"></v-textarea>
<v-textarea label="Note" v-model="mutableMealPlan.note"></v-textarea>
</v-col>
</v-row>
</v-form>
@@ -60,7 +56,10 @@ import {DateTime} from "luxon";
import RecipeCard from "@/components/display/RecipeCard.vue";
import {useMealPlanStore} from "@/stores/MealPlanStore";
import {VNumberInput} from 'vuetify/labs/VNumberInput' //TODO remove once component is out of labs
import {VDateInput} from 'vuetify/labs/VDateInput' //TODO remove once component is out of labs
import Multiselect from '@vueform/multiselect'
import ModelSelect from "@/components/inputs/ModelSelect.vue";
import {useMessageStore} from "@/stores/MessageStore";
const props = defineProps(
{
@@ -69,12 +68,16 @@ const props = defineProps(
)
const dialog = ref(false)
let mutableMealPlan = ref(props.mealPlan)
let mutableMealPlan = ref(newMealPlan())
const dateRangeValue = ref([] as Date[])
if (props.mealPlan != undefined) {
mutableMealPlan.value = props.mealPlan
}
watchEffect(() => {
if (props.mealPlan != undefined) {
mutableMealPlan.value = props.mealPlan
} else {
mutableMealPlan.value = newMealPlan()
}
@@ -84,6 +87,14 @@ function saveMealPlan() {
if (mutableMealPlan.value != undefined) {
mutableMealPlan.value.recipe = mutableMealPlan.value.recipe as RecipeOverview
if (dateRangeValue.value != null) {
mutableMealPlan.value.fromDate = dateRangeValue.value[0]
mutableMealPlan.value.toDate = dateRangeValue.value[dateRangeValue.value.length-1]
} else {
useMessageStore().addError('Missing Dates')
return
}
console.log('calling save method')
useMealPlanStore().createOrUpdate(mutableMealPlan.value).catch(err => {
// TODO handle error
@@ -100,23 +111,6 @@ function newMealPlan() {
} as MealPlan
}
async function mealTypeSearch(searchQuery: string) {
console.log('called search')
const api = new ApiApi()
return await api.apiMealTypeList()
}
async function shareUserSearch(searchQuery: string) {
console.log('called su search')
const api = new ApiApi()
return await api.apiUserList()
}
async function recipeSearch(searchQuery: string) {
console.log('called recipe search')
const api = new ApiApi()
return (await api.apiRecipeList({query: searchQuery})).results
}
</script>

View File

@@ -0,0 +1,156 @@
<template>
<v-dialog max-width="70vw" min-height="80vh" activator="parent">
<template v-slot:default="{ isActive }">
<v-card>
<v-card-title>
Nachrichten <v-btn icon="fas fa-times" variant="flat" size="x-small" class="mt-2 float-right " @click="isActive.value = false"></v-btn>
</v-card-title>
<v-card-text>
<h4>Filter</h4>
<v-text-field
class="mt-2"
v-model="search"
label="Search"
prepend-inner-icon="mdi-magnify"
variant="outlined"
clearable
hide-details
single-line
></v-text-field>
<v-btn-toggle
class="mt-2"
v-model="typeFilter"
variant="outlined"
divided
multiple>
<v-btn :value="MessageType.SUCCESS">
<v-icon icon="fas fa-eye" class="me-2" color="success"></v-icon>
Success
</v-btn>
<v-btn :value="MessageType.INFO">
<v-icon icon="fas fa-eye" class="me-2" color="info"></v-icon>
Info
</v-btn>
<v-btn :value="MessageType.WARNING">
<v-icon icon="fas fa-eye" class="me-2" color="warning"></v-icon>
Warning
</v-btn>
<v-btn :value="MessageType.ERROR">
<v-icon icon="fas fa-eye" class="me-2" color="error"></v-icon>
Error
</v-btn>
</v-btn-toggle>
<v-data-table
:headers="tableHeaders"
:items="displayItems"
:sort-by="sortBy"
:search="search"
>
<template v-slot:item.type="{ value }">
<v-chip :color="value">
{{ value }}
</v-chip>
</template>
<template v-slot:item.actions="{ item }">
<v-icon icon="fas fa-search" @click="showDetailDialog = true; detailItem = item"></v-icon>
<v-icon class="ms-1" icon="fas fa-copy" @click="copy(JSON.stringify({'type': item.type, 'createdAt': item.createdAt, 'msg': item.msg, 'data': item.data}));"></v-icon>
</template>
</v-data-table>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="useMessageStore().deleteAllMessages()" color="error">Alle Löschen</v-btn>
<v-btn @click="addTestMessage()" color="warning">Test Nachricht</v-btn>
<v-btn @click="isActive.value = false">Close</v-btn>
</v-card-actions>
</v-card>
</template>
</v-dialog>
<v-dialog v-model="showDetailDialog" max-width="50vw">
<v-card>
<v-card-title>
Nachricht Details <small>{{ DateTime.fromSeconds(detailItem.createdAt).toLocaleString(DateTime.DATETIME_MED) }}</small>
</v-card-title>
<v-divider></v-divider>
<v-card-text>
<v-label>Typ</v-label>
<br/>
<v-chip :color="detailItem.type">{{ detailItem.type }}</v-chip>
<br/>
<v-label class="mt-2">Nachricht</v-label>
<p>{{ detailItem.msg }}</p>
<v-label class="mt-2">Data</v-label>
<pre style="white-space: pre-wrap;" v-if="detailItem.data != null">{{ detailItem.data }}</pre>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
text="Close Dialog"
@click="showDetailDialog = false"
></v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import {computed, ref} from 'vue'
import {Message, MessageType, useMessageStore} from "@/stores/MessageStore";
import {DateTime} from "luxon";
import {useClipboard} from "@vueuse/core";
const {copy} = useClipboard()
const displayItems = computed(() => {
let items = [] as Message[]
useMessageStore().messages.forEach(m => {
if (typeFilter.value.includes(m.type)) {
items.push(m)
}
})
return items
})
const sortBy = ref([{key: 'createdAt', order: 'desc'}])
const search = ref('')
const tableHeaders = ref([
{title: 'Type', key: 'type'},
{
title: 'Created',
key: 'createdAt',
value: (item: Message) => `${DateTime.fromSeconds(item.createdAt).toLocaleString(DateTime.DATETIME_MED)}`,
},
{title: 'Message', key: 'msg'},
{title: 'Actions', key: 'actions', align: 'end'},
])
const typeFilter = ref([MessageType.SUCCESS, MessageType.INFO, MessageType.WARNING, MessageType.ERROR])
const detailItem = ref({} as Message)
const showDetailDialog = ref(false)
function addTestMessage() {
let types = [MessageType.SUCCESS, MessageType.ERROR, MessageType.INFO, MessageType.WARNING]
useMessageStore().addMessage(types[Math.floor(Math.random() * types.length)], `Lorem Ipsum ${Math.random() * 1000}`, 5000, {json: "data", 'msg': 'whatever', data: 1})
}
</script>
<style scoped>
</style>

View File

@@ -61,7 +61,7 @@ const calendarItemHeight = computed(() => {
onMounted(() => {
let api = new ApiApi()
api.apiMealPlanList().then(r => {
mealPlans.value = r
mealPlans.value = r.results
})
})

View File

@@ -0,0 +1,87 @@
<template>
<v-snackbar
v-model="showSnackbar"
:timer="true"
:timeout="visibleMessage.showTimeout"
:color="visibleMessage.type"
:vertical="vertical"
:location="location"
multi-line
>
<small>{{ DateTime.fromSeconds(visibleMessage.createdAt).toLocaleString(DateTime.DATETIME_MED) }}</small> <br/>
{{ visibleMessage.msg }}
<template v-slot:actions>
<v-btn variant="text">View <message-list-dialog></message-list-dialog> </v-btn>
<v-btn variant="text" @click="removeItem()">
<span v-if="useMessageStore().snackbarQueue.length > 1">Next ({{ useMessageStore().snackbarQueue.length - 1 }})</span>
<span v-else>Close</span>
</v-btn>
</template>
</v-snackbar>
</template>
<script setup lang="ts">
import {ref} from 'vue'
import {Message, useMessageStore} from "@/stores/MessageStore";
import {DateTime} from "luxon";
import MessageListDialog from "@/components/dialogs/MessageListDialog.vue";
const props = defineProps({
/**
* passed to VSnackbar location prop https://vuetifyjs.com/en/api/v-snackbar/#props-location
* Specifies the anchor point for positioning the component, using directional cues to align it either horizontally, vertically, or both…
*/
location: {
required: false,
type: String,
default: 'bottom'
},
/**
* passed to VSnackbar vertical prop https://vuetifyjs.com/en/api/v-snackbar/#props-vertical
* Stacks snackbar content on top of the actions (button).
*/
vertical: {
required: false,
type: Boolean,
default: false
}
})
const timeoutId = ref(-1)
const visibleMessage = ref({} as Message)
const showSnackbar = ref(false)
useMessageStore().$subscribe((mutation, state) => {
if ('snackbarQueue' in state && mutation.events.type == 'add') {
processQueue()
}
})
function processQueue() {
if (timeoutId.value == -1 && useMessageStore().snackbarQueue.length > 0) {
visibleMessage.value = useMessageStore().snackbarQueue[0]
showSnackbar.value = true
timeoutId.value = setTimeout(() => {
useMessageStore().snackbarQueue.shift()
timeoutId.value = -1
processQueue()
}, visibleMessage.value.showTimeout + 50)
}
}
function removeItem() {
showSnackbar.value = false
clearTimeout(timeoutId.value)
timeoutId.value = -1
useMessageStore().snackbarQueue.shift()
processQueue()
}
</script>
<style scoped>
</style>

View File

@@ -1,98 +1,117 @@
<template>
<v-input>
<!--TODO Problems: 1. behind other cards when those are underneath the element, making card overflow visible breaks cards -->
<VueMultiselect
:id="id"
v-model="selected_items"
:options="items"
:close-on-select="true"
:clear-on-select="true"
:hide-selected="multiple"
:preserve-search="true"
:internal-search="false"
:limit="limit"
:placeholder="model"
:label="label"
track-by="id"
:multiple="multiple"
:taggable="allowCreate"
tag-placeholder="TODO CREATE PLACEHOLDER"
:loading="search_loading"
@search-change="debouncedSearchFunction"
@input="selectionChanged"
@tag="addItem"
@open="search('')"
:disabled="disabled"
>
</VueMultiselect>
</v-input>
<!-- &lt;!&ndash;TODO Problems: 1. behind other cards when those are underneath the element, making card overflow visible breaks cards &ndash;&gt;-->
<!-- <VueMultiselect-->
<!-- :id="id"-->
<!-- v-model="selected_items"-->
<!-- :options="items"-->
<!-- :close-on-select="true"-->
<!-- :clear-on-select="true"-->
<!-- :hide-selected="multiple"-->
<!-- :preserve-search="true"-->
<!-- :internal-search="false"-->
<!-- :limit="limit"-->
<!-- :placeholder="model"-->
<!-- :label="label"-->
<!-- track-by="id"-->
<!-- :multiple="multiple"-->
<!-- :taggable="allowCreate"-->
<!-- tag-placeholder="TODO CREATE PLACEHOLDER"-->
<!-- :loading="search_loading"-->
<!-- @search-change="debouncedSearchFunction"-->
<!-- @input="selectionChanged"-->
<!-- @tag="addItem"-->
<!-- @open="search('')"-->
<!-- :disabled="disabled"-->
<!-- class="material-multiselect"-->
<!-- >-->
<!-- </VueMultiselect>-->
<Multiselect
class="material-multiselect z-max"
v-model="model"
:options="search"
:delay="300"
:object="true"
valueProp="id"
:label="label"
:searchable="true"
:strict="false"
:disabled="disabled"
/>
</v-input>
</template>
<script lang="ts" setup>
import {computed, onMounted, ref, Ref,} from 'vue'
import {ApiApi} from "@/openapi/index.js";
import {useDebounceFn} from "@vueuse/core";
import {GenericModel, getModelFromStr} from "@/types/Models";
import VueMultiselect from 'vue-multiselect'
import {computed, onMounted, PropType, ref, Ref} from "vue"
import {ApiApi} from "@/openapi/index.js"
import {useDebounceFn} from "@vueuse/core"
import {GenericModel, getModelFromStr} from "@/types/Models"
import Multiselect from '@vueform/multiselect'
const props = defineProps(
{
model: {type: String, required: true},
multiple: {type: Boolean, default: true},
limit: {type: Number, default: 25},
allowCreate: {type: Boolean, default: false},
const emit = defineEmits(['update:modelValue'])
id: {type: String, required: false, default: Math.random().toString()},
const props = defineProps({
model: {type: String, required: true},
// not verified
search_on_load: {type: Boolean, default: false},
id: {type: String, required: false, default: Math.random().toString()},
// not verified
clearable: {type: Boolean, default: false,},
chips: {type: Boolean, default: undefined,},
multiple: {type: Boolean, default: true},
limit: {type: Number, default: 25},
allowCreate: {type: Boolean, default: false},
itemName: {type: String, default: 'name'},
itemValue: {type: String, default: 'id'},
search_on_load: {type: Boolean, default: false},
clearable: {type: Boolean, default: false},
chips: {type: Boolean, default: undefined},
placeholder: {type: String, default: undefined},
label: {type: String, default: "name"},
parent_variable: {type: String, default: undefined},
itemName: {type: String, default: "name"},
itemValue: {type: String, default: "id"},
sticky_options: {
type: Array,
default() {
return []
},
placeholder: {type: String, default: undefined},
label: {type: String, default: "name"},
parent_variable: {type: String, default: undefined},
sticky_options: {
type: Array,
default() {
return []
},
initial_selection: {
type: Array,
default() {
return []
},
},
initial_single_selection: {
type: Object,
default: undefined,
},
initial_selection: {
type: Array,
default() {
return []
},
},
initial_single_selection: {
type: Object,
default: undefined,
},
disabled: {type: Boolean, default: false},
})
disabled: {type: Boolean, default: false,},
}
)
const model = defineModel()
const model_class = ref({} as GenericModel<any>)
const items: Ref<Array<any>> = ref([])
const selected_items: Ref<Array<any> | any> = ref(undefined)
const search_query = ref('')
const search_query = ref("")
const search_loading = ref(false)
const elementId = ref((Math.random() * 100000).toString())
onMounted(() => {
model_class.value = getModelFromStr(props.model)
if (props.search_on_load) {
debouncedSearchFunction('')
debouncedSearchFunction("")
}
})
@@ -108,16 +127,9 @@ const debouncedSearchFunction = useDebounceFn((query: string) => {
* @param query input to search for on the API
*/
function search(query: string) {
search_loading.value = true
model_class.value.list(query).then(r => {
items.value = r
if (props.allowCreate && search_query.value != '') {
// TODO check if search_query is already in items
items.value.unshift({id: null, name: `Create "${search_query.value}"`})
}
}).catch(err => {
return model_class.value.list(query).then((r) => {
return r
}).catch((err) => {
//useMessageStore().addMessage(MessageType.ERROR, err, 8000)
}).finally(() => {
search_loading.value = false
@@ -129,7 +141,7 @@ function addItem(item: string) {
const api = new ApiApi()
api.apiKeywordList()
model_class.value.create(item).then(createdObj => {
model_class.value.create(item).then((createdObj) => {
//StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE)
if (selected_items.value instanceof Array) {
selected_items.value.push(createdObj)
@@ -146,12 +158,19 @@ function addItem(item: string) {
}
function selectionChanged() {
//this.$emit("change", { var: this.parent_variable, val: this.selected_objects })
emit('update:modelValue', selected_items)
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
<style src="@vueform/multiselect/themes/default.css"></style>
<style scoped>
</style>
.material-multiselect {
--ms-line-height: 2.5;
--ms-bg: rgba(235, 235, 235, 0.75);
--ms-border-color: 0;
--ms-border-color-active: 0;
border-bottom: 4px #0f0f0f;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
</style>

View File

@@ -1,77 +1,80 @@
<template>
<v-container>
<v-container>
<horizontal-meal-plan-window></horizontal-meal-plan-window>
<!--TODO ideas for "start page": new recipes, meal plan, "last year/month/cooked long ago", high rated, random keyword -->
<!--TODO if nothing comes up for a category, hide the element, probably move fetch logic into component -->
<horizontal-recipe-scroller title="New Recipes" :skeletons="4" :recipes="new_recipes" icon="fas fa-calendar-alt"></horizontal-recipe-scroller>
<horizontal-recipe-scroller title="Top Rated" :skeletons="2" :recipes="high_rated_recipes" icon="fas fa-star"></horizontal-recipe-scroller>
<horizontal-recipe-scroller :title="random_keyword.label" :skeletons="4" :recipes="random_keyword_recipes" icon="fas fa-tags" v-if="random_keyword.label"></horizontal-recipe-scroller>
</v-container>
<horizontal-meal-plan-window></horizontal-meal-plan-window>
<!--TODO ideas for "start page": new recipes, meal plan, "last year/month/cooked long ago", high rated, random keyword -->
<!--TODO if nothing comes up for a category, hide the element, probably move fetch logic into component -->
<horizontal-recipe-scroller title="New Recipes" :skeletons="4" :recipes="new_recipes" icon="fas fa-calendar-alt"></horizontal-recipe-scroller>
<horizontal-recipe-scroller title="Top Rated" :skeletons="2" :recipes="high_rated_recipes" icon="fas fa-star"></horizontal-recipe-scroller>
<horizontal-recipe-scroller
:title="random_keyword.label"
:skeletons="4"
:recipes="random_keyword_recipes"
icon="fas fa-tags"
v-if="random_keyword.label"
></horizontal-recipe-scroller>
</v-container>
</template>
<script lang="ts">
import {defineComponent, ref} from 'vue'
import {ApiApi, Keyword, Recipe, RecipeOverview} from "@/openapi";
import KeywordsComponent from "@/components/display/KeywordsBar.vue";
import RecipeCardComponent from "@/components/display/RecipeCard.vue";
import GlobalSearchDialog from "@/components/inputs/GlobalSearchDialog.vue";
import RecipeCard from "@/components/display/RecipeCard.vue";
import HorizontalRecipeScroller from "@/components/display/HorizontalRecipeWindow.vue";
import {DateTime} from "luxon";
import {useMealPlanStore} from "@/stores/MealPlanStore";
import HorizontalMealPlanWindow from "@/components/display/HorizontalMealPlanWindow.vue";
import MealPlanDialog from "@/components/dialogs/MealPlanDialog.vue";
import { defineComponent, ref } from "vue"
import { ApiApi, Keyword, Recipe, RecipeOverview } from "@/openapi"
import KeywordsComponent from "@/components/display/KeywordsBar.vue"
import RecipeCardComponent from "@/components/display/RecipeCard.vue"
import GlobalSearchDialog from "@/components/inputs/GlobalSearchDialog.vue"
import RecipeCard from "@/components/display/RecipeCard.vue"
import HorizontalRecipeScroller from "@/components/display/HorizontalRecipeWindow.vue"
import { DateTime } from "luxon"
import { useMealPlanStore } from "@/stores/MealPlanStore"
import HorizontalMealPlanWindow from "@/components/display/HorizontalMealPlanWindow.vue"
import MealPlanDialog from "@/components/dialogs/MealPlanDialog.vue"
import ModelSelect from "@/components/inputs/ModelSelect.vue"
export default defineComponent({
name: "StartPage",
components: {MealPlanDialog, HorizontalMealPlanWindow, HorizontalRecipeScroller, RecipeCard, GlobalSearchDialog, RecipeCardComponent, KeywordsComponent},
computed: { },
data() {
return {
recipes: [] as Recipe[],
items: Array.from({length: 50}, (k, v) => v + 1),
name: "StartPage",
components: { ModelSelect, MealPlanDialog, HorizontalMealPlanWindow, HorizontalRecipeScroller, RecipeCard, GlobalSearchDialog, RecipeCardComponent, KeywordsComponent },
computed: {},
data() {
return {
recipes: [] as Recipe[],
items: Array.from({ length: 50 }, (k, v) => v + 1),
new_recipes: [] as RecipeOverview[],
high_rated_recipes: [] as RecipeOverview[],
random_keyword: {} as Keyword,
random_keyword_recipes: [] as RecipeOverview[],
}
},
mounted() {
const api = new ApiApi()
new_recipes: [] as RecipeOverview[],
high_rated_recipes: [] as RecipeOverview[],
random_keyword: {} as Keyword,
random_keyword_recipes: [] as RecipeOverview[],
}
},
mounted() {
const api = new ApiApi()
api.apiRecipeList({_new: 'true', pageSize: 16}).then(r => {
if (r.results != undefined) { // TODO openapi generator makes arrays nullable for some reason
this.new_recipes = r.results
}
api.apiRecipeList({ _new: "true", pageSize: 16 }).then((r) => {
if (r.results != undefined) {
// TODO openapi generator makes arrays nullable for some reason
this.new_recipes = r.results
}
})
api.apiRecipeList({ rating: 4, pageSize: 16 }).then((r) => {
if (r.results != undefined) {
this.high_rated_recipes = r.results
}
})
api.apiKeywordList({ random: "true", limit: "1" }).then((r) => {
if (r.results != undefined && r.results.length > 0) {
this.random_keyword = r.results[0]
api.apiRecipeList({ keywords: r.results[0].id }).then((r) => {
if (r.results != undefined) {
this.random_keyword_recipes = r.results
}
})
api.apiRecipeList({rating: 4, pageSize: 16}).then(r => {
if (r.results != undefined) {
this.high_rated_recipes = r.results
}
})
api.apiKeywordList({random: 'true', limit: '1'}).then(r => {
if (r.results != undefined && r.results.length > 0) {
this.random_keyword = r.results[0]
api.apiRecipeList({keywords: r.results[0].id}).then(r => {
if (r.results != undefined) {
this.random_keyword_recipes = r.results
}
})
}
})
},
methods: {}
}
})
},
methods: {},
})
</script>
<style scoped>
</style>
<style scoped></style>

View File

@@ -2,6 +2,7 @@ import {acceptHMRUpdate, defineStore} from "pinia"
import {ApiApi, MealPlan} from "@/openapi";
import {computed, ref} from "vue";
import {DateTime} from "luxon";
import {ErrorMessageType, MessageType, useMessageStore} from "@/stores/MessageStore";
const _STORE_ID = "meal_plan_store"
@@ -54,10 +55,12 @@ export const useMealPlanStore = defineStore(_STORE_ID, () => {
const api = new ApiApi()
return api.apiMealPlanList({fromDate: DateTime.fromJSDate(from_date).toISODate() as string, toDate: DateTime.fromJSDate(to_date).toISODate() as string}).then(r => {
r.forEach((p) => {
r.results.forEach((p) => {
plans.value.set(p.id, p)
})
currently_updating.value = [new Date(0), new Date(0)]
}).catch((err) => {
useMessageStore().addError(ErrorMessageType.FETCH_ERROR, err)
})
}
return new Promise(() => {
@@ -65,7 +68,7 @@ export const useMealPlanStore = defineStore(_STORE_ID, () => {
}
function createOrUpdate(object: MealPlan) {
if(object.id == undefined){
if (object.id == undefined) {
return createObject(object)
} else {
return updateObject(object)
@@ -74,32 +77,32 @@ export const useMealPlanStore = defineStore(_STORE_ID, () => {
function createObject(object: MealPlan) {
const api = new ApiApi()
return api.apiMealPlanCreate({mealPlanRequest: object}).then((r) => {
//StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_CREATE)
return api.apiMealPlanCreate({mealPlan: object}).then((r) => {
useMessageStore().addMessage(MessageType.SUCCESS, 'Created successfully', 7000, object)
plans.value.set(r.id, r)
return r
}).catch((err) => {
//StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE, err)
useMessageStore().addError(ErrorMessageType.CREATE_ERROR, err)
})
}
function updateObject(object: MealPlan) {
const api = new ApiApi()
return api.apiMealPlanUpdate({id: object.id, mealPlanRequest: object}).then((r) => {
//StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_UPDATE)
useMessageStore().addMessage(MessageType.SUCCESS, 'Updated successfully', 7000, object)
plans.value.set(r.id, r)
}).catch((err) => {
//StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err)
useMessageStore().addError(ErrorMessageType.UPDATE_ERROR, err)
})
}
function deleteObject(object: MealPlan) {
const api = new ApiApi()
return api.apiMealPlanDestroy({id: object.id}).then((r) => {
//StandardToasts.makeStandardToast(this, StandardToasts.SUCCESS_DELETE)
useMessageStore().addMessage(MessageType.INFO, 'Deleted successfully', 7000, object)
plans.value.delete(object.id)
}).catch((err) => {
//StandardToasts.makeStandardToast(this, StandardToasts.FAIL_DELETE, err)
useMessageStore().addError(ErrorMessageType.DELETE_ERROR, err)
})
}

View File

@@ -0,0 +1,96 @@
import {acceptHMRUpdate, defineStore} from 'pinia'
import {ref} from "vue";
import {useStorage} from "@vueuse/core";
import {DateTime} from "luxon";
/** @enum {string} different message types */
export enum MessageType {
ERROR = 'error',
WARNING = 'warning',
INFO = 'info',
SUCCESS = 'success',
}
/** @enum {string} pre defined error messages */
export enum ErrorMessageType {
FETCH_ERROR = 'Fetch Error',
UPDATE_ERROR = 'Update Error',
CREATE_ERROR = 'Update Error',
DELETE_ERROR = 'Update Error',
}
/**
* Type Message holding all required contents of a message
*/
export class Message {
type = {} as MessageType
createdAt = -1
showTimeout = 0
msg = ""
data = {} as any
code = ''
constructor(type: MessageType, msg: string, showTimeout?: number, data?: any) {
if (typeof showTimeout === 'undefined') {
showTimeout = 0
}
if (typeof data === 'undefined') {
data = {}
}
this.type = type
this.msg = msg
this.showTimeout = showTimeout
this.data = data
this.createdAt = DateTime.now().toSeconds()
}
toString() {
return {'type': this.type, 'createdAt': this.createdAt, 'msg': this.msg, 'data': this.data}
}
}
export const useMessageStore = defineStore('message_store', () => {
let messages = useStorage('LOCAL_MESSAGES', [] as Message[])
let snackbarQueue = ref([] as Message[])
/**
* Add a message to the message store. If showTimeout is greater than 0 it is also added to the display queue.
* @param {MessageType} type type of message
* @param {String} msg message text
* @param {number} showTimeout optional number of ms to show message to user, set to 0 or leave undefined for silent message
* @param {string} data optional additional data only shown in log
*/
function addMessage(type: MessageType, msg: string, showTimeout?: number, data?: any) {
let message = new Message(type, msg, showTimeout, data)
messages.value.push(message)
if (message.showTimeout > 0) {
snackbarQueue.value.push(message)
}
}
/**
* shorthand function to quickly add an error message
* @param errorType pre defined error type
* @param data optional error data
*/
function addError(errorType: ErrorMessageType | string, data?: any) {
addMessage(MessageType.ERROR, errorType, 7000, data)
}
/**
* delete all messages from store
*/
function deleteAllMessages() {
messages.value = [] as Message[]
}
return {snackbarQueue, messages, addMessage, addError, deleteAllMessages}
})
// enable hot reload for store
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useMessageStore, import.meta.hot))
}

View File

@@ -1,4 +1,4 @@
import {ApiApi, Keyword as IKeyword, Food as IFood, RecipeOverview as IRecipeOverview, Recipe as IRecipe, Unit as IUnit} from "@/openapi";
import {ApiApi, Keyword as IKeyword, Food as IFood, RecipeOverview as IRecipeOverview, Recipe as IRecipe, Unit as IUnit, MealType as IMealType} from "@/openapi";
export function getModelFromStr(model_name: String) {
switch (model_name.toLowerCase()) {
@@ -14,6 +14,9 @@ export function getModelFromStr(model_name: String) {
case 'recipe': {
return new Recipe
}
case 'mealtype': {
return new MealType
}
default: {
throw Error(`Invalid Model ${model_name}, did you forget to register it in Models.ts?`)
}
@@ -99,4 +102,24 @@ export class Recipe extends GenericModel<IRecipeOverview> {
}
})
}
}
export class MealType extends GenericModel<IMealType> {
create(name: string) {
const api = new ApiApi()
return api.apiMealTypeCreate({mealType: {name: name} as IMealType}).then(r => {
return r as unknown as IRecipeOverview
})
}
list(query: string) {
const api = new ApiApi()
return api.apiMealTypeList({}).then(r => {
if (r.results) {
return r.results
} else {
return []
}
})
}
}

View File

@@ -1212,10 +1212,10 @@ vuedraggable@^4.1.0:
dependencies:
sortablejs "1.14.0"
vuetify@^3.5.16:
version "3.5.16"
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.5.16.tgz#5046aab39bfa536f0d99c5be4f9d91a7245c3246"
integrity sha512-jyApfATreFMkgjvK0bL7ntZnr+p9TU73+4E3kX6fIvUitdAP9fltG7yj+v3k14HLqZRSNhTL1GhQ95DFx631zw==
vuetify@^3.6.1:
version "3.6.1"
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.6.1.tgz#bd26f1ee53c532a4f9880a5f376064f53efd9947"
integrity sha512-fzcY9LNuLZUwXG4XyklkGJwFv/ejQaERAgE5e+U5M9dtbF9ZRs56mGi2uqOhQOv6o+vbikpHZ/rfHfhb/XlO0g==
w3c-xmlserializer@^4.0.0:
version "4.0.0"