diff --git a/mod/cal.php b/mod/cal.php
deleted file mode 100644
index d5b0487a4a..0000000000
--- a/mod/cal.php
+++ /dev/null
@@ -1,253 +0,0 @@
-.
- *
- * The calendar module
- *
- * This calendar is for profile visitors and contains only the events
- * of the profile owner
- */
-
-use Friendica\App;
-use Friendica\Content\Nav;
-use Friendica\Content\Widget;
-use Friendica\Core\Renderer;
-use Friendica\Core\System;
-use Friendica\Database\DBA;
-use Friendica\DI;
-use Friendica\Model\Event;
-use Friendica\Model\Item;
-use Friendica\Model\User;
-use Friendica\Module\BaseProfile;
-use Friendica\Network\HTTPException;
-use Friendica\Util\DateTimeFormat;
-use Friendica\Util\Temporal;
-
-function cal_init(App $a)
-{
-	if (DI::config()->get('system', 'block_public') && !DI::userSession()->isAuthenticated()) {
-		throw new HTTPException\ForbiddenException(DI::l10n()->t('Access denied.'));
-	}
-
-	if (DI::args()->getArgc() < 2) {
-		throw new HTTPException\ForbiddenException(DI::l10n()->t('Access denied.'));
-	}
-
-	Nav::setSelected('events');
-
-	// if it's a json request abort here becaus we don't
-	// need the widget data
-	if (!empty(DI::args()->getArgv()[2]) && (DI::args()->getArgv()[2] === 'json')) {
-		return;
-	}
-
-	$owner = User::getOwnerDataByNick(DI::args()->getArgv()[1]);
-	if (empty($owner)) {
-		throw new HTTPException\NotFoundException(DI::l10n()->t('User not found.'));
-	}
-
-	if (empty(DI::page()['aside'])) {
-		DI::page()['aside'] = '';
-	}
-
-	DI::page()['aside'] .= Widget\VCard::getHTML($owner);
-	DI::page()['aside'] .= Widget\CalendarExport::getHTML($owner['uid']);
-
-	return;
-}
-
-function cal_content(App $a)
-{
-	$owner = User::getOwnerDataByNick(DI::args()->getArgv()[1]);
-	if (empty($owner)) {
-		throw new HTTPException\NotFoundException(DI::l10n()->t('User not found.'));
-	}
-
-	Nav::setSelected('events');
-
-	// get the translation strings for the callendar
-	$i18n = Event::getStrings();
-
-	DI::page()->registerStylesheet('view/asset/fullcalendar/dist/fullcalendar.min.css');
-	DI::page()->registerStylesheet('view/asset/fullcalendar/dist/fullcalendar.print.min.css', 'print');
-	DI::page()->registerFooterScript('view/asset/moment/min/moment-with-locales.min.js');
-	DI::page()->registerFooterScript('view/asset/fullcalendar/dist/fullcalendar.min.js');
-
-	$htpl = Renderer::getMarkupTemplate('event_head.tpl');
-	DI::page()['htmlhead'] .= Renderer::replaceMacros($htpl, [
-		'$module_url' => '/cal/' . $owner['nickname'],
-		'$modparams' => 2,
-		'$i18n' => $i18n,
-	]);
-
-	$mode = 'view';
-	$y = 0;
-	$m = 0;
-	$ignored = (!empty($_REQUEST['ignored']) ? intval($_REQUEST['ignored']) : 0);
-
-	// Setup permissions structures
-	$owner_uid = intval($owner['uid']);
-
-	$contact_id = DI::userSession()->getRemoteContactID($owner['uid']);
-
-	$remote_contact = $contact_id && DBA::exists('contact', ['id' => $contact_id, 'uid' => $owner['uid']]);
-
-	$is_owner = DI::userSession()->getLocalUserId() == $owner['uid'];
-
-	if ($owner['hidewall'] && !$is_owner && !$remote_contact) {
-		DI::sysmsg()->addNotice(DI::l10n()->t('Access to this profile has been restricted.'));
-		return;
-	}
-
-	// get the permissions
-	$sql_perms = Item::getPermissionsSQLByUserId($owner_uid);
-	// we only want to have the events of the profile owner
-	$sql_extra = " AND `event`.`cid` = 0 " . $sql_perms;
-
-	// get the tab navigation bar
-	$tabs = BaseProfile::getTabsHTML($a, 'cal', false, $owner['nickname'], $owner['hide-friends']);
-
-	// The view mode part is similiar to /mod/events.php
-	if ($mode == 'view') {
-		$thisyear = DateTimeFormat::localNow('Y');
-		$thismonth = DateTimeFormat::localNow('m');
-		if (!$y) {
-			$y = intval($thisyear);
-		}
-
-		if (!$m) {
-			$m = intval($thismonth);
-		}
-
-		// Put some limits on dates. The PHP date functions don't seem to do so well before 1900.
-		// An upper limit was chosen to keep search engines from exploring links millions of years in the future.
-
-		if ($y < 1901) {
-			$y = 1900;
-		}
-
-		if ($y > 2099) {
-			$y = 2100;
-		}
-
-		$nextyear = $y;
-		$nextmonth = $m + 1;
-		if ($nextmonth > 12) {
-			$nextmonth = 1;
-			$nextyear ++;
-		}
-
-		$prevyear = $y;
-		if ($m > 1) {
-			$prevmonth = $m - 1;
-		} else {
-			$prevmonth = 12;
-			$prevyear --;
-		}
-
-		$dim = Temporal::getDaysInMonth($y, $m);
-		$start = sprintf('%d-%d-%d %d:%d:%d', $y, $m, 1, 0, 0, 0);
-		$finish = sprintf('%d-%d-%d %d:%d:%d', $y, $m, $dim, 23, 59, 59);
-
-
-		if (!empty(DI::args()->getArgv()[2]) && (DI::args()->getArgv()[2] === 'json')) {
-			if (!empty($_GET['start'])) {
-				$start = $_GET['start'];
-			}
-
-			if (!empty($_GET['end'])) {
-				$finish = $_GET['end'];
-			}
-		}
-
-		$start = DateTimeFormat::utc($start);
-		$finish = DateTimeFormat::utc($finish);
-
-		// put the event parametes in an array so we can better transmit them
-		$event_params = [
-			'event_id'      => intval($_GET['id'] ?? 0),
-			'start'         => $start,
-			'finish'        => $finish,
-			'ignore'        => $ignored,
-		];
-
-		// get events by id or by date
-		if ($event_params['event_id']) {
-			$r = Event::getListById($owner_uid, $event_params['event_id'], $sql_extra);
-		} else {
-			$r = Event::getListByDate($owner_uid, $event_params, $sql_extra);
-		}
-
-		$links = [];
-
-		if (DBA::isResult($r)) {
-			$r = Event::sortByDate($r);
-			foreach ($r as $rr) {
-				$j = DateTimeFormat::local($rr['start'], 'j');
-				if (empty($links[$j])) {
-					$links[$j] = DI::baseUrl() . '/' . DI::args()->getCommand() . '#link-' . $j;
-				}
-			}
-		}
-
-		// transform the event in a usable array
-		$events = Event::prepareListForTemplate($r);
-
-		if (!empty(DI::args()->getArgv()[2]) && (DI::args()->getArgv()[2] === 'json')) {
-			System::jsonExit($events);
-		}
-
-		// links: array('href', 'text', 'extra css classes', 'title')
-		if (!empty($_GET['id'])) {
-			$tpl = Renderer::getMarkupTemplate("event.tpl");
-		} else {
-			$tpl = Renderer::getMarkupTemplate("events_js.tpl");
-		}
-
-		// Get rid of dashes in key names, Smarty3 can't handle them
-		foreach ($events as $key => $event) {
-			$event_item = [];
-			foreach ($event['item'] as $k => $v) {
-				$k = str_replace('-', '_', $k);
-				$event_item[$k] = $v;
-			}
-			$events[$key]['item'] = $event_item;
-		}
-
-		$o = Renderer::replaceMacros($tpl, [
-			'$tabs' => $tabs,
-			'$title' => DI::l10n()->t('Events'),
-			'$view' => DI::l10n()->t('View'),
-			'$previous' => [DI::baseUrl() . "/events/$prevyear/$prevmonth", DI::l10n()->t('Previous'), '', ''],
-			'$next' => [DI::baseUrl() . "/events/$nextyear/$nextmonth", DI::l10n()->t('Next'), '', ''],
-			'$calendar' => Temporal::getCalendarTable($y, $m, $links, ' eventcal'),
-			'$events' => $events,
-			"today" => DI::l10n()->t("today"),
-			"month" => DI::l10n()->t("month"),
-			"week" => DI::l10n()->t("week"),
-			"day" => DI::l10n()->t("day"),
-			"list" => DI::l10n()->t("list"),
-		]);
-
-		if (!empty($_GET['id'])) {
-			System::httpExit($o);
-		}
-
-		return $o;
-	}
-}
diff --git a/mod/events.php b/mod/events.php
deleted file mode 100644
index b87120672f..0000000000
--- a/mod/events.php
+++ /dev/null
@@ -1,540 +0,0 @@
-.
- *
- * The events module
- */
-
-use Friendica\App;
-use Friendica\Content\Nav;
-use Friendica\Content\Widget\CalendarExport;
-use Friendica\Core\ACL;
-use Friendica\Core\Logger;
-use Friendica\Core\Protocol;
-use Friendica\Core\Renderer;
-use Friendica\Core\System;
-use Friendica\Core\Theme;
-use Friendica\Core\Worker;
-use Friendica\Database\DBA;
-use Friendica\DI;
-use Friendica\Model\Conversation;
-use Friendica\Model\Event;
-use Friendica\Model\Item;
-use Friendica\Model\Post;
-use Friendica\Model\User;
-use Friendica\Module\BaseProfile;
-use Friendica\Module\Security\Login;
-use Friendica\Util\DateTimeFormat;
-use Friendica\Util\Strings;
-use Friendica\Util\Temporal;
-use Friendica\Worker\Delivery;
-
-function events_init(App $a)
-{
-	if (!DI::userSession()->getLocalUserId()) {
-		return;
-	}
-
-	if (empty(DI::page()['aside'])) {
-		DI::page()['aside'] = '';
-	}
-
-	$cal_widget = CalendarExport::getHTML(DI::userSession()->getLocalUserId());
-
-	DI::page()['aside'] .= $cal_widget;
-
-	return;
-}
-
-function events_post(App $a)
-{
-	Logger::debug('post', ['request' => $_REQUEST]);
-	if (!DI::userSession()->getLocalUserId()) {
-		return;
-	}
-
-	$event_id = !empty($_POST['event_id']) ? intval($_POST['event_id']) : 0;
-	$cid = !empty($_POST['cid']) ? intval($_POST['cid']) : 0;
-	$uid = DI::userSession()->getLocalUserId();
-
-	$start_text  = Strings::escapeHtml($_REQUEST['start_text'] ?? '');
-	$finish_text = Strings::escapeHtml($_REQUEST['finish_text'] ?? '');
-
-	$nofinish = intval($_POST['nofinish'] ?? 0);
-
-	$share = intval($_POST['share'] ?? 0);
-
-	// The default setting for the `private` field in event_store() is false, so mirror that
-	$private_event = false;
-
-	$start  = DBA::NULL_DATETIME;
-	$finish = DBA::NULL_DATETIME;
-
-	if ($start_text) {
-		$start = $start_text;
-	}
-
-	if ($finish_text) {
-		$finish = $finish_text;
-	}
-
-	$start = DateTimeFormat::convert($start, 'UTC', $a->getTimeZone());
-	if (!$nofinish) {
-		$finish = DateTimeFormat::convert($finish, 'UTC', $a->getTimeZone());
-	}
-
-	// Don't allow the event to finish before it begins.
-	// It won't hurt anything, but somebody will file a bug report
-	// and we'll waste a bunch of time responding to it. Time that
-	// could've been spent doing something else.
-
-	$summary  = trim($_POST['summary']  ?? '');
-	$desc     = trim($_POST['desc']     ?? '');
-	$location = trim($_POST['location'] ?? '');
-	$type     = 'event';
-
-	$params = [
-		'summary'     => $summary,
-		'description' => $desc,
-		'location'    => $location,
-		'start'       => $start_text,
-		'finish'      => $finish_text,
-		'nofinish'    => $nofinish,
-	];
-
-	$action = ($event_id == '') ? 'new' : 'event/' . $event_id;
-	$onerror_path = 'events/' . $action . '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986);
-
-	if (strcmp($finish, $start) < 0 && !$nofinish) {
-		DI::sysmsg()->addNotice(DI::l10n()->t('Event can not end before it has started.'));
-		if (intval($_REQUEST['preview'])) {
-			System::httpExit(DI::l10n()->t('Event can not end before it has started.'));
-		}
-		DI::baseUrl()->redirect($onerror_path);
-	}
-
-	if (!$summary || ($start === DBA::NULL_DATETIME)) {
-		DI::sysmsg()->addNotice(DI::l10n()->t('Event title and start time are required.'));
-		if (intval($_REQUEST['preview'])) {
-			System::httpExit(DI::l10n()->t('Event title and start time are required.'));
-		}
-		DI::baseUrl()->redirect($onerror_path);
-	}
-
-	$self = \Friendica\Model\Contact::getPublicIdByUserId($uid);
-
-	$aclFormatter = DI::aclFormatter();
-
-	if ($share) {
-		$user = User::getById($uid, ['allow_cid', 'allow_gid', 'deny_cid', 'deny_gid']);
-		if (!DBA::isResult($user)) {
-			return;
-		}
-
-		$str_contact_allow = isset($_REQUEST['contact_allow']) ? $aclFormatter->toString($_REQUEST['contact_allow']) : $user['allow_cid'] ?? '';
-		$str_group_allow   = isset($_REQUEST['group_allow'])   ? $aclFormatter->toString($_REQUEST['group_allow'])   : $user['allow_gid'] ?? '';
-		$str_contact_deny  = isset($_REQUEST['contact_deny'])  ? $aclFormatter->toString($_REQUEST['contact_deny'])  : $user['deny_cid']  ?? '';
-		$str_group_deny    = isset($_REQUEST['group_deny'])    ? $aclFormatter->toString($_REQUEST['group_deny'])    : $user['deny_gid']  ?? '';
-
-		$visibility = $_REQUEST['visibility'] ?? '';
-		if ($visibility === 'public') {
-			// The ACL selector introduced in version 2019.12 sends ACL input data even when the Public visibility is selected
-			$str_contact_allow = $str_group_allow = $str_contact_deny = $str_group_deny = '';
-		} else if ($visibility === 'custom') {
-			// Since we know from the visibility parameter the item should be private, we have to prevent the empty ACL
-			// case that would make it public. So we always append the author's contact id to the allowed contacts.
-			// See https://github.com/friendica/friendica/issues/9672
-			$str_contact_allow .= $aclFormatter->toString($self);
-		}
-	} else {
-		$str_contact_allow = $aclFormatter->toString($self);
-		$str_group_allow = $str_contact_deny = $str_group_deny = '';
-	}
-
-	// Make sure to set the `private` field as true. This is necessary to
-	// have the posts show up correctly in Diaspora if an event is created
-	// as visible only to self at first, but then edited to display to others.
-	if (strlen($str_group_allow) || strlen($str_contact_allow) || strlen($str_group_deny) || strlen($str_contact_deny)) {
-		$private_event = true;
-	}
-
-	$datarray = [
-		'start'     => $start,
-		'finish'    => $finish,
-		'summary'   => $summary,
-		'desc'      => $desc,
-		'location'  => $location,
-		'type'      => $type,
-		'nofinish'  => $nofinish,
-		'uid'       => $uid,
-		'cid'       => $cid,
-		'allow_cid' => $str_contact_allow,
-		'allow_gid' => $str_group_allow,
-		'deny_cid'  => $str_contact_deny,
-		'deny_gid'  => $str_group_deny,
-		'private'   => $private_event,
-		'id'        => $event_id,
-	];
-
-	if (intval($_REQUEST['preview'])) {
-		System::httpExit(Event::getHTML($datarray));
-	}
-
-	$event_id = Event::store($datarray);
-
-	$item = ['network' => Protocol::DFRN, 'protocol' => Conversation::PARCEL_DIRECT, 'direction' => Conversation::PUSH];	
-	$item = Event::getItemArrayForId($event_id, $item);
-	if (Item::insert($item)) {
-		$uri_id = $item['uri-id'];
-	} else {
-		$uri_id = 0;
-	}
-
-	if (!$cid && $uri_id) {
-		Worker::add(Worker::PRIORITY_HIGH, "Notifier", Delivery::POST, (int)$uri_id, (int)$uid);
-	}
-
-	DI::baseUrl()->redirect('events');
-}
-
-function events_content(App $a)
-{
-	if (!DI::userSession()->getLocalUserId()) {
-		DI::sysmsg()->addNotice(DI::l10n()->t('Permission denied.'));
-		return Login::form();
-	}
-
-	if (DI::args()->getArgc() == 1) {
-		$_SESSION['return_path'] = DI::args()->getCommand();
-	}
-
-	if ((DI::args()->getArgc() > 2) && (DI::args()->getArgv()[1] === 'ignore') && intval(DI::args()->getArgv()[2])) {
-		DBA::update('event', ['ignore' => true], ['id' => DI::args()->getArgv()[2], 'uid' => DI::userSession()->getLocalUserId()]);
-	}
-
-	if ((DI::args()->getArgc() > 2) && (DI::args()->getArgv()[1] === 'unignore') && intval(DI::args()->getArgv()[2])) {
-		DBA::update('event', ['ignore' => false], ['id' => DI::args()->getArgv()[2], 'uid' => DI::userSession()->getLocalUserId()]);
-	}
-
-	if ($a->getThemeInfoValue('events_in_profile')) {
-		Nav::setSelected('home');
-	} else {
-		Nav::setSelected('events');
-	}
-
-	// get the translation strings for the callendar
-	$i18n = Event::getStrings();
-
-	DI::page()->registerStylesheet('view/asset/fullcalendar/dist/fullcalendar.min.css');
-	DI::page()->registerStylesheet('view/asset/fullcalendar/dist/fullcalendar.print.min.css', 'print');
-	DI::page()->registerFooterScript('view/asset/moment/min/moment-with-locales.min.js');
-	DI::page()->registerFooterScript('view/asset/fullcalendar/dist/fullcalendar.min.js');
-
-	$htpl = Renderer::getMarkupTemplate('event_head.tpl');
-	DI::page()['htmlhead'] .= Renderer::replaceMacros($htpl, [
-		'$module_url' => '/events',
-		'$modparams' => 1,
-		'$i18n' => $i18n,
-	]);
-
-	$o = '';
-	$tabs = '';
-	// tabs
-	if ($a->getThemeInfoValue('events_in_profile')) {
-		$tabs = BaseProfile::getTabsHTML($a, 'events', true, $a->getLoggedInUserNickname(), false);
-	}
-
-	$mode = 'view';
-	$y = 0;
-	$m = 0;
-	$ignored = !empty($_REQUEST['ignored']) ? intval($_REQUEST['ignored']) : 0;
-
-	if (DI::args()->getArgc() > 1) {
-		if (DI::args()->getArgc() > 2 && DI::args()->getArgv()[1] == 'event') {
-			$mode = 'edit';
-			$event_id = intval(DI::args()->getArgv()[2]);
-		}
-		if (DI::args()->getArgc() > 2 && DI::args()->getArgv()[1] == 'drop') {
-			$mode = 'drop';
-			$event_id = intval(DI::args()->getArgv()[2]);
-		}
-		if (DI::args()->getArgc() > 2 && DI::args()->getArgv()[1] == 'copy') {
-			$mode = 'copy';
-			$event_id = intval(DI::args()->getArgv()[2]);
-		}
-		if (DI::args()->getArgv()[1] === 'new') {
-			$mode = 'new';
-			$event_id = 0;
-		}
-		if (DI::args()->getArgc() > 2 && intval(DI::args()->getArgv()[1]) && intval(DI::args()->getArgv()[2])) {
-			$mode = 'view';
-			$y = intval(DI::args()->getArgv()[1]);
-			$m = intval(DI::args()->getArgv()[2]);
-		}
-	}
-
-	// The view mode part is similiar to /mod/cal.php
-	if ($mode == 'view') {
-		$thisyear  = DateTimeFormat::localNow('Y');
-		$thismonth = DateTimeFormat::localNow('m');
-		if (!$y) {
-			$y = intval($thisyear);
-		}
-		if (!$m) {
-			$m = intval($thismonth);
-		}
-
-		// Put some limits on dates. The PHP date functions don't seem to do so well before 1900.
-		// An upper limit was chosen to keep search engines from exploring links millions of years in the future.
-
-		if ($y < 1901) {
-			$y = 1900;
-		}
-		if ($y > 2099) {
-			$y = 2100;
-		}
-
-		$dim    = Temporal::getDaysInMonth($y, $m);
-		$start  = sprintf('%d-%d-%d %d:%d:%d', $y, $m, 1, 0, 0, 0);
-		$finish = sprintf('%d-%d-%d %d:%d:%d', $y, $m, $dim, 23, 59, 59);
-
-		// put the event parametes in an array so we can better transmit them
-		$event_params = [
-			'event_id'      => intval($_GET['id'] ?? 0),
-			'start'         => $start,
-			'finish'        => $finish,
-			'ignore'        => $ignored,
-		];
-
-		// get events by id or by date
-		if ($event_params['event_id']) {
-			$r = Event::getListById(DI::userSession()->getLocalUserId(), $event_params['event_id']);
-		} else {
-			$r = Event::getListByDate(DI::userSession()->getLocalUserId(), $event_params);
-		}
-
-		$links = [];
-
-		if (DBA::isResult($r)) {
-			$r = Event::sortByDate($r);
-			foreach ($r as $rr) {
-				$j = DateTimeFormat::local($rr['start'], 'j');
-				if (empty($links[$j])) {
-					$links[$j] = DI::baseUrl() . '/' . DI::args()->getCommand() . '#link-' . $j;
-				}
-			}
-		}
-
-		$events = [];
-
-		// transform the event in a usable array
-		if (DBA::isResult($r)) {
-			$r = Event::sortByDate($r);
-			$events = Event::prepareListForTemplate($r);
-		}
-
-		if (!empty($_GET['id'])) {
-			$tpl = Renderer::getMarkupTemplate("event.tpl");
-		} else {
-			$tpl = Renderer::getMarkupTemplate("events_js.tpl");
-		}
-
-		// Get rid of dashes in key names, Smarty3 can't handle them
-		foreach ($events as $key => $event) {
-			$event_item = [];
-			foreach ($event['item'] as $k => $v) {
-				$k = str_replace('-', '_', $k);
-				$event_item[$k] = $v;
-			}
-			$events[$key]['item'] = $event_item;
-		}
-
-		// ACL blocks are loaded in modals in frio
-		DI::page()->registerFooterScript(Theme::getPathForFile('asset/typeahead.js/dist/typeahead.bundle.js'));
-		DI::page()->registerFooterScript(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.js'));
-		DI::page()->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput.css'));
-		DI::page()->registerStylesheet(Theme::getPathForFile('js/friendica-tagsinput/friendica-tagsinput-typeahead.css'));
-
-		$o = Renderer::replaceMacros($tpl, [
-			'$tabs'      => $tabs,
-			'$title'     => DI::l10n()->t('Events'),
-			'$view'      => DI::l10n()->t('View'),
-			'$new_event' => [DI::baseUrl() . '/events/new', DI::l10n()->t('Create New Event'), '', ''],
-			'$previous'  => [DI::baseUrl() . '/events/$prevyear/$prevmonth', DI::l10n()->t('Previous'), '', ''],
-			'$next'      => [DI::baseUrl() . '/events/$nextyear/$nextmonth', DI::l10n()->t('Next'), '', ''],
-			'$calendar'  => Temporal::getCalendarTable($y, $m, $links, ' eventcal'),
-
-			'$events'    => $events,
-
-			'$today' => DI::l10n()->t('today'),
-			'$month' => DI::l10n()->t('month'),
-			'$week'  => DI::l10n()->t('week'),
-			'$day'   => DI::l10n()->t('day'),
-			'$list'  => DI::l10n()->t('list'),
-		]);
-
-		if (!empty($_GET['id'])) {
-			System::httpExit($o);
-		}
-
-		return $o;
-	}
-
-	if (($mode === 'edit' || $mode === 'copy') && $event_id) {
-		$orig_event = DBA::selectFirst('event', [], ['id' => $event_id, 'uid' => DI::userSession()->getLocalUserId()]);
-	}
-
-	// Passed parameters overrides anything found in the DB
-	if (in_array($mode, ['edit', 'new', 'copy'])) {
-		$share_checked = '';
-		$share_disabled = '';
-
-		if (empty($orig_event)) {
-			$orig_event = User::getById(DI::userSession()->getLocalUserId(), ['allow_cid', 'allow_gid', 'deny_cid', 'deny_gid']);;
-		} elseif ($orig_event['allow_cid'] !== '<' . DI::userSession()->getLocalUserId() . '>'
-			|| $orig_event['allow_gid']
-			|| $orig_event['deny_cid']
-			|| $orig_event['deny_gid']) {
-			$share_checked = ' checked="checked" ';
-		}
-
-		// In case of an error the browser is redirected back here, with these parameters filled in with the previous values
-		if (!empty($_REQUEST['nofinish']))    {$orig_event['nofinish']    = $_REQUEST['nofinish'];}
-		if (!empty($_REQUEST['summary']))     {$orig_event['summary']     = $_REQUEST['summary'];}
-		if (!empty($_REQUEST['desc']))        {$orig_event['desc']        = $_REQUEST['desc'];}
-		if (!empty($_REQUEST['location']))    {$orig_event['location']    = $_REQUEST['location'];}
-		if (!empty($_REQUEST['start']))       {$orig_event['start']       = $_REQUEST['start'];}
-		if (!empty($_REQUEST['finish']))      {$orig_event['finish']      = $_REQUEST['finish'];}
-
-		$n_checked = (!empty($orig_event['nofinish']) ? ' checked="checked" ' : '');
-
-		$t_orig = $orig_event['summary']  ?? '';
-		$d_orig = $orig_event['desc']     ?? '';
-		$l_orig = $orig_event['location'] ?? '';
-		$eid = $orig_event['id'] ?? 0;
-		$cid = $orig_event['cid'] ?? 0;
-		$uri = $orig_event['uri'] ?? '';
-
-		if ($cid || $mode === 'edit') {
-			$share_disabled = 'disabled="disabled"';
-		}
-
-		$sdt = $orig_event['start'] ?? 'now';
-		$fdt = $orig_event['finish'] ?? 'now';
-
-		$syear  = DateTimeFormat::local($sdt, 'Y');
-		$smonth = DateTimeFormat::local($sdt, 'm');
-		$sday   = DateTimeFormat::local($sdt, 'd');
-
-		$shour   = !empty($orig_event) ? DateTimeFormat::local($sdt, 'H') : '00';
-		$sminute = !empty($orig_event) ? DateTimeFormat::local($sdt, 'i') : '00';
-
-		$fyear  = DateTimeFormat::local($fdt, 'Y');
-		$fmonth = DateTimeFormat::local($fdt, 'm');
-		$fday   = DateTimeFormat::local($fdt, 'd');
-
-		$fhour   = !empty($orig_event) ? DateTimeFormat::local($fdt, 'H') : '00';
-		$fminute = !empty($orig_event) ? DateTimeFormat::local($fdt, 'i') : '00';
-
-		if (!$cid && in_array($mode, ['new', 'copy'])) {
-			$acl = ACL::getFullSelectorHTML(DI::page(), $a->getLoggedInUserId(), false, ACL::getDefaultUserPermissions($orig_event));
-		} else {
-			$acl = '';
-		}
-
-		// If we copy an old event, we need to remove the ID and URI
-		// from the original event.
-		if ($mode === 'copy') {
-			$eid = 0;
-			$uri = '';
-		}
-
-		$tpl = Renderer::getMarkupTemplate('event_form.tpl');
-
-		$o .= Renderer::replaceMacros($tpl, [
-			'$post' => DI::baseUrl() . '/events',
-			'$eid'  => $eid,
-			'$cid'  => $cid,
-			'$uri'  => $uri,
-
-			'$title' => DI::l10n()->t('Event details'),
-			'$desc' => DI::l10n()->t('Starting date and Title are required.'),
-			'$s_text' => DI::l10n()->t('Event Starts:') . ' *',
-			'$s_dsel' => Temporal::getDateTimeField(
-				new DateTime(),
-				DateTime::createFromFormat('Y', intval($syear) + 5),
-				DateTime::createFromFormat('Y-m-d H:i', "$syear-$smonth-$sday $shour:$sminute"),
-				DI::l10n()->t('Event Starts:'),
-				'start_text',
-				true,
-				true,
-				'',
-				'',
-				true
-			),
-			'$n_text' => DI::l10n()->t('Finish date/time is not known or not relevant'),
-			'$n_checked' => $n_checked,
-			'$f_text' => DI::l10n()->t('Event Finishes:'),
-			'$f_dsel' => Temporal::getDateTimeField(
-				new DateTime(),
-				DateTime::createFromFormat('Y', intval($fyear) + 5),
-				DateTime::createFromFormat('Y-m-d H:i', "$fyear-$fmonth-$fday $fhour:$fminute"),
-				DI::l10n()->t('Event Finishes:'),
-				'finish_text',
-				true,
-				true,
-				'start_text'
-			),
-			'$d_text' => DI::l10n()->t('Description:'),
-			'$d_orig' => $d_orig,
-			'$l_text' => DI::l10n()->t('Location:'),
-			'$l_orig' => $l_orig,
-			'$t_text' => DI::l10n()->t('Title:') . ' *',
-			'$t_orig' => $t_orig,
-			'$summary' => ['summary', DI::l10n()->t('Title:'), $t_orig, '', '*'],
-			'$sh_text' => DI::l10n()->t('Share this event'),
-			'$share' => ['share', DI::l10n()->t('Share this event'), $share_checked, '', $share_disabled],
-			'$sh_checked' => $share_checked,
-			'$nofinish' => ['nofinish', DI::l10n()->t('Finish date/time is not known or not relevant'), $n_checked],
-			'$preview' => DI::l10n()->t('Preview'),
-			'$acl' => $acl,
-			'$submit' => DI::l10n()->t('Submit'),
-			'$basic' => DI::l10n()->t('Basic'),
-			'$advanced' => DI::l10n()->t('Advanced'),
-			'$permissions' => DI::l10n()->t('Permissions'),
-		]);
-
-		return $o;
-	}
-
-	// Remove an event from the calendar and its related items
-	if ($mode === 'drop' && $event_id) {
-		$ev = Event::getListById(DI::userSession()->getLocalUserId(), $event_id);
-
-		// Delete only real events (no birthdays)
-		if (DBA::isResult($ev) && $ev[0]['type'] == 'event') {
-			Item::deleteForUser(['id' => $ev[0]['itemid']], DI::userSession()->getLocalUserId());
-		}
-
-		if (Post::exists(['id' => $ev[0]['itemid']])) {
-			DI::sysmsg()->addNotice(DI::l10n()->t('Failed to remove event'));
-		}
-
-		DI::baseUrl()->redirect('events');
-	}
-}
diff --git a/src/Content/Nav.php b/src/Content/Nav.php
index 23f5dcf072..17b6412df2 100644
--- a/src/Content/Nav.php
+++ b/src/Content/Nav.php
@@ -46,7 +46,7 @@ class Nav
 		'settings'  => null,
 		'contacts'  => null,
 		'delegation'=> null,
-		'events'    => null,
+		'calendar'  => null,
 		'register'  => null
 	];
 
