Vue Timeline Meal Planner

Use the timeline summary mode/event listing to create a weekly meal planner. Meal types are represented as resources with a custom template. Events are the actual meals with custom properties, like calories or notes.

The add/edit form shows up in a custom popover that opens on double click for meal creation and on click for editing.

The popover header is customized with a custom header template using the header template slot.

By default the second dimension of the timeline (vertical axis) is reserved for resources, however it can be configured and used as "time slots" if the times are not relevant.

Get started with this example

Demo

Loading the live demo

Preview in full screen

Build a weekly meal planner with the Timeline — meal types as color-coded resource rows, custom add/edit popover for creating or updating meals.

Code

<script setup>
import {
  formatDate,
  getJson,
  MbscButton,
  MbscEventcalendar,
  MbscInput,
  MbscPopup,
  MbscSegmented,
  MbscSegmentedGroup,
  MbscSnackbar,
  MbscTextarea,
  setOptions 
} from '@mobiscroll/vue'
import { onMounted, ref } from 'vue'

setOptions({
  locale:  ,
  theme: ' ',
  themeVariant: ' '
})

const types = [
  {
    id: 1,
    name: 'Breakfast',
    color: '#e20f0f',
    kcal: '300 - 400 kcal',
    icon: '🍳'
  },
  {
    id: 2,
    name: 'Elevenses',
    color: '#157d13',
    kcal: '100 - 200 kcal',
    icon: '🍌'
  },
  {
    id: 3,
    name: 'Lunch',
    color: '#32a6de',
    kcal: '500 - 700 kcal',
    icon: '🍜'
  },
  {
    id: 4,
    name: 'Dinner',
    color: '#e29d1d',
    kcal: '400 - 600 kcal',
    icon: '🥙'
  },
  {
    id: 5,
    name: 'Snack',
    color: '#68169c',
    kcal: '100 - 200 kcal',
    icon: '🥨'
  }
]

const myView = {
  timeline: {
    type: 'week',
    eventDisplay: 'fill'
  }
}

const responsivePopup = {
  medium: {
    display: 'center',
    width: 400,
    fullScreen: false,
    touchUi: false,
    showOverlay: false
  }
}

const popupAddButtons = [
  'cancel',
  {
    handler: () => {
      saveEvent()
    },
    keyCode: 'enter',
    text: 'Add',
    cssClass: 'mbsc-popup-button-primary'
  }
]

const popupEditButtons = [
  'cancel',
  {
    handler: () => {
      saveEvent()
    },
    keyCode: 'enter',
    text: 'Save',
    cssClass: 'mbsc-popup-button-primary'
  }
]

const myMeals = ref([])
const tempMeal = ref(null)
const isPopupOpen = ref(false)
const isEdit = ref(false)
const name = ref('')
const calories = ref('')
const notes = ref('')
const headerPrimary = ref('')
const headerDate = ref('')
const type = ref(1)
const popupButtons = ref([])
const isSnackbarOpen = ref(false)
const snackbarButton = ref({})

function myDefaultEvent() {
  return {
    title: 'New meal',
    allDay: true
  }
}

function saveEvent() {
  tempMeal.value.title = name.value
  tempMeal.value.calories = calories.value
  tempMeal.value.notes = notes.value
  tempMeal.value.allDay = true
  tempMeal.value.resource = type.value
  if (isEdit.value) {
    // Update the event in the list
    myMeals.value = [...myMeals.value]
  } else {
    // Add the new event to the list
    myMeals.value = [...myMeals.value, tempMeal.value]
  }
  // Close the popup
  isPopupOpen.value = false
}

function loadPopupForm(event) {
  name.value = event.title
  calories.value = event.calories
  notes.value = event.notes
}

function deleteEvent(event) {
  myMeals.value = myMeals.value.filter((item) => item.id !== event.id)
  snackbarButton.value = {
    action: () => {
      myMeals.value = [...myMeals.value, event]
    },
    text: 'Undo'
  }
  isSnackbarOpen.value = true
}

function handleEventClick(args) {
  const event = args.event
  const resource = args.resourceObj
  isEdit.value = true
  tempMeal.value = event
  // Fill popup form with event data
  loadPopupForm(event)
  // Set popup options
  popupButtons.value = popupEditButtons
  headerPrimary.value = resource.name
  headerDate.value = formatDate('DDDD, DD MMMM YYYY', new Date(event.start))
  type.value = event.resource
  // Open the popup
  isPopupOpen.value = true
}

function handleEventCreated(args) {
  const event = args.event
  isEdit.value = false
  tempMeal.value = event
  // Fill popup form with event data
  loadPopupForm(event)
  // Set popup options
  popupButtons.value = popupAddButtons
  headerPrimary.value = 'New meal'
  headerDate.value = formatDate('DDDD, DD MMMM YYYY', new Date(event.start))
  type.value = event.resource
  // Open the popup
  isPopupOpen.value = true
}

function handleEventDeleted(args) {
  deleteEvent(args.event)
}

function handleDelete() {
  deleteEvent(tempMeal)
  isPopupOpen.value = false
}

function handlePopupClose() {
  isPopupOpen.value = false
}

function handleSnackbarClose() {
  isSnackbarOpen.value = false
}

onMounted(() => {
  getJson(
    'https://trial.mobiscroll.com/meal-planner/',
    (events) => {
      myMeals.value = events
    },
    'jsonp'
  )
})
</script>

