diff --git a/.gitignore b/.gitignore
index def6982f2b7f282d733a774b381cd45776a96e07..7aaa03b77830a1627282c2fd70f21db90771b14e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,8 +10,8 @@ vite.config.js.timestamp-*
 vite.config.ts.timestamp-*
 .devcontainer
 static/flyer
-static/stundenplaene/*
+static/stundenplaene
 static/images
 static/uploads
-!static/stundenplaene/branding.png
-#src/lib/server/fonts
+#!static/stundenplaene/branding.png
+src/lib/server/fonts
diff --git a/src/lib/components/AdminLayout.svelte b/src/lib/components/AdminLayout.svelte
index dd96c6b7b6f2d1e9d0a2009ec03a59742db03d47..da8fa9101456fe11cb8bed0a409097d15088685e 100644
--- a/src/lib/components/AdminLayout.svelte
+++ b/src/lib/components/AdminLayout.svelte
@@ -44,6 +44,7 @@
 			<DropdownItem href="/admin/rallye/station">Stationen</DropdownItem>
 			<DropdownItem href="/admin/rallye/points">Punkte</DropdownItem>
 		</Dropdown>
+		<NavLi href="/admin/mr-x">Mr. X</NavLi>
 		<NavLi href="/admin/rabatte">Rabatte</NavLi>
 		<NavLi href="/admin/flyer">Flyer</NavLi>
 		<NavLi href="/admin/uploads">Uploads</NavLi>
diff --git a/src/lib/components/H1.svelte b/src/lib/components/H1.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..63db73a53cd0cf192deb6ff5cfaf350f6a242051
--- /dev/null
+++ b/src/lib/components/H1.svelte
@@ -0,0 +1,10 @@
+<script lang="ts">
+	import { Heading } from "flowbite-svelte";
+	import type { Snippet } from "svelte";
+	
+	let { children, ...restProps }: { children: Snippet } = $props();
+</script>
+
+<Heading tag="h1" customSize="text-4xl font-bold" class="mb-6 text-center" {...restProps}>
+	{@render children()}
+</Heading>
diff --git a/src/lib/components/H2.svelte b/src/lib/components/H2.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..13081e72125ed9307c825f36a66fd69e5ab2fd88
--- /dev/null
+++ b/src/lib/components/H2.svelte
@@ -0,0 +1,10 @@
+<script lang="ts">
+	import { Heading } from "flowbite-svelte";
+	import type { Snippet } from "svelte";
+	
+	let { children, ...restProps }: { children: Snippet } = $props();
+</script>
+
+<Heading tag="h2" customSize="text-3xl font-bold" class="mb-3 mt-6" {...restProps}>
+	{@render children()}
+</Heading>
diff --git a/src/lib/components/H3.svelte b/src/lib/components/H3.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..195f898eb10672307e9ed06e49eaa229053d61a9
--- /dev/null
+++ b/src/lib/components/H3.svelte
@@ -0,0 +1,10 @@
+<script lang="ts">
+	import { Heading } from "flowbite-svelte";
+	import type { Snippet } from "svelte";
+	
+	let { children, ...restProps }: { children: Snippet } = $props();
+</script>
+
+<Heading tag="h3" customSize="text-xl font-bold" class="mb-2 mt-5" {...restProps}>
+	{@render children()}
+</Heading>
diff --git a/src/lib/components/Image.svelte b/src/lib/components/Image.svelte
index 856c5ebd5986bc1879019b8cf26a76804803d31e..db5e2ad8c2319746018974f9358ec65895bf51e3 100644
--- a/src/lib/components/Image.svelte
+++ b/src/lib/components/Image.svelte
@@ -2,12 +2,13 @@
 	import type { ImageMetadata } from "$lib/server/images";
 	import { locale } from "$lib/i18n/i18n";
 	import { onMount } from "svelte";
+	import type { HTMLAttributes } from "svelte/elements";
 	
 	let { sizes, src, autosize = false, ...restProps }: {
 		sizes?: string,
 		src: ImageMetadata<any>,
 		autosize?: boolean,
-	} = $props();
+	} & Partial<HTMLAttributes<HTMLImageElement>> = $props();
 	
 	function groupBy<T>(arr: T[], propExtractor: (t: T)=>string): Record<string, T[]> {
 		let result = {} as Record<string, T[]>;
diff --git a/src/lib/server/database/entities/MrX.entity.ts b/src/lib/server/database/entities/MrX.entity.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2095160a611fd3af1d86ae5d1b4675232853655c
--- /dev/null
+++ b/src/lib/server/database/entities/MrX.entity.ts
@@ -0,0 +1,51 @@
+import { sql } from "../db";
+
+export class MrXEntry {
+	id!: number;
+	mrx!: number;
+	time!: Date;
+	text!: string|null;
+	image!: string|null;
+	
+	static parse(data: any): MrXEntry|undefined {
+		if(!data) return;
+		return Object.assign(new MrXEntry(), data, { time: new Date(data.time) });
+	}
+	static async create(entry: Omit<MrXEntry, "id">): Promise<number> {
+		return (await sql`INSERT INTO mrx_entries ${sql(entry)} RETURNING id`)[0].id;
+	}
+	static async update(id: number, entry: Partial<MrXEntry>): Promise<void> {
+		await sql`UPDATE mrx_entries SET ${sql(entry)} WHERE id = ${id}`;
+	}
+	static async delete(id: number): Promise<void> {
+		await sql`DELETE FROM mrx_entries WHERE id = ${id}`;
+	}
+}
+
+export class MrX {
+	id!: number;
+	name!: string;
+	entries!: MrXEntry[];
+	
+	static parse(data: any): MrX|undefined {
+		if(!data) return;
+		const m = Object.assign(new MrX(), data);
+		m.entries = (data.entries as any[] || [])
+				.map(e => MrXEntry.parse(e))
+				.filter(e=>!!e)
+				.sort((a,b)=>a.time.getTime()-b.time.getTime());
+		return m;
+	}
+	static async getById(id: number): Promise<MrX|undefined> {
+		const row = (await sql`SELECT row_to_json(mrx.*) AS mr_x, json_agg(row_to_json(mrx_entries.*)) AS entries FROM mrx LEFT JOIN mrx_entries ON mrx.id = mrx_entries.mrx WHERE mrx.id = ${id} GROUP BY mrx.id`)[0];
+		if(!row) return;
+		return MrX.parse(Object.assign(row.mrX, { entries: row.entries }));
+	}
+	static async getAll(): Promise<MrX[]> {
+		const rows = await sql`SELECT row_to_json(mrx.*) AS mr_x, json_agg(row_to_json(mrx_entries.*)) AS entries FROM mrx LEFT JOIN mrx_entries ON mrx.id = mrx_entries.mrx GROUP BY mrx.id`;
+		return rows.map((row: any) => MrX.parse(Object.assign(row.mrX, { entries: row.entries }))!);
+	}
+	static async create(mrX: Omit<MrX, "id"|"entries">): Promise<number> {
+		return (await sql`INSERT INTO mrx ${sql(mrX)} RETURNING id`)[0].id;
+	}
+}
diff --git a/src/lib/server/database/migrations/0_init.sql b/src/lib/server/database/migrations/0_init.sql
index e29e563b6ecadc07951c2b5d94a48f7c3fbfef8e..30662e5ce123ee59e9bf943b62499d8190632d66 100644
--- a/src/lib/server/database/migrations/0_init.sql
+++ b/src/lib/server/database/migrations/0_init.sql
@@ -179,18 +179,14 @@ CREATE TABLE  IF NOT EXISTS master_freshers (
 );
 
 CREATE TABLE IF NOT EXISTS mrx (
-	"id" SERIAL PRIMARY KEY
+	"id" SERIAL PRIMARY KEY,
+	"name" TEXT NOT NULL
 );
 
 CREATE TABLE IF NOT EXISTS mrx_entries (
 	"id" SERIAL PRIMARY KEY,
 	"mrx" INT NOT NULL REFERENCES mrx(id),
-	"date" DATE NOT NULL,
-	"text" TEXT NOT NULL
-);
-
-CREATE TABLE IF NOT EXISTS mrx_attachments (
-	"id" SERIAL PRIMARY KEY,
-	"mrx_entry" INT NOT NULL REFERENCES mrx_entries(id),
-	"path" TEXT NOT NULL
+	"time" TIMESTAMP NOT NULL,
+	"text" TEXT,
+	"image" TEXT
 );
diff --git a/src/routes/(non-admin)/mr-x/+page.server.ts b/src/routes/(non-admin)/mr-x/+page.server.ts
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..919a8a259514f14893e10d0b33bb4caafa32d67a 100644
--- a/src/routes/(non-admin)/mr-x/+page.server.ts
+++ b/src/routes/(non-admin)/mr-x/+page.server.ts
@@ -0,0 +1,8 @@
+import { MrX } from '$lib/server/database/entities/MrX.entity.js';
+
+export async function load(event){
+	const mrXs = await MrX.getAll();
+	return {
+		mrXs: mrXs.map(mrX => ({ id: mrX.id, name: mrX.name })),
+	};
+}
diff --git a/src/routes/(non-admin)/mr-x/+page.svelte b/src/routes/(non-admin)/mr-x/+page.svelte
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..bdb80dd5a2079baf3b56e8adf168c5a08c01e20c 100644
--- a/src/routes/(non-admin)/mr-x/+page.svelte
+++ b/src/routes/(non-admin)/mr-x/+page.svelte
@@ -0,0 +1,14 @@
+<script lang="ts">
+	import H1 from "$lib/components/H1.svelte";
+	import { Button } from "flowbite-svelte";
+	
+	let { data } = $props();
+</script>
+
+<H1>Mr. X</H1>
+
+<div class="flex flex-col items-center gap-3">
+	{#each data.mrXs as mrX}
+	<Button href="/mr-x/{mrX.id}" class="w-full sm:max-w-96 md:max-w-xl" size="xl">{mrX.name}</Button>
+	{/each}
+</div>
diff --git a/src/routes/(non-admin)/mr-x/[id=number]/+page.server.ts b/src/routes/(non-admin)/mr-x/[id=number]/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bba948954ab39cefbf20390366815a345914df68
--- /dev/null
+++ b/src/routes/(non-admin)/mr-x/[id=number]/+page.server.ts
@@ -0,0 +1,11 @@
+import type { MrXData } from './data/+server.js';
+
+export async function load(event){
+	const data = await event.fetch(`/mr-x/${event.params.id}/data`).then(r => r.json()) as MrXData;
+	return {
+		mrX: {
+			...data.mrX,
+			entries: data.mrX.entries.map(e=>({...e, time: new Date(e.time)})),
+		}
+	}
+}
diff --git a/src/routes/(non-admin)/mr-x/[id=number]/+page.svelte b/src/routes/(non-admin)/mr-x/[id=number]/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..3041d586be6da8e3b24c4d0442595967b806fb91
--- /dev/null
+++ b/src/routes/(non-admin)/mr-x/[id=number]/+page.svelte
@@ -0,0 +1,135 @@
+<script lang="ts">
+	import H1 from "$lib/components/H1.svelte";
+	import { onMount } from "svelte";
+	import type { MrXData } from "./data/+server.js";
+	import { Card, P, Span } from "flowbite-svelte";
+	import Image from "$lib/components/Image.svelte";
+	import MarkdownIt from "markdown-it";
+	
+	let { data } = $props();
+	
+	let mrX = $state(data.mrX);
+	
+	onMount(()=>{
+		const interval = setInterval(async ()=>{
+			const response = await fetch(`/mr-x/${mrX.id}/data`);
+			if(response.ok){
+				const json = await response.json() as MrXData;
+				mrX = json.mrX;
+				mrX.entries = mrX.entries.map(entry=>({...entry, time: new Date(entry.time)}));
+			}
+		}, 30_000);
+		return ()=>{
+			clearInterval(interval);
+		};
+	});
+	
+	const md = new MarkdownIt({
+		breaks: true,
+		linkify: true,
+		quotes: "“”‘’",
+		html: true,
+	});
+</script>
+
+<style>
+	/* add styles again bc tailwind removes them all :( */
+	:global(.md-wrapper h1) {
+		font-size: 2rem;
+		font-weight: 700;
+		padding-bottom: .3em;
+	}
+	:global(.md-wrapper h2) {
+		font-size: 1.75rem;
+		font-weight: 600;
+		padding-bottom: .3em;
+	}
+	:global(.md-wrapper h3) {
+		font-size: 1.5rem;
+		font-weight: 600;
+		padding-bottom: .3em;
+	}
+	:global(.md-wrapper h4) {
+		font-size: 1.25rem;
+		font-weight: 600;
+		padding-bottom: .3em;
+	}
+	:global(.md-wrapper h5) {
+		font-size: 1rem;
+		font-weight: 600;
+		padding-bottom: .3em;
+	}
+	:global(.md-wrapper h6) {
+		font-size: .875rem;
+		font-weight: 600;
+		padding-bottom: .3em;
+	}
+	:global(.md-wrapper p) {
+		padding-bottom: .75em;
+	}
+	:global(.md-wrapper blockquote) {
+		margin: 0 0 .5em 0;
+		padding: 0 .5em;
+		border-left: 4px solid #d2d6dc;
+	}
+	:global(.dark .md-wrapper blockquote) {
+		border-left-color: #4b5563;
+	}
+	:global(.md-wrapper blockquote p) {
+		padding: .2em 0;
+	}
+	:global(.md-wrapper blockquote blockquote){
+		margin-bottom: 0;
+	}
+	:global(.md-wrapper pre) {
+		padding: 1em;
+		background-color: #f3f4f6;
+		border-radius: .5em;
+		margin-bottom: .5em;
+		overflow-x: auto;
+	}
+	:global(.dark .md-wrapper pre) {
+		background-color: #31384f;
+	}
+	:global(.md-wrapper code) {
+		background-color: #f3f4f6;
+		padding: .1em .3em;
+		border-radius: .3em;
+	}
+	:global(.dark .md-wrapper code) {
+		background-color: #31384f;
+	}
+	:global(.md-wrapper pre code){
+		background-color: transparent;
+		padding: 0;
+		border-radius: 0;
+	}
+	:global(.md-wrapper a) {
+		color: #2563EB;
+		text-decoration: underline;
+	}
+	:global(.dark .md-wrapper a) {
+		color: #90CDF4;
+	}
+</style>
+
+<H1>{mrX.name}</H1>
+
+<div class="w-full flex flex-col items-center gap-3">
+	{#each mrX.entries as entry (entry.id)}
+	<Card class="w-full max-w-2xl relative">
+		<Span class="relative -top-3 -left-2 -mb-2 text-sm font-normal text-gray-600 dark:text-gray-400">{entry.time.toLocaleString()}</Span>
+		{#if entry.text}
+		<div class="text-base text-gray-900 dark:text-white leading-normal font-normal text-left whitespace-normal {entry.image ? "mb-2" : "-mb-4"} md-wrapper">
+			{@html md.render(entry.text)}
+		</div>
+		{/if}
+		{#if entry.image}
+		<div class="flex justify-center max-h-svh">
+			<Image src={entry.image} autosize class="max-h-full object-contain" />
+		</div>
+		{/if}
+	</Card>
+	{/each}
+</div>
+
diff --git a/src/routes/(non-admin)/mr-x/[id=number]/data/+server.ts b/src/routes/(non-admin)/mr-x/[id=number]/data/+server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ce7ee21c1f9f2f341ccbc4dd352f30a0bd14c9dc
--- /dev/null
+++ b/src/routes/(non-admin)/mr-x/[id=number]/data/+server.ts
@@ -0,0 +1,23 @@
+import { MrX } from '$lib/server/database/entities/MrX.entity.js';
+import { getImages, type ImageMetadata } from '$lib/server/images';
+import { error } from '@sveltejs/kit';
+import path from 'path';
+
+export type MrXData = {
+	mrX: Omit<MrX, "entries"> & { entries: Array<Omit<MrX['entries'][number], "image"> & { image?: ImageMetadata<never> }> };
+};
+
+export async function GET(event){
+	const id = Number(event.params.id);
+	const mrX = await MrX.getById(id);
+	if(!mrX) error(404, 'Mr. X not found');
+	const images = await getImages<never>(path.join('mrx', String(id)));
+	const now = new Date();
+	const firstFutureEntryIndex = mrX.entries.findLastIndex(e => e.time <= now) + 1;
+	// TODO maybe add next entry time?
+	const entries = mrX.entries.slice(0, firstFutureEntryIndex).map(e => ({
+		...e,
+		image: e.image ? images.find(i => i.identifier === e.image) : undefined,
+	}));
+	return new Response(JSON.stringify({ mrX: { ...mrX, entries } } satisfies MrXData));
+}
diff --git a/src/routes/admin/mr-x/+page.server.ts b/src/routes/admin/mr-x/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..70cc323b5ed29b90412ae03cdaadfa101c7d4a33
--- /dev/null
+++ b/src/routes/admin/mr-x/+page.server.ts
@@ -0,0 +1,19 @@
+import { MrX } from '$lib/server/database/entities/MrX.entity.js';
+import { error } from '@sveltejs/kit';
+
+export async function load(){
+	const mrXs = await MrX.getAll();
+	return {
+		mrXs: mrXs.map(m => ({id: m.id, name: m.name})),
+	};
+}
+
+export const actions = {
+	new: async function(event){
+		const data = await event.request.formData();
+		const name = data.get('name');
+		if(!name) error(400, 'Name is required');
+		if(typeof name !== 'string') error(400, 'Invalid name');
+		return await MrX.create({name});
+	},
+};
diff --git a/src/routes/admin/mr-x/+page.svelte b/src/routes/admin/mr-x/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..547bc80520a5569e790f1f96ba09f2b83d888e41
--- /dev/null
+++ b/src/routes/admin/mr-x/+page.svelte
@@ -0,0 +1,23 @@
+<script lang="ts">
+	import { enhance } from "$app/forms";
+	import { Breadcrumb, BreadcrumbItem, Button, Input, Li, List } from "flowbite-svelte";
+	
+	let { data } = $props();
+</script>
+
+<Breadcrumb>
+	<BreadcrumbItem href="/admin">Admin</BreadcrumbItem>
+	<BreadcrumbItem href="/admin/mr-x">Mr. X</BreadcrumbItem>
+</Breadcrumb>
+
+<List list="none">
+	{#each data.mrXs as item}
+	<Li class="mb-2"><Button href="/admin/mr-x/{item.id}" class="min-w-28">{item.name}</Button></Li>
+	{/each}
+	<Li>
+		<form method="post" action="?/new" class="flex gap-3" use:enhance>
+			<Input type="text" placeholder="Name" name="name" />
+			<Button type="submit">Erstellen</Button>
+		</form>
+	</Li>
+</List>
diff --git a/src/routes/admin/mr-x/[id=number]/+page.server.ts b/src/routes/admin/mr-x/[id=number]/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..615929a63fa6d20e7ac395c49b0a999d0b9575bb
--- /dev/null
+++ b/src/routes/admin/mr-x/[id=number]/+page.server.ts
@@ -0,0 +1,92 @@
+import { locales } from '$lib/i18n/i18n.js';
+import { MrX, MrXEntry } from '$lib/server/database/entities/MrX.entity.js';
+import { deleteImage, getImages, uploadImage } from '$lib/server/images';
+import type { Localized } from '$lib/utils.js';
+import { error, redirect } from '@sveltejs/kit';
+import path from 'path';
+
+export async function load(event){
+	const id = Number(event.params.id);
+	const mrX = await MrX.getById(id);
+	if(!mrX) redirect(307, '/admin/mr-x');
+	const images = await getImages<never>(path.join('mrx', String(id)));
+	const entries = mrX.entries.map(e => ({
+		...e,
+		image: e.image ? images.find(i => i.identifier === e.image) : undefined,
+	}));
+	return {
+		mrX: {
+			...mrX,
+			entries,
+		},
+	}
+}
+
+export const actions = {
+	new: async event => {
+		const mrX = await MrX.getById(Number(event.params.id));
+		if(!mrX) error(404, 'Mr. X not found');
+		const data = await event.request.formData();
+		const datetime = data.get('datetime');
+		const text = data.get('text');
+		const image = data.get('image');
+		if(!datetime || typeof datetime !== 'string') error(400, 'Invalid datetime');
+		const time = new Date(datetime);
+		if(isNaN(time.getTime())) error(400, 'Invalid datetime');
+		if(text && typeof text !== 'string') error(400, 'Invalid text');
+		if(image !== null && !(image instanceof File)) error(400, 'Invalid image');
+		let imageIdentifier: string|null = null;
+		if(image && image.size > 0){
+			const buffer = await image.arrayBuffer();
+			const description = Object.fromEntries(locales.map(l=>[l, text ?? ""])) as Localized;
+			const img = await uploadImage(buffer, description, path.join('mrx', String(mrX.id)));
+			imageIdentifier = img.identifier;
+		}
+		return await MrXEntry.create({ mrx: mrX.id, time, text, image: imageIdentifier });
+	},
+	update: async event => {
+		const mrX = await MrX.getById(Number(event.params.id));
+		if(!mrX) error(404, 'Mr. X not found');
+		const data = await event.request.formData();
+		const id = data.get('entry');
+		const time = data.get('time');
+		const text = data.get('text');
+		const image = data.get('image');
+		const removeImage = data.get('removeImage') === 'true';
+		if(!id || typeof id !== 'string' || !/^[1-9]\d*$/.test(id)) error(400, 'Invalid entry');
+		const entryId = Number(id);
+		const entry = mrX.entries.find(e => e.id === entryId);
+		if(!entry) error(404, 'Entry not found');
+		if(!time || typeof time !== 'string') error(400, 'Invalid datetime');
+		const newTime = new Date(time);
+		if(isNaN(newTime.getTime())) error(400, 'Invalid datetime');
+		if(text && typeof text !== 'string') error(400, 'Invalid text');
+		if(image !== null && !(image instanceof File)) error(400, 'Invalid image');
+		let imageIdentifier: string|null = entry.image;
+		if(imageIdentifier && (removeImage || (image && image.size > 0))){
+			await deleteImage(path.join("mrx", String(mrX.id)), imageIdentifier);
+			imageIdentifier = null;
+		}
+		if(image && image.size > 0){
+			const buffer = await image.arrayBuffer();
+			const description = Object.fromEntries(locales.map(l=>[l, text ?? ""])) as Localized;
+			const img = await uploadImage(buffer, description, path.join('mrx', String(mrX.id)));
+			imageIdentifier = img.identifier;
+		}
+		await MrXEntry.update(entryId, { time: newTime, text, image: imageIdentifier });
+	},
+	delete: async event => {
+		const mrX = await MrX.getById(Number(event.params.id));
+		if(!mrX) error(404, 'Mr. X not found');
+		const data = await event.request.formData();
+		const id = data.get('entry');
+		if(!id || typeof id !== 'string' || !/^[1-9]\d*$/.test(id)) error(400, 'Invalid entry');
+		const entryId = Number(id);
+		const entry = mrX.entries.find(e => e.id === entryId);
+		if(!entry) error(404, 'Entry not found');
+		if(entry.image){
+			await deleteImage(path.join("mrx", String(mrX.id)), entry.image);
+		}
+		await MrXEntry.delete(entryId);
+	},
+};
diff --git a/src/routes/admin/mr-x/[id=number]/+page.svelte b/src/routes/admin/mr-x/[id=number]/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..37091d68bc297646e498f7ecced3fd3515abc400
--- /dev/null
+++ b/src/routes/admin/mr-x/[id=number]/+page.svelte
@@ -0,0 +1,35 @@
+<script lang="ts">
+	import H1 from "$lib/components/H1.svelte";
+	import { Breadcrumb, BreadcrumbItem, Button, Card, Input, Textarea } from "flowbite-svelte";
+	import Entry from "./Entry.svelte";
+	import { enhance } from "$app/forms";
+	import { invalidateAll } from "$app/navigation";
+	
+	let { data } = $props();
+</script>
+
+<Breadcrumb>
+	<BreadcrumbItem href="/admin">Admin</BreadcrumbItem>
+	<BreadcrumbItem href="/admin/mr-x">Mr. X</BreadcrumbItem>
+	<BreadcrumbItem>{data.mrX.name}</BreadcrumbItem>
+</Breadcrumb>
+
+<H1>{data.mrX.name}</H1>
+
+<div class="w-full flex flex-col items-center gap-3">
+	{#each data.mrX.entries as entry (entry.id)}
+	<Entry {entry} />
+	{/each}
+	<Card class="w-full max-w-2xl">
+		<form method="post" action="?/new" enctype="multipart/form-data" use:enhance={({formData})=>{
+			const datetime = new Date(formData.get("datetime") as string);
+			formData.set("datetime", datetime.toISOString()); // Convert to ISO string to add timezone information
+			return ()=>invalidateAll();
+		}}>
+			<Input name="time" type="datetime-local" class="mb-2" required />
+			<Textarea name="text" class="mb-2" />
+			<Input name="image" type="file" accept="image/png, image/jpeg" class="mb-2" />
+			<Button type="submit" class="w-full">Erstellen</Button>
+		</form>
+	</Card>
+</div>
diff --git a/src/routes/admin/mr-x/[id=number]/Entry.svelte b/src/routes/admin/mr-x/[id=number]/Entry.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..69ae9a2917d25b8fd65a2de5bee7846903781570
--- /dev/null
+++ b/src/routes/admin/mr-x/[id=number]/Entry.svelte
@@ -0,0 +1,49 @@
+<script lang="ts">
+	import { Card, Textarea, Button, ButtonGroup, Input } from "flowbite-svelte";
+	import type { MrXData } from "../../../(non-admin)/mr-x/[id=number]/data/+server";
+	import { enhance } from "$app/forms";
+	import Image from "$lib/components/Image.svelte";
+	
+	type Props = {
+		entry: MrXData["mrX"]["entries"][number];
+	};
+	
+	let { entry }: Props = $props();
+	
+	let removeImage = $state(false);
+	let imageInput = $state<HTMLInputElement | null>(null);
+</script>
+
+<Card class="w-full max-w-2xl">
+	<form method="post" action="?/update" enctype="multipart/form-data" use:enhance={({formData})=>{
+			const datetime = new Date(formData.get("time") as string);
+			formData.set("time", datetime.toISOString()); // Convert to ISO string to add timezone information
+			return ({update})=>update();
+		}}>
+		<input type="hidden" name="entry" value={entry.id} />
+		<Input name="time" type="datetime-local" class="mb-2" required value="{entry.time.getFullYear()}-{String(entry.time.getMonth()+1).padStart(2, "0")}-{String(entry.time.getDate()).padStart(2, "0")}T{String(entry.time.getHours()).padStart(2, "0")}:{String(entry.time.getMinutes()).padStart(2, "0")}" />
+		<Textarea name="text" class="mb-2" value={entry.text} />
+		{#if entry.image}
+		{#if !removeImage}
+		<div class="flex justify-center max-h-svh mb-2">
+			<Image src={entry.image} autosize class="max-h-full object-contain" />
+		</div>
+		<ButtonGroup class="mb-2">
+			<Button on:click={()=>{
+				removeImage = true;
+				if(imageInput) imageInput.value = "";
+			}}>Bild entfernen</Button>
+			<Button on:click={()=>imageInput?.click()}>Bild ersetzen</Button>
+		</ButtonGroup>
+		{/if}
+		<input bind:this={imageInput} name="image" type="file" accept="image/png, image/jpeg" class={removeImage ? "mb-2" : "hidden"} onchange={()=>removeImage=true} />
+		<input type="hidden" name="removeImage" value={removeImage ? "true" : null} />
+		{:else}
+		<Input name="image" type="file" accept="image/png, image/jpeg" class="mb-2" />
+		{/if}
+		<div class="flex gap-2">
+			<Button type="submit" class="w-full">Speichern</Button>
+			<Button type="submit" formaction="?/delete" color="red">Löschen</Button>
+		</div>
+	</form>
+</Card>