@@ -165,7 +165,7 @@ class Nav
 			'apps'          => null,
 			'community'     => null,
 			'home'          => null,
-			'events'        => null,
+			'calendar'      => null,
 			'login'         => null,
 			'logout'        => null,
 			'langselector'  => null,
@@ -193,7 +193,7 @@ class Nav
 			$nav['usermenu'][] = ['profile/' . $a->getLoggedInUserNickname() . '/profile', DI::l10n()->t('Profile'), '', DI::l10n()->t('Your profile page')];
 			$nav['usermenu'][] = ['photos/' . $a->getLoggedInUserNickname(), DI::l10n()->t('Photos'), '', DI::l10n()->t('Your photos')];
 			$nav['usermenu'][] = ['profile/' . $a->getLoggedInUserNickname() . '/media', DI::l10n()->t('Media'), '', DI::l10n()->t('Your postings with media')];
-			$nav['usermenu'][] = ['events/', DI::l10n()->t('Events'), '', DI::l10n()->t('Your events')];
+			$nav['usermenu'][] = ['calendar/', DI::l10n()->t('Calendar'), '', DI::l10n()->t('Your calendar')];
 			$nav['usermenu'][] = ['notes/', DI::l10n()->t('Personal notes'), '', DI::l10n()->t('Your personal notes')];
 
 			// user info
