---
title: "React Timeline Meal Planner Example | Mobiscroll"
component: "Timeline"
framework: "React"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/react/timeline/meal-planner"
---

# Meal Planner - React Timeline demo

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

- [Live demo](https://demo.mobiscroll.com/react/timeline/meal-planner)
- [View React JS code on GitHub](https://github.com/acidb/mobiscroll-demos-react/tree/main/src/demos/eventcalendar/timeline/meal-planner)
- [View React TS code on GitHub](https://github.com/acidb/mobiscroll-demos-react-ts/tree/main/src/demos/eventcalendar/timeline/meal-planner)
- This demo in other frameworks: [JavaScript](https://demo.mobiscroll.com/javascript/timeline/meal-planner.md), [jQuery](https://demo.mobiscroll.com/jquery/timeline/meal-planner.md), [Angular](https://demo.mobiscroll.com/angular/timeline/meal-planner.md), [Vue](https://demo.mobiscroll.com/vue/timeline/meal-planner.md)

## Description

Use the [timeline summary mode/event listing](https://demo.mobiscroll.com/react/timeline/event-listing) to create a weekly meal planner. Meal types are represented as resources with a [custom template](https://demo.mobiscroll.com/react/scheduler/custom-resource-header-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 renderer function using the [`renderHeader`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderHeader) option.

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.

## What this demo shows

- Displays a weekly timeline-based meal planner where days are shown horizontally and meal types are shown as resource rows.
- **Resources** Each resource row represents a meal type, and each meal type has its own color so related events are easy to distinguish.
- **Events** Events represent the planned food item for a specific day and meal, using the resource color across the timeline cell.
- **Editing** Clicking an existing meal opens a popup with an edit form for updating the meal name, calories, and notes.
- **Deleting** The edit popup includes a delete action that removes the selected meal immediately, then shows a snackbar confirmation with an Undo action to restore it.
- **Creating** Double-clicking an empty timeline cell opens a popup for creating a new meal for the selected day and meal row.
- **Form behavior** The create popup lets users enter the meal name, calories, and notes before saving the new event.

## Best for

- **Meal planning** Weekly meal planning workflows for restaurants, schools, kindergartens, nursing homes, and similar institutions.
- **Day-based planning** Scenarios where items need to be planned by day without showing exact start and end times.
- **Resource-based organization** Setups where color-coded meal categories or resource rows make planned items easier to scan.
- **Direct calendar editing** Applications that need users to add, edit, delete, and restore planned items directly from the calendar UI.

## API used in this demo

- [`theme`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-themeVariant) - option
- [`display`](https://mobiscroll.com/docs/react/popup/api#opt-display) - option
- [`width`](https://mobiscroll.com/docs/react/popup/api#opt-width) - option
- [`fullScreen`](https://mobiscroll.com/docs/react/popup/api#opt-fullScreen) - option
- [`touchUi`](https://mobiscroll.com/docs/react/popup/api#opt-touchUi) - option
- [`showOverlay`](https://mobiscroll.com/docs/react/popup/api#opt-showOverlay) - option
- [`cssClass`](https://mobiscroll.com/docs/react/popup/api#opt-cssClass) - option
- [`view`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-view) - option
- [`data`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-data) - option
- [`resources`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-resources) - option
- [`dragToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToCreate) - option
- [`dragToResize`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToResize) - option
- [`dragToMove`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToMove) - option
- [`clickToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-clickToCreate) - option
- [`extendDefaultEvent`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-extendDefaultEvent) - option
- [`onEventClick`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventClick) - event
- [`onEventCreated`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventCreated) - event
- [`onEventDeleted`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventDeleted) - event
- [`renderResource`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderResource) - renderer
- [`renderTimelineEventContent`](https://mobiscroll.com/docs/react/eventcalendar/api#renderer-renderTimelineEventContent) - renderer
- [`cssClass`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-cssClass) - option
- [`renderHeader`](https://mobiscroll.com/docs/react/popup/api#renderer-renderHeader) - renderer
- [`buttons`](https://mobiscroll.com/docs/react/popup/api#opt-buttons) - option
- [`isOpen`](https://mobiscroll.com/docs/react/popup/api#opt-isOpen) - option
- [`onClose`](https://mobiscroll.com/docs/react/popup/api#event-onClose) - event
- [`responsive`](https://mobiscroll.com/docs/react/popup/api#opt-responsive) - option
- [`onClose`](https://mobiscroll.com/docs/react/notifications/snackbar#event-onClose) - event

## Code

### JSX

```jsx
import {
  Button,
  Eventcalendar,
  formatDate,
  getJson,
  Input,
  Popup,
  Segmented,
  SegmentedGroup,
  setOptions,
  Snackbar,
  Textarea,
} from '@mobiscroll/react';
import { useCallback, useEffect, useMemo, useState } from 'react';

setOptions({
  theme: 'ios',
  themeVariant: 'light'
});

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 viewSettings = {
  timeline: {
    type: 'week',
    eventDisplay: 'fill',
  },
};

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

function App() {
  const [myMeals, setMyMeals] = useState([]);
  const [tempMeal, setTempMeal] = useState(null);
  const [isPopupOpen, setPopupOpen] = useState(false);
  const [isEdit, setEdit] = useState(false);
  const [name, setName] = useState('');
  const [calories, setCalories] = useState('');
  const [notes, setNotes] = useState('');
  const [headerPrimary, setHeaderPrimary] = useState('');
  const [headerDate, setHeaderDate] = useState('');
  const [type, setType] = useState(1);
  const [isSnackbarOpen, setSnackbarOpen] = useState(false);

  const saveEvent = useCallback(() => {
    const newEvent = {
      id: tempMeal.id,
      title: name,
      calories: calories,
      notes: notes,
      start: tempMeal.start,
      end: tempMeal.end,
      resource: tempMeal.resource,
    };
    if (isEdit) {
      // Update the event in the list
      const index = myMeals.findIndex((x) => x.id === tempMeal.id);
      const newEventList = [...myMeals];

      newEventList.splice(index, 1, newEvent);
      setMyMeals(newEventList);
    } else {
      // Add the new event to the list
      setMyMeals([...myMeals, newEvent]);
    }
    // Close the popup
    setPopupOpen(false);
  }, [isEdit, myMeals, calories, notes, name, tempMeal]);

  const deleteEvent = useCallback(
    (event) => {
      setMyMeals(myMeals.filter((item) => item.id !== event.id));
      setTempMeal(event);
      setSnackbarOpen(true);
    },
    [myMeals],
  );

  const loadPopupForm = useCallback((event) => {
    setName(event.title);
    setCalories(event.calories);
    setNotes(event.notes);
  }, []);

  // Handle popup form changes
  const nameChange = useCallback((ev) => {
    setName(ev.target.value);
  }, []);

  const caloriesChange = useCallback((ev) => {
    setCalories(ev.target.value);
  }, []);

  const notesChange = useCallback((ev) => {
    setNotes(ev.target.value);
  }, []);

  const onDeleteClick = useCallback(() => {
    deleteEvent(tempMeal);
    setPopupOpen(false);
  }, [deleteEvent, tempMeal]);

  const renderPopupHeader = useCallback(
    () => (
      <>
        <div className="md-meal-planner-header-primary">{headerPrimary}</div>
        <div className="md-meal-planner-header-date">{headerDate}</div>
      </>
    ),
    [headerPrimary, headerDate],
  );

  // Scheduler options
  const handleEventClick = useCallback(
    (args) => {
      const event = args.event;
      const resource = args.resourceObj;
      setHeaderPrimary(resource.name);
      setHeaderDate(formatDate('DDDD, DD MMMM YYYY', new Date(event.start)));
      setType(event.resource);
      setEdit(true);
      setTempMeal({ ...event });
      // Fill popup form with event data
      loadPopupForm(event);
      setPopupOpen(true);
    },
    [loadPopupForm],
  );

  const handleEventCreated = useCallback(
    (args) => {
      const event = args.event;
      setHeaderPrimary('New meal');
      setHeaderDate(formatDate('DDDD, DD MMMM YYYY', new Date(event.start)));
      setType(event.resource);
      setEdit(false);
      setTempMeal(event);
      // Fill popup form with event data
      loadPopupForm(event);
      // Open the popup
      setPopupOpen(true);
    },
    [loadPopupForm],
  );

  const typeChange = useCallback(
    (ev) => {
      const value = +ev.target.value;
      setType(value);
      setTempMeal({ ...tempMeal, resource: value });
    },
    [tempMeal],
  );

  const handleEventDeleted = useCallback(
    (args) => {
      deleteEvent(args.event);
    },
    [deleteEvent],
  );

  // Popup options
  const popupButtons = useMemo(() => {
    if (isEdit) {
      return [
        'cancel',
        {
          handler: () => {
            saveEvent();
          },
          keyCode: 'enter',
          text: 'Save',
          cssClass: 'mbsc-popup-button-primary',
        },
      ];
    } else {
      return [
        'cancel',
        {
          handler: () => {
            saveEvent();
          },
          keyCode: 'enter',
          text: 'Add',
          cssClass: 'mbsc-popup-button-primary',
        },
      ];
    }
  }, [isEdit, saveEvent]);

  const onPopupClose = useCallback(() => {
    if (!isEdit) {
      // Refresh the list, if add popup was canceled, to remove the temporary event
      setMyMeals([...myMeals]);
    }
    setPopupOpen(false);
  }, [isEdit, myMeals]);

  const extendMyDefaultEvent = useCallback(
    () => ({
      title: 'New meal',
      allDay: true,
    }),
    [],
  );

  const renderMyResource = (resource) => (
    <div className="md-meal-planner-cont">
      <div className="md-meal-planner-title" style={{ color: resource.color }}>
        <span className="md-meal-planner-icon" dangerouslySetInnerHTML={{ __html: resource.icon }}></span>
        {resource.name}
      </div>
      <div className="md-meal-planner-kcal">{resource.kcal}</div>
    </div>
  );

  const myScheduleEvent = useCallback((args) => {
    const event = args.original;
    return (
      <div className="md-meal-planner-event">
        <div className="md-meal-planner-event-title">{event.title}</div>
        {event.calories && <div className="md-meal-planner-event-desc">Calories {event.calories} kcal</div>}
      </div>
    );
  }, []);

  const handleSnackbarClose = useCallback(() => {
    setSnackbarOpen(false);
  }, []);

  useEffect(() => {
    getJson(
      'https://trial.mobiscroll.com/meal-planner/',
      (events) => {
        setMyMeals(events);
      },
      'jsonp',
    );
  }, []);

  return (
    <div>
      <Eventcalendar
        view={viewSettings}
        data={myMeals}
        resources={types}
        dragToCreate={false}
        dragToResize={false}
        dragToMove={true}
        clickToCreate={true}
        extendDefaultEvent={extendMyDefaultEvent}
        onEventClick={handleEventClick}
        onEventCreated={handleEventCreated}
        onEventDeleted={handleEventDeleted}
        renderResource={renderMyResource}
        renderTimelineEventContent={myScheduleEvent}
        cssClass="md-meal-planner-calendar"
      />
      <Popup
        display="bottom"
        fullScreen={true}
        contentPadding={false}
        renderHeader={renderPopupHeader}
        buttons={popupButtons}
        isOpen={isPopupOpen}
        onClose={onPopupClose}
        responsive={responsivePopup}
        cssClass="md-meal-planner-popup"
      >
        <SegmentedGroup onChange={typeChange} value={type}>
          {types.map((type) => (
            <Segmented value={type.id} key={type.id}>
              {type.name}
            </Segmented>
          ))}
        </SegmentedGroup>
        <div className="mbsc-form-group">
          <Input label="Name" value={name} onChange={nameChange} />
          <Input label="Calories" value={calories} onChange={caloriesChange} />
          <Textarea label="Notes" value={notes} onChange={notesChange} />
        </div>
        {isEdit && (
          <div className="mbsc-button-group">
            <Button className="mbsc-button-block" color="danger" variant="outline" onClick={onDeleteClick}>
              Delete meal
            </Button>
          </div>
        )}
      </Popup>
      <Snackbar
        message="Event deleted"
        isOpen={isSnackbarOpen}
        onClose={handleSnackbarClose}
        button={{
          action: () => {
            setMyMeals((prevEvents) => [...prevEvents, tempMeal]);
          },
          text: 'Undo',
        }}
      />
    </div>
  );
}

export default App;
```

### TSX

```tsx
import {
  Button,
  Eventcalendar,
  formatDate,
  getJson,
  Input,
  MbscCalendarEvent,
  MbscCalendarEventData,
  MbscEventcalendarView,
  MbscEventClickEvent,
  MbscEventCreateEvent,
  MbscEventDeletedEvent,
  MbscPopupButton,
  MbscPopupOptions,
  MbscResource,
  MbscResponsiveOptions,
  Popup,
  Segmented,
  SegmentedGroup,
  setOptions,
  Snackbar,
  Textarea,
} from '@mobiscroll/react';
import { ChangeEvent, FC, useCallback, useEffect, useMemo, useState } from 'react';

setOptions({
  theme: 'ios',
  themeVariant: 'light'
});

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 viewSettings: MbscEventcalendarView = {
  timeline: {
    type: 'week',
    eventDisplay: 'fill',
  },
};

const responsivePopup: MbscResponsiveOptions<MbscPopupOptions> = {
  medium: {
    display: 'center',
    width: 400,
    fullScreen: false,
    touchUi: false,
    showOverlay: false,
  },
};

const App: FC = () => {
  const [myMeals, setMyMeals] = useState<MbscCalendarEvent[]>([]);
  const [tempMeal, setTempMeal] = useState<MbscCalendarEvent>();
  const [isPopupOpen, setOpen] = useState<boolean>(false);
  const [isEdit, setEdit] = useState<boolean>(false);
  const [name, setName] = useState<string>('');
  const [calories, setCalories] = useState<string>('');
  const [notes, setNotes] = useState<string>('');
  const [headerPrimary, setHeaderPrimary] = useState<string>('');
  const [headerDate, setHeaderDate] = useState<string>('');
  const [type, setType] = useState<number>(1);
  const [isSnackbarOpen, setSnackbarOpen] = useState(false);

  const saveEvent = useCallback(() => {
    const newEvent = {
      id: tempMeal!.id,
      title: name,
      calories: calories,
      notes: notes,
      start: tempMeal!.start,
      end: tempMeal!.end,
      resource: tempMeal!.resource,
    };

    if (isEdit) {
      // Update the event in the list
      const index = myMeals.findIndex((x) => x.id === tempMeal!.id);
      const newEventList = [...myMeals];

      newEventList.splice(index, 1, newEvent);
      setMyMeals(newEventList);
    } else {
      // Add the new event to the list
      setMyMeals([...myMeals, newEvent]);
    }

    // Close the popup
    setOpen(false);
  }, [isEdit, myMeals, calories, notes, name, tempMeal]);

  const deleteEvent = useCallback(
    (event: MbscCalendarEvent) => {
      setMyMeals(myMeals.filter((item) => item.id !== event.id));
      setTempMeal(event);
      setSnackbarOpen(true);
    },
    [myMeals],
  );

  const loadPopupForm = useCallback((event: MbscCalendarEvent) => {
    setName(event.title!);
    setCalories(event.calories);
    setNotes(event.notes);
  }, []);

  // Handle popup form changes

  const nameChange = useCallback((ev: ChangeEvent<HTMLInputElement>) => {
    setName(ev.target.value);
  }, []);

  const caloriesChange = useCallback((ev: ChangeEvent<HTMLInputElement>) => {
    setCalories(ev.target.value);
  }, []);

  const notesChange = useCallback((ev: ChangeEvent<HTMLInputElement>) => {
    setNotes(ev.target.value);
  }, []);

  const onDeleteClick = useCallback(() => {
    deleteEvent(tempMeal!);
    setOpen(false);
  }, [deleteEvent, tempMeal]);

  const renderPopupHeader = useCallback(
    () => (
      <>
        <div className="md-meal-planner-header-primary">{headerPrimary}</div>
        <div className="md-meal-planner-header-date">{headerDate}</div>
      </>
    ),
    [headerPrimary, headerDate],
  );

  // Scheduler options
  const handleEventClick = useCallback(
    (args: MbscEventClickEvent) => {
      const event = args.event;
      const resource = args.resourceObj!;
      setHeaderPrimary(resource.name!);
      setHeaderDate(formatDate('DDDD, DD MMMM YYYY', new Date(event.start as string)));
      setType(+event.resource!);
      setEdit(true);
      setTempMeal({ ...event });
      // Fill popup form with event data
      loadPopupForm(event);
      setOpen(true);
    },
    [loadPopupForm],
  );

  const handleEventCreated = useCallback(
    (args: MbscEventCreateEvent) => {
      const event = args.event;
      setHeaderPrimary('New meal');
      setHeaderDate(formatDate('DDDD, DD MMMM YYYY', new Date(event.start as string)));
      setType(+event.resource!);
      setEdit(false);
      setTempMeal(event);
      // Fill popup form with event data
      loadPopupForm(event);
      // Open the popup
      setOpen(true);
    },
    [loadPopupForm],
  );

  const typeChange = useCallback(
    (ev: ChangeEvent<HTMLInputElement>) => {
      const value = ev.target.value;
      setType(+value);
      setTempMeal({ ...tempMeal, resource: +value });
    },
    [tempMeal],
  );

  const handleEventDeleted = useCallback(
    (args: MbscEventDeletedEvent) => {
      deleteEvent(args.event);
    },
    [deleteEvent],
  );

  // Popup options
  const popupButtons = useMemo<(string | MbscPopupButton)[]>(() => {
    if (isEdit) {
      return [
        'cancel',
        {
          handler: () => {
            saveEvent();
          },
          keyCode: 'enter',
          text: 'Save',
          cssClass: 'mbsc-popup-button-primary',
        },
      ];
    } else {
      return [
        'cancel',
        {
          handler: () => {
            saveEvent();
          },
          keyCode: 'enter',
          text: 'Add',
          cssClass: 'mbsc-popup-button-primary',
        },
      ];
    }
  }, [isEdit, saveEvent]);

  const onPopupClose = useCallback(() => {
    if (!isEdit) {
      // Refresh the list, if add popup was canceled, to remove the temporary event
      setMyMeals([...myMeals]);
    }
    setOpen(false);
  }, [isEdit, myMeals]);

  const extendMyDefaultEvent = useCallback(
    () => ({
      title: 'New meal',
      allDay: true,
    }),
    [],
  );

  const renderMyResource = (resource: MbscResource) => (
    <div className="md-meal-planner-cont">
      <div className="md-meal-planner-title" style={{ color: resource.color }}>
        <span className="md-meal-planner-icon" dangerouslySetInnerHTML={{ __html: resource.icon }}></span>
        {resource.name}
      </div>
      <div className="md-meal-planner-kcal">{resource.kcal}</div>
    </div>
  );

  const myScheduleEvent = useCallback((args: MbscCalendarEventData) => {
    const event = args.original!;
    return (
      <div className="md-meal-planner-event">
        <div className="md-meal-planner-event-title">{event.title}</div>
        {event.calories && <div className="md-meal-planner-event-desc">Calories {event.calories} kcal</div>}
      </div>
    );
  }, []);

  const handleSnackbarClose = useCallback(() => {
    setSnackbarOpen(false);
  }, []);

  useEffect(() => {
    getJson(
      'https://trial.mobiscroll.com/meal-planner/',
      (events: MbscCalendarEvent[]) => {
        setMyMeals(events);
      },
      'jsonp',
    );
  }, []);

  return (
    <div>
      <Eventcalendar
        view={viewSettings}
        data={myMeals}
        resources={types}
        dragToCreate={false}
        dragToResize={false}
        dragToMove={true}
        clickToCreate={true}
        extendDefaultEvent={extendMyDefaultEvent}
        onEventClick={handleEventClick}
        onEventCreated={handleEventCreated}
        onEventDeleted={handleEventDeleted}
        renderResource={renderMyResource}
        renderTimelineEventContent={myScheduleEvent}
        cssClass="md-meal-planner-calendar"
      />
      <Popup
        display="bottom"
        fullScreen={true}
        contentPadding={false}
        renderHeader={renderPopupHeader}
        buttons={popupButtons}
        isOpen={isPopupOpen}
        onClose={onPopupClose}
        responsive={responsivePopup}
        cssClass="md-meal-planner-popup"
      >
        <SegmentedGroup onChange={typeChange} value={type}>
          {types.map((type) => (
            <Segmented value={type.id} key={type.id}>
              {type.name}
            </Segmented>
          ))}
        </SegmentedGroup>
        <div className="mbsc-form-group">
          <Input label="Name" value={name} onChange={nameChange} />
          <Input label="Calories" value={calories} onChange={caloriesChange} />
          <Textarea label="Notes" value={notes} onChange={notesChange} />
        </div>
        {isEdit && (
          <div className="mbsc-button-group">
            <Button className="mbsc-button-block" color="danger" variant="outline" onClick={onDeleteClick}>
              Delete meal
            </Button>
          </div>
        )}
      </Popup>
      <Snackbar
        message="Event deleted"
        isOpen={isSnackbarOpen}
        onClose={handleSnackbarClose}
        button={{
          action: () => {
            setMyMeals((prevEvents) => [...prevEvents, tempMeal!]);
          },
          text: 'Undo',
        }}
      />
    </div>
  );
};
export default App;
```

### CSS

```css
.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;
}
```

## Implementation notes

- Use the timeline view with `type: 'week'` and `eventDisplay: 'fill'` so each meal event fills the full timeline cell for its day.
- Define resources for each meal type (Breakfast, Elevenses, Lunch, Dinner, Snack) with a `color`, a recommended `kcal` range, and an emoji `icon`. Resources serve as the vertical axis; no time grid is shown.
- Use `renderResource` (Angular: `resourceTemplate`, Vue: `#resource` slot) to display the meal type icon, name (in the resource color), and recommended calorie range in each resource row header.
- Use `renderTimelineEventContent` (Angular: `timelineEventContentTemplate`, Vue: `#timelineEventContent` slot) to render a custom event body showing the meal title and calorie count inside the timeline cell.
- Enable `clickToCreate` so a single click on an empty cell creates a new meal. Set `dragToMove: true` to allow moving meals to different days or resource rows. Set `dragToCreate: false` and `dragToResize: false`.
- Use `extendDefaultEvent` to pre-populate newly created events with a default title and `allDay: true`.
- Use `onEventCreated` to open the add popup. Read `args.event` to set the popup header date and derive the meal type from the event's `resource`.
- Use `onEventClick` to open the edit popup. Read `args.resourceObj.name` and format `args.event.start` with `formatDate` to populate the popup header.
- Use `onEventDeleted` to handle keyboard-triggered deletions by calling the same delete helper as the Delete button in the popup.
- Build the popup with the `Popup` component; default to `display="bottom"` with `fullScreen: true` on mobile, and use the `responsive` option to switch to `display: 'center'` at the medium breakpoint.
- Render a custom two-line popup header via the Popup's `renderHeader` prop (Angular: `headerTemplate` input on `<mbsc-popup>`, Vue: `#header` slot on `<MbscPopup>`) - the Popup's own renderer, distinct from the calendar's header renderer: a primary line showing the meal type name or "New meal", and a secondary line with the formatted date.
- Include a `SegmentedGroup` with one `Segmented` per meal type so users can change the category from within the popup. On change, update the in-progress event's `resource` to the selected ID.
- Add `Input` fields for name and calories and a `Textarea` for notes. Show a "Save" button in edit mode and an "Add" button in create mode, both alongside a Cancel action.
- For deletion, clicking the Delete button in the edit popup removes the event from state and shows a `Snackbar` with an Undo action that restores the meal.
- On popup close when the add form was canceled, refresh the events list to remove the temporary placeholder event that was added to the timeline.

## Run it in your project

1. If not signed up yet, or don't own a license, you can [start a free trial](https://mobiscroll.com/starttrial).

2. Clone the repository:

   ```
   git clone https://github.com/acidb/mobiscroll-demos-react.git
   ```

3. If not installed yet, install the Mobiscroll CLI from NPM:

   ```
   npm install -g @mobiscroll/cli
   ```

4. Config Mobiscroll - open a terminal window in the root directory and run:

   ```
   mobiscroll config react
   ```

5. Install dependencies - open a terminal window in the root directory and run:

   ```
   npm install
   ```

6. Run the project:

   ```
   npm run dev
   ```

On a trial, add `--trial` to the config command: `mobiscroll config react --trial`.

Running a demo locally requires a Mobiscroll trial or a license; setup is per framework at https://mobiscroll.com/docs/react/getting-started/installation

## Related demos

- Previous: [Restaurant Shift Scheduling](https://demo.mobiscroll.com/react/timeline/restaurant-shift-management.md) - Build a restaurant shift planning interface with the React Timeline — named shift columns with live occupancy counts and hierarchical staff groups by role.
- Next: [Dynamic Resource Availability](https://demo.mobiscroll.com/react/timeline/dynamically-color-and-invalidate.md) - Color-code resource rows and restrict drop zones by task type on a React Timeline — compatible resources turn green, incompatible ones gray during drag.