<template>
  <MbscEventcalendar
    cssClass="md-meal-planner-calendar"
    :view="myView"
    :data="myMeals"
    :resources="types"
    :dragToCreate="false"
    :dragToResize="false"
    :dragToMove="true"
    :clickToCreate="true"
    :extendDefaultEvent="myDefaultEvent"
    @event-click="handleEventClick"
    @event-created="handleEventCreated"
    @event-deleted="handleEventDeleted"
  >
    <template #resource="resource">
      <div class="md-meal-planner-cont">
        <div class="md-meal-planner-title" :style="{ color: resource.color }">
          <span class="md-meal-planner-icon">{{ resource.icon }}</span>
          {{ resource.name }}
        </div>
        <div class="md-meal-planner-kcal">{{ resource.kcal }}</div>
      </div>
    </template>
    <template #timelineEventContent="args">
      <div class="md-meal-planner-event">
        <div class="md-meal-planner-event-title">{{ args.original.title }}</div>
        <div v-if="args.original.calories" class="md-meal-planner-event-desc">
          Calories {{ args.original.calories }} kcal
        </div>
      </div>
    </template>
  </MbscEventcalendar>
  <MbscPopup
    cssClass="md-meal-planner-popup"
    display="bottom"
    :fullScreen="true"
    :contentPadding="false"
    :buttons="popupButtons"
    :isOpen="isPopupOpen"
    :responsive="responsivePopup"
    @close="handlePopupClose"
  >
    <template #header>
      <div class="md-meal-planner-header-primary">{{ headerPrimary }}</div>
      <div class="md-meal-planner-header-date">{{ headerDate }}</div>
    </template>
    <MbscSegmentedGroup v-model="type">
      <template v-for="t in types" :key="t.id">
        <MbscSegmented :value="t.id">{{ t.name }}</MbscSegmented>
      </template>
    </MbscSegmentedGroup>
    <div class="mbsc-form-group">
      <MbscInput label="Name" v-model="name" />
      <MbscInput label="Calories" v-model="calories" />
      <MbscTextarea label="Notes" v-model="notes" />
    </div>
    <div v-if="isEdit" class="mbsc-button-group">
      <MbscButton
        className="mbsc-button-block"
        color="danger"
        variant="outline"
        @click="handleDelete"
      >
        Delete meal
      </MbscButton>
    </div>
  </MbscPopup>
  <MbscSnackbar
    message="Event deleted"
    :button="snackbarButton"
    :isOpen="isSnackbarOpen"
    @close="handleSnackbarClose"
  />
</template>

<style>
.md-meal-planner-header-primary {
  font-size: 16px;
  line-height: 20px;
}

.md-meal-planner-header-date {
  font-size: 14px;
  font-weight: 600;
  line-height: 20px;
  opacity: 0.6;
}

.md-meal-planner-cont {
  position: relative;
  padding-left: 34px;
  text-align: left;
}

.md-meal-planner-title {
  font-size: 20px;
}

.md-meal-planner-kcal {
  color: #929292;
}

.md-meal-planner-icon {
  position: absolute;
  left: 0;
}

.md-meal-planner-calendar.mbsc-calendar .mbsc-schedule-event-all-day-inner {
  height: 40px;
  display: flex;
  align-items: center;
}

.md-meal-planner-calendar .md-meal-planner-event-title {
  font-size: 12px;
  line-height: 18px;
}

.md-meal-planner-calendar .md-meal-planner-event-desc {
  opacity: 0.6;
  line-height: 18px;
}

.md-meal-planner-popup
  .mbsc-segmented-item:first-child
  .mbsc-selected
  .mbsc-button-bg.mbsc-material,
.md-meal-planner-popup .mbsc-segmented-item:first-child .mbsc-selected.mbsc-windows,
.md-meal-planner-popup .mbsc-segmented-item:first-child .mbsc-segmented-selectbox-inner {
  background: #f08786;
}

.md-meal-planner-popup
  .mbsc-segmented-item:nth-child(2)
  .mbsc-selected
  .mbsc-button-bg.mbsc-material,
.md-meal-planner-popup .mbsc-segmented-item:nth-child(2) .mbsc-selected.mbsc-windows,
.md-meal-planner-popup .mbsc-segmented-item:nth-child(2) .mbsc-segmented-selectbox-inner {
  background: #8abe89;
}

.md-meal-planner-popup
  .mbsc-segmented-item:nth-child(3)
  .mbsc-selected
  .mbsc-button-bg.mbsc-material,
.md-meal-planner-popup .mbsc-segmented-item:nth-child(3) .mbsc-selected.mbsc-windows,
.md-meal-planner-popup .mbsc-segmented-item:nth-child(3) .mbsc-segmented-selectbox-inner {
  background: #99d3ef;
}

.md-meal-planner-popup
  .mbsc-segmented-item:nth-child(4)
  .mbsc-selected
  .mbsc-button-bg.mbsc-material,
.md-meal-planner-popup .mbsc-segmented-item:nth-child(4) .mbsc-selected.mbsc-windows,
.md-meal-planner-popup .mbsc-segmented-item:nth-child(4) .mbsc-segmented-selectbox-inner {
  background: #f0ce8e;
}

.md-meal-planner-popup .mbsc-segmented-item:last-child .mbsc-selected .mbsc-button-bg.mbsc-material,
.md-meal-planner-popup .mbsc-segmented-item:last-child .mbsc-selected.mbsc-windows,
.md-meal-planner-popup .mbsc-segmented-item:last-child .mbsc-segmented-selectbox-inner {
  background: #b48bce;
}
</style>

Duplicate Session

You have signed in on a different machine. To keep everything in sync, you have been signed out of this session.

If you need to invite team members, manage seats and accounts you can do it from the licenses page.

Duplicate Session

We noticed you are logged in on a different machine and have closed your old session.

If you need to invite team members, manage seats and accounts you can do it from the licenses page.