@@ -257,7 +257,7 @@ class Nav
 		}
 
 		if (DI::userSession()->getLocalUserId()) {
-			$nav['events'] = ['events', DI::l10n()->t('Events'), '', DI::l10n()->t('Events and Calendar')];
+			$nav['calendar'] = ['calendar', DI::l10n()->t('Calendar'), '', DI::l10n()->t('Calendar')];
 		}
 
 		$nav['directory'] = [$gdirpath, DI::l10n()->t('Directory'), '', DI::l10n()->t('People directory')];
diff --git a/src/Model/Event.php b/src/Model/Event.php
index e2235fa664..8745a787f6 100644
--- a/src/Model/Event.php
+++ b/src/Model/Event.php
@@ -24,15 +24,17 @@ namespace Friendica\Model;
 use Friendica\Content\Text\BBCode;
 use Friendica\Core\Hook;
 use Friendica\Core\Logger;
-use Friendica\Core\Protocol;
 use Friendica\Core\Renderer;
 use Friendica\Core\System;
 use Friendica\Database\DBA;
 use Friendica\DI;
+use Friendica\Network\HTTPException\NotFoundException;
+use Friendica\Network\HTTPException\UnauthorizedException;
 use Friendica\Protocol\Activity;
 use Friendica\Util\DateTimeFormat;
 use Friendica\Util\Map;
 use Friendica\Util\Strings;
