diff --git a/.gitignore b/.gitignore
index 31bc78031052e6b3dd0acecbc5fe5a0c877665de..7aaa03b77830a1627282c2fd70f21db90771b14e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,5 +12,6 @@ vite.config.ts.timestamp-*
 static/flyer
 static/stundenplaene
 static/images
+static/uploads
 #!static/stundenplaene/branding.png
 src/lib/server/fonts
diff --git a/src/lib/components/AdminLayout.svelte b/src/lib/components/AdminLayout.svelte
index 5810cece1defe5b7fbf3166a07e06a2cdcf2659f..dd96c6b7b6f2d1e9d0a2009ec03a59742db03d47 100644
--- a/src/lib/components/AdminLayout.svelte
+++ b/src/lib/components/AdminLayout.svelte
@@ -1,16 +1,23 @@
 <script lang="ts">
+	import { page } from "$app/stores";
 	import { signOut } from "@auth/sveltekit/client";
 	import { DarkMode, Dropdown, DropdownDivider, DropdownItem, NavBrand, NavHamburger, NavLi, NavUl, Navbar } from "flowbite-svelte";
 	import { ChevronDownOutline } from "flowbite-svelte-icons";
 	import type { Snippet } from "svelte";
 	
 	let { children }: {children: Snippet} = $props();
+	
+	function processActiveUrl(url: string) {
+		return url;
+	}
+	
+	const activeUrl = $derived(processActiveUrl($page.url.pathname));
 </script>
 
 <Navbar>
 	<NavBrand href="/admin">Fachschaft I/1</NavBrand>
 	<NavHamburger />
-	<NavUl>
+	<NavUl {activeUrl}>
 		<NavLi class="cursor-pointer">Einstellungen<ChevronDownOutline class="w-4 h-4 ms-1 text-primary-800 dark:text-white inline" /></NavLi>
 		<Dropdown class="w-44 z-20">
 			<DropdownItem href="/admin">Allgemein</DropdownItem>
@@ -39,6 +46,7 @@
 		</Dropdown>
 		<NavLi href="/admin/rabatte">Rabatte</NavLi>
 		<NavLi href="/admin/flyer">Flyer</NavLi>
+		<NavLi href="/admin/uploads">Uploads</NavLi>
 		<NavLi href="javascript:void 0" onclick={()=>signOut({callbackUrl: "/"})}>Logout</NavLi>
 		<li><DarkMode btnClass="block py-2 pr-4 pl-3 md:p-0 rounded text-gray-700 hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-primary-700 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent cursor-pointer w-full" /></li>
 	</NavUl>
diff --git a/src/lib/components/Image.svelte b/src/lib/components/Image.svelte
index 0ebef914c6d4238a0e333eea841e1df1ccb5cf9d..856c5ebd5986bc1879019b8cf26a76804803d31e 100644
--- a/src/lib/components/Image.svelte
+++ b/src/lib/components/Image.svelte
@@ -3,9 +3,11 @@
 	import { locale } from "$lib/i18n/i18n";
 	import { onMount } from "svelte";
 	
-	export let sizes: string | undefined = undefined;
-	export let src: ImageMetadata<any>;
-	export let autosize = false;
+	let { sizes, src, autosize = false, ...restProps }: {
+		sizes?: string,
+		src: ImageMetadata<any>,
+		autosize?: boolean,
+	} = $props();
 	
 	function groupBy<T>(arr: T[], propExtractor: (t: T)=>string): Record<string, T[]> {
 		let result = {} as Record<string, T[]>;
@@ -46,5 +48,5 @@
 	{#each sources as {type, sizes: srcset}}
 	<source {type} {srcset} />
 	{/each}
-	<img bind:this={imgElement} src={defaultImage.url} {sizes} width="{src.width}" height="{src.height}" alt={src.description?.[$locale]} {...$$restProps} />
+	<img bind:this={imgElement} src={defaultImage.url} {sizes} width="{src.width}" height="{src.height}" alt={src.description?.[$locale]} {...restProps} />
 </picture>
diff --git a/src/lib/components/MailInput.svelte b/src/lib/components/MailInput.svelte
index aa4de07fdad46165f875b8b69f44546eb719b9d6..d339c6b1e2fc074aa5f61e6e3ce639b4ad7e36d3 100644
--- a/src/lib/components/MailInput.svelte
+++ b/src/lib/components/MailInput.svelte
@@ -2,7 +2,7 @@
 	import Degree from "$lib/degrees";
 	import Gender from "$lib/genders";
 	import { locale, locales } from "$lib/i18n/i18n";
-	import { variables, conditions, renderTemplate, parseTemplate, compileTemplate } from "$lib/mail";
+	import { variables, conditions, renderTemplate, parseTemplate, compileTemplate, formatters, subjectFormatter, type PartialConfig, type TemplateType } from "$lib/mail";
 	import type { StudyProgram } from "$lib/server/database/entities/StudyProgram.entity";
 	import type { Tutor } from "$lib/server/database/entities/Tutor.entity";
 	import type { TutorTraining } from "$lib/server/database/entities/TutorTraining.entity";
@@ -22,7 +22,9 @@
 		buttonType = "button",
 		disabled = false,
 		buttonDisabled = disabled,
-	}: {text: Localized, subject: Localized, replyTo?: string|null, replyToOptions?: string[], allowCustomReplyTo?: boolean, studyPrograms: StudyProgram[], trainings: TutorTraining[], onsubmit: ()=>void, buttonText?: string, buttonType: HTMLButtonElement["type"], buttonDisabled?: boolean, disabled?: boolean} = $props();
+		config,
+		type,
+	}: {text: Localized, subject: Localized, replyTo?: string|null, replyToOptions?: string[], allowCustomReplyTo?: boolean, studyPrograms: StudyProgram[], trainings: TutorTraining[], onsubmit: ()=>void, buttonText?: string, buttonType: HTMLButtonElement["type"], buttonDisabled?: boolean, disabled?: boolean, config: PartialConfig, type: TemplateType} = $props();
 	
 	let customReplyTo = $state(false);
 	
@@ -53,18 +55,19 @@
 		trained: false,
 		dietaryRestriction: "vegan, Eiweißallergie",
 		training: trainings[0],
