Meal Planner
Usage
theme="ios" You can turn auto theming on and let it decide based on the platform.
Usage
themeVariant="light" You can turn auto dark mode on based on system settings.
Usage
clickToCreate={true}
// click to create is on dragToCreate={true}
// drag to create is on dragToMove={true}
// reordering is on dragToResize={true}
// resizing is on eventDelete={true}
// deleting is on Fine-tune drag & drop with these options, which are false by default.
Usage
locale={localeEn} Selecting the locale loads the localized settings and the translated UI text.
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 renderer function using the 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.
Demo

Code
View code on GitHub
import {
Button,
Eventcalendar,
formatDate,
getJson,
Input,
Popup,
Segmented,
SegmentedGroup,
setOptions,
Snackbar,
Textarea ,
} from '@mobiscroll/react';
import { useCallback, useEffect, useMemo, useState } from 'react';
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 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;
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({
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 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;
.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;
}