+use Friendica\Util\Temporal;
 use Friendica\Util\XML;
 
 /**
@@ -499,17 +501,27 @@ class Event
 	 *
 	 * @param int    $owner_uid The User ID of the owner of the event
 	 * @param int    $event_id  The ID of the event in the event table
-	 * @param string $sql_extra
+	 * @param string $nickname  a possible nickname to search for instead of the own uid
 	 * @return array Query result
 	 * @throws \Exception
 	 */
-	public static function getListById(int $owner_uid, int $event_id, string $sql_extra = ''): array
+	public static function getByIdAndUid(int $owner_uid, int $event_id, string $nickname = null): array
 	{
-		$return = [];
+		if (!empty($nickname)) {
+			$owner = static::getOwnerForNickname($nickname, true);
+			$owner_uid = $owner['uid'];
 
-		// Ownly allow events if there is a valid owner_id.
+			// get the permissions
+			$sql_perms = Item::getPermissionsSQLByUserId($owner_uid);
+			// we only want to have the events of the profile owner
+			$sql_extra = " AND `event`.`cid` = 0 " . $sql_perms;
+		} else {
+			$sql_extra = "";
+		}
+
+		// Only allow events if there is a valid owner_id.
 		if ($owner_uid == 0) {
-			return $return;
+			return [];
 		}
 
 		// Query for the event by event id
@@ -518,34 +530,99 @@ class Event
 			WHERE `event`.`uid` = ? AND `event`.`id` = ? $sql_extra",
 			$owner_uid, $event_id));
 
-		if (DBA::isResult($events)) {
-			$return = self::removeDuplicates($events);
+		if (empty($events)) {
+			throw new NotFoundException(DI::l10n()->t('Event not found.'));
+		} else {
+			$events = self::removeDuplicates($events);
+			return $events[0];
+		}
+	}
+
+	/**
+	 * Returns the owner array of a given nickname
+	 * Additionally, it can check if the owner array is selectable
+	 *
+	 * @param string $nickname
+	 * @param bool   $check
+	 *
+	 * @return array the owner array
+	 * @throws NotFoundException The given nickname does not exist
+	 * @throws UnauthorizedException The access for the given nickname is restricted
+	 */
+	public static function getOwnerForNickname(string $nickname, bool $check = true): array
+	{
+		$owner = User::getOwnerDataByNick($nickname);
+		if (empty($owner)) {
+			throw new NotFoundException(DI::l10n()->t('User not found.'));
 		}
 
-		return $return;
+		if ($check) {
+			$contact_id = DI::userSession()->getRemoteContactID($owner['uid']);
+
+			$remote_contact = $contact_id && DBA::exists('contact', ['id' => $contact_id, 'uid' => $owner['uid']]);
+
+			$is_owner = DI::userSession()->getLocalUserId() == $owner['uid'];
+
+			if ($owner['hidewall'] && !$is_owner && !$remote_contact) {
+				throw new UnauthorizedException(DI::l10n()->t('Access to this profile has been restricted.'));
+			}
+		}
+
+		return $owner;
 	}
 
 	/**
 	 * Get all events in a specific time frame.
 	 *
-	 * @param int    $owner_uid    The User ID of the owner of the events.
-	 * @param array  $event_params An associative array with
-	 *                             int 'ignore' =>
-	 *                             string 'start' => Start time of the timeframe.
-	 *                             string 'finish' => Finish time of the timeframe.
-	 *
-	 * @param string $sql_extra    Additional sql conditions (e.g. permission request).
+	 * @param int         $owner_uid The User ID of the owner of the events.
+	 * @param string|null $start     Start time of the timeframe.
+	 * @param string|null $finish    Finish time of the timeframe.
+	 * @param bool        $ignore
+	 * @param string|null $nickname
 	 *
 	 * @return array Query results.
-	 * @throws \Exception
+	 * @throws NotFoundException
+	 * @throws UnauthorizedException
 	 */