+		sentTrainingMail: false,
 	});
 	
 	let [mailHtml, compilationError]: [string, string|null] = $derived.by(()=>{
 		try {
-			return [renderTemplate(compileTemplate(parseTemplate(text), exampleTutor)), null] as const;
+			return [renderTemplate(compileTemplate(type, parseTemplate(type, text), exampleTutor, config)), null] as const;
 		} catch(ex) {
 			return ["", (ex as Error).message] as const;
 		}
 	});
 	let [compiledSubject, subjectError]: [string, string|null] = $derived.by(()=>{
 		try {
-			return [compileTemplate(parseTemplate(subject), exampleTutor, ({de, en})=>`${de} / ${en}`), null] as const;
+			return [compileTemplate(type, parseTemplate(type, subject), exampleTutor, config, subjectFormatter), null] as const;
 		} catch(ex) {
 			return ["", (ex as Error).message] as const;
 		}
@@ -97,7 +100,7 @@
 	{#each locales as locale}
 	<Label class="w-full">
 		Betreff ({locale})
-		<Input name="subject[{locale}]" bind:value={subject[locale]} oninput={e=>subject=Object.assign(subject,{[locale]:e.target.value})} {disabled} />
+		<Input name="subject[{locale}]" bind:value={subject[locale]} oninput={e=>subject=Object.assign(subject,{[locale]:e.target.value})} {disabled} required={locale==="de"} />
 	</Label>
 	{/each}
 </div>
@@ -107,7 +110,7 @@
 {#each locales as locale}
 <Label class="mb-1">
 	Text ({locale})
-	<Textarea name="text[{locale}]" bind:value={text[locale]} class="min-h-36" {disabled} />
+	<Textarea name="text[{locale}]" bind:value={text[locale]} class="min-h-36" {disabled} required />
 </Label>
 {/each}
 {#if compilationError}
@@ -171,11 +174,19 @@
 	<AccordionItem>
 		<Span slot="header">Variablen</Span>
 		<List tag="ul" position="outside" class="ml-4">
-			{#each variables as variable}
+			{#each variables[type] as variable}
 			<Li><code>{"{{"}{variable.name}{"}}"}</code> - {variable.description}</Li>
 			{/each}
 		</List>
 	</AccordionItem>
+	<AccordionItem>
+		<Span slot="header">Formatierer</Span>
+		<List tag="ul" position="outside" class="ml-4">
+			{#each Object.entries(formatters) as [name, {description}]}
+			<Li><code>{name}</code> - {description}</Li>
+			{/each}
+		</List>
+	</AccordionItem>
 	<AccordionItem>
 		<Span slot="header">Bedingungen</Span>
 		<List tag="ul" position="outside" class="ml-4">
@@ -200,6 +211,12 @@
 						<Li>Variablen werden durch doppelte geschweifte Klammern gekennzeichnet: <code>{"{{"}variablenname{"}}"}</code>.</Li>
 					</List>
 				</Li>
+				<Li>
+					<b>Formatierung</b>
+					<List tag="ul" position="outside" class="ml-6">
+						<Li>Werte von Variablen können formatiert werden, indem die Formatierer getrennt durch <code>|</code> nach dem Variablennamen gelistet werden, z.B. <code>{"{{"}variablenname|formatierer1|formatierer2{"}}"}</code>. Die Formatierer werden in der angegebenen Reihenfolge angewandt.</Li>
+					</List>
+				</Li>
 				<Li>
 					<b>Bedingte Anweisungen</b>
 					<List tag="ul" position="outside" class="ml-6">
diff --git a/src/lib/components/MainLayout.svelte b/src/lib/components/MainLayout.svelte
index 648f75e4bedbe611d15a0ef290690f0919509701..7fa7e2ce866e4caf6297db889daa027d9651c224 100644
--- a/src/lib/components/MainLayout.svelte
+++ b/src/lib/components/MainLayout.svelte
@@ -43,7 +43,6 @@
 			{#each headerLinks as link}
 			<NavLi href={link}>{$LL.Navbar.Links[link as keyof Translation["Navbar"]["Links"]]()}</NavLi>
 			{/each}
-			<NavLi href="/upload">Momente teilen</NavLi> <!-- TODO i18n -->
 			<NavLi class="cursor-pointer">
 				{$LL.Navbar.Language()}<ChevronDownOutline class="w-4 h-4 ms-1 text-primary-800 dark:text-white inline" />
 			</NavLi>
diff --git a/src/lib/components/TutorFAQ.svelte b/src/lib/components/TutorFAQ.svelte
index 6e0c834dfc801fca9dc2e7ef98316795bbd362dd..7edf09bf2ad0afe3e371130ba40d41f7ccade459 100644
--- a/src/lib/components/TutorFAQ.svelte
+++ b/src/lib/components/TutorFAQ.svelte
@@ -1,9 +1,11 @@
-<script>
+<script lang="ts">
 	import { Accordion, AccordionItem, Span, P, A, Heading } from "flowbite-svelte";
 	import { LL } from "$lib/i18n/i18n";
 	import LocalizedText from "./LocalizedText.svelte";
 	
-	export let title = $LL.Tutor.FAQ.Headline();
+	let { title }: { title?: string } = $props();
+	
+	if(!title) title = $LL.Tutor.FAQ.Headline();
 </script>
 
 {#if title}
diff --git a/src/lib/i18n/de.ts b/src/lib/i18n/de.ts
index 971425166710efbd158b8b7699522bf14d7aa4fd..a378edefa593984ab96d1a9df58151e9cf473fd3 100644
--- a/src/lib/i18n/de.ts
+++ b/src/lib/i18n/de.ts
@@ -32,6 +32,7 @@ export default {
 		"/admin/tutor/training": "Tutschulungen verwalten",
 		"/admin/tutor/training/[id=number]": "Tutschulung bearbeiten: {training.date}",
 		"/admin/tutor/training/new": "Neue Tutschulung erstellen",
+		"/admin/uploads": "Hochgeladene Bilder",
 		"/admin/user": "Berechtigungen verwalten",
 	},
 	Navbar: {
@@ -42,6 +43,7 @@ export default {
 			"/eswe": "Erstsemesterwochenende",
 			"/flyer": "Flyer",
 			"/tutor": "Tutor",
+			"/upload": "Momente teilen",
 		},
 		Language: "Sprache",
 		Languages: {
diff --git a/src/lib/i18n/en.ts b/src/lib/i18n/en.ts
index 48d0b60ed6ee0ca93fa9951c8a91abe21e9e01fa..b308da7c0d211534a02216acd645c528bd629778 100644
--- a/src/lib/i18n/en.ts
+++ b/src/lib/i18n/en.ts
@@ -34,6 +34,7 @@ export default {
 		"/admin/tutor/training": "Manage Tutor Trainings",
 		"/admin/tutor/training/[id=number]": "Edit tutor training: {training.date}",
 		"/admin/tutor/training/new": "Create new tutor training",
+		"/admin/uploads": "Uploaded photos",
 		"/admin/user": "Manage Permissions",
 	},
 	Navbar: {
@@ -44,6 +45,7 @@ export default {
 			"/eswe": "Freshers' Weekend",
 			"/flyer": "Flyer",
 			"/tutor": "Tutor",
+			"/upload": "Share Moments",
 		},
 		Language: "Language",
 		Languages: {
diff --git a/src/lib/mail.ts b/src/lib/mail.ts
index 17b9b8d45ee9e654472f48112bbd5de2e0cfedcd..4ca86b645afc068287f8a0272602f2f9ffd0ec90 100644
--- a/src/lib/mail.ts
+++ b/src/lib/mail.ts
@@ -4,6 +4,7 @@ import { locales, type Locale } from "./i18n/i18n";
 import type { Tutor } from "./server/database/entities/Tutor.entity";
 import markdownit from "markdown-it";
 import type { Localized } from "./utils";
+import type { Config } from "./server/database/entities/Config.entity";
 
 const md = markdownit({
 	breaks: true,
@@ -17,65 +18,178 @@ export type MailTemplate = {
 	replyTo: string;
 	subject: Localized;
 	text: Localized;
+	type: TemplateType;
 };
 
-export type Variable = {
-	name: string;
-	description: string;
-	replacement: (t: Tutor, l: Locale)=>string;
-};
-export const variables: Variable[] = [
-	{
-		name: "vorname",
-		description: "Vorname des Empfängers",
-		replacement: t=>t.firstname,
+export const formatters: Record<string, { description: string, format: (l: Locale, arg: unknown)=>string }> = {
+	dateLong: {
+		description: "Formatiert ein Datum, z.B. 01. Januar 1970",
+		format: (locale, arg)=>new Intl.DateTimeFormat(locale, { year: "numeric", month: "long", day: "2-digit" }).format(arg as Date),
 	},
-	{
-		name: "nachname",
-		description: "Nachname des Empfängers",
-		replacement: t=>t.lastname,
+	dateShort: {
+		description: "Formatiert ein Datum, z.B. 1. Januar",
+		format: (locale, arg)=>new Intl.DateTimeFormat(locale, { month: "long", day: "numeric" }).format(arg as Date),
 	},
-	{
-		name: "spitzname",
-		description: "Spitzname des Empfängers",
-		replacement: t=>t.nickname ?? "",
+	dateWeekday: {
+		description: "Gibt den Wochentag eines Datums zurück, z.B. Montag",
+		format: (locale, arg)=>new Intl.DateTimeFormat(locale, { weekday: "long" }).format(arg as Date),
 	},
-	{
-		name: "email",
-		description: "E-Mail-Adresse des Empfängers",
-		replacement: t=>t.email,
+	dateWeekdayShort: {
+		description: "Gibt den Wochentag eines Datums zurück, z.B. Mo",
+		format: (locale, arg)=>new Intl.DateTimeFormat(locale, { weekday: "short" }).format(arg as Date),
 	},
-	{
-		name: "telefon",
-		description: "Telefonnummer des Empfängers",
-		replacement: t=>t.phone,
+	dateRangeLong: {
+		description: "Formatiert einen Datumsbereich, z.B. 1. - 2. Januar 1970",
+		format: (locale, arg)=>new Intl.DateTimeFormat(locale, { year: "numeric", month: "long", day: "2-digit" }).formatRange((arg as [Date, Date])[0], (arg as [Date, Date])[1]),
 	},
-	{
-		name: "studiengang",
-		description: "Studiengang des Empfängers",
-		replacement: (t, l)=>t.studyProgram.name[l],
+	dateRangeShort: {
+		description: "Formatiert einen Datumsbereich, z.B. 1. - 2. Januar",
+		format: (locale, arg)=>new Intl.DateTimeFormat(locale, { month: "long", day: "numeric" }).formatRange((arg as [Date, Date])[0], (arg as [Date, Date])[1]),
 	},
-	{
-		name: "abschluss",
-		description: "Abschluss des Empfängers",
-		replacement: (t, l)=>Degree[t.degree][l],
+	time: {
+		description: "Formatiert eine Uhrzeit, z.B. 12:34",
+		format: (locale, arg)=>new Intl.DateTimeFormat(locale, { hour: "2-digit", minute: "2-digit" }).format(arg as Date),
 	},
-	{
-		name: "schulung.datum",
-		description: "Datum der Schulung des Empfängers",
-		replacement: (t, l) => t.training ? new Intl.DateTimeFormat(l, { year: "numeric", month: "long", day: "2-digit" }).format(Date.parse(t?.training?.date)) : {de: "keine Schulung", en: "no training"}[l],
+	timeRange: {
+		description: "Formatiert einen Zeitbereich, z.B. 12:34 - 13:45 Uhr",
+		format: (locale, arg)=>new Intl.DateTimeFormat(locale, { hour: "2-digit", minute: "2-digit" }).formatRange((arg as [Date, Date])[0], (arg as [Date, Date])[1]),
 	},
-	{
-		name: "schulung.ort",
-		description: "Ort der Schulung des Empfängers",
-		replacement: (t, l)=>t?.training?.location ?? {de: "keine Schulung", en: "no training"}[l],
+	lowercase: {
+		description: "Konvertiert den Text in Kleinbuchstaben",
+		format: (_, arg)=>String(arg).toLowerCase(),
 	},
-	{
-		name: "cotutorwunsch",
-		description: "Name des gewünschten Co-Tutors",
-		replacement: t=>t.coTutorWish,
+	uppercase: {
+		description: "Konvertiert den Text in Großbuchstaben",
+		format: (_, arg)=>String(arg).toUpperCase(),
 	},
-];
+	titlecase: {
+		description: "Konvertiert den ersten Buchstaben jedes Wortes in Großbuchstaben",
+		format: (_, arg)=>String(arg).replace(/\b\w/g, l=>l.toUpperCase()),
+	},
+};
+
+export const types = ["tutor"] as const;
+export type TemplateType = typeof types[number];
+
+export const configProps = ["currentSemester", "fresherWeekStart", "fresherWeekEnd", "rallyDate", "trainingsStart", "trainingsEnd"] as const satisfies (keyof Config)[];
+export type PartialConfig = Pick<Config, typeof configProps[number]>;
+export type Variable = {
+	name: string;
+	description: string;
+	replacement: (t: Tutor, l: Locale, c: PartialConfig)=>unknown;
+};
+export const variables: Record<TemplateType, Variable[]> = {
+	tutor: [
+		{
+			name: "vorname",
+			description: "Vorname des Empfängers",
+			replacement: t=>t.firstname,
+		},
+		{
+			name: "nachname",
+			description: "Nachname des Empfängers",
+			replacement: t=>t.lastname,
+		},
+		{
+			name: "spitzname",
+			description: "Spitzname des Empfängers",
+			replacement: t=>t.nickname ?? "",
+		},
+		{
+			name: "email",
+			description: "E-Mail-Adresse des Empfängers",
+			replacement: t=>t.email,
+		},
+		{
+			name: "telefon",
+			description: "Telefonnummer des Empfängers",
+			replacement: t=>t.phone,
+		},
+		{
+			name: "studiengang",
+			description: "Studiengang des Empfängers",
+			replacement: (t, l)=>t.studyProgram.name[l],
+		},
+		{
+			name: "abschluss",
+			description: "Abschluss des Empfängers",
+			replacement: (t, l)=>Degree[t.degree][l],
+		},
+		{
+			name: "schulung.datum",
+			description: "Datum der Schulung des Empfängers",
+			replacement: (t) => new Date(t.training?.date ?? 0),
+		},
+		{
+			name: "schulung.ort",
+			description: "Ort der Schulung des Empfängers",
+			replacement: (t, l)=>t?.training?.location ?? {de: "keine Schulung", en: "no training"}[l],
+		},
+		{
+			name: "cotutorwunsch",
+			description: "Name des gewünschten Co-Tutors",
+			replacement: t=>t.coTutorWish,
+		},
+		{
+			name: "semester",
+			description: "Semester der Erstiwoche",
+			replacement: (t, l, c)=>c.currentSemester,
+		},
+		{
+			name: "erstiwoche.start",
+			description: "Startdatum der Erstiwoche",
+			replacement: (t, l, c)=>new Date(c.fresherWeekStart),
+		},
+		{
+			name: "erstiwoche.ende",
+			description: "Enddatum der Erstiwoche",
+			replacement: (t, l, c)=>new Date(c.fresherWeekEnd),
+		},
+		{
+			name: "erstiwoche.zeitraum",
+			description: "Zeitraum der Erstiwoche",
+			replacement: (t, l, c)=>[new Date(c.fresherWeekStart), new Date(c.fresherWeekEnd)],
+		},
+		{
+			name: "schulung.zeit",
+			description: "Zeitraum der Schulung",
+			replacement: (t, l, c)=>{
+				const [startHour, startMinute] = c.trainingsStart.split(":").map(v=>Number(v));
+				const [endHour, endMinute] = c.trainingsEnd.split(":").map(v=>Number(v));
+				const start = new Date(t.training?.date ?? 0);
+				start.setHours(startHour, startMinute);
+				const end = new Date(start);
+				end.setHours(endHour, endMinute);
+				return [start, end];
+			},
+		},
+		{
+			name: "schulung.start",
+			description: "Startzeit der Schulungen",
+			replacement: (t, l, c)=>{
+				const [startHour, startMinute] = c.trainingsStart.split(":").map(v=>Number(v));
+				const date = new Date(t.training?.date ?? 0);
+				date.setHours(startHour, startMinute);
+				return date;
+			},
+		},
+		{
+			name: "schulung.ende",
+			description: "Endzeit der Schulungen",
+			replacement: (t, l, c)=>{
+				const [endHour, endMinute] = c.trainingsEnd.split(":").map(v=>Number(v));
+				const date = new Date(t.training?.date ?? 0);
+				date.setHours(endHour, endMinute);
+				return date;
+			},
+		},
+		{
+			name: "rallye.datum",
+			description: "Datum der Rallye",
+			replacement: (t, l, c)=>new Date(c.rallyDate),
+		},
+	],
+};
 
 export type Condition = {
 	name: string;
@@ -83,91 +197,96 @@ export type Condition = {
 	arguments?: string;
 	condition: (t: Tutor, args: string[])=>boolean;
 };
-export const conditions: Condition[] = [
-	{
-		name: "hatCotutorWunsch",
-		description: "Der Tutor hat einen Co-Tutor-Wunsch",
-		condition: t=>!!t.coTutorWish,
-	},
-	{
-		name: "geschult",
-		description: "Der Tutor wurde geschult",
-		condition: t=>t.trained,
-	},
-	{
-		name: "hatSchulung",
-		description: "Der Tutor hat eine Schulung",
-		condition: t=>!!t.training,
-	},
-	{
-		name: "schulung",
-		description: "Der Tutor hat eine bestimmte Schulung",
-		arguments: "Datum der Schulungen (YYYY-MM-DD)",
-		condition: (t, args)=>!!t.training && args.includes(t.training.date),
-	},
-	{
-		name: "abschluss",
-		description: `Der Tutor strebt einen bestimmten Abschluss an (${Object.keys(Degree).join("/")})`,
-		condition: (t, args)=>args.includes(t.degree),
-	},
-	{
-		name: "hatSpitzname",
-		description: "Der Tutor hat einen Spitznamen angegeben",
-		condition: t=>!!t.nickname,
-	},
-	{
-		name: "mentor",
-		description: "Der Tutor ist Mentor",
-		condition: t=>t.mentor,
-	},
-	{
-		name: "studiengang",
-		description: "Der Tutor ist in einem Studiengang",
-		arguments: `Liste aller zu prüfenden Studiengänge (deutscher Name)`,
-		condition: (t, args)=>args.includes(t.studyProgram.name.de),
-	},
-	{
-		name: "essenseinschränkung",
-		description: "Der Tutor hat eine Essenseinschränkung",
-		condition: t=>!!t.dietaryRestriction,
-	},
-	{
-		name: "geschlecht",
-		description: "Der Tutor hat ein bestimmtes Geschlecht",
-		arguments: `Geschlecht(er) (${Object.keys(Gender).join("/")})`,
-		condition: (t, args)=>args.includes(t.gender),
-	},
-	{
-		name: "not",
-		description: "Negiert eine Bedingung",
-		arguments: "Bedingung, Argumente falls notwendig",
-		condition: (t, [name, ...args])=>{
-			const condition = conditions.find(c=>c.name === name);
-			if(!condition) throw new Error(`Unknown condition ${name}`);
-			return !condition.condition(t, args);
+export const conditions: Record<TemplateType, Condition[]> = {
+	tutor: [
+		{
+			name: "hatCotutorWunsch",
+			description: "Der Tutor hat einen Co-Tutor-Wunsch",
+			condition: t=>!!t.coTutorWish,
 		},
-	},
-];
+		{
+			name: "geschult",
+			description: "Der Tutor wurde geschult",
+			condition: t=>t.trained,
+		},
+		{
+			name: "hatSchulung",
+			description: "Der Tutor hat eine Schulung",
+			condition: t=>!!t.training,
+		},
+		{
+			name: "schulung",
+			description: "Der Tutor hat eine bestimmte Schulung",
+			arguments: "Datum der Schulungen (YYYY-MM-DD)",
+			condition: (t, args)=>!!t.training && args.includes(t.training.date),
+		},
+		{
+			name: "abschluss",
+			description: `Der Tutor strebt einen bestimmten Abschluss an (${Object.keys(Degree).join("/")})`,
+			condition: (t, args)=>args.includes(t.degree),
+		},
+		{
+			name: "hatSpitzname",
+			description: "Der Tutor hat einen Spitznamen angegeben",
+			condition: t=>!!t.nickname,
+		},
+		{
+			name: "mentor",
+			description: "Der Tutor ist Mentor",
+			condition: t=>t.mentor,
+		},
+		{
+			name: "studiengang",
+			description: "Der Tutor ist in einem Studiengang",
+			arguments: `Liste aller zu prüfenden Studiengänge (deutscher Name)`,
+			condition: (t, args)=>args.includes(t.studyProgram.name.de),
+		},
+		{
+			name: "essenseinschränkung",
+			description: "Der Tutor hat eine Essenseinschränkung",
+			condition: t=>!!t.dietaryRestriction,
+		},
+		{
+			name: "geschlecht",
+			description: "Der Tutor hat ein bestimmtes Geschlecht",
+			arguments: `Geschlecht(er) (${Object.keys(Gender).join("/")})`,
+			condition: (t, args)=>args.includes(t.gender),
+		},
+		{
+			name: "not",
+			description: "Negiert eine Bedingung",
+			arguments: "Bedingung, Argumente falls notwendig",
+			condition: (t, [name, ...args])=>{
+				const condition = conditions.tutor.find(c=>c.name === name);
+				if(!condition) throw new Error(`Unknown condition ${name}`);
+				return !condition.condition(t, args);
+			},
+		},
+	],
+};
+
+export const bodyFormatter = ({ de, en }: Localized) => `-------- english version below --------\n\n${de}\n\n-------- english version --------\n\n${en}`;
+export const subjectFormatter = ({ de, en }: Localized) => [de, en].filter(s=>s).join(" / ");
 
-export function compileTemplate(parts: Localized<Part[]>, tutor: Tutor, formatter: (l: Localized)=>string = ({de, en})=>`-------- english version below --------\n\n${de}\n\n-------- english version --------\n\n${en}`): string {
+export function compileTemplate(type: TemplateType, parts: Localized<Part[]>, tutor: Tutor, config: PartialConfig, formatter: (l: Localized)=>string = bodyFormatter): string {
 	const result = {} as Localized<string>;
 	for(const locale of locales){
-		result[locale] = parts[locale].map(p=>compilePart(p, tutor, locale)).join("");
+		result[locale] = parts[locale].map(p=>compilePart(type, p, tutor, locale, config)).join("");
 	}
 	return formatter(result);
 }
 
-function compilePart(part: Part, tutor: Tutor, locale: Locale): string {
+function compilePart(type: TemplateType, part: Part, tutor: Tutor, locale: Locale, config: PartialConfig): string {
 	switch(part.type){
 		case "text":
 			return part.text;
 		case "variable":
-			return part.replacement(tutor, locale);
+			return String(part.replacement(tutor, locale, config));
 		case "condition":
 			if(part.condition(tutor, part.args)){
-				return part.then.map(p=>compilePart(p, tutor, locale)).join("");
+				return part.then.map(p=>compilePart(type, p, tutor, locale, config)).join("");
 			}else{
-				return part.else.map(p=>compilePart(p, tutor, locale)).join("");
+				return part.else.map(p=>compilePart(type, p, tutor, locale, config)).join("");
 			}
 	}
 }
@@ -213,13 +332,13 @@ function tokenize(input: string): Token[] {
 }
 
 type Part = {type: "text", text: string} | {type: "variable", replacement: Variable["replacement"]} | {type: "condition", condition: Condition["condition"], args: string[], then: Part[], else: Part[]};
-export function parseTemplate(template: Localized): Localized<Part[]>;
-export function parseTemplate(template: string): Part[];
-export function parseTemplate(template: Localized|string){
+export function parseTemplate(type: TemplateType, template: Localized): Localized<Part[]>;
+export function parseTemplate(type: TemplateType, template: string): Part[];
+export function parseTemplate(type: TemplateType, template: Localized|string){
 	if(typeof template === "object"){
 		const result = {} as Localized<Part[]>;
 		for(const locale of locales){
-			result[locale] = parseTemplate(template[locale]);
+			result[locale] = parseTemplate(type, template[locale]);
 		}
 		return result;
 	}
@@ -233,21 +352,27 @@ export function parseTemplate(template: Localized|string){
 			const [name, ...args] = token.value.split(" ");
 			if(name === "if" && args.length > 0){
 				const conditionName = args.shift();
-				const condition = conditions.find(c=>c.name === conditionName);
+				const condition = conditions[type].find(c=>c.name === conditionName);
 				if(!condition) throw new Error(`Unknown condition ${conditionName}`);
-				const [then, els] = consumeCondition(tokens);
+				const [then, els] = consumeCondition(type, tokens);
 				parts.push({type: "condition", condition: condition.condition, args, then, else: els});
 			}else{
-				const variable = variables.find(v=>v.name === name);
+				const [variableName, ...formatterNames] = name.split("|");
+				const variable = variables[type].find(v=>v.name === variableName);
 				if(!variable) throw new Error(`Unknown variable ${name}`);
-				parts.push({type: "variable", replacement: variable.replacement});
+				const fs = formatterNames.map(f=>formatters[f]);
+				if(fs.some(f=>!f)) throw new Error(`Unknown formatter ${formatterNames.find((_, i)=>!fs[i])}`);
+				const replacement = (t: Tutor, l: Locale, c: PartialConfig)=>{
+					return fs.reduce((v, f)=>f.format(l, v), variable.replacement(t, l, c));
+				};
+				parts.push({type: "variable", replacement});
 			}
 		}
 	}
 	return parts;
 }
 
-function consumeCondition(tokens: Token[]): [Part[], Part[]] {
+function consumeCondition(type: TemplateType, tokens: Token[]): [Part[], Part[]] {
 	const thenParts: Part[] = [];
 	const elseParts: Part[] = [];
 	let inElse = false;
@@ -260,9 +385,9 @@ function consumeCondition(tokens: Token[]): [Part[], Part[]] {
 			const [name, ...args] = token.value.split(" ");
 			if(name === "if" && args.length > 0){
 				const conditionName = args.shift();
-				const condition = conditions.find(c=>c.name === conditionName);
+				const condition = conditions[type].find(c=>c.name === conditionName);
 				if(!condition) throw new Error(`Unknown condition ${conditionName}`);
-				const [then, els] = consumeCondition(tokens);
+				const [then, els] = consumeCondition(type, tokens);
 				const part: Part = {type: "condition", condition: condition.condition, args, then, else: els};
 				if(inElse) elseParts.push(part);
 				else thenParts.push(part);
@@ -271,9 +396,9 @@ function consumeCondition(tokens: Token[]): [Part[], Part[]] {
 				if(args.length > 0) {
 					if(args[0] !== "if") throw new Error(`Unexpected argument "${args[0]}" to else`);
 					const [, name, ...args2] = args;
-					const condition = conditions.find(c=>c.name === name);
+					const condition = conditions[type].find(c=>c.name === name);
 					if(!condition) throw new Error(`Unknown condition ${name}`);
-					const [then, els] = consumeCondition(tokens);
+					const [then, els] = consumeCondition(type, tokens);
 					const part: Part = {type: "condition", condition: condition.condition, args: args2, then, else: els};
 					elseParts.push(part);
 					return [thenParts, elseParts];
@@ -282,10 +407,16 @@ function consumeCondition(tokens: Token[]): [Part[], Part[]] {
 			}else if(name === "/if"){
 				return [thenParts, elseParts];
 			}else{
-				const variable = variables.find(v=>v.name === name);
+				const [variableName, ...formatterNames] = name.split("|");
+				const variable = variables[type].find(v=>v.name === variableName);
 				if(!variable) throw new Error(`Unknown variable ${name}`);
-				if(inElse) elseParts.push({type: "variable", replacement: variable.replacement});
-				else thenParts.push({type: "variable", replacement: variable.replacement});
+				const fs = formatterNames.map(f=>formatters[f]);
+				if(fs.some(f=>!f)) throw new Error(`Unknown formatter ${formatterNames.find((_, i)=>!fs[i])}`);
+				const replacement = (t: Tutor, l: Locale, c: PartialConfig)=>{
+					return fs.reduce((v, f)=>f.format(l, v), variable.replacement(t, l, c));
+				};
+				if(inElse) elseParts.push({type: "variable", replacement});
+				else thenParts.push({type: "variable", replacement});
 			}
 		}
 	}
diff --git a/src/lib/server/database/entities/TutorTraining.entity.ts b/src/lib/server/database/entities/TutorTraining.entity.ts
index 45f05d805b0330a92c162772f392d976c731f42e..5a5c9f831ac67422ed44077e6fc341819475501c 100644
--- a/src/lib/server/database/entities/TutorTraining.entity.ts
+++ b/src/lib/server/database/entities/TutorTraining.entity.ts
@@ -14,7 +14,7 @@ export class TutorTraining {
 		if(!training) return undefined;
 		const t: TutorTraining = Object.assign(new TutorTraining(), training);
 		t.participants = ((training.participants || []) as any[])
-			.filter(tutor=>tutor)
+			.filter(tutor=>tutor?.tutor)
 			.map(tutor=>Object.assign(Tutor.parse({...tutor.tutor, studyProgram: tutor.studyProgram})!, {training: t}));
 		return t;
 	}
@@ -23,7 +23,7 @@ export class TutorTraining {
 		return TutorTraining.parse((await sql`SELECT tutor_trainings.*, array_agg(json_build_object('tutor', row_to_json(tutors.*), 'studyProgram', row_to_json(study_programs.*))) AS participants FROM tutor_trainings LEFT JOIN tutors ON tutor_trainings.id=tutors.training JOIN study_programs ON tutors.study_program=study_programs.id WHERE tutor_trainings.id=${id} GROUP BY tutor_trainings.id`)[0]);
 	}
 	static async getAll(): Promise<TutorTraining[]> {
-		return (await sql`SELECT tutor_trainings.*, array_agg(json_build_object('tutor', row_to_json(tutors.*), 'studyProgram', row_to_json(study_programs.*))) AS participants FROM tutor_trainings LEFT JOIN tutors ON tutor_trainings.id=tutors.training JOIN study_programs ON tutors.study_program=study_programs.id GROUP BY tutor_trainings.id`).map(row=>TutorTraining.parse(row)!);
+		return (await sql`SELECT tutor_trainings.*, array_agg(json_build_object('tutor', row_to_json(tutors.*), 'studyProgram', row_to_json(study_programs.*))) AS participants FROM tutor_trainings LEFT JOIN tutors ON tutor_trainings.id=tutors.training LEFT JOIN study_programs ON tutors.study_program=study_programs.id GROUP BY tutor_trainings.id`).map(row=>TutorTraining.parse(row)!);
 	}
 	static async create(training: Omit<TutorTraining, "id"|"participants">): Promise<number> {
 		return (await sql`INSERT INTO tutor_trainings ${sql(training)} 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 75fbd5f64ad86cb79dc7b9813e0b76b4188b0863..4fa987a18824ab7f2f9960f77d85377bbb9c15a2 100644
--- a/src/lib/server/database/migrations/0_init.sql
+++ b/src/lib/server/database/migrations/0_init.sql
@@ -46,7 +46,7 @@ CREATE TABLE IF NOT EXISTS tutorials (
 
 CREATE TABLE IF NOT EXISTS tutor_trainings (
 	"id" SERIAL PRIMARY KEY,
-	"date" DATE NOT NULL,
+	"date" TEXT NOT NULL,
 	"location" TEXT NOT NULL,
 	"max_participants" INT NOT NULL DEFAULT 0,
 	"internal" BOOLEAN NOT NULL DEFAULT FALSE,
@@ -165,3 +165,12 @@ CREATE TABLE IF NOT EXISTS schedule_entries (
 	"end_uncertainty" INT NOT NULL DEFAULT 0,
 	"date" DATE NOT NULL
 );
+
+CREATE TABLE  IF NOT EXISTS master_freshers (
+	"id" SERIAL PRIMARY KEY,
+	"firstname" TEXT NOT NULL,
+	"lastname" TEXT NOT NULL,
+	"email" TEXT NOT NULL,
+	"study_program" TEXT NOT NULL,
+	"aachen_experience" TEXT NOT NULL
+);
diff --git a/src/lib/server/mail.ts b/src/lib/server/mail.ts
index d0cab1df748b2cf5274883f63e4c4e936aa588eb..7ed38385690bec9bbf50df5c01995822f6b2155b 100644
--- a/src/lib/server/mail.ts
+++ b/src/lib/server/mail.ts
@@ -2,7 +2,7 @@ import { env } from "$env/dynamic/private";
 import nodemailer from "nodemailer";
 import { Tutor } from "./database/entities/Tutor.entity";
 import type Mail from "nodemailer/lib/mailer";
-import { compileTemplate, parseTemplate, renderTemplate, type MailTemplate } from "$lib/mail";
+import { compileTemplate, parseTemplate, renderTemplate, subjectFormatter, type MailTemplate } from "$lib/mail";
 import { Config } from "./database/entities/Config.entity";
 import ical from "ical-generator";
 import type SMTPTransport from "nodemailer/lib/smtp-transport";
@@ -120,12 +120,13 @@ export function sendMail({ template, tutors, attachments = [], from, icalEvent }
 	icalEvent?: Mail.IcalAttachment,
 }): Promise<SendMailResult> {
 	const replyTo = template.replyTo === "-" ? undefined : template.replyTo;
-	const textParts = parseTemplate(template.subject);
-	const subjectParts = parseTemplate(template.subject);
+	const textParts = parseTemplate(template.type, template.subject);
+	const subjectParts = parseTemplate(template.type, template.subject);
+	const config = Config.get();
 	return Promise.all(tutors.map(tutor => {
-		const compiled = compileTemplate(textParts, tutor);
+		const compiled = compileTemplate(template.type, textParts, tutor, config);
 		const rendered = renderTemplate(compiled);
-		const subject = compileTemplate(subjectParts, tutor, ({de, en})=>`${de} / ${en}`);
+		const subject = compileTemplate(template.type, subjectParts, tutor, config, subjectFormatter);
 		return transporter.sendMail({
 			to: tutor.email,
 			from: from || env.MAIL_FROM,
diff --git a/src/routes/(non-admin)/datenschutz/+page.svelte b/src/routes/(non-admin)/datenschutz/+page.svelte
index b6a1bb6348baff0ead153a704189aa5c5eea8b1f..5e74045377a513c164de91b073cd8f9b52221699 100644
--- a/src/routes/(non-admin)/datenschutz/+page.svelte
+++ b/src/routes/(non-admin)/datenschutz/+page.svelte
@@ -2,7 +2,7 @@
 	import { LL, type Translation, type TranslationFunction } from "$lib/i18n/i18n";
 	import { Heading, P } from "flowbite-svelte";
 	
-	$: paragraphs = Object.keys($LL.PrivacyPolicy.Paragraphs) as (keyof Translation["PrivacyPolicy"]["Paragraphs"])[];
+	let paragraphs = $derived(Object.keys($LL.PrivacyPolicy.Paragraphs) as (keyof Translation["PrivacyPolicy"]["Paragraphs"])[]);
 	
 	type Paragraph = { Headline: TranslationFunction; Content: TranslationFunction; };
 </script>
diff --git a/src/routes/(non-admin)/eswe/+page.svelte b/src/routes/(non-admin)/eswe/+page.svelte
index 645c160dbc2b2da2b7ee0e1110a9c014caf3e866..39d70938b7940289e8e9d72fd121ae81b3711544 100644
--- a/src/routes/(non-admin)/eswe/+page.svelte
+++ b/src/routes/(non-admin)/eswe/+page.svelte
@@ -3,11 +3,11 @@
 	import { LL, locale } from "$lib/i18n/i18n";
 	import Image from "$lib/components/Image.svelte";
 	
-	export let data;
+	let { data } = $props();
 	
 	const images = data.images;
-	let showModal = false;
-	let index = 0;
+	let showModal = $state(false);
+	let index = $state(0);
 	function openModal(idx: number){
 		index = idx;
 		showModal = true;
@@ -89,7 +89,7 @@
 
 <!-- images array is just a dummy, item is not an image property object but the index within the gallery -->
 <Gallery items={Array.from({length: images.length}, (_, i)=>i)} class="gap-4 grid-cols-2 md:grid-cols-3" let:item={index}>
-	<button on:click={()=>openModal(index)}>
+	<button onclick={()=>openModal(index)}>
 		<Image src={images[index]} class="h-auto max-w-full rounded-lg" loading="lazy" sizes="(min-width: 768px) 33vw, min(50vw, 363px)" autosize/>
 	</button>
 </Gallery>
diff --git a/src/routes/(non-admin)/flyer/+page.svelte b/src/routes/(non-admin)/flyer/+page.svelte
index 3701fe7d2c169d583022374db476db24f1a976a2..41380b547051d4996dda9c3ad554ed2aa1414fd1 100644
--- a/src/routes/(non-admin)/flyer/+page.svelte
+++ b/src/routes/(non-admin)/flyer/+page.svelte
@@ -5,10 +5,10 @@
 	import LL from '$lib/i18n/i18n';
 	import Image from '$lib/components/Image.svelte';
 	
-	export let data;
+	let { data } = $props();
 	
 	const slideTransition = (x: Element) => fade(x, { duration: 700, easing: quintOut });
-	let index = 0;
+	let index = $state(0);
 </script>
 
 <div class="max-w-3xl mx-auto space-y-4">
diff --git a/src/routes/(non-admin)/impressum/+page.svelte b/src/routes/(non-admin)/impressum/+page.svelte
index 2474eb4e702a997e6ffc29e8eedabc69e2289f0d..9ec9bde09fff1eb95258e05e35bc5364e4cf3a84 100644
--- a/src/routes/(non-admin)/impressum/+page.svelte
+++ b/src/routes/(non-admin)/impressum/+page.svelte
@@ -1,8 +1,6 @@
 <script lang="ts">
-    import { LL } from "$lib/i18n/i18n";
-    import { A, Blockquote, Heading, P } from "flowbite-svelte";
-
-	
+	import { LL } from "$lib/i18n/i18n";
+	import { A, Blockquote, Heading, P } from "flowbite-svelte";
 </script>
 
 <Heading tag="h1" customSize="text-4xl font-bold" class="mb-4 text-center">{$LL.Legal.Headline()}</Heading>
diff --git a/src/routes/(non-admin)/information/+page.svelte b/src/routes/(non-admin)/information/+page.svelte
index 30b1e847bfe308486c6be8d594bda6fe0536dd05..1d28791d3361e2bda8d957bf5db22741c567f9f1 100644
--- a/src/routes/(non-admin)/information/+page.svelte
+++ b/src/routes/(non-admin)/information/+page.svelte
@@ -2,9 +2,10 @@
 	import { A, Button, Heading, Label, Li, List, P, Select } from 'flowbite-svelte';
 	import { LL, locale } from "$lib/i18n/i18n";
 	
-	export let data;
-	let selectedStudyProgram = Object.keys(data.schedules)[0];
-	$: studyProgramName = data.schedules[selectedStudyProgram]?.studyProgramName[$locale];
+	let { data } = $props();
+	
+	let selectedStudyProgram = $state(Object.keys(data.schedules)[0]);
+	let studyProgramName = $derived(data.schedules[selectedStudyProgram]?.studyProgramName[$locale]);
 </script>
 
 <Heading tag="h2" customSize="text-3xl font-bold" class="mb-6">{$LL.Information.Schedule()}</Heading>
diff --git a/src/routes/(non-admin)/rabatte/+page.svelte b/src/routes/(non-admin)/rabatte/+page.svelte
index e6f61bb4be689d4070e26bd1cd7a220ca98ad190..0d56365ef9a1e8829a04ed8027c2d7cc8a75f0fb 100644
--- a/src/routes/(non-admin)/rabatte/+page.svelte
+++ b/src/routes/(non-admin)/rabatte/+page.svelte
@@ -9,7 +9,7 @@
 	import { makeSearchable, matches } from "./assets/search";
 	import { locale } from "$lib/i18n/i18n";
 	
-	export let data;
+	let { data } = $props();
 	
 	function getValidityStatus(startDate: string, endDate: string, currentDate: number): number{
 		if(startDate && endDate){
@@ -55,10 +55,10 @@
 			});
 	const allTags = data.allTags;
 	// TODO rerun filter again on locale change
-	$: searchTexts = Object.fromEntries(locations.map(location=>[location.id, makeSearchable(location.title, location.description[$locale], location.address)]));
-	let onlyOpen = false;
-	let search = "";
-	let tags: number[] = [];
+	let searchTexts = $derived(Object.fromEntries(locations.map(location=>[location.id, makeSearchable(location.title, location.description[$locale], location.address)])));
+	let onlyOpen = $state(false);
+	let search = $state("");
+	let tags: number[] = $state([]);
 	const filter = (search: string, tags: number[], onlyOpen: boolean)=>(location: typeof locations[number])=>{
 		if(onlyOpen && !location.isOpen && !location.openingSoon) return false;
 		if(search && !matches(searchTexts[location.id], search)) return false;
@@ -72,9 +72,11 @@
 	let completelyHidden = false;
 	let minOffsetY = 0;
 	
-	let showOnMap: (id: number)=>void;
+	let leafletMap: LeafletMap|undefined = $state();
+	let locationList: LocationList|undefined = $state();
+	let showOnMap: (id: number)=>void = $derived(leafletMap?.showOnMap||(()=>{}));
 	let isScrolling = false;
-	let locationElements: {[k: number]: HTMLDivElement};
+	let locationElements: {[k: number]: HTMLDivElement} = $derived(locationList?.locationElements||{});
 	function scrollTo(locationId: number, highlight=true){
 		const element = locationElements[locationId];
 		if(!element) return;
@@ -219,9 +221,9 @@
 </style>
 
 <div class="map-container z-40 mx-auto max-w-6xl px-4" bind:this={mapContainer}>
-	<LeafletMap {locations} {scrollTo} bind:showOnMap holidays={data.holidays} />
+	<LeafletMap bind:this={leafletMap} {locations} {scrollTo} holidays={data.holidays} />
 </div>
 <div class="list-container">
 	<LocationFilter {allTags} bind:onlyOpen bind:search bind:tags />
-	<LocationList {locations} holidays={data.holidays} filter={filter(search, tags, onlyOpen)} showOnMap={focusLocationOnMap} bind:locationElements bind:searchTags={tags} />
+	<LocationList bind:this={locationList} {locations} holidays={data.holidays} filter={filter(search, tags, onlyOpen)} showOnMap={focusLocationOnMap} bind:searchTags={tags} />
 </div>
diff --git a/src/routes/(non-admin)/rabatte/assets/LeafletMap.svelte b/src/routes/(non-admin)/rabatte/assets/LeafletMap.svelte
index e0c5b23fb5533527f6658acd023aeaea7bd10374..4f6f65cdccdcfa966c48375a5008a17c91847b45 100644
--- a/src/routes/(non-admin)/rabatte/assets/LeafletMap.svelte
+++ b/src/routes/(non-admin)/rabatte/assets/LeafletMap.svelte
@@ -8,10 +8,11 @@
 	import type { Map, Marker } from 'leaflet';
 	let leaflet: typeof import("leaflet");
 	
-	export let
+	let { locations, scrollTo, holidays }: {
 		locations: (Location&{closingSoon: boolean, openingSoon: boolean, isOpen: boolean})[],
 		scrollTo: (id: number)=>void,
 		holidays: string[];
+	} = $props();
 	
 	let map: Map;
 	let mapElement: HTMLDivElement;
diff --git a/src/routes/(non-admin)/rabatte/assets/Location.svelte b/src/routes/(non-admin)/rabatte/assets/Location.svelte
index 414cb99bb4533f91e0c38d4d231be3b44b011c62..f82ca954bff650917ce2323c5d2ca4354c2b837f 100644
--- a/src/routes/(non-admin)/rabatte/assets/Location.svelte
+++ b/src/routes/(non-admin)/rabatte/assets/Location.svelte
@@ -7,20 +7,23 @@
 	import type { Discount as LocationType, Tag } from "$lib/server/database/entities/Discount.entity";
 	import type { MouseEventHandler } from "svelte/elements";
 	
-	export let
+	let { location, showOnMap, searchTags = $bindable(), locationElement = $bindable(), holidays }: {
 		location: LocationType&{status:number,closingSoon:boolean,isOpen:boolean,openingSoon:boolean},
 		showOnMap: MouseEventHandler<HTMLAnchorElement>,
-		searchTags: Tag[],
+		searchTags: number[],
 		locationElement: HTMLElement,
-		holidays: string[];
+		holidays: string[]
+	} = $props();
 	
-	let element: HTMLDivElement;
-	$: locationElement = element?.parentElement as HTMLElement;
+	let element: HTMLDivElement = null as unknown as HTMLDivElement;
+	$effect(()=>{
+		locationElement = element?.parentElement as HTMLElement;
+	});
 	
-	$: open = formatOpeningHours(getOpeningHoursForNextDays(holidays, location.openingHours, new Date(), 7, true), $locale);
-	let showAllOpeningHours = false;
+	let open = $derived(formatOpeningHours(getOpeningHoursForNextDays(holidays, location.openingHours, new Date(), 7, true), $locale));
+	let showAllOpeningHours = $state(false);
 	
-	$: rangeFormatter = new Intl.DateTimeFormat($locale, {day: "2-digit", month: "long"});
+	let rangeFormatter = $derived(new Intl.DateTimeFormat($locale, {day: "2-digit", month: "long"}));
 </script>
 
 <Card padding="sm" size="md">
@@ -59,7 +62,7 @@
 			{showAllOpeningHours ? $LL.Discounts.Item.ShowLess() : $LL.Discounts.Item.ShowMore()}
 		</Button>
 	</div>
-	<a href="#{location.id}" on:click={showOnMap} class="font-semibold text-gray-900 dark:text-white mb-3 mt-auto"><!--<Icon name="map-location-outline" size="md" class="inline" />--> {location.address}</a>
+	<a href="#{location.id}" onclick={showOnMap} class="font-semibold text-gray-900 dark:text-white mb-3 mt-auto"><!--<Icon name="map-location-outline" size="md" class="inline" />--> {location.address}</a>
 	<ButtonGroup class="w-max">
 		{#each location.tags as tag}
 			<!-- TODO highlight searched button -->
diff --git a/src/routes/(non-admin)/rabatte/assets/LocationFilter.svelte b/src/routes/(non-admin)/rabatte/assets/LocationFilter.svelte
index 8d57762db824e1a5c48c9fb727d3d4e9279b0bb3..a352149a5b930bf1f9b6d09089cbef20bb3aa355 100644
--- a/src/routes/(non-admin)/rabatte/assets/LocationFilter.svelte
+++ b/src/routes/(non-admin)/rabatte/assets/LocationFilter.svelte
@@ -3,7 +3,12 @@
 	import { LL, locale } from "$lib/i18n/i18n";
 	import type { Tag } from "$lib/server/database/entities/Discount.entity";
 
-	export let onlyOpen: boolean, search: string, tags: number[], allTags: Tag[];
+	let { onlyOpen = $bindable(), search = $bindable(), tags = $bindable(), allTags }: {
+		onlyOpen: boolean,
+		search: string,
+		tags: number[],
+		allTags: Tag[]
+	} = $props();
 </script>
 
 <div class="mb-4">
diff --git a/src/routes/(non-admin)/rabatte/assets/LocationList.svelte b/src/routes/(non-admin)/rabatte/assets/LocationList.svelte
index c8b73aa805a6e573a15a10cd83623bfceb8e17dc..f2eb4b2e689edca1b2efeeec2c52b58331fda921 100644
--- a/src/routes/(non-admin)/rabatte/assets/LocationList.svelte
+++ b/src/routes/(non-admin)/rabatte/assets/LocationList.svelte
@@ -2,16 +2,17 @@
 	import Location from './Location.svelte';
 	import { LL } from "$lib/i18n/i18n";
 	import { P } from 'flowbite-svelte';
-	import type { Discount, Tag } from '$lib/server/database/entities/Discount.entity';
+	import type { Discount } from '$lib/server/database/entities/Discount.entity';
 	
-	export let
+	let { locations, filter, showOnMap, searchTags = $bindable(), holidays }: {
 		locations: (Discount&{isOpen:boolean,openingSoon:boolean,closingSoon:boolean,status:number})[],
 		filter: (location: typeof locations[number])=>boolean,
 		showOnMap: (id: number)=>void,
-		searchTags: Tag[],
-		holidays: string[];
+		searchTags: number[],
+		holidays: string[],
+	} = $props();
 	export const locationElements: {[k: number]: HTMLDivElement} = {};
-	$: filteredLocations = locations.filter(filter);
+	let filteredLocations = $derived(locations.filter(filter));
 </script>
 
 <style>
@@ -22,9 +23,9 @@
 
 <div class="grid gap-4 mb-5">
 	{#each filteredLocations as location (location.id)}
-		<Location {location} {holidays} bind:searchTags showOnMap={()=>showOnMap(location.id)} bind:locationElement={locationElements[location.id]} />
+	<Location {location} {holidays} bind:searchTags showOnMap={()=>showOnMap(location.id)} bind:locationElement={locationElements[location.id]} />
 	{/each}
 	{#if filteredLocations.length === 0}
-		<P>{$LL.Discounts.Search.NoResults()}</P>
+	<P>{$LL.Discounts.Search.NoResults()}</P>
 	{/if}
 </div>
diff --git a/src/routes/(non-admin)/tutor/+page.svelte b/src/routes/(non-admin)/tutor/+page.svelte
index 91661472983ae97e384a6dc2f2d60c106b4c90e0..4f0f45c667de230f4c1a7a1360a8c9b5d55276e7 100644
--- a/src/routes/(non-admin)/tutor/+page.svelte
+++ b/src/routes/(non-admin)/tutor/+page.svelte
@@ -4,9 +4,9 @@
 	import { LL, locale } from "$lib/i18n/i18n";
 	import LocalizedText from "$lib/components/LocalizedText.svelte";
 	
-	export let data;
+	let { data } = $props();
 	
-	$: dateFormatter = new Intl.DateTimeFormat($locale, {day: "2-digit", month: "long", year: "numeric", weekday: "long"});
+	let dateFormatter = $derived(new Intl.DateTimeFormat($locale, {day: "2-digit", month: "long", year: "numeric", weekday: "long"}));
 </script>
 
 <Heading tag="h1" customSize="text-4xl font-bold" class="mb-6 text-center">{$LL.Tutor.Headline()}</Heading>
@@ -44,12 +44,12 @@
 
 <div class="flex mb-6 justify-center">
 	{#if data.registrationOpen}
-		<Button href="/tutor/register" size="xl">
-			<svg aria-hidden="true" class="mr-2 -ml-1 w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
-			{$LL.Tutor.RegistrationButton()}
-		</Button>
+	<Button href="/tutor/register" size="xl">
+		<svg aria-hidden="true" class="mr-2 -ml-1 w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
+		{$LL.Tutor.RegistrationButton()}
+	</Button>
 	{:else}
-		<Button disabled size="xl">{$LL.Tutor.RegistrationClosed()}</Button>
+	<Button disabled size="xl">{$LL.Tutor.RegistrationClosed()}</Button>
 	{/if}
 </div>
 
diff --git a/src/routes/(non-admin)/tutor/register/+page.server.ts b/src/routes/(non-admin)/tutor/register/+page.server.ts
index 1e2dd3ab1c0425cc0a27129fdde1bccb630d0fb6..5a2baf85ee875055f30657de0778b8faa3d1f0c1 100644
--- a/src/routes/(non-admin)/tutor/register/+page.server.ts
+++ b/src/routes/(non-admin)/tutor/register/+page.server.ts
@@ -47,7 +47,7 @@ export const actions = {
 		if(!Number.isInteger(studyProgramId) || studyProgramId < 0){
 			throw error(422, "Ungültiger Studiengang");
 		}
-		if(typeof email !== "string" || !(email.endsWith("@rwth-aachen.de") || email.endsWith(".rwth-aachen.de"))){
+		if(typeof email !== "string" || !email.includes("@") || !(email.endsWith("@rwth-aachen.de") || email.endsWith(".rwth-aachen.de"))){
 			throw error(422, "Ungültige E-Mail Adresse");
 		}
 		if(typeof birthdayString !== "string" || !birthdayString.match(/^\d{4}-\d{2}-\d{2}$/)){
diff --git a/src/routes/(non-admin)/tutor/register/+page.svelte b/src/routes/(non-admin)/tutor/register/+page.svelte
index aa45f9c2f02f581b1f9afc00ed2a30e7dd87eb25..75cc83d7184f5ce2eb81e9aa35b52e36b0bae440 100644
--- a/src/routes/(non-admin)/tutor/register/+page.svelte
+++ b/src/routes/(non-admin)/tutor/register/+page.svelte
@@ -7,17 +7,18 @@
 	import LocalizedText from "$lib/components/LocalizedText.svelte";
 	import { addMessage } from "$lib/messages.js";
 	
-	export let data;
+	let { data } = $props();
 	
-	let birthdayError = false;
-	let ageError = false;
-	let emailError = -1;
+	let birthdayError = $state(false);
+	let ageError = $state(false);
+	let emailError = $state(-1);
 	
-	let success = false;
-	let error: {message: string}|undefined;
-	let waitlist: boolean;
+	let success = $state(false);
+	let error: {message: string}|undefined = $state();
+	let waitlist: boolean = $state(false);
 	
-	let studyProgram: number;
+	let studyProgramId: number|null = $state(null);
+	let studyProgram = $derived(data.studyPrograms.find(sp=>sp.id===studyProgramId));
 	
 	function validateEmail(email: any) {
 		if(typeof email !== "string" || !(email.endsWith("@rwth-aachen.de") || email.endsWith(".rwth-aachen.de"))){
@@ -46,7 +47,7 @@
 		return now + 1000 * 60 * 60 * 24 < date; // can't sign up the day before and later
 	}
 	
-	$: dateFormatter = new Intl.DateTimeFormat($locale, {day: "2-digit", month: "long", year: "numeric", weekday: "short"});
+	let dateFormatter = $derived(new Intl.DateTimeFormat($locale, {day: "2-digit", month: "long", year: "numeric", weekday: "short"}));
 </script>
 
 <Heading tag="h1" customSize="text-4xl font-bold" class="mb-6 text-center">{$LL.Tutor.SignUp.Headline()}</Heading>
@@ -136,9 +137,9 @@
 		</Label>
 		<Label class="mb-2">
 			{$LL.Tutor.SignUp.StudyProgram()}
-			<Select name="study-program" required placeholder={$LL.Tutor.SignUp.PleaseChoose()} items={data.studyPrograms.map(x=>({name: x.name[$locale], value: x.id}))} bind:value={studyProgram} />
-			{#if data.studyPrograms.find(sp=>sp.id===studyProgram)?.hasWaitlist}
-			<Helper color="red">{$LL.Tutor.SignUp.StudyProgramWaitlist({studyProgram: data.studyPrograms.find(sp=>sp.id===studyProgram)?.name[$locale]??""})}</Helper>
+			<Select name="study-program" required placeholder={$LL.Tutor.SignUp.PleaseChoose()} items={data.studyPrograms.map(x=>({name: x.name[$locale], value: x.id}))} bind:value={studyProgramId} />
+			{#if studyProgram?.hasWaitlist}
+			<Helper color="red">{$LL.Tutor.SignUp.StudyProgramWaitlist({studyProgram: studyProgram!.name[$locale]??""})}</Helper>
 			{/if}
 		</Label>
 		<Label class="mb-2">
diff --git a/src/routes/(non-admin)/upload/+page.svelte b/src/routes/(non-admin)/upload/+page.svelte
index fb04694d11370179fd27c2e8211ca8ad34cd8158..e9a0eb1cbdf06d5a82a8ee44daaf379421daf91e 100644
--- a/src/routes/(non-admin)/upload/+page.svelte
+++ b/src/routes/(non-admin)/upload/+page.svelte
@@ -3,9 +3,10 @@
 	import { addMessage } from '$lib/messages';
 	import { Heading, P, Fileupload, Button, Checkbox } from 'flowbite-svelte';
 	import { LL } from "$lib/i18n/i18n";
+	
+	let disabled = $state(false);
 </script>
 
-<!-- TODO i18n -->
 <Heading tag="h1" customSize="text-4xl font-bold" class="mb-6 text-center">{$LL.ShareMoments.Headline()}</Heading>
 
 <P class="mb-3">{$LL.ShareMoments.Description()}</P>
@@ -13,16 +14,20 @@
 <P class="mb-3">{$LL.ShareMoments.Disclaimer2()}</P>
 <P class="mb-3">{$LL.ShareMoments.LookingForwards()}</P>
 
-<form action="?/upload" method="post" enctype="multipart/form-data" use:enhance={()=>({result, update})=>{
-	if(result.type === "success"){
-		addMessage({ type: "success", text: $LL.ShareMoments.UploadSuccess() });
-		update();
-	}else{
-		addMessage({ type: "error", text: $LL.ShareMoments.UploadError() });
-		console.error(result);
+<form action="?/upload" method="post" enctype="multipart/form-data" use:enhance={()=>{
+	disabled = true;
+	return ({result, update})=>{
+		disabled = false;
+		if(result.type === "success"){
+			addMessage({ type: "success", text: $LL.ShareMoments.UploadSuccess() });
+			update();
+		}else{
+			addMessage({ type: "error", text: $LL.ShareMoments.UploadError() });
+			console.error(result);
+		}
 	}
 }}>
 	<Fileupload name="files" accept="image/*" multiple required class="mb-2" />
 	<Checkbox class="mb-2" required>{$LL.ShareMoments.Compliance()}</Checkbox>
-	<Button type="submit">{$LL.ShareMoments.Upload()}</Button>
+	<Button type="submit" {disabled}>{$LL.ShareMoments.Upload()}</Button>
 </form>
diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte
index 651ad3a7d68c394d6d743ac410e4a64c901f6118..a529e7987107820c8798c48f8400b78d21924d8d 100644
--- a/src/routes/admin/+page.svelte
+++ b/src/routes/admin/+page.svelte
@@ -49,12 +49,13 @@
 			<option value="/eswe">/eswe</option>
 			<option value="/rabatte">/rabatte</option>
 			<option value="/flyer">/flyer</option>
+			<option value="/upload">/upload</option>
 		</Select>
 	</Label>
 	<Label class="mb-2">
 		Im Header angezeigte Links
 		<!-- die hier verwendete Reihenfolge in items bestimmt die Reihenfolge im Header selbst -->
-		<MultiSelect {disabled} name="headerLinks" value={data.config.headerLinks as string[]} items={["/information","/rabatte","/eswe","/flyer","/tutor"].map(link=>({name: link, value: link}))} />
+		<MultiSelect {disabled} name="headerLinks" value={data.config.headerLinks as string[]} items={["/information","/rabatte","/eswe","/flyer","/tutor","/upload"].map(link=>({name: link, value: link}))} />
 	</Label>
 	{#if canEdit}
 		<Button type="submit" class="mb-2">Speichern</Button>
diff --git a/src/routes/admin/eswe/+page.svelte b/src/routes/admin/eswe/+page.svelte
index bdd265a5fd6f25d20091bb2e8e3efdb8b8d6ab84..1b4e98420fcc31ab68f844600aed6500b349b652 100644
--- a/src/routes/admin/eswe/+page.svelte
+++ b/src/routes/admin/eswe/+page.svelte
@@ -5,7 +5,7 @@
 	import { addMessage } from "$lib/messages.js";
 	import { Breadcrumb, BreadcrumbItem, Button, Input, Label, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte";
 	
-	export let data;
+	let { data } = $props();
 </script>
 
 <Breadcrumb class="mb-4">
diff --git a/src/routes/admin/rabatte/+page.svelte b/src/routes/admin/rabatte/+page.svelte
index 479b9b3ac8030ac2223acb3d65754bec6a6ec8e8..114194cfb64046b5b8c111568b1429050461ca4a 100644
--- a/src/routes/admin/rabatte/+page.svelte
+++ b/src/routes/admin/rabatte/+page.svelte
@@ -1,12 +1,11 @@
 <script lang="ts">
 	import { Breadcrumb, BreadcrumbItem, Button, Input, Label, P, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell, Textarea } from 'flowbite-svelte';
-	import type { PageData } from './$types';
 	import { locales } from '$lib/i18n/i18n';
 	import { enhance } from '$app/forms';
 	import { goto, invalidateAll } from '$app/navigation';
 	import { addMessage } from '$lib/messages';
 	
-	export let data: PageData;
+	let { data } = $props();
 	
 	const rangeFormatter = new Intl.DateTimeFormat('de', {
 		year: 'numeric',
@@ -18,14 +17,14 @@
 		fetch(`/admin/rabatte/${id}`, {
 			method: 'DELETE',
 		}).then(() => {
-			location.reload();
+			invalidateAll();
 		});
 	}
 	function deleteTag(id: number){
 		fetch(`/admin/rabatte/tag/${id}`, {
 			method: "DELETE",
 		}).then(()=>{
-			location.reload();
+			invalidateAll();
 		});
 	}
 </script>
@@ -100,10 +99,10 @@
 </Table>
 
 {#each data.tags as tag (tag.id)}
-<form id="tag-{tag.id}" action="?/updateTag" method="post" use:enhance={()=>({result})=>{
+<form id="tag-{tag.id}" action="?/updateTag" method="post" use:enhance={()=>({result, update})=>{
 	if(result.type === "success"){
 		addMessage({type: "success", text: "Tag gespeichert"});
-		invalidateAll();
+		update({ reset: false });
 	}else{
 		addMessage({type: "error", text: "Fehler beim Speichern"});
 		console.error(result);
@@ -112,10 +111,10 @@
 	<input type="hidden" name="id" value={tag.id} />
 </form>
 {/each}
-<form id="tag-new" action="?/createTag" method="post" use:enhance={()=>({result})=>{
+<form id="tag-new" action="?/createTag" method="post" use:enhance={()=>({result, update})=>{
 	if(result.type === "success"){
 		addMessage({type: "success", text: "Tag erstellt"});
-		invalidateAll();
+		update();
 	}else{
 		addMessage({type: "error", text: "Fehler beim Erstellen"});
 		console.error(result);
diff --git a/src/routes/admin/rabatte/[id=number]/+page.svelte b/src/routes/admin/rabatte/[id=number]/+page.svelte
index 319a46b1c74d18ad9bd580121e51ee87754cd4e4..ecf70bcbf8456e7371a0a489fdbb19e8b5908209 100644
--- a/src/routes/admin/rabatte/[id=number]/+page.svelte
+++ b/src/routes/admin/rabatte/[id=number]/+page.svelte
@@ -1,19 +1,17 @@
 <script lang="ts">
 	import { Breadcrumb, BreadcrumbItem, Button, Input, Label, MultiSelect, Textarea } from 'flowbite-svelte';
-	import type { PageData } from './$types';
 	import { locale, locales } from '$lib/i18n/i18n';
 	import { onDestroy, onMount } from 'svelte';
 	import type { Map, Marker } from 'leaflet';
 	import { enhance } from '$app/forms';
-	import { invalidateAll } from '$app/navigation';
 	import OpeningHoursInput from './OpeningHoursInput.svelte';
 	import { addMessage } from '$lib/messages';
 	
-	export let data: PageData;
+	let { data } = $props();
 	
-	let [latitude, longitude] = data.discount.location;
-	let openingHours = data.discount.openingHours;
-	let tags = data.discount.tags.map(tag=>tag.id);
+	let latitude = $state(data.discount.location[0]);
+	let longitude = $state(data.discount.location[1]);
+	let tags = $state(data.discount.tags.map(tag=>tag.id));
 	
 	let mapElement: HTMLDivElement;
 	let map: Map;
@@ -100,10 +98,10 @@
 	<BreadcrumbItem href="/admin/rallye/{data.discount.id}">{data.discount.title}</BreadcrumbItem>
 </Breadcrumb>
 
-<form method="post" action="?/update" use:enhance={()=>({result})=>{
+<form method="post" action="?/update" use:enhance={()=>({result, update})=>{
 	if(result.type === "success"){
 		addMessage({type: "success", text: "Daten gespeichert"});
-		invalidateAll();
+		update({ reset: false });
 	}else{
 		addMessage({type: "error", text: "Fehler beim Speichern"});
 		console.error(result);
@@ -140,7 +138,7 @@
 		<MultiSelect name="tags" bind:value={tags} items={data.tags.map(t=>({name: t.name[$locale], value: t.id}))} />
 	</Label>
 	<div class="mb-2">
-		<OpeningHoursInput bind:openingHours />
+		<OpeningHoursInput openingHours={data.discount.openingHours} />
 	</div>
 	<div class="mx-auto w-full h-72 mb-2">
 		<div class="h-full" bind:this={mapElement}></div>
diff --git a/src/routes/admin/rabatte/[id=number]/OpeningHoursInput.svelte b/src/routes/admin/rabatte/[id=number]/OpeningHoursInput.svelte
index 4bd81c0812a0ce2e0c45078e6935accc0e6723d0..50a89ef5b4bef8c024fab3e0a8b2c659f2e27d53 100644
--- a/src/routes/admin/rabatte/[id=number]/OpeningHoursInput.svelte
+++ b/src/routes/admin/rabatte/[id=number]/OpeningHoursInput.svelte
@@ -2,7 +2,8 @@
 	import type { Discount } from "$lib/server/database/entities/Discount.entity";
 	import { Button, CloseButton, Input, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte";
 	
-	export let openingHours: Discount["openingHours"];
+	let { openingHours }: { openingHours: Discount["openingHours"] } = $props();
+	
 	for(const day of [1,2,3,4,5,6,7] as const){
 		if(!openingHours[day]) openingHours[day] = [];
 	}
@@ -19,7 +20,7 @@
 		<TableHeadCell>Öffnungszeiten</TableHeadCell>
 	</TableHead>
 	<TableBody>
-		{#each Object.keys(openingHours).filter(day=>openingHours[day]) as day, i}
+		{#each Object.keys(openingHours).filter(day=>openingHours[day as keyof typeof openingHours]) as day, i}
 		<TableBodyRow>
 			<TableBodyCell class="px-3 py-1">
 				{#if ["1","2","3","4","5","6","7"].includes(day)}
@@ -30,12 +31,12 @@
 				<CloseButton name="entfernen" class="p-1 my-0" on:click={()=>openingHours = Object.assign(openingHours, { holiday: undefined })} />
 				<input name="openingHours[{i}]" type="hidden" value="holiday" />
 				{:else}
-				<Input name="openingHours[{i}]" type="date" bind:value={day} class="w-min inline" />
+				<Input name="openingHours[{i}]" type="date" value={day} class="w-min inline" />
 				<CloseButton name="entfernen" class="p-1 my-0" on:click={()=>openingHours = Object.assign(openingHours, { [day]: undefined })} />
 				{/if}
 			</TableBodyCell>
 			<TableBodyCell class="px-3 py-1">
-				{#each openingHours[day]||[] as [start, end], j}
+				{#each openingHours[day as keyof typeof openingHours]||[] as [start, end], j}
 				<div class="w-min mx-1 inline-block rtl:text-right px-2 py-[0.15rem] border focus-within:ring-1 focus-within:border-primary-500 focus-within:ring-primary-500 dark:focus-within:border-primary-500 dark:focus-within:ring-primary-500 bg-gray-50 text-gray-900 dark:bg-gray-700 dark:text-white border-gray-300 dark:border-gray-600 text-sm rounded-lg">
 					<input name="openingHours[{i}][{j}][sh]" class="w-[2em] p-1 border-none focus:ring-0 [appearance:textfield] bg-gray-50 text-gray-900 dark:bg-gray-700 dark:text-white" type="number" step="1" min="0" max="23" bind:value={start[0]} required />
 					:
@@ -45,10 +46,10 @@
 					<input name="openingHours[{i}][{j}][eh]" class="w-[2em] p-1 border-none focus:ring-0 [appearance:textfield] bg-gray-50 text-gray-900 dark:bg-gray-700 dark:text-white" type="number" step="1" min="0" max="47" bind:value={end[0]} required />
 					:
 					<input name="openingHours[{i}][{j}][em]" class="w-[2em] p-1 border-none focus:ring-0 [appearance:textfield] bg-gray-50 text-gray-900 dark:bg-gray-700 dark:text-white" type="number" step="1" min="0" max="59" bind:value={end[1]} required />
-					<CloseButton name="entfernen" class="p-1 my-0" on:click={()=>openingHours = Object.assign(openingHours, {[day]: openingHours[day].filter((_,k)=>k!==j)})} />
+					<CloseButton name="entfernen" class="p-1 my-0" on:click={()=>openingHours = Object.assign(openingHours, {[day]: openingHours[day as keyof typeof openingHours]?.filter((_,k)=>k!==j)})} />
 				</div>
 				{/each}
-				<Button on:click={()=>openingHours = Object.assign(openingHours, {[day]: [...(openingHours[day]||[]), [[0,0],[0,0]]]})}>+</Button>
+				<Button on:click={()=>openingHours = Object.assign(openingHours, {[day]: [...(openingHours[day as keyof typeof openingHours]||[]), [[0,0],[0,0]]]})}>+</Button>
 			</TableBodyCell>
 		</TableBodyRow>
 		{/each}
@@ -59,7 +60,7 @@
 				{/if}
 				<Button on:click={()=>{
 					let date = new Date();
-					while(openingHours[date.toISOString().slice(0,10)]) date.setDate(date.getDate()+1);
+					while(openingHours[date.toISOString().slice(0,10) as keyof typeof openingHours]) date.setDate(date.getDate()+1);
 					openingHours = Object.assign(openingHours, {[date.toISOString().slice(0,10)]: []});
 				}}>bestimmten Tag hinzufügen</Button>
 			</TableBodyCell>
diff --git a/src/routes/admin/rallye/points/+page.svelte b/src/routes/admin/rallye/points/+page.svelte
index 601b18d8c9fcebbdfedf40d7d03ac7856d17b6e4..57f8bc57ea7ee90c10886924cbdc2796d3f0cc62 100644
--- a/src/routes/admin/rallye/points/+page.svelte
+++ b/src/routes/admin/rallye/points/+page.svelte
@@ -1,5 +1,5 @@
 <script lang="ts">
-	import { Breadcrumb, BreadcrumbItem, Input, Label, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte";
+	import { Breadcrumb, BreadcrumbItem, Button, Heading, Input, Label, Li, List, Modal, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte";
 	
 	let { data } = $props();
 	
@@ -17,7 +17,7 @@
 		return points.map(p => p / max * maxPoints);
 	}
 	
-	function normalize(){
+	function normalizeStations(){
 		return points.map((_, stationIndex) => normalizeStation(stationIndex));
 	}
 	
@@ -41,18 +41,34 @@
 	}
 	
 	function evaluate(){
-		const normalized = normalize();
+		const normalized = normalizeStations();
 		const transposed = normalized[0].map((_, i) => normalized.map(row => row[i]));
 		const numberOfStationsToGrade = Math.min(6, stationIds.length);
 		for(let tutorialIndex = 0; tutorialIndex < transposed.length; tutorialIndex++){
 			transposed[tutorialIndex].sort((a, b) => (b.points+b.bribe) - (a.points+a.bribe)).splice(numberOfStationsToGrade);
 		}
 		const normalizedQuestionnaires = normalizeQuestionnaires();
-		return transposed.map((points, tutorialIndex) => ({
-			tutorialId: tutorialIds[tutorialIndex],
+		const result = transposed.map((points, tutorialIndex) => ({
+			tutorial: data.tutorials[tutorialIndex],
 			points: points.reduce((acc, point) => acc + point.points + point.bribe + normalizedQuestionnaires[tutorialIndex], 0)
 		})).sort((a, b) => b.points - a.points);
+		let ranking = [];
+		let i = 0;
+		while(i < result.length){
+			let curr = i;
+			ranking.push([result[i]]);
+			i++;
+			for(let j = curr+1; j < result.length && result[j].points === result[curr].points; j++){
+				ranking[curr].push(result[j]);
+				ranking.push([]);
+				i++;
+			}
+		}
+		return ranking;
 	}
+	
+	let showModal = $state(false);
+	let ranking = $derived(showModal ? evaluate() : []);
 </script>
 
 <Breadcrumb class="mb-4">
@@ -61,6 +77,28 @@
 	<BreadcrumbItem href="/admin/rallye/points">Punkte</BreadcrumbItem>
 </Breadcrumb>
 
+<Modal bind:open={showModal} outsideclose autoclose>
+	<Heading tag="h2" customSize="text-2xl" class="text-center mb-3">Auswertung</Heading>
+	<Heading tag="h3" customSize="text-xl" class="mb-2">Erster Platz</Heading>
+	<List list="none">
+		{#each ranking[0] as {tutorial, points}}
+		<Li>{tutorial.name} ({points} Punkte)</Li>
+		{/each}
+	</List>
+	<Heading tag="h3" customSize="text-xl" class="mb-2">Zweiter Platz</Heading>
+	<List list="none">
+		{#each ranking[1] as {tutorial, points}}
+		<Li>{tutorial.name} ({points} Punkte)</Li>
+		{/each}
+	</List>
+	<Heading tag="h3" customSize="text-xl" class="mb-2">Dritter Platz</Heading>
+	<List list="none">
+		{#each ranking[2] as {tutorial, points}}
+		<Li>{tutorial.name} ({points} Punkte)</Li>
+		{/each}
+	</List>
+</Modal>
+
 {#each data.tutorials as tutorial}
 {#each data.stations as station}
 <form id="form-{station.id}/{tutorial.id}" method="post" action="?/updateStation">
@@ -73,6 +111,8 @@
 </form>
 {/each}
 
+<Button on:click={() => showModal = true} class="mb-4">Gewinner auswerten</Button>
+
 <Table>
 	<TableHead>
 		<TableHeadCell></TableHeadCell>
@@ -82,6 +122,7 @@
 	</TableHead>
 	<TableBody>
 		{#each data.stations as station, stationIndex}
+		{@const normalized = normalizeStation(stationIndex)}
 		<TableBodyRow>
 			<TableBodyCell>{station.name}</TableBodyCell>
 			{#each data.tutorials as tutorial, tutorialIndex}
diff --git a/src/routes/admin/schedule/+page.svelte b/src/routes/admin/schedule/+page.svelte
index 0d680d740c97d85cd1aa1be48425597a5c2b2cf2..ab9f572b5f54d13a1971ab9c1cbda614fdd76b55 100644
--- a/src/routes/admin/schedule/+page.svelte
+++ b/src/routes/admin/schedule/+page.svelte
@@ -7,14 +7,14 @@
 	import { locale, locales, LL } from "$lib/i18n/i18n";
 	import { addMessage } from "$lib/messages.js";
 	
-	export let data;
+	let { data } = $props();
 	
 	function pad(num: number){
 		return num.toString().padStart(2, "0");
 	}
 	
-	let showModal = false;
-	let scheduleToDelete: Schedule;
+	let showModal = $state(false);
+	let scheduleToDelete: Schedule|undefined = $state();
 	function confirmDeleteSchedule(schedule: Schedule){
 		scheduleToDelete = schedule;
 		showModal = true;
@@ -25,7 +25,7 @@
 		});
 	}
 	
-	let brandingImageElement: HTMLImageElement;
+	let brandingImageElement: HTMLImageElement|undefined = $state();
 </script>
 
 <Breadcrumb class="mb-4">
@@ -48,7 +48,7 @@
 		}}>
 			<img src="/stundenplaene/branding.png" class="mb-2 max-h-28" bind:this={brandingImageElement} alt="Branding für Stundenpläne" />
 			<Input type="file" name="image" accept="image/png" on:change={e=>{
-				brandingImageElement.src = URL.createObjectURL(e.target?.files[0]);
+				brandingImageElement!.src = URL.createObjectURL(e.target?.files[0]);
 			}} />
 			<Button type="submit">Speichern</Button>
 		</form>
@@ -221,12 +221,12 @@
 
 {#if data.user.permissions.has(Permission.UPDATE_SCHEDULES)}
 <div class="mb-6">
-	<form method="post" action="?/createSchedule" use:enhance={()=>({result})=>{
+	<form method="post" action="?/createSchedule" use:enhance={()=>({result, update})=>{
 			if(result.type === "success"){
-				addMessage({type: "success", text: "Einstellungen gespeichert"});
-				invalidateAll();
+				addMessage({type: "success", text: "Stundenplan erstellt"});
+				update();
 			}else{
-				addMessage({type: "error", text: "Fehler beim Speichern der Einstellungen"});
+				addMessage({type: "error", text: "Fehler beim Erstellen des Stundenplans"});
 				console.error(result);
 			}
 		}}>
@@ -282,10 +282,10 @@
 
 <Modal open={showModal} title="Bestätigen" autoclose outsideclose>
 	<P>
-		Soll der Stundenplan für {scheduleToDelete.studyProgram.name[$locale]} wirklich gelöscht werden?
+		Soll der Stundenplan für {scheduleToDelete!.studyProgram.name[$locale]} wirklich gelöscht werden?
 	</P>
 	<svelte:fragment slot="footer">
 		<Button>Behalten</Button>
-		<Button color="alternative" on:click={()=>deleteSchedule(scheduleToDelete.id)}>Löschen</Button>
+		<Button color="alternative" on:click={()=>deleteSchedule(scheduleToDelete!.id)}>Löschen</Button>
 	</svelte:fragment>
 </Modal>
diff --git a/src/routes/admin/schedule/[id=number]/+page.svelte b/src/routes/admin/schedule/[id=number]/+page.svelte
index b164378427e56dee7d92bdcfa446c9871ffeaa23..c0bb1d7bf2bccfa4fee12be37052b14c1ca907c6 100644
--- a/src/routes/admin/schedule/[id=number]/+page.svelte
+++ b/src/routes/admin/schedule/[id=number]/+page.svelte
@@ -5,7 +5,8 @@
 	import type { Timeslot } from "$lib/server/database/entities/Schedule.entity.js";
 	import { addMessage } from "$lib/messages.js";
 
-	export let data;
+	let { data } = $props();
+	
 	const schedule = Object.assign(data.schedule, {timeslots: data.schedule.timeslots.sort((a,b)=>a.date.localeCompare(b.date))});
 	
 	function pad(num: number){
diff --git a/src/routes/admin/schedule/fonts/+page.svelte b/src/routes/admin/schedule/fonts/+page.svelte
index 051a881294ace29220f242028326661e3f9e68fe..4f0abbbfaeee5b5151f25c7e562b3609aee4b08a 100644
--- a/src/routes/admin/schedule/fonts/+page.svelte
+++ b/src/routes/admin/schedule/fonts/+page.svelte
@@ -4,7 +4,8 @@
 	import { addMessage } from "$lib/messages.js";
 	import { Breadcrumb, BreadcrumbItem, Button, Input, Label, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte";
 
-	export let data;
+	let { data } = $props();
+	
 	function transformFontName(filename: string): string{
 		// remove file extension
 		// add space before the last one of consequent capital letters
@@ -17,7 +18,7 @@
 			.replace(/([A-Z])(?=[A-Z][a-z])/g, "$1 ")
 			.replace(/([a-z])(?=[A-Z])/g, "$1 ");
 	}
-	let fontName = "";
+	let fontName = $state("");
 </script>
 
 <Breadcrumb class="mb-4">
diff --git a/src/routes/admin/setup/+page.server.ts b/src/routes/admin/setup/+page.server.ts
index e5af9ebbff13709d9ba081114f3eda818e17b1ad..6d0c9e56f70e16b29a094d0bc6cb32151fa780f9 100644
--- a/src/routes/admin/setup/+page.server.ts
+++ b/src/routes/admin/setup/+page.server.ts
@@ -102,11 +102,13 @@ export const actions = {
 					replyTo: "-",
 					subject: { de: "", en: "" },
 					text: { de: "", en: "" },
+					type: "tutor",
 				},
 				trainingInformation: {
 					replyTo: "-",
 					subject: { de: "", en: "" },
 					text: { de: "", en: "" },
+					type: "tutor",
 				},
 			},
 			trainingMailReminderDays: 7,
diff --git a/src/routes/admin/studyprogram/+page.svelte b/src/routes/admin/studyprogram/+page.svelte
index 63a21f64b824d139f8ef7479a84594ff746955aa..c5dc601ba10afc520aee0c9c9ce2d46adfb3024c 100644
--- a/src/routes/admin/studyprogram/+page.svelte
+++ b/src/routes/admin/studyprogram/+page.svelte
@@ -6,7 +6,7 @@
 	import type { StudyProgram } from '$lib/server/database/entities/StudyProgram.entity.js';
 	import { Breadcrumb, BreadcrumbItem, Button, Input, Label, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from 'flowbite-svelte';
 	
-	export let data;
+	let { data } = $props();
 	
 	function confirmStudyProgramDelete(studyProgram: StudyProgram) {
 		if (confirm('Wirklich löschen?')) { // TODO
@@ -38,10 +38,10 @@
 	<input type="hidden" name="id" value={studyProgram.id} />
 </form>
 {/each}
-<form method="post" action="?/createStudyProgram" id="new" use:enhance={()=>({result})=>{
+<form method="post" action="?/createStudyProgram" id="new" use:enhance={()=>({result, update})=>{
 	if(result.type === "success"){
 		addMessage({type: "success", text: "Studiengang erstellt"});
-		invalidateAll();
+		update();
 	}else{
 		addMessage({type: "error", text: "Fehler beim Erstellen"});
 		console.error(result);
diff --git a/src/routes/admin/templates/+page.server.ts b/src/routes/admin/templates/+page.server.ts
index 608799ffb9adfc176d087ddea6e91f2087002330..9bd9ab2b684ddac6f8fb479ecbea8f47365c012f 100644
--- a/src/routes/admin/templates/+page.server.ts
+++ b/src/routes/admin/templates/+page.server.ts
@@ -1,9 +1,9 @@
 import { locales } from '$lib/i18n/i18n';
-import type { MailTemplate } from '$lib/mail';
+import { configProps, type MailTemplate } from '$lib/mail';
 import { Config } from '$lib/server/database/entities/Config.entity';
 import { StudyProgram } from '$lib/server/database/entities/StudyProgram.entity';
 import { TutorTraining } from '$lib/server/database/entities/TutorTraining.entity';
-import type { Localized } from '$lib/utils';
+import { pick, type Localized } from '$lib/utils';
 import { error } from '@sveltejs/kit';
 import type { PageServerLoad } from './$types';
 import { Permission } from '$lib/perms';
@@ -17,10 +17,11 @@ export const load = (async () => {
 		trainingMailReminderDays: config.trainingMailReminderDays,
 		studyPrograms: studyPrograms.map(sp => ({ id: sp.id, name: sp.name })),
 		trainings: trainings.map(t => ({ id: t.id, date: t.date, location: t.location })),
+		config: pick(config, configProps as unknown as (keyof Config)[]),
 	};
 }) satisfies PageServerLoad;
 
-function parseMail(data: FormData): MailTemplate {
+function parseMail(data: FormData): Pick<MailTemplate, "replyTo"|"subject"|"text"> {
 	const replyTo = data.get('replyTo');
 	if (typeof replyTo !== 'string') error(400, 'Missing replyTo');
 	const subject = {} as Localized;
diff --git a/src/routes/admin/templates/+page.svelte b/src/routes/admin/templates/+page.svelte
index f760d6598b7e852d5d9bb482310bfbf0d06b3333..d63f358edafe6e0150a63c178f77b49a67a5ac32 100644
--- a/src/routes/admin/templates/+page.svelte
+++ b/src/routes/admin/templates/+page.svelte
@@ -40,6 +40,8 @@
 				buttonText="Speichern"
 				buttonType="submit"
 				disabled={!canEdit}
+				config={data.config}
+				type={templates.tutorRegistered.type}
 			/>
 		</form>
 	</TabItem>
@@ -69,6 +71,8 @@
 				buttonText="Speichern"
 				buttonType="submit"
 				disabled={!canEdit}
+				config={data.config}
+				type={templates.trainingInformation.type}
 			/>
 	</TabItem>
 </Tabs>
diff --git a/src/routes/admin/tutor/+page.svelte b/src/routes/admin/tutor/+page.svelte
index 983573156ee5d6444bd8459cbd54acd2af63aec9..18206e1e69a287886ce4e9ac020b03fa3f215f2c 100644
--- a/src/routes/admin/tutor/+page.svelte
+++ b/src/routes/admin/tutor/+page.svelte
@@ -7,6 +7,7 @@
 	import type { Tutor } from "$lib/server/database/entities/Tutor.entity";
 	
 	export let data;
+	
 	let sortedTutors = data.tutors;
 	
 	$: dateFormatter = new Intl.DateTimeFormat($locale, { day: "2-digit", month: "2-digit", year: "numeric" });
diff --git a/src/routes/admin/tutor/mail/+page.server.ts b/src/routes/admin/tutor/mail/+page.server.ts
index 89af0bede7e18e9a32f1d0a8029474c66fad9c21..ed6c25077f93a14b3b8910de60857ddd1682a3ad 100644
--- a/src/routes/admin/tutor/mail/+page.server.ts
+++ b/src/routes/admin/tutor/mail/+page.server.ts
@@ -1,7 +1,10 @@
+import { configProps } from '$lib/mail';
 import { Permission } from '$lib/perms';
+import { Config } from '$lib/server/database/entities/Config.entity';
 import { StudyProgram } from '$lib/server/database/entities/StudyProgram.entity';
 import { Tutor } from '$lib/server/database/entities/Tutor.entity';
 import { TutorTraining } from '$lib/server/database/entities/TutorTraining.entity';
+import { pick } from '$lib/utils';
 import type { PageServerLoad } from './$types';
 
 export const load = (async event => {
@@ -9,10 +12,12 @@ export const load = (async event => {
 	const tutors = event.locals.user.permissions.has(Permission.VIEW_TUTORS) ? (await Tutor.getAll()).map(t=>({id: t.id, firstname: t.firstname, lastname: t.lastname})) : null;
 	const degrees = (await StudyProgram.getAll()).map(s=>({id: s.id, name: s.name}));
 	const trainings = (await TutorTraining.getAll()).map(t=>({id: t.id, date: t.date, location: t.location}));
+	const config = Config.get();
 	return {
 		studyPrograms,
 		tutors,
 		degrees,
 		trainings,
+		config: pick(config, configProps as unknown as (keyof Config)[]),
 	};
 }) satisfies PageServerLoad;
diff --git a/src/routes/admin/tutor/mail/+page.svelte b/src/routes/admin/tutor/mail/+page.svelte
index 17ef6d2c7ae0c76e7b1c445177d9fe7bebb15c3e..fbe83ebace82e128a3da6fc6b27d256c6b52085c 100644
--- a/src/routes/admin/tutor/mail/+page.svelte
+++ b/src/routes/admin/tutor/mail/+page.svelte
@@ -81,4 +81,4 @@
 {/each}
 <Button color="green" on:click={addFilter} class="mt-2">+</Button>
 
-<MailInput bind:text={mailText} bind:subject={mailSubject} bind:replyTo={mailReplyTo} replyToOptions={[data.user.email, "esa@fsmpi.rwth-aachen.de"]} allowCustomReplyTo studyPrograms={data.studyPrograms} trainings={data.trainings} onsubmit={sendMail} />
+<MailInput bind:text={mailText} bind:subject={mailSubject} bind:replyTo={mailReplyTo} replyToOptions={[data.user.email, "esa@fsmpi.rwth-aachen.de"]} allowCustomReplyTo studyPrograms={data.studyPrograms} trainings={data.trainings} onsubmit={sendMail} config={data.config} type="tutor" />
diff --git a/src/routes/admin/tutor/training/new/+page.server.ts b/src/routes/admin/tutor/training/new/+page.server.ts
index 04a3a9f98575ca527c4d535d87587e7b9896d552..356790e81eae82301c2e136107c3d1ee6c62108a 100644
--- a/src/routes/admin/tutor/training/new/+page.server.ts
+++ b/src/routes/admin/tutor/training/new/+page.server.ts
@@ -18,7 +18,7 @@ export const actions = {
 		if(!Number.isInteger(maxParticipants) || maxParticipants < 0) error(400, "Invalid maxParticipants");
 		if(typeof notes !== "string") error(400, "Invalid notes");
 		if(internal !== null && internal !== "on") error(400, "Invalid internal");
-		const id = TutorTraining.create({ date, location, maxParticipants, notes, internal: internal === "on" });
+		const id = await TutorTraining.create({ date, location, maxParticipants, notes, internal: internal === "on" });
 		return { id };
 	},
 };
diff --git a/src/routes/admin/uploads/+page.server.ts b/src/routes/admin/uploads/+page.server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f7a464fc16a7762fdfb48aa2a627145d47b9d440
--- /dev/null
+++ b/src/routes/admin/uploads/+page.server.ts
@@ -0,0 +1,10 @@
+import type { PageServerLoad } from './$types';
+import fs from "node:fs/promises";
+
+export const load = (async () => {
+	await fs.mkdir("static/uploads", { recursive: true });
+	const filenames = await fs.readdir("static/uploads");
+	return {
+		filenames: filenames.map(f=>`/uploads/${f}`),
+	};
+}) satisfies PageServerLoad;
diff --git a/src/routes/admin/uploads/+page.svelte b/src/routes/admin/uploads/+page.svelte
new file mode 100644
index 0000000000000000000000000000000000000000..3f7ff90ec57102a3f5661daebc2a932afbcd0373
--- /dev/null
+++ b/src/routes/admin/uploads/+page.svelte
@@ -0,0 +1,22 @@
+<script lang="ts">
+	import { Li, List, P } from "flowbite-svelte";
+	
+	let { data } = $props();
+</script>
+
+{#if data.filenames.length === 0}
+<P>Bisher wurden noch keine Bilder hochgeladen.</P>
+{:else}
+<List list="none">
+	{#each data.filenames as filename}
+	<Li class="mb-4">
+		<div>
+			<a href={filename} download>
+				<!-- svelte-ignore a11y_missing_attribute -->
+				<img src={filename} class="w-36 max-h-36 max-w-full object-contain inline-block" />
+			</a>
+		</div>
+	</Li>
+	{/each}
+</List>
+{/if}