-	public static function getListByDate(int $owner_uid, array $event_params, string $sql_extra = ''): array
+	public static function getListByDate(int $owner_uid, string $start = null, string $finish = null, bool $ignore = false, string $nickname = null): array
 	{
-		$return = [];
+		if (!empty($nickname)) {
+			$owner     = static::getOwnerForNickname($nickname, true);
+			$owner_uid = $owner['uid'];
+
+			// get the permissions
+			$sql_perms = Item::getPermissionsSQLByUserId($owner_uid);
+			// we only want to have the events of the profile owner
+			$sql_extra = " AND `event`.`cid` = 0 " . $sql_perms;
+		} else {
+			$sql_extra = "";
+		}
 
 		// Only allow events if there is a valid owner_id.
 		if ($owner_uid == 0) {
-			return $return;
+			return [];
+		}
+
+		if (empty($start) || empty($finish)) {
+
+			$y = intval(DateTimeFormat::localNow('Y'));
+			$m = intval(DateTimeFormat::localNow('m'));
+
+			// Put some limit on dates. The PHP date functions don't seem to do so well before 1900.
+			if ($y < 1901) {
+				$y = 1900;
+			}
+
+			if ($y > 2099) {
+				$y = 2100;
+			}
+
+			if (empty($start)) {
+				$start = sprintf('%d-%d-%d %d:%d:%d', $y, $m, 1, 0, 0, 0);
+			} else {
+				$dim    = Temporal::getDaysInMonth($y, $m);
+				$finish = sprintf('%d-%d-%d %d:%d:%d', $y, $m, $dim, 23, 59, 59);
+			}
 		}
 
 		// Query for the event by date.
@@ -554,15 +631,12 @@ class Event
 				WHERE `event`.`uid` = ? AND `event`.`ignore` = ?
 				AND (`finish` >= ? OR (`nofinish` AND `start` >= ?)) AND `start` <= ?
 				" . $sql_extra,
-				$owner_uid, $event_params['ignore'],
-				$event_params['start'], $event_params['start'], $event_params['finish']
+			$owner_uid, $ignore,
+			$start, $start, $finish
 		));
 
-		if (DBA::isResult($events)) {
-			$return = self::removeDuplicates($events);
-		}
-
-		return $return;
+		$events = self::removeDuplicates($events ?? []);
+		return self::sortByDate($events);
 	}
 
 	/**
@@ -577,77 +651,86 @@ class Event
 	{
 		$event_list = [];
 
-		$last_date = '';
-		$fmt = DI::l10n()->t('l, F j');
 		foreach ($event_result as $event) {
-			$item = Post::selectFirst(['plink', 'author-name', 'author-network', 'author-id', 'author-avatar', 'author-link', 'private', 'uri-id'], ['id' => $event['itemid']]);
-			if (!DBA::isResult($item)) {
-				// Using default values when no item had been found
-				$item = ['plink' => '', 'author-name' => '', 'author-avatar' => '', 'author-link' => '', 'private' => Item::PUBLIC, 'uri-id' => ($event['uri-id'] ?? 0)];
-			}
-
-			$event = array_merge($event, $item);
-
-			$start = DateTimeFormat::local($event['start'], 'c');
-			$j     = DateTimeFormat::local($event['start'], 'j');
-			$day   = DateTimeFormat::local($event['start'], $fmt);
-			$day   = DI::l10n()->getDay($day);
-
-			if ($event['nofinish']) {
-				$end = null;
-			} else {
-				$end = DateTimeFormat::local($event['finish'], 'c');
-			}
-
-			$is_first = ($day !== $last_date);
-
-			$last_date = $day;
-
-			// Show edit and drop actions only if the user is the owner of the event and the event
-			// is a real event (no bithdays).
-			$edit = null;
-			$copy = null;
-			$drop = null;
-			if (DI::userSession()->getLocalUserId() && DI::userSession()->getLocalUserId() == $event['uid'] && $event['type'] == 'event') {
-				$edit = !$event['cid'] ? [DI::baseUrl() . '/events/event/' . $event['id'], DI::l10n()->t('Edit event')     , '', ''] : null;
-				$copy = !$event['cid'] ? [DI::baseUrl() . '/events/copy/' . $event['id'] , DI::l10n()->t('Duplicate event'), '', ''] : null;
-				$drop =                  [DI::baseUrl() . '/events/drop/' . $event['id'] , DI::l10n()->t('Delete event')   , '', ''];
-			}
-
-			$title = BBCode::convertForUriId($event['uri-id'], Strings::escapeHtml($event['summary']));
-			if (!$title) {
-				list($title, $_trash) = explode("
 $event['id'],
-				'start'    => $start,
-				'end'      => $end,
-				'allDay'   => false,
-				'title'    => $title,
-				'j'        => $j,
-				'd'        => $day,
-				'edit'     => $edit,
-				'drop'     => $drop,
-				'copy'     => $copy,
-				'is_first' => $is_first,
-				'item'     => $event,
-				'html'     => $html,
-				'plink'    => Item::getPlink($event),
-			];
+			$event_list[] = static::prepareForItem($event);
 		}
 
 		return $event_list;
 	}
 
+	/**
+	 * Convert an one event in an array which could be used by the events template.
+	 *
+	 * @param array $event Event query array.
+	 * @return array Event array for the template.
+	 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
+	 * @throws \ImagickException
+	 */
+	public static function prepareForItem(array $event): array
+	{
+		$fmt = DI::l10n()->t('l, F j');
+
+		$item = Post::selectFirst(['plink', 'author-name', 'author-network', 'author-id', 'author-avatar', 'author-link', 'private', 'uri-id'], ['id' => $event['itemid']]);
+		if (!DBA::isResult($item)) {
+			// Using default values when no item had been found
+			$item = ['plink' => '', 'author-name' => '', 'author-avatar' => '', 'author-link' => '', 'private' => Item::PUBLIC, 'uri-id' => ($event['uri-id'] ?? 0)];
+		}
+
+		$event = array_merge($event, $item);
+
+		$start = DateTimeFormat::local($event['start'], 'c');
+		$j     = DateTimeFormat::local($event['start'], 'j');
+		$day   = DateTimeFormat::local($event['start'], $fmt);
+		$day   = DI::l10n()->getDay($day);
+
+		if ($event['nofinish']) {
+			$end = null;
+		} else {
+			$end = DateTimeFormat::local($event['finish'], 'c');
+		}
+
+		// Show edit and drop actions only if the user is the owner of the event and the event
+		// is a real event (no bithdays).
+		$edit = null;
+		$copy = null;
+		$drop = null;
+		if (DI::userSession()->getLocalUserId() && DI::userSession()->getLocalUserId() == $event['uid'] && $event['type'] == 'event') {
+			$edit = !$event['cid'] ? [DI::baseUrl() . '/calendar/event/edit/' . $event['id'], DI::l10n()->t('Edit event')     , '', ''] : null;
+			$copy = !$event['cid'] ? [DI::baseUrl() . '/calendar/event/copy/' . $event['id'] , DI::l10n()->t('Duplicate event'), '', ''] : null;
+			$drop =                  [DI::baseUrl() . '/calendar/api/delete/' . $event['id'] , DI::l10n()->t('Delete event')   , '', ''];
+		}
+
+		$title = BBCode::convertForUriId($event['uri-id'], Strings::escapeHtml($event['summary']));
+		if (!$title) {
+			[$title, $_trash] = explode("
 $event['id'],
+			'start'    => $start,
+			'end'      => $end,
+			'allDay'   => false,
+			'title'    => $title,
+			'j'        => $j,
+			'd'        => $day,
+			'edit'     => $edit,
+			'drop'     => $drop,
+			'copy'     => $copy,
+			'item'     => $event,
+			'html'     => $html,
+			'plink'    => Item::getPlink($event),
+		];
+	}
+
 	/**
 	 * Format event to export format (ical/csv).
 	 *
@@ -1018,4 +1101,9 @@ class Event
 		// Check if self::store() was success
 		return (self::store($values) > 0);
 	}
+
+	public static function setIgnore(int $uid, int $eventId, bool $ignore = true)
+	{
+		DBA::update('event', ['ignore' => $ignore], ['id' => $eventId, 'uid' => $uid]);
+	}
 }
diff --git a/src/Module/BaseNotifications.php b/src/Module/BaseNotifications.php
index a011961b67..d0c0ae3627 100644
--- a/src/Module/BaseNotifications.php
+++ b/src/Module/BaseNotifications.php
@@ -90,11 +90,11 @@ abstract class BaseNotifications extends BaseModule
 	 */
 	abstract public function getNotifications();
 
-	public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, IHandleUserSessions $userSession, array $server, array $parameters = [])
+	public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, IHandleUserSessions $session, array $server, array $parameters = [])
 	{
 		parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
 
-		if (!$userSession->getLocalUserId()) {
+		if (!$session->getLocalUserId()) {
 			throw new ForbiddenException($this->t('Permission denied.'));
 		}
 
diff --git a/src/Module/BaseProfile.php b/src/Module/BaseProfile.php
index 3937fa07d2..16ba224c9c 100644
--- a/src/Module/BaseProfile.php
+++ b/src/Module/BaseProfile.php
@@ -81,23 +81,23 @@ class BaseProfile extends BaseModule
 		// the calendar link for the full featured events calendar
 		if ($is_owner && $a->getThemeInfoValue('events_in_profile')) {
 			$tabs[] = [
-				'label' => DI::l10n()->t('Events'),
-				'url'   => DI::baseUrl() . '/events',
-				'sel'   => $current == 'events' ? 'active' : '',
-				'title' => DI::l10n()->t('Events and Calendar'),
-				'id'    => 'events-tab',
-				'accesskey' => 'e',
+				'label' => DI::l10n()->t('Calendar'),
+				'url'   => DI::baseUrl() . '/calendar',
+				'sel'   => $current == 'calendar' ? 'active' : '',
+				'title' => DI::l10n()->t('Calendar'),
+				'id'    => 'calendar-tab',
+				'accesskey' => 'c',
 			];
 			// if the user is not the owner of the calendar we only show a calendar
 			// with the public events of the calendar owner
 		} elseif (!$is_owner) {
 			$tabs[] = [
-				'label' => DI::l10n()->t('Events'),
-				'url'   => DI::baseUrl() . '/cal/' . $nickname,
-				'sel'   => $current == 'cal' ? 'active' : '',
-				'title' => DI::l10n()->t('Events and Calendar'),
-				'id'    => 'events-tab',
-				'accesskey' => 'e',
+				'label' => DI::l10n()->t('Calendar'),
+				'url'   => DI::baseUrl() . '/calendar/show/' . $nickname,
+				'sel'   => $current == 'calendar' ? 'active' : '',
+				'title' => DI::l10n()->t('Calendar'),
+				'id'    => 'calendar-tab',
+				'accesskey' => 'c',
 			];
 		}
 
diff --git a/src/Module/Calendar/Event/API.php b/src/Module/Calendar/Event/API.php
new file mode 100644
index 0000000000..b11da27e3c
--- /dev/null
+++ b/src/Module/Calendar/Event/API.php
@@ -0,0 +1,277 @@
+.
+ *
+ */
+
+namespace Friendica\Module\Calendar\Event;
+
+use Friendica\App;
+use Friendica\BaseModule;
+use Friendica\Core\L10n;
+use Friendica\Core\Protocol;
+use Friendica\Core\Session\Capability\IHandleUserSessions;
+use Friendica\Core\System;
+use Friendica\Core\Worker;
+use Friendica\Database\DBA;
+use Friendica\Model\Contact;
+use Friendica\Model\Conversation;
+use Friendica\Model\Event;
+use Friendica\Model\Item;
+use Friendica\Model\Post;
+use Friendica\Model\User;
+use Friendica\Module\Response;
+use Friendica\Navigation\SystemMessages;
+use Friendica\Network\HTTPException\BadRequestException;
+use Friendica\Network\HTTPException\UnauthorizedException;
+use Friendica\Util\ACLFormatter;
+use Friendica\Util\DateTimeFormat;
+use Friendica\Util\Profiler;
+use Friendica\Util\Strings;
+use Friendica\Worker\Delivery;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Basic API class for events
+ * currently supports create, delete, ignore, unignore
+ *
+ * @todo: make create/update as REST-call instead of POST
+ */
+class API extends BaseModule
+{
+	const ACTION_CREATE   = 'create';
+	const ACTION_DELETE   = 'delete';
+	const ACTION_IGNORE   = 'ignore';
+	const ACTION_UNIGNORE = 'unignore';
+
+	const ALLOWED_ACTIONS = [
+		self::ACTION_CREATE,
+		self::ACTION_DELETE,
+		self::ACTION_IGNORE,
+		self::ACTION_UNIGNORE,
+	];
+
+	/** @var IHandleUserSessions */
+	protected $session;
+	/** @var SystemMessages */
+	protected $sysMessages;
+	/** @var ACLFormatter */
+	protected $aclFormatter;
+	/** @var string */
+	protected $timezone;
+
+	public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, IHandleUserSessions $session, SystemMessages $sysMessages, ACLFormatter $aclFormatter, App $app, array $server, array $parameters = [])
+	{
+		parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
+
+		$this->session      = $session;
+		$this->sysMessages  = $sysMessages;
+		$this->aclFormatter = $aclFormatter;
+		$this->timezone     = $app->getTimeZone();
+
+		if (!$this->session->getLocalUserId()) {
+			throw new UnauthorizedException($this->t('Permission denied.'));
+		}
+	}
+
+	protected function post(array $request = [])
+	{
+		$this->createEvent($request);
+	}
+
+	protected function rawContent(array $request = [])
+	{
+		if (empty($this->parameters['action']) || !in_array($this->parameters['action'], self::ALLOWED_ACTIONS)) {
+			throw new BadRequestException($this->t('Invalid Request'));
+		}
+
+		// CREATE is done per POSt, so nothing to do left
+		if ($this->parameters['action'] === static::ACTION_CREATE) {
+			return;
+		}
+
+		if (empty($this->parameters['id'])) {
+			throw new BadRequestException($this->t('Event id is missing.'));
+		}
+
+		$returnPath = $request['return_path'] ?? 'calendar';
+
+		switch ($this->parameters['action']) {
+			case self::ACTION_IGNORE:
+				Event::setIgnore($this->session->getLocalUserId(), $this->parameters['id']);
+				break;
+			case self::ACTION_UNIGNORE:
+				Event::setIgnore($this->session->getLocalUserId(), $this->parameters['id'], false);
+				break;
+			case self::ACTION_DELETE:
+				// Remove an event from the calendar and its related items
+				$event = Event::getByIdAndUid($this->session->getLocalUserId(), $this->parameters['id']);
+
+				// Delete only real events (no birthdays)
+				if (!empty($event) && $event['type'] == 'event') {
+					Item::deleteForUser(['id' => $event['itemid']], $this->session->getLocalUserId());
+				}
+
+				if (Post::exists(['id' => $event['itemid']])) {
+					$this->sysMessages->addNotice($this->t('Failed to remove event'));
+				}
+				break;
+			default:
+				throw new BadRequestException($this->t('Invalid Request'));
+		}
+
+		$this->baseUrl->redirect($returnPath);
+	}
+
+	protected function createEvent(array $request)
+	{
+		$eventId = !empty($request['event_id']) ? intval($request['event_id']) : 0;
+		$uid     = (int)$this->session->getLocalUserId();
+		$cid     = !empty($request['cid']) ? intval($request['cid']) : 0;
+
+		$strStartDateTime  = Strings::escapeHtml($request['start_text'] ?? '');
+		$strFinishDateTime = Strings::escapeHtml($request['finish_text'] ?? '');
+
+		$noFinish = intval($request['nofinish'] ?? 0);
+
+		$share     = intval($request['share'] ?? 0);
+		$isPreview = intval($request['preview'] ?? 0);
+
+		$start = DateTimeFormat::convert($strStartDateTime ?? DBA::NULL_DATETIME, $this->timezone);
+		if (!$noFinish) {
+			$finish = DateTimeFormat::convert($strFinishDateTime ?? DBA::NULL_DATETIME, 'UTC', $this->timezone);
+		} else {
+			$finish = DBA::NULL_DATETIME;
+		}
+
+		// Don't allow the event to finish before it begins.
+		// It won't hurt anything, but somebody will file a bug report,
+		// and we'll waste a bunch of time responding to it. Time that
+		// could've been spent doing something else.
+
+		$summary  = trim($request['summary'] ?? '');
+		$desc     = trim($request['desc'] ?? '');
+		$location = trim($request['location'] ?? '');
+		$type     = 'event';
+
+		$params = [
+			'summary'     => $summary,
+			'description' => $desc,
+			'location'    => $location,
+			'start'       => $strStartDateTime,
+			'finish'      => $strFinishDateTime,
+			'nofinish'    => $noFinish,
+		];
+
+		$action          = empty($eventId) ? 'new' : 'edit/' . $eventId;
+		$redirectOnError = 'calendar/event/' . $action . '?' . http_build_query($params, '', '&', PHP_QUERY_RFC3986);
+
+		if (strcmp($finish, $start) < 0 && !$noFinish) {
+			if ($isPreview) {
+				System::httpExit($this->t('Event can not end before it has started.'));
+				return;
+			} else {
+				$this->sysMessages->addNotice($this->t('Event can not end before it has started.'));
+				$this->baseUrl->redirect($redirectOnError);
+			}
+		}
+
+		if (empty($summary) || ($start === DBA::NULL_DATETIME)) {
+			if ($isPreview) {
+				System::httpExit($this->t('Event title and start time are required.'));
+				return;
+			} else {
+				$this->sysMessages->addNotice($this->t('Event title and start time are required.'));
+				$this->baseUrl->redirect($redirectOnError);
+			}
+		}
+
+		$self = Contact::getPublicIdByUserId($uid);
+
+		$aclFormatter = $this->aclFormatter;
+
+		if ($share) {
+			$user = User::getById($uid, ['allow_cid', 'allow_gid', 'deny_cid', 'deny_gid']);
+			if (empty($user)) {
+				$this->logger->warning('Cannot find user for an event.', ['uid' => $uid, 'event' => $eventId]);
+				$this->response->setStatus(500);
+				return;
+			}
+
+			$strAclContactAllow = isset($request['contact_allow']) ? $aclFormatter->toString($request['contact_allow']) : $user['allow_cid'] ?? '';
+			$strAclGroupAllow   = isset($request['group_allow']) ? $aclFormatter->toString($request['group_allow']) : $user['allow_gid'] ?? '';
+			$strContactDeny     = isset($request['contact_deny']) ? $aclFormatter->toString($request['contact_deny']) : $user['deny_cid'] ?? '';
+			$strGroupDeny       = isset($request['group_deny']) ? $aclFormatter->toString($request['group_deny']) : $user['deny_gid'] ?? '';
+
+			$visibility = $request['visibility'] ?? '';
+			if ($visibility === 'public') {
+				// The ACL selector introduced in version 2019.12 sends ACL input data even when the Public visibility is selected
+				$strAclContactAllow = $strAclGroupAllow = $strContactDeny = $strGroupDeny = '';
+			} else if ($visibility === 'custom') {
+				// Since we know from the visibility parameter the item should be private, we have to prevent the empty ACL
+				// case that would make it public. So we always append the author's contact id to the allowed contacts.
+				// See https://github.com/friendica/friendica/issues/9672
+				$strAclContactAllow .= $aclFormatter->toString($self);
+			}
+		} else {
+			$strAclContactAllow = $aclFormatter->toString($self);
+			$strAclGroupAllow   = $strContactDeny = $strGroupDeny = '';
+		}
+
+		$datarray = [
+			'start'     => $start,
+			'finish'    => $finish,
+			'summary'   => $summary,
+			'desc'      => $desc,
+			'location'  => $location,
+			'type'      => $type,
+			'nofinish'  => $noFinish,
+			'uid'       => $uid,
+			'cid'       => $cid,
+			'allow_cid' => $strAclContactAllow,
+			'allow_gid' => $strAclGroupAllow,
+			'deny_cid'  => $strContactDeny,
+			'deny_gid'  => $strGroupDeny,
+			'id'        => $eventId,
+		];
+
+		if (intval($request['preview'])) {
+			System::httpExit(Event::getHTML($datarray));
+			return;
+		}
+
+		$eventId = Event::store($datarray);
+
+		$newItem = Event::getItemArrayForId($eventId, [
+			'network'   => Protocol::DFRN,
+			'protocol'  => Conversation::PARCEL_DIRECT,
+			'direction' => Conversation::PUSH
+		]);
+		if (Item::insert($newItem)) {
+			$uriId = (int)$newItem['uri-id'];
+		} else {
+			$uriId = 0;
+		}
+
+		if (!$cid && $uriId) {
+			Worker::add(Worker::PRIORITY_HIGH, "Notifier", Delivery::POST, $uriId, $uid);
+		}
+
+		$this->baseUrl->redirect('calendar');
+	}
+}
diff --git a/src/Module/Calendar/Event/Form.php b/src/Module/Calendar/Event/Form.php
new file mode 100644
index 0000000000..b458f95d78
--- /dev/null
+++ b/src/Module/Calendar/Event/Form.php
@@ -0,0 +1,253 @@
+.
+ *
+ */
+
+namespace Friendica\Module\Calendar\Event;
+
+use Friendica\App;
+use Friendica\BaseModule;
+use Friendica\Content\Widget\CalendarExport;
+use Friendica\Core\ACL;
+use Friendica\Core\L10n;
+use Friendica\Core\Renderer;
+use Friendica\Core\Session\Capability\IHandleUserSessions;
+use Friendica\Model\Event as EventModel;
+use Friendica\Model\User;
+use Friendica\Module\Response;
+use Friendica\Module\Security\Login;
+use Friendica\Navigation\SystemMessages;
+use Friendica\Network\HTTPException\BadRequestException;
+use Friendica\Util\ACLFormatter;
+use Friendica\Util\DateTimeFormat;
+use Friendica\Util\Profiler;
+use Friendica\Util\Temporal;
+use Psr\Log\LoggerInterface;
+
+/**
+ * The editor-view of an event
+ */
+class Form extends BaseModule
+{
+	const MODE_NEW  = 'new';
+	const MODE_EDIT = 'edit';
+	const MODE_COPY = 'copy';
+
+	const ALLOWED_MODES = [
+		self::MODE_NEW,
+		self::MODE_EDIT,
+		self::MODE_COPY,
+	];
+
+	/** @var IHandleUserSessions */
+	protected $session;
+	/** @var SystemMessages */
+	protected $sysMessages;
+	/** @var ACLFormatter */
+	protected $aclFormatter;
+	/** @var App\Page */
+	protected $page;
+
+	public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, IHandleUserSessions $session, SystemMessages $sysMessages, ACLFormatter $aclFormatter, App\Page $page, array $server, array $parameters = [])
+	{
+		parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
+
+		$this->session      = $session;
+		$this->sysMessages  = $sysMessages;
+		$this->aclFormatter = $aclFormatter;
+		$this->page         = $page;
+	}
+
+	protected function content(array $request = []): string
+	{
+		if (empty($this->parameters['mode']) || !in_array($this->parameters['mode'], self::ALLOWED_MODES)) {
+			throw new BadRequestException($this->t('Invalid Request'));
+		}
+
+		if (!$this->session->getLocalUserId()) {
+			$this->sysMessages->addNotice($this->t('Permission denied.'));
+			return Login::form();
+		}
+
+		$mode = $this->parameters['mode'];
+
+		if (($mode === self::MODE_EDIT || $mode === self::MODE_COPY)) {
+			if (empty($this->parameters['id'])) {
+				throw new BadRequestException('Invalid Request');
+			}
+			$orig_event = EventModel::getByIdAndUid($this->session->getLocalUserId(), $this->parameters['id']);
+			if (empty($orig_event)) {
+				throw new BadRequestException('Invalid Request');
+			}
+		}
+
+		if ($mode === self::MODE_NEW) {
+			$this->session->set('return_path', $this->args->getCommand());
+		}
+
+		// get the translation strings for the calendar
+		$i18n = EventModel::getStrings();
+
+		$this->page->registerStylesheet('view/asset/fullcalendar/dist/fullcalendar.min.css');
+		$this->page->registerStylesheet('view/asset/fullcalendar/dist/fullcalendar.print.min.css', 'print');
+		$this->page->registerFooterScript('view/asset/moment/min/moment-with-locales.min.js');
+		$this->page->registerFooterScript('view/asset/fullcalendar/dist/fullcalendar.min.js');
+
+		$htpl                   = Renderer::getMarkupTemplate('calendar/calendar_head.tpl');
+		$this->page['htmlhead'] .= Renderer::replaceMacros($htpl, [
+			'$calendar_api' => $this->baseUrl . '/calendar/api/get',
+			'$event_api'    => $this->baseUrl . '/calendar/event/show',
+			'$modparams' => 2,
+			'$i18n' => $i18n,
+		]);
+
+		$share_checked  = '';
+		$share_disabled = '';
+
+		if (empty($orig_event)) {
+			$orig_event = User::getById($this->session->getLocalUserId(), ['allow_cid', 'allow_gid', 'deny_cid',
+																		   'deny_gid']);;
+		} else if ($orig_event['allow_cid'] !== '<' . $this->session->getLocalUserId() . '>'
+				   || $orig_event['allow_gid']
+				   || $orig_event['deny_cid']
+				   || $orig_event['deny_gid']) {
+			$share_checked = ' checked="checked" ';
+		}
+
+		// In case of an error the browser is redirected back here, with these parameters filled in with the previous values
+		if (!empty($request['nofinish'])) {
+			$orig_event['nofinish'] = $request['nofinish'];
+		}
+		if (!empty($request['summary'])) {
+			$orig_event['summary'] = $request['summary'];
+		}
+		if (!empty($request['desc'])) {
+			$orig_event['desc'] = $request['desc'];
+		}
+		if (!empty($request['location'])) {
+			$orig_event['location'] = $request['location'];
+		}
+		if (!empty($request['start'])) {
+			$orig_event['start'] = $request['start'];
+		}
+		if (!empty($request['finish'])) {
+			$orig_event['finish'] = $request['finish'];
+		}
+
+		$n_checked = (!empty($orig_event['nofinish']) ? ' checked="checked" ' : '');
+
+		$t_orig = $orig_event['summary'] ?? '';
+		$d_orig = $orig_event['desc'] ?? '';
+		$l_orig = $orig_event['location'] ?? '';
+		$eid    = $orig_event['id'] ?? 0;
+		$cid    = $orig_event['cid'] ?? 0;
+		$uri    = $orig_event['uri'] ?? '';
+
+		if ($cid || $mode === 'edit') {
+			$share_disabled = 'disabled="disabled"';
+		}
+
+		$sdt = $orig_event['start'] ?? 'now';
+		$fdt = $orig_event['finish'] ?? 'now';
+
+		$syear  = DateTimeFormat::local($sdt, 'Y');
+		$smonth = DateTimeFormat::local($sdt, 'm');
+		$sday   = DateTimeFormat::local($sdt, 'd');
+
+		$shour   = !empty($orig_event) ? DateTimeFormat::local($sdt, 'H') : '00';
+		$sminute = !empty($orig_event) ? DateTimeFormat::local($sdt, 'i') : '00';
+
+		$fyear  = DateTimeFormat::local($fdt, 'Y');
+		$fmonth = DateTimeFormat::local($fdt, 'm');
+		$fday   = DateTimeFormat::local($fdt, 'd');
+
+		$fhour   = !empty($orig_event) ? DateTimeFormat::local($fdt, 'H') : '00';
+		$fminute = !empty($orig_event) ? DateTimeFormat::local($fdt, 'i') : '00';
+
+		if (!$cid && in_array($mode, [self::MODE_NEW, self::MODE_COPY])) {
+			$acl = ACL::getFullSelectorHTML($this->page, $this->session->getLocalUserId(), false, ACL::getDefaultUserPermissions($orig_event));
+		} else {
+			$acl = '';
+		}
+
+		// If we copy an old event, we need to remove the ID and URI
+		// from the original event.
+		if ($mode === self::MODE_COPY) {
+			$eid = 0;
+			$uri = '';
+		}
+
+		$this->page['aside'] .= CalendarExport::getHTML($this->session->getLocalUserId());
+
+		$tpl = Renderer::getMarkupTemplate('calendar/event_form.tpl');
+
+		return Renderer::replaceMacros($tpl, [
+			'$post' => $this->baseUrl . '/calendar/api/create',
+			'$eid'  => $eid,
+			'$cid'  => $cid,
+			'$uri'  => $uri,
+
+			'$title'       => $this->t('Event details'),
+			'$desc'        => $this->t('Starting date and Title are required.'),
+			'$s_text'      => $this->t('Event Starts:') . ' *',
+			'$s_dsel'      => Temporal::getDateTimeField(
+				new \DateTime(),
+				\DateTime::createFromFormat('Y', intval($syear) + 5),
+				\DateTime::createFromFormat('Y-m-d H:i', "$syear-$smonth-$sday $shour:$sminute"),
+				$this->t('Event Starts:'),
+				'start_text',
+				true,
+				true,
+				'',
+				'',
+				true
+			),
+			'$n_text'      => $this->t('Finish date/time is not known or not relevant'),
+			'$n_checked'   => $n_checked,
+			'$f_text'      => $this->t('Event Finishes:'),
+			'$f_dsel'      => Temporal::getDateTimeField(
+				new \DateTime(),
+				\DateTime::createFromFormat('Y', intval($fyear) + 5),
+				\DateTime::createFromFormat('Y-m-d H:i', "$fyear-$fmonth-$fday $fhour:$fminute"),
+				$this->t('Event Finishes:'),
+				'finish_text',
+				true,
+				true,
+				'start_text'
+			),
+			'$d_text'      => $this->t('Description:'),
+			'$d_orig'      => $d_orig,
+			'$l_text'      => $this->t('Location:'),
+			'$l_orig'      => $l_orig,
+			'$t_text'      => $this->t('Title:') . ' *',
+			'$t_orig'      => $t_orig,
+			'$summary'     => ['summary', $this->t('Title:'), $t_orig, '', '*'],
+			'$sh_text'     => $this->t('Share this event'),
+			'$share'       => ['share', $this->t('Share this event'), $share_checked, '', $share_disabled],
+			'$sh_checked'  => $share_checked,
+			'$nofinish'    => ['nofinish', $this->t('Finish date/time is not known or not relevant'), $n_checked],
+			'$preview'     => $this->t('Preview'),
+			'$acl'         => $acl,
+			'$submit'      => $this->t('Submit'),
+			'$basic'       => $this->t('Basic'),
+			'$advanced'    => $this->t('Advanced'),
+			'$permissions' => $this->t('Permissions'),
+		]);
+	}
+}
diff --git a/src/Module/Calendar/Json.php b/src/Module/Calendar/Event/Get.php
similarity index 54%
rename from src/Module/Calendar/Json.php
rename to src/Module/Calendar/Event/Get.php
index 08481e6aca..f0824d15a5 100644
--- a/src/Module/Calendar/Json.php
+++ b/src/Module/Calendar/Event/Get.php
@@ -19,90 +19,58 @@
  *
  */
 
-namespace Friendica\Module\Calendar;
+namespace Friendica\Module\Calendar\Event;
 
+use Friendica\App;
+use Friendica\Core\L10n;
+use Friendica\Core\Session\Capability\IHandleUserSessions;
 use Friendica\Core\System;
-use Friendica\Database\DBA;
-use Friendica\DI;
 use Friendica\Model\Event;
 use Friendica\Model\Item;
 use Friendica\Model\Post;
+use Friendica\Module\Response;
 use Friendica\Network\HTTPException;
 use Friendica\Util\DateTimeFormat;
-use Friendica\Util\Temporal;
+use Friendica\Util\Profiler;
+use Psr\Log\LoggerInterface;
 
-class Json extends \Friendica\BaseModule
+/**
+ * GET-Controller for event
+ * returns the result as JSON
+ */
+class Get extends \Friendica\BaseModule
 {
+	/** @var IHandleUserSessions */
+	protected $session;
+
+	public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, IHandleUserSessions $session, array $server, array $parameters = [])
+	{
+		parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
+
+		$this->session = $session;
+	}
+
 	protected function rawContent(array $request = [])
 	{
-		if (!DI::userSession()->getLocalUserId()) {
+		if (!$this->session->getLocalUserId()) {
 			throw new HTTPException\UnauthorizedException();
 		}
 
-		$y = intval(DateTimeFormat::localNow('Y'));
-		$m = intval(DateTimeFormat::localNow('m'));
-
-		// Put some limit on dates. The PHP date functions don't seem to do so well before 1900.
-		if ($y < 1901) {
-			$y = 1900;
-		}
-
-		$dim    = Temporal::getDaysInMonth($y, $m);
-		$start  = sprintf('%d-%d-%d %d:%d:%d', $y, $m, 1, 0, 0, 0);
-		$finish = sprintf('%d-%d-%d %d:%d:%d', $y, $m, $dim, 23, 59, 59);
-
-		if (!empty($request['start'])) {
-			$start = $request['start'];
-		}
-
-		if (!empty($request['end'])) {
-			$finish = $request['end'];
-		}
-
-		// put the event parametes in an array so we can better transmit them
-		$event_params = [
-			'event_id' => intval($request['id'] ?? 0),
-			'start'    => $start,
-			'finish'   => $finish,
-			'ignore'   => 0,
-		];
-
 		// get events by id or by date
-		if ($event_params['event_id']) {
-			$r = Event::getListById(DI::userSession()->getLocalUserId(), $event_params['event_id']);
+		if (!empty($request['id'])) {
+			$events = [Event::getByIdAndUid($this->session->getLocalUserId(), $request['id'], $this->parameters['nickname'] ?? null)];
 		} else {
-			$r = Event::getListByDate(DI::userSession()->getLocalUserId(), $event_params);
+			$events = Event::getListByDate($this->session->getLocalUserId(), $request['start'] ?? '', $request['end'] ?? '', false, $this->parameters['nickname'] ?? null);
 		}
 
-		$links = [];
-
-		if (DBA::isResult($r)) {
-			$r = Event::sortByDate($r);
-			foreach ($r as $rr) {
-				$j = DateTimeFormat::utc($rr['start'], 'j');
-				if (empty($links[$j])) {
-					$links[$j] = DI::baseUrl() . '/' . DI::args()->getCommand() . '#link-' . $j;
-				}
-			}
-		}
-
-		$events = [];
-
-		// transform the event in a usable array
-		if (DBA::isResult($r)) {
-			$events = Event::sortByDate($r);
-
-			$events = self::map($events);
-		}
-
-		System::jsonExit($events);
+		System::jsonExit($events ? self::map($events) : []);
 	}
 
 	private static function map(array $events): array
 	{
 		return array_map(function ($event) {
 			$item = Post::selectFirst(['plink', 'author-name', 'author-avatar', 'author-link', 'private', 'uri-id'], ['id' => $event['itemid']]);
-			if (!DBA::isResult($item)) {
+			if (empty($item)) {
 				// Using default values when no item had been found
 				$item = ['plink' => '', 'author-name' => '', 'author-avatar' => '', 'author-link' => '', 'private' => Item::PUBLIC, 'uri-id' => ($event['uri-id'] ?? 0)];
 			}
diff --git a/src/Module/Calendar/Event/Show.php b/src/Module/Calendar/Event/Show.php
new file mode 100644
index 0000000000..2c606cf300
--- /dev/null
+++ b/src/Module/Calendar/Event/Show.php
@@ -0,0 +1,84 @@
+.
+ *
+ */
+
+namespace Friendica\Module\Calendar\Event;
+
+use Friendica\App;
+use Friendica\BaseModule;
+use Friendica\Core\L10n;
+use Friendica\Core\Renderer;
+use Friendica\Core\Session\Capability\IHandleUserSessions;
+use Friendica\Core\System;
+use Friendica\Model\Event;
+use Friendica\Module\Response;
+use Friendica\Network\HTTPException;
+use Friendica\Util\Profiler;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Displays one specific event in a