diff --git a/build.gradle.kts b/build.gradle.kts
index 4c479e9ecad7ab912cb27f79bb2d47c60427ae4c..a9704e6d5cd1f904d565343cb110dd097c0e50e1 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -8,8 +8,8 @@ version = "2.0.0"
 
 val javaVersion = JavaVersion.VERSION_17
 
-val libradorVersion = "1.0.3-SNAPSHOT6"
-val labradoorVersion = "1.3.1-SNAPSHOT"
+val libradorVersion = "1.1.0"
+val labradoorVersion = "1.4.0-SNAPSHOT"
 val queryDslVersion = "4.4.0"
 
 // A definition of all dependencies and repositories where to find them that need to
@@ -174,6 +174,15 @@ configure<SpotlessExtension> {
         trimTrailingWhitespace()
         endWithNewline()
     }
+    format("html") {
+        target("src/main/resources/templates/**/*.html", "src/main/resources/static/js/**/*.js", "src/main/resources/scss/**/*.scss")
+        prettier("2.6").config(mapOf(
+            "tabWidth" to 4, "semi" to true,
+            "printWidth" to 100,
+            "bracketSameLine" to true,
+            "arrowParens" to "avoid",
+            "htmlWhitespaceSensitivity" to "ignore"))
+    }
 }
 
 /////// TASKS ///////
@@ -341,14 +350,11 @@ dependencies {
     implementation("org.webjars:webjars-locator-core")
     implementation("org.webjars:jquery:3.5.1")
     implementation("org.webjars:jquery-cookie:1.4.1-1")
-    implementation("org.webjars:bootstrap:4.3.1")
-    implementation("org.webjars:bootstrap-select:1.13.8")
-    implementation("org.webjars:font-awesome:5.10.1")
+    implementation("org.webjars:font-awesome:6.1.2")
 	implementation("org.webjars:sockjs-client:1.5.1")
     implementation("org.webjars:stomp-websocket:2.3.4")
     implementation("org.webjars:handlebars:4.0.14")
     implementation("org.webjars:chartjs:2.7.0")
-    implementation("org.webjars:tempusdominus-bootstrap-4:5.1.2")
     implementation("org.webjars:momentjs:2.24.0")
     implementation("org.webjars:codemirror:5.50.0")
     implementation("org.webjars:fullcalendar:5.9.0")
diff --git a/deploy.sh b/deploy.sh
new file mode 100755
index 0000000000000000000000000000000000000000..9e172fe6a9873bd89c213c6bc4ec1e9329b0ad2f
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+ssh-add ~/.ssh/rsa_personal
+mv src/main/resources/application.yaml .
+./gradlew clean
+./gradlew assemble
+mv application.yaml src/main/resources
+scp build/libs/queue* rubenbackx.nl:/home/ruben/
+ssh rubenbackx.nl
+
diff --git a/src/main/java/nl/tudelft/queue/DevDatabaseLoader.java b/src/main/java/nl/tudelft/queue/DevDatabaseLoader.java
index 1f8d2cae6f753571b51a6da45c6ebe3fcd080e1e..cf50fbdcc4f0e22deb2cf99d30742c984d5f2381 100644
--- a/src/main/java/nl/tudelft/queue/DevDatabaseLoader.java
+++ b/src/main/java/nl/tudelft/queue/DevDatabaseLoader.java
@@ -35,7 +35,7 @@ import nl.tudelft.labracore.api.EditionControllerApi;
 import nl.tudelft.labracore.api.ModuleControllerApi;
 import nl.tudelft.labracore.api.RoomControllerApi;
 import nl.tudelft.labracore.api.dto.*;
-import nl.tudelft.labracore.lib.security.user.Person;
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.queue.dto.create.embeddables.ExamLabConfigCreateDTO;
 import nl.tudelft.queue.dto.create.embeddables.SlottedLabConfigCreateDTO;
 import nl.tudelft.queue.dto.create.labs.ExamLabCreateDTO;
@@ -193,6 +193,7 @@ public class DevDatabaseLoader implements InitializingBean {
 
 	private void initLabs() {
 		oopOldL0 = ls.createSessions(RegularLabCreateDTO.builder()
+				.organisationalUnit(oopNow.getId())
 				.name("OOP Lab 0ld")
 				.eolGracePeriod(15)
 				.modules(Set.of(oopOldModuleA.getId()))
@@ -202,9 +203,10 @@ public class DevDatabaseLoader implements InitializingBean {
 						.map(RoomSummaryDTO::getId).collect(Collectors.toSet()))
 				.communicationMethod(CommunicationMethod.TA_VISIT_STUDENT)
 				.slot(new Slot(LocalDateTime.now(), LocalDateTime.now().plusMinutes(225).minusYears(1)))
-				.build(), oopNow.getId(), REGULAR).get(0);
+				.build(), REGULAR).get(0);
 
 		oopL1 = ls.createSessions(RegularLabCreateDTO.builder()
+				.organisationalUnit(oopNow.getId())
 				.name("OOP Lab 1")
 				.eolGracePeriod(15)
 				.modules(Set.of(oopNowModuleA.getId()))
@@ -214,9 +216,10 @@ public class DevDatabaseLoader implements InitializingBean {
 				.communicationMethod(CommunicationMethod.TA_VISIT_STUDENT)
 				.slot(new Slot(LocalDateTime.now().minusYears(1),
 						LocalDateTime.now().plusMinutes(225)))
-				.build(), oopNow.getId(), REGULAR).get(0);
+				.build(), REGULAR).get(0);
 
 		oopL2 = ls.createSessions(RegularLabCreateDTO.builder()
+				.organisationalUnit(oopNow.getId())
 				.name("OOP Lab 2")
 				.eolGracePeriod(15)
 				.modules(Set.of(oopNowModuleA.getId()))
@@ -229,9 +232,10 @@ public class DevDatabaseLoader implements InitializingBean {
 				.communicationMethod(CommunicationMethod.TA_VISIT_STUDENT)
 				.slot(new Slot(LocalDateTime.now().plusWeeks(1),
 						LocalDateTime.now().plusWeeks(1).plusMinutes(225)))
-				.build(), oopNow.getId(), REGULAR).get(0);
+				.build(), REGULAR).get(0);
 
 		oopE = ls.createSessions(ExamLabCreateDTO.builder()
+				.organisationalUnit(oopNow.getId())
 				.name("Exam")
 				.eolGracePeriod(15)
 				.slottedLabConfig(SlottedLabConfigCreateDTO.builder()
@@ -246,9 +250,10 @@ public class DevDatabaseLoader implements InitializingBean {
 				.communicationMethod(CommunicationMethod.TA_VISIT_STUDENT)
 				.slot(new Slot(LocalDateTime.now(),
 						LocalDateTime.now().plusMinutes(225)))
-				.build(), oopNow.getId(), REGULAR).get(0);
+				.build(), REGULAR).get(0);
 
 		adsL1I = ls.createSessions(RegularLabCreateDTO.builder()
+				.organisationalUnit(adsNow.getId())
 				.name("ADS Lab 1")
 				.eolGracePeriod(15)
 				.modules(Set.of(adsNowModuleI.getId()))
@@ -259,9 +264,10 @@ public class DevDatabaseLoader implements InitializingBean {
 						.map(RoomSummaryDTO::getId).collect(Collectors.toSet()))
 				.communicationMethod(CommunicationMethod.TA_VISIT_STUDENT)
 				.slot(new Slot(LocalDateTime.now(), LocalDateTime.now().plusMinutes(225)))
-				.build(), adsNow.getId(), REGULAR).get(0);
+				.build(), REGULAR).get(0);
 
 		adsL1A = ls.createSessions(RegularLabCreateDTO.builder()
+				.organisationalUnit(adsNow.getId())
 				.name("Analysis Lab 1")
 				.eolGracePeriod(15)
 				.modules(Set.of(adsNowModuleA.getId()))
@@ -271,9 +277,10 @@ public class DevDatabaseLoader implements InitializingBean {
 						.map(RoomSummaryDTO::getId).collect(Collectors.toSet()))
 				.communicationMethod(CommunicationMethod.TA_VISIT_STUDENT)
 				.slot(new Slot(LocalDateTime.now(), LocalDateTime.now().plusMinutes(225)))
-				.build(), adsNow.getId(), REGULAR).get(0);
+				.build(), REGULAR).get(0);
 
 		adsL2I = ls.createSessions(RegularLabCreateDTO.builder()
+				.organisationalUnit(adsNow.getId())
 				.name("ADS Lab 2")
 				.eolGracePeriod(15)
 				.modules(Set.of(adsNowModuleI.getId()))
@@ -286,7 +293,7 @@ public class DevDatabaseLoader implements InitializingBean {
 				.communicationMethod(CommunicationMethod.TA_VISIT_STUDENT)
 				.slot(new Slot(LocalDateTime.now().plusWeeks(1),
 						LocalDateTime.now().plusWeeks(1).plusMinutes(225)))
-				.build(), adsNow.getId(), REGULAR).get(0);
+				.build(), REGULAR).get(0);
 	}
 
 	private void initRequests() {
diff --git a/src/main/java/nl/tudelft/queue/cache/ClusterCacheManager.java b/src/main/java/nl/tudelft/queue/cache/ClusterCacheManager.java
deleted file mode 100644
index 84972774ba151f731a23fb103516782ebaef950c..0000000000000000000000000000000000000000
--- a/src/main/java/nl/tudelft/queue/cache/ClusterCacheManager.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Queue - A Queueing system that can be used to handle labs in higher education
- * Copyright (C) 2016-2021  Delft University of Technology
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-package nl.tudelft.queue.cache;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-import nl.tudelft.labracore.api.ClusterControllerApi;
-import nl.tudelft.labracore.api.dto.ClusterDetailsDTO;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.web.context.annotation.RequestScope;
-
-@Component
-@RequestScope
-public class ClusterCacheManager extends CoreCacheManager<Long, ClusterDetailsDTO> {
-	@Autowired
-	private ClusterControllerApi cApi;
-
-	@Override
-	protected List<ClusterDetailsDTO> fetch(List<Long> ids) {
-		return ids.stream()
-				.map(id -> cApi.getClusterById(id).block())
-				.collect(Collectors.toList());
-	}
-
-	@Override
-	protected Long getId(ClusterDetailsDTO dto) {
-		return dto.getId();
-	}
-}
diff --git a/src/main/java/nl/tudelft/queue/cache/CoreCacheManager.java b/src/main/java/nl/tudelft/queue/cache/CoreCacheManager.java
deleted file mode 100644
index e1e309f37f2a340304aabf576948f99c1c606413..0000000000000000000000000000000000000000
--- a/src/main/java/nl/tudelft/queue/cache/CoreCacheManager.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Queue - A Queueing system that can be used to handle labs in higher education
- * Copyright (C) 2016-2021  Delft University of Technology
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-package nl.tudelft.queue.cache;
-
-import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import org.springframework.http.HttpStatus;
-import org.springframework.web.server.ResponseStatusException;
-
-import com.google.common.collect.Sets;
-
-public abstract class CoreCacheManager<ID, DTO> {
-	protected final Map<ID, DTO> cache = new ConcurrentHashMap<>();
-	protected final Set<ID> empties = new HashSet<>();
-
-	/**
-	 * Fetches the objects currently missing from the cache. These objects are fetched through a list of their
-	 * IDs.
-	 *
-	 * @param  ids The list of IDs of objects to fetch.
-	 * @return     The list of fetched objects.
-	 */
-	protected abstract List<DTO> fetch(List<ID> ids);
-
-	/**
-	 * Gets the ID of a particular instance of the data object.
-	 *
-	 * @param  dto The DTO data object that will be cached.
-	 * @return     The ID of the DTO object.
-	 */
-	protected abstract ID getId(DTO dto);
-
-	/**
-	 * @return The maximum number of items to be fetched from Labracore in one go.
-	 */
-	protected int batchSize() {
-		return 128;
-	}
-
-	/**
-	 * Used to manually check the status of the cache and invalidate the cache if necessary.
-	 */
-	protected synchronized void validateCache() {
-	}
-
-	/**
-	 * Gets a list of DTOs from a list of ids. The list of ids gets broken up into already cached items and to
-	 * be fetched items. The to be fetched are then fetched and the already cached are just fetched from
-	 * cache.
-	 *
-	 * @param  ids The ids to find matching DTOs for.
-	 * @return     The list of DTOs found to be matching.
-	 */
-	public List<DTO> get(List<ID> ids) {
-		validateCache();
-
-		List<ID> misses = ids.stream()
-				.filter(id -> !cache.containsKey(id) && !empties.contains(id))
-				.collect(Collectors.toList());
-		if (!misses.isEmpty()) {
-			forEachBatch(misses, batch -> registerImpl(new HashSet<>(batch), fetch(batch)));
-		}
-
-		return ids.stream()
-				.map(cache::get)
-				.collect(Collectors.toList());
-	}
-
-	/**
-	 * Gets a list of DTOs from a stream of IDs. This method is mostly used as a convenience method wrapping
-	 * {@link #get(List)}.
-	 *
-	 * @param  ids The stream of ids to be collected and requested from the cache.
-	 * @return     The list of DTOs with the given IDs.
-	 */
-	public List<DTO> get(Stream<ID> ids) {
-		return get(ids.collect(Collectors.toList()));
-	}
-
-	/**
-	 * Gets a single DTO from its id. If the DTO is already cached, no external lookup is done.
-	 *
-	 * @param  id The id of the DTO to lookup.
-	 * @return    The requested DTO if it exists, otherwise nothing.
-	 */
-	public Optional<DTO> get(ID id) {
-		if (id == null) {
-			return Optional.empty();
-		}
-		return Optional.of(get(List.of(id)))
-				.filter(l -> !l.isEmpty())
-				.map(l -> l.get(0));
-	}
-
-	/**
-	 * Gets the DTO with the given id or throws an exception with status code 404 if none such DTO could be
-	 * found.
-	 *
-	 * @param  id The id of the DTO to find.
-	 * @return    The found DTO with the given id.
-	 */
-	public DTO getOrThrow(ID id) {
-		var l = get(List.of(id));
-		if (l == null || l.isEmpty()) {
-			throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Could not find DTO by id " + id);
-		}
-		return l.get(0);
-	}
-
-	/**
-	 * Registers an entry into the cache after validating the cache first.
-	 *
-	 * @param dto The DTO that is to be registered.
-	 */
-	public void register(DTO dto) {
-		validateCache();
-		registerImpl(dto);
-	}
-
-	/**
-	 * Registers a list of entries into the cache after validating the cache first.
-	 *
-	 * @param dtos The DTOs that are to be registered.
-	 */
-	public void register(List<DTO> dtos) {
-		validateCache();
-		registerImpl(dtos);
-	}
-
-	/**
-	 * Implementation of the register method. Registers an entry into the cache.
-	 *
-	 * @param dto The DTO that is to be registered.
-	 */
-	private void registerImpl(DTO dto) {
-		cache.put(getId(dto), dto);
-		registerAdditionally(dto);
-	}
-
-	/**
-	 * Implementation of the register method. Registers a list of entries into the cache.
-	 *
-	 * @param dtos The DTOs that are to be registered.
-	 */
-	private void registerImpl(List<DTO> dtos) {
-		dtos.forEach(this::registerImpl);
-	}
-
-	/**
-	 * Implementation of the register method. Registers a list of entries into the cache and also registers
-	 * the IDs that were requested but not returned as 'empty' IDs.
-	 *
-	 * @param requests The set of requested IDs.
-	 * @param dtos     The list of DTOs returned from the request.
-	 */
-	private void registerImpl(Set<ID> requests, List<DTO> dtos) {
-		dtos.forEach(this::registerImpl);
-		empties.addAll(Sets.difference(requests,
-				dtos.stream().map(this::getId).collect(Collectors.toSet())));
-	}
-
-	/**
-	 * Performs additional registers in other caches if necessary.
-	 *
-	 * @param dto The DTO that is being registered.
-	 */
-	protected void registerAdditionally(DTO dto) {
-	}
-
-	/**
-	 * Helper function for iterating over batches of items. The batch size is determined through the
-	 * {@link #batchSize()} method in implementations of this class. Each created batch will be passed on to
-	 * the consumer function f.
-	 *
-	 * @param ids The list of ids that are to be batched.
-	 * @param f   The consumer of each batch.
-	 */
-	private void forEachBatch(List<ID> ids, Consumer<List<ID>> f) {
-		List<ID> batch = new ArrayList<>();
-		for (ID id : ids) {
-			batch.add(id);
-
-			if (batch.size() >= batchSize()) {
-				f.accept(batch);
-				batch.clear();
-			}
-		}
-
-		if (!batch.isEmpty()) {
-			f.accept(batch);
-		}
-	}
-
-}
diff --git a/src/main/java/nl/tudelft/queue/cache/CourseCacheManager.java b/src/main/java/nl/tudelft/queue/cache/CourseCacheManager.java
deleted file mode 100644
index a841a89a44e23436f095b1e5b1fa29be257a2b5b..0000000000000000000000000000000000000000
--- a/src/main/java/nl/tudelft/queue/cache/CourseCacheManager.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Queue - A Queueing system that can be used to handle labs in higher education
- * Copyright (C) 2016-2021  Delft University of Technology
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-package nl.tudelft.queue.cache;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-import nl.tudelft.labracore.api.CourseControllerApi;
-import nl.tudelft.labracore.api.dto.CourseDetailsDTO;
-import nl.tudelft.labracore.api.dto.CourseSummaryDTO;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.web.context.annotation.RequestScope;
-
-@Component
-@RequestScope
-public class CourseCacheManager extends CoreCacheManager<Long, CourseDetailsDTO> {
-	@Autowired
-	private CourseControllerApi api;
-
-	public List<CourseDetailsDTO> getAll() {
-		var courses = api.getAllCoursesById(api.getAllCourses().collectList().block().stream()
-				.map(CourseSummaryDTO::getId).collect(Collectors.toList())).collectList().block();
-		register(courses);
-
-		return courses;
-	}
-
-	@Override
-	protected List<CourseDetailsDTO> fetch(List<Long> ids) {
-		return api.getAllCoursesById(ids).collectList().block();
-	}
-
-	@Override
-	protected Long getId(CourseDetailsDTO dto) {
-		return dto.getId();
-	}
-}
diff --git a/src/main/java/nl/tudelft/queue/cache/EditionCollectionCacheManager.java b/src/main/java/nl/tudelft/queue/cache/EditionCollectionCacheManager.java
deleted file mode 100644
index c1c37e0646697e80de01053b3ef7346c89b296a4..0000000000000000000000000000000000000000
--- a/src/main/java/nl/tudelft/queue/cache/EditionCollectionCacheManager.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Queue - A Queueing system that can be used to handle labs in higher education
- * Copyright (C) 2016-2021  Delft University of Technology
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-package nl.tudelft.queue.cache;
-
-import java.util.List;
-
-import nl.tudelft.labracore.api.EditionCollectionControllerApi;
-import nl.tudelft.labracore.api.dto.EditionCollectionDetailsDTO;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.web.context.annotation.RequestScope;
-
-@Component
-@RequestScope
-public class EditionCollectionCacheManager extends CoreCacheManager<Long, EditionCollectionDetailsDTO> {
-	@Autowired
-	private EditionCollectionControllerApi api;
-
-	@Override
-	protected List<EditionCollectionDetailsDTO> fetch(List<Long> ids) {
-		return api.getEditionCollectionsById(ids).collectList().block();
-	}
-
-	@Override
-	protected Long getId(EditionCollectionDetailsDTO dto) {
-		return dto.getId();
-	}
-}
diff --git a/src/main/java/nl/tudelft/queue/cache/EditionRolesCacheManager.java b/src/main/java/nl/tudelft/queue/cache/EditionRolesCacheManager.java
deleted file mode 100644
index ce9ebe998c178569cc96ed7e54e0bbeaf6b3062c..0000000000000000000000000000000000000000
--- a/src/main/java/nl/tudelft/queue/cache/EditionRolesCacheManager.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Queue - A Queueing system that can be used to handle labs in higher education
- * Copyright (C) 2016-2021  Delft University of Technology
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-package nl.tudelft.queue.cache;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import nl.tudelft.labracore.api.EditionControllerApi;
-import nl.tudelft.labracore.api.dto.RolePersonDetailsDTO;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.web.context.annotation.RequestScope;
-
-@Component
-@RequestScope
-public class EditionRolesCacheManager extends CoreCacheManager<Long, EditionRolesCacheManager.RoleHolder> {
-	/**
-	 * Holds a list of roles cached by the ID of the edition that was to be looked up.
-	 */
-	@Data
-	@AllArgsConstructor
-	public static class RoleHolder {
-		private Long id;
-		private List<RolePersonDetailsDTO> roles;
-	}
-
-	private final EditionControllerApi eApi;
-
-	public EditionRolesCacheManager(@Autowired EditionControllerApi eApi) {
-		this.eApi = eApi;
-	}
-
-	@Override
-	protected List<RoleHolder> fetch(List<Long> ids) {
-		return ids.stream()
-				.map(id -> new RoleHolder(id, eApi.getEditionParticipants(id).collectList().block()))
-				.collect(Collectors.toList());
-	}
-
-	@Override
-	protected Long getId(RoleHolder roleHolder) {
-		return roleHolder.id;
-	}
-
-	@Override
-	protected int batchSize() {
-		// Set batch size to MAX because we already do batches of n=1 in fetch.
-		return Integer.MAX_VALUE;
-	}
-}
diff --git a/src/main/java/nl/tudelft/queue/cache/ModuleCacheManager.java b/src/main/java/nl/tudelft/queue/cache/ModuleCacheManager.java
deleted file mode 100644
index 3ab0cf2e4fd1ad0b0bd05a719a95396ff6bd7f81..0000000000000000000000000000000000000000
--- a/src/main/java/nl/tudelft/queue/cache/ModuleCacheManager.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Queue - A Queueing system that can be used to handle labs in higher education
- * Copyright (C) 2016-2021  Delft University of Technology
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-package nl.tudelft.queue.cache;
-
-import java.util.List;
-
-import nl.tudelft.labracore.api.ModuleControllerApi;
-import nl.tudelft.labracore.api.dto.ModuleDetailsDTO;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.web.context.annotation.RequestScope;
-
-@Component
-@RequestScope
-public class ModuleCacheManager extends CoreCacheManager<Long, ModuleDetailsDTO> {
-	@Autowired
-	private ModuleControllerApi api;
-
-	@Override
-	protected List<ModuleDetailsDTO> fetch(List<Long> ids) {
-		return api.getModulesById(ids).collectList().block();
-	}
-
-	@Override
-	protected Long getId(ModuleDetailsDTO dto) {
-		return dto.getId();
-	}
-
-	@Override
-	protected int batchSize() {
-		return 1;
-	}
-}
diff --git a/src/main/java/nl/tudelft/queue/cache/PersonCacheManager.java b/src/main/java/nl/tudelft/queue/cache/PersonCacheManager.java
deleted file mode 100644
index 476877f9846c3403a50f8da52fc2ee42719573a9..0000000000000000000000000000000000000000
--- a/src/main/java/nl/tudelft/queue/cache/PersonCacheManager.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Queue - A Queueing system that can be used to handle labs in higher education
- * Copyright (C) 2016-2021  Delft University of Technology
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-package nl.tudelft.queue.cache;
-
-import java.util.List;
-
-import nl.tudelft.labracore.api.PersonControllerApi;
-import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.web.context.annotation.ApplicationScope;
-
-@Component
-@ApplicationScope
-public class PersonCacheManager extends TimedCacheManager<Long, PersonSummaryDTO> {
-	@Autowired
-	private PersonControllerApi api;
-
-	/**
-	 * Creates a new person cache by setting the timeout on its timed cache implementation to 5 minutes.
-	 */
-	public PersonCacheManager() {
-		super(5 * 60 * 1000L);
-	}
-
-	@Override
-	protected List<PersonSummaryDTO> fetch(List<Long> ids) {
-		return api.getPeopleById(ids).collectList().block();
-	}
-
-	@Override
-	protected Long getId(PersonSummaryDTO dto) {
-		return dto.getId();
-	}
-}
diff --git a/src/main/java/nl/tudelft/queue/cache/RoleCacheManager.java b/src/main/java/nl/tudelft/queue/cache/RoleCacheManager.java
deleted file mode 100644
index 277de71911c1c2bfc82d674407dd5461fc89d7b1..0000000000000000000000000000000000000000
--- a/src/main/java/nl/tudelft/queue/cache/RoleCacheManager.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Queue - A Queueing system that can be used to handle labs in higher education
- * Copyright (C) 2016-2021  Delft University of Technology
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-package nl.tudelft.queue.cache;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-import nl.tudelft.labracore.api.RoleControllerApi;
-import nl.tudelft.labracore.api.dto.Id;
-import nl.tudelft.labracore.api.dto.RoleDetailsDTO;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.web.context.annotation.RequestScope;
-
-@Component
-@RequestScope
-public class RoleCacheManager extends CoreCacheManager<Id, RoleDetailsDTO> {
-	@Autowired
-	private RoleControllerApi api;
-
-	@Autowired
-	private PersonCacheManager pCache;
-
-	@Override
-	protected List<RoleDetailsDTO> fetch(List<Id> ids) {
-		return api.getRolesById(
-				ids.stream().map(Id::getEditionId).collect(Collectors.toList()),
-				ids.stream().map(Id::getPersonId).collect(Collectors.toList()))
-				.collectList().block();
-	}
-
-	@Override
-	protected Id getId(RoleDetailsDTO dto) {
-		return dto.getId();
-	}
-
-	@Override
-	protected void registerAdditionally(RoleDetailsDTO dto) {
-		pCache.register(dto.getPerson());
-	}
-}
diff --git a/src/main/java/nl/tudelft/queue/cache/RoomCacheManager.java b/src/main/java/nl/tudelft/queue/cache/RoomCacheManager.java
deleted file mode 100644
index 6e4765c41a6808ae748bf910b3e1201225bbff2d..0000000000000000000000000000000000000000
--- a/src/main/java/nl/tudelft/queue/cache/RoomCacheManager.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Queue - A Queueing system that can be used to handle labs in higher education
- * Copyright (C) 2016-2021  Delft University of Technology
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-package nl.tudelft.queue.cache;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-import nl.tudelft.labracore.api.RoomControllerApi;
-import nl.tudelft.labracore.api.dto.RoomDetailsDTO;
-import nl.tudelft.labracore.api.dto.RoomSummaryDTO;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.web.context.annotation.RequestScope;
-
-@Component
-@RequestScope
-public class RoomCacheManager extends CoreCacheManager<Long, RoomDetailsDTO> {
-	@Autowired
-	private RoomControllerApi api;
-
-	public List<RoomDetailsDTO> getAll() {
-		var rooms = api.getAllRoomsById(api.getAllRooms().collectList().block().stream()
-				.map(RoomSummaryDTO::getId).collect(Collectors.toList())).collectList().block();
-		register(rooms);
-
-		return rooms;
-	}
-
-	@Override
-	protected List<RoomDetailsDTO> fetch(List<Long> ids) {
-		return api.getAllRoomsById(ids).collectList().block();
-	}
-
-	@Override
-	protected Long getId(RoomDetailsDTO dto) {
-		return dto.getId();
-	}
-}
diff --git a/src/main/java/nl/tudelft/queue/cache/StudentGroupCacheManager.java b/src/main/java/nl/tudelft/queue/cache/StudentGroupCacheManager.java
deleted file mode 100644
index 286f15067d80c7ca1a45d5f23d909926c33e5e3a..0000000000000000000000000000000000000000
--- a/src/main/java/nl/tudelft/queue/cache/StudentGroupCacheManager.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Queue - A Queueing system that can be used to handle labs in higher education
- * Copyright (C) 2016-2021  Delft University of Technology
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-package nl.tudelft.queue.cache;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-import lombok.AllArgsConstructor;
-import lombok.NoArgsConstructor;
-import nl.tudelft.labracore.api.StudentGroupControllerApi;
-import nl.tudelft.labracore.api.dto.RolePersonLayer1DTO;
-import nl.tudelft.labracore.api.dto.StudentGroupDetailsDTO;
-import nl.tudelft.labracore.api.dto.StudentGroupSummaryDTO;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.lang.Nullable;
-import org.springframework.stereotype.Component;
-import org.springframework.web.context.annotation.RequestScope;
-
-@Component
-@RequestScope
-@NoArgsConstructor
-@AllArgsConstructor
-public class StudentGroupCacheManager extends CoreCacheManager<Long, StudentGroupDetailsDTO> {
-	private final Map<Long, List<StudentGroupDetailsDTO>> attemptedPersons = new HashMap<>();
-
-	@Autowired
-	private StudentGroupControllerApi api;
-
-	@Autowired
-	private PersonCacheManager pCache;
-
-	/**
-	 * Gets all student groups of a single person by their id.
-	 *
-	 * @param  personId The id of the person to lookup student groups for.
-	 * @return          The list of student group details fetched for this person.
-	 */
-	public List<StudentGroupDetailsDTO> getByPerson(Long personId) {
-		if (attemptedPersons.containsKey(personId)) {
-			return attemptedPersons.get(personId);
-		}
-
-		var sgs = fetch(api.getGroupsForPerson(personId)
-				.map(StudentGroupSummaryDTO::getId)
-				.collectList().block());
-		attemptedPersons.put(personId, sgs);
-
-		return sgs;
-	}
-
-	@Override
-	protected List<StudentGroupDetailsDTO> fetch(@Nullable List<Long> ids) {
-		if (ids == null || ids.isEmpty()) {
-			return List.of();
-		}
-		return api.getStudentGroupsById(ids).collectList().block();
-	}
-
-	@Override
-	protected Long getId(StudentGroupDetailsDTO dto) {
-		return dto.getId();
-	}
-
-	@Override
-	protected void registerAdditionally(StudentGroupDetailsDTO dto) {
-		pCache.register(Objects.requireNonNull(dto.getMembers()).stream()
-				.map(RolePersonLayer1DTO::getPerson).collect(Collectors.toList()));
-	}
-}
diff --git a/src/main/java/nl/tudelft/queue/cache/TimedCacheManager.java b/src/main/java/nl/tudelft/queue/cache/TimedCacheManager.java
deleted file mode 100644
index c4fd1645d4ebafa1878e150ea0422f10527c9801..0000000000000000000000000000000000000000
--- a/src/main/java/nl/tudelft/queue/cache/TimedCacheManager.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Queue - A Queueing system that can be used to handle labs in higher education
- * Copyright (C) 2016-2021  Delft University of Technology
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-package nl.tudelft.queue.cache;
-
-public abstract class TimedCacheManager<ID, DTO> extends CoreCacheManager<ID, DTO> {
-	/**
-	 * The default timeout of the timed cache.
-	 */
-	public static final long DEFAULT_TIMEOUT = 60000L;
-
-	/**
-	 * The set timeout of the timed cache in milliseconds. This many milliseconds after the last cache
-	 * invalidation, this cache is invalidated again.
-	 */
-	private final long timeout;
-
-	/**
-	 * The last time in milliseconds since epoch (system-time) that this cache was (in)validated.
-	 * {@link #timeout} milliseconds after this timestamp, the cache is invalidated (again).
-	 */
-	private long lastValidation = System.currentTimeMillis();
-
-	/**
-	 * Constructs a new timed cache with the timeout set to {@link #DEFAULT_TIMEOUT}.
-	 */
-	public TimedCacheManager() {
-		this.timeout = DEFAULT_TIMEOUT;
-	}
-
-	/**
-	 * Constructs a new timed cache with the timeout set to the given timeout.
-	 *
-	 * @param timeout The timeout to set internally.
-	 */
-	public TimedCacheManager(long timeout) {
-		this.timeout = timeout;
-	}
-
-	@Override
-	protected synchronized void validateCache() {
-		long now = System.currentTimeMillis();
-		if (lastValidation + timeout < now) {
-			cache.clear();
-			empties.clear();
-			lastValidation = now;
-		}
-	}
-}
diff --git a/src/main/java/nl/tudelft/queue/config/LibradorConfig.java b/src/main/java/nl/tudelft/queue/config/LibradorConfig.java
index 9e3e93df89f34e855c10d44e78e9d589bacfd25e..156a104f29954334255e380cc98e2b794b5d1cd3 100644
--- a/src/main/java/nl/tudelft/queue/config/LibradorConfig.java
+++ b/src/main/java/nl/tudelft/queue/config/LibradorConfig.java
@@ -34,6 +34,7 @@ import org.springframework.context.annotation.Configuration;
 @EnableLibrador
 @Configuration
 public class LibradorConfig {
+
 	/**
 	 * Bean for the {@link IdMapperBuilder} that is used by Librador to create proper conversions within the
 	 * used {@link org.modelmapper.ModelMapper}.
diff --git a/src/main/java/nl/tudelft/queue/controller/AdminController.java b/src/main/java/nl/tudelft/queue/controller/AdminController.java
index d97482e0df5116397796e7ad4ad11c71afeadb61..f70b1eb401fc6ec1b3d7805a296fe8f265633f73 100644
--- a/src/main/java/nl/tudelft/queue/controller/AdminController.java
+++ b/src/main/java/nl/tudelft/queue/controller/AdminController.java
@@ -27,10 +27,10 @@ import nl.tudelft.labracore.api.EditionControllerApi;
 import nl.tudelft.labracore.api.ProgramControllerApi;
 import nl.tudelft.labracore.api.dto.*;
 import nl.tudelft.labracore.api.dto.CourseCreateDTO;
+import nl.tudelft.labracore.lib.cache.EditionCacheManager;
+import nl.tudelft.labracore.lib.cache.SessionCacheManager;
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
-import nl.tudelft.labracore.lib.security.user.Person;
-import nl.tudelft.queue.cache.EditionCacheManager;
-import nl.tudelft.queue.cache.SessionCacheManager;
 import nl.tudelft.queue.dto.create.QueueCourseCreateDTO;
 import nl.tudelft.queue.model.labs.Lab;
 import nl.tudelft.queue.repository.LabRepository;
diff --git a/src/main/java/nl/tudelft/queue/controller/AssignmentController.java b/src/main/java/nl/tudelft/queue/controller/AssignmentController.java
index ca8421552088e8a66be989a7231fa5083bb82434..097f0c89fd81bb02711def088a2009a1fb1e4b9b 100644
--- a/src/main/java/nl/tudelft/queue/controller/AssignmentController.java
+++ b/src/main/java/nl/tudelft/queue/controller/AssignmentController.java
@@ -19,20 +19,22 @@ package nl.tudelft.queue.controller;
 
 import nl.tudelft.labracore.api.AssignmentControllerApi;
 import nl.tudelft.labracore.api.dto.AssignmentCreateDTO;
+import nl.tudelft.labracore.api.dto.AssignmentPatchDTO;
 import nl.tudelft.labracore.api.dto.EditionDetailsDTO;
 import nl.tudelft.labracore.api.dto.ModuleDetailsDTO;
-import nl.tudelft.queue.cache.AssignmentCacheManager;
-import nl.tudelft.queue.cache.EditionCacheManager;
-import nl.tudelft.queue.cache.ModuleCacheManager;
+import nl.tudelft.labracore.lib.cache.AssignmentCacheManager;
+import nl.tudelft.labracore.lib.cache.EditionCacheManager;
+import nl.tudelft.labracore.lib.cache.ModuleCacheManager;
+import nl.tudelft.labracore.lib.data.Assignment;
+import nl.tudelft.labracore.lib.data.Module;
 import nl.tudelft.queue.dto.create.QueueAssignmentCreateDTO;
+import nl.tudelft.queue.dto.patch.QueueAssignmentPatchDTO;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Controller;
 import org.springframework.ui.Model;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.*;
 
 @Controller
 public class AssignmentController {
@@ -49,89 +51,53 @@ public class AssignmentController {
 	@Autowired
 	private AssignmentControllerApi aApi;
 
-	/**
-	 * Gets the page for creating an assignment. This is a basic page with only the most basic of properties
-	 * for an assignment. Further adjustments or creation options should be made available in Portal or later
-	 * in Queue.
-	 *
-	 * @param  moduleId The id of the module to add the assignment to.
-	 * @param  model    The model to fill out for Thymeleaf template resolution.
-	 * @return          The Thymeleaf template to resolve.
-	 */
-	@GetMapping("/module/{moduleId}/assignment/create")
-	@PreAuthorize("@permissionService.canManageModule(#moduleId)")
-	public String getAssignmentCreatePage(@PathVariable Long moduleId,
-			Model model) {
-		return addCreateAssignmentAttributes(moduleId, new QueueAssignmentCreateDTO(moduleId), model);
-	}
-
 	/**
 	 * Handles a Post request to add a new assignment. The assignment is passed through a
 	 * {@link QueueAssignmentCreateDTO}. After adding the assignment successfully, the user is redirected back
-	 * to the edition/modules page. If something goes wrong during conversion, the user is instead directed
-	 * back to the assignment create page.
+	 * to the edition/modules page.
 	 *
-	 * @param  mId   The id of the module to add the assignment to.
-	 * @param  dto   The dto containing information for creating the assignment.
-	 * @param  model The model to fill out for Thymeleaf template resolution.
-	 * @return       A redirect back to the edition/modules page.
+	 * @param  dto The dto containing information for creating the assignment.
+	 * @return     A redirect back to the edition/modules page.
 	 */
-	@PostMapping("/module/{mId}/assignment/create")
-	@PreAuthorize("@permissionService.canManageModule(#mId)")
-	public String createAssignment(@PathVariable Long mId,
-			QueueAssignmentCreateDTO dto,
-			Model model) {
-		dto.setModuleId(mId);
-
+	@PostMapping("/assignment")
+	@PreAuthorize("@permissionService.canManageModule(#dto.module)")
+	public String createAssignment(QueueAssignmentCreateDTO dto) {
 		AssignmentCreateDTO create = dto.apply();
-		if (dto.hasErrors()) {
-			return addCreateAssignmentAttributes(mId, dto, model);
-		}
-
 		aApi.addAssignment(create).block();
-
-		ModuleDetailsDTO module = mCache.getOrThrow(mId);
-		return "redirect:/edition/" + module.getEdition().getId() + "/modules";
+		return "redirect:/edition/" + Module.byId(dto.getModule()).getEdition().value().getId() + "/modules";
 	}
 
 	/**
-	 * Gets the assignment removal page. This page is simply to confirm whether the user really wants to
-	 * delete the assignment with the given id.
+	 * Handles a Patch request to add a new assignment. The assignment is passed through a
+	 * {@link QueueAssignmentCreateDTO}. After patching the assignment successfully, the user is redirected
+	 * back to the edition/modules page.
 	 *
-	 * @param  assignmentId The id of the assignment that the user might want to delete.
-	 * @param  model        The model to fill out for Thymeleaf template resolution.
-	 * @return              The Thymeleaf template to resolve.
+	 * @param  dto The dto containing information for creating the assignment.
+	 * @return     A redirect back to the edition/modules page.
 	 */
-	@GetMapping("/assignment/{assignmentId}/remove")
-	@PreAuthorize("@permissionService.canManageAssignment(#assignmentId)")
-	public String getAssignmentRemovePage(@PathVariable Long assignmentId,
-			Model model) {
-		var assignment = aCache.getOrThrow(assignmentId);
-		var module = mCache.getOrThrow(assignment.getModule().getId());
-		var edition = eCache.getOrThrow(module.getEdition().getId());
-
-		model.addAttribute("edition", edition);
-		model.addAttribute("assignment", assignment);
-
-		return "assignment/remove";
+	@PatchMapping("/assignment")
+	@PreAuthorize("@permissionService.canManageAssignment(#dto.assignment)")
+	public String editAssignment(QueueAssignmentPatchDTO dto) {
+		AssignmentPatchDTO patch = dto.apply();
+		aApi.patchAssignment(dto.getAssignment(), patch).block();
+		return "redirect:/edition/"
+				+ Assignment.byId(dto.getAssignment()).getModule().flatMap(Module::getEdition).get().getId()
+				+ "/modules";
 	}
 
 	/**
 	 * Deletes the assignment with the given id and redirects back to the modules page for the relevant
 	 * edition.
 	 *
-	 * @param  assignmentId The id of the assignment to delete.
-	 * @return              A redirect back to the edition/modules page.
+	 * @param  id The id of the assignment to delete.
+	 * @return    A redirect back to the edition/modules page.
 	 */
-	@PostMapping("/assignment/{assignmentId}/remove")
-	@PreAuthorize("@permissionService.canManageAssignment(#assignmentId)")
-	public String removeAssignment(@PathVariable Long assignmentId) {
-		var assignment = aCache.getOrThrow(assignmentId);
-		var module = mCache.getOrThrow(assignment.getModule().getId());
-
-		aApi.deleteAssignment(assignmentId).block();
-
-		return "redirect:/edition/" + module.getEdition().getId() + "/modules";
+	@DeleteMapping("/assignment")
+	@PreAuthorize("@permissionService.canManageAssignment(#id)")
+	public String removeAssignment(@RequestParam Long id) {
+		aApi.deleteAssignment(id).block();
+		return "redirect:/edition/" + Assignment.byId(id).getModule().get().getEdition().get().getId()
+				+ "/modules";
 	}
 
 	/**
diff --git a/src/main/java/nl/tudelft/queue/cache/AssignmentCacheManager.java b/src/main/java/nl/tudelft/queue/controller/BuildingController.java
similarity index 51%
rename from src/main/java/nl/tudelft/queue/cache/AssignmentCacheManager.java
rename to src/main/java/nl/tudelft/queue/controller/BuildingController.java
index 5dbe488d4320dbdce903517d2acd743b63183c6b..609b75f01f3625645c23962d289f86388a9b4d74 100644
--- a/src/main/java/nl/tudelft/queue/cache/AssignmentCacheManager.java
+++ b/src/main/java/nl/tudelft/queue/controller/BuildingController.java
@@ -15,30 +15,30 @@
  * You should have received a copy of the GNU Affero General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
-package nl.tudelft.queue.cache;
+package nl.tudelft.queue.controller;
 
-import java.util.List;
+import nl.tudelft.labracore.api.dto.BuildingDetailsDTO;
+import nl.tudelft.labracore.lib.data.Building;
 
-import nl.tudelft.labracore.api.AssignmentControllerApi;
-import nl.tudelft.labracore.api.dto.AssignmentDetailsDTO;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
 
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.web.context.annotation.RequestScope;
+@Controller
+@RequestMapping("/building")
+public class BuildingController {
 
-@Component
-@RequestScope
-public class AssignmentCacheManager extends CoreCacheManager<Long, AssignmentDetailsDTO> {
-	@Autowired
-	private AssignmentControllerApi api;
-
-	@Override
-	protected List<AssignmentDetailsDTO> fetch(List<Long> ids) {
-		return api.getAllAssignmentsById(ids).collectList().block();
+	/**
+	 * Gets the details of a building.
+	 *
+	 * @param  id The id of the building
+	 * @return    The details of the building
+	 */
+	@GetMapping("/{id}")
+	public @ResponseBody BuildingDetailsDTO getBuilding(@PathVariable Long id) {
+		return Building.byId(id).getDto();
 	}
 
-	@Override
-	protected Long getId(AssignmentDetailsDTO dto) {
-		return dto.getId();
-	}
 }
diff --git a/src/main/java/nl/tudelft/queue/controller/EditionController.java b/src/main/java/nl/tudelft/queue/controller/EditionController.java
index 27e431c762304cc02c38c0867fb4707c6accdb63..3d4060f2ba54145c326ac191674eceba75564057 100644
--- a/src/main/java/nl/tudelft/queue/controller/EditionController.java
+++ b/src/main/java/nl/tudelft/queue/controller/EditionController.java
@@ -22,8 +22,8 @@ import static nl.tudelft.labracore.lib.LabracoreApiUtil.fromPageable;
 import static nl.tudelft.queue.PageUtil.toPage;
 
 import java.io.IOException;
-import java.util.Comparator;
-import java.util.List;
+import java.time.LocalDateTime;
+import java.util.*;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
@@ -32,15 +32,14 @@ import javax.validation.Valid;
 
 import nl.tudelft.labracore.api.*;
 import nl.tudelft.labracore.api.dto.*;
+import nl.tudelft.labracore.lib.cache.*;
+import nl.tudelft.labracore.lib.data.Edition;
+import nl.tudelft.labracore.lib.data.Module;
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
-import nl.tudelft.labracore.lib.security.user.Person;
 import nl.tudelft.librador.dto.view.View;
-import nl.tudelft.queue.cache.*;
-import nl.tudelft.queue.csv.EmptyCsvException;
-import nl.tudelft.queue.csv.InvalidCsvException;
-import nl.tudelft.queue.dto.create.CourseRequestCreateDTO;
 import nl.tudelft.queue.dto.create.QueueEditionCreateDTO;
-import nl.tudelft.queue.dto.create.QueueRoleCreateDTO;
+import nl.tudelft.queue.dto.patch.QueueEditionPatchDTO;
 import nl.tudelft.queue.dto.util.EditionFilterDTO;
 import nl.tudelft.queue.dto.view.QueueSessionSummaryDTO;
 import nl.tudelft.queue.model.LabRequest;
@@ -67,9 +66,6 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Controller;
 import org.springframework.ui.Model;
 import org.springframework.web.bind.annotation.*;
-import org.springframework.web.multipart.MultipartFile;
-import org.springframework.web.reactive.function.client.WebClientResponseException;
-import org.springframework.web.servlet.mvc.support.RedirectAttributes;
 
 @Controller
 public class EditionController {
@@ -101,6 +97,9 @@ public class EditionController {
 	@Autowired
 	private ModuleControllerApi mApi;
 
+	@Autowired
+	private BuildingControllerApi bApi;
+
 	@Autowired
 	private CourseCacheManager cCache;
 
@@ -154,19 +153,12 @@ public class EditionController {
 			@PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable pageable,
 			Model model) {
 		var filter = es.getFilter("/editions");
-		var editions = eApi
-				.getEditionsPageActiveOrTaughtBy(person.getId(), fromPageable(pageable), filter.getPrograms(),
-						filter.getNameSearch())
-				.block();
+		fillEditionListModel(person, pageable, filter, model);
 
-		eCache.register(editions.getContent());
-		erCache.get(editions.getContent().stream().map(EditionDetailsDTO::getId));
-
-		model.addAttribute("editions",
-				new PageImpl<>(editions.getContent(), pageable, editions.getTotalElements()));
-		model.addAttribute("programs", cCache.getAll()
-				.stream().map(CourseDetailsDTO::getProgram).distinct()
-				.collect(Collectors.toList()));
+		model.addAttribute("programs",
+				cCache.get(cApi.getAllCourses().map(CourseSummaryDTO::getId).collectList().block())
+						.stream().map(CourseDetailsDTO::getProgram).distinct()
+						.collect(Collectors.toList())); // TODO
 		model.addAttribute("filter", filter);
 
 		model.addAttribute("page", "catalog");
@@ -175,53 +167,70 @@ public class EditionController {
 	}
 
 	/**
-	 * Submits the programme filters for the editions page.
+	 * Gets the correct edition view, i.e. modules if an edition has no modules or no assignments, labs
+	 * otherwise or if the user is a student.
 	 *
-	 * @param  programs The programmes to filter on
-	 * @return          The page to load
+	 * @return A redirect to the correct view.
 	 */
-	@PostMapping("/editions/filter")
-	public String submitFilters(@RequestParam(required = false) List<Long> programs,
-			@RequestParam(required = false) String nameSearch,
-			RedirectAttributes redirectAttributes) {
-		es.storeFilter(new EditionFilterDTO(programs, nameSearch), "/editions");
-		redirectAttributes.addAttribute("page", 0);
-		return "redirect:/editions";
+	@GetMapping("/edition/{id}")
+	public String getEditionView(@AuthenticatedPerson Person person, @PathVariable Long id) {
+		if (!ps.canViewEdition(id)) {
+			return "redirect:/edition/{id}/staff";
+		}
+		Edition edition = Edition.byId(id);
+		List<Module> modules = Module
+				.byIds(edition.getModules().value().stream().map(Module::getId).toList());
+		if (edition.getSessions().value().isEmpty() ||
+				((edition.getModules().value().isEmpty()
+						|| modules.stream().allMatch(m -> m.getAssignments().value().isEmpty()))
+						&& ps.canManageModules(id))) {
+			return "redirect:/edition/{id}/modules";
+		}
+		return "redirect:/edition/{id}/labs";
 	}
 
 	/**
-	 * Gets the page for teachers to fill out when requesting a new course to be made.
+	 * Gets the list of editions, filtered, and with a pageable.
 	 *
-	 * @param  model The model to fill out for Thymeleaf template resolution.
-	 * @return       The Thymeleaf template to resolve.
+	 * @param  person   The currently authenticated person asking for the editions page.
+	 * @param  pageable The pageable object representing the current page and size to display.
+	 * @param  model    The model to fill out for Thymeleaf template resolution.
+	 * @return          The Thymeleaf template to resolve.
 	 */
-	@GetMapping("/editions/request-course")
-	@PreAuthorize("@permissionService.isAdminOrTeacher()")
-	public String getRequestCoursePage(Model model) {
-		model.addAttribute("dto", new CourseRequestCreateDTO());
+	@GetMapping("/editions/list")
+	public String getEditionListOnly(@AuthenticatedPerson Person person,
+			@PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable pageable,
+			Model model) {
+		var filter = es.getFilter("/editions");
+		fillEditionListModel(person, pageable, filter, model);
+		return "edition/table";
+	}
 
-		return "course/request";
+	private void fillEditionListModel(Person person, Pageable pageable, EditionFilterDTO filter,
+			Model model) {
+		var editions = eApi
+				.getEditionsPageActiveOrTaughtBy(person.getId(), fromPageable(pageable), filter.getPrograms(),
+						filter.getNameSearch())
+				.block();
+
+		eCache.register(editions.getContent());
+		erCache.get(editions.getContent().stream().map(EditionDetailsDTO::getId));
+
+		model.addAttribute("editions",
+				new PageImpl<>(editions.getContent(), pageable, editions.getTotalElements()));
 	}
 
 	/**
-	 * Gets the confirmation page for users that have the intention of enrolling in the given course edition.
-	 * Entrance into this page is given whenever the authenticated person does not have a role in the given
-	 * edition yet. If the person is already enrolled, they will be redirected to the catalog page.
+	 * Submits the programme filters for the editions page.
 	 *
-	 * @param  editionId The id of the edition the user wants to enrol in.
-	 * @param  model     The model to fill out for Thymeleaf template resolution.
-	 * @return           The Thymeleaf template to resolve.
+	 * @param  programs The programmes to filter on
+	 * @return          The page to load
 	 */
-	@GetMapping("/edition/{editionId}/enrol")
-	@PreAuthorize("@permissionService.canEnrolForEdition(#editionId) " +
-			"|| @permissionService.canViewEdition(#editionId)")
-	public String getEditionEnrolView(@PathVariable Long editionId, Model model) {
-		if (!ps.canEnrolForEdition(editionId)) {
-			return "redirect:/editions";
-		}
-		model.addAttribute("edition", eCache.getOrThrow(editionId));
-
-		return "edition/enrol";
+	@PostMapping("/editions/filter")
+	public ResponseEntity<Void> submitFilters(@RequestParam(required = false) List<Long> programs,
+			@RequestParam(required = false) String nameSearch) {
+		es.storeFilter(new EditionFilterDTO(programs, nameSearch), "/editions");
+		return ResponseEntity.ok().build();
 	}
 
 	/**
@@ -232,28 +241,35 @@ public class EditionController {
 	 * @param  editionId The id of the edition the user wants to enrol in.
 	 * @return           A redirect to the edition info page for the given edition edition.
 	 */
-	@PostMapping("/edition/{editionId}/enrol")
+	@PostMapping("/edition/enrol")
 	@PreAuthorize("@permissionService.canEnrolForEdition(#editionId)")
-	public String enrolForEdition(@AuthenticatedPerson Person person,
-			@PathVariable Long editionId) {
+	public String enrolForEdition(@AuthenticatedPerson Person person, @RequestParam Long editionId) {
 		eApi.addStudentsToEdition(editionId, List.of(person.getUsername()), false).block();
 
 		return "redirect:/edition/" + editionId;
 	}
 
 	/**
-	 * Gets the main course edition info panel page. This page shows the most generic information on a course
-	 * edition taken from Labracore.
+	 * Gets the course edition info panel on staff. This page should be shown to people that do not have
+	 * permission to edit the participants list and cannot see all students.
 	 *
-	 * @param  editionId The id of the edition to display.
+	 * @param  editionId The id of the edition for which to display participants.
 	 * @param  model     The model to fill out for Thymeleaf template resolution.
 	 * @return           The Thymeleaf template to resolve.
 	 */
-	@GetMapping("/edition/{editionId}")
-	public String getEditionView(@PathVariable Long editionId, Model model) {
+	@GetMapping("/edition/{editionId}/staff")
+	public String getEditionStaffView(@PathVariable Long editionId, Model model) {
+		var roles = erCache.getOrThrow(editionId).getRoles().stream()
+				.sorted(Comparator.comparing(r -> r.getPerson().getDisplayName()))
+				.toList();
 		model.addAttribute("edition", eCache.getOrThrow(editionId));
-
-		return "edition/view/info";
+		model.addAttribute("teachers",
+				roles.stream().filter(r -> r.getType() == RolePersonDetailsDTO.TypeEnum.TEACHER).toList());
+		model.addAttribute("headTas",
+				roles.stream().filter(r -> r.getType() == RolePersonDetailsDTO.TypeEnum.HEAD_TA).toList());
+		model.addAttribute("tas",
+				roles.stream().filter(r -> r.getType() == RolePersonDetailsDTO.TypeEnum.TA).toList());
+		return "edition/view/staff";
 	}
 
 	/**
@@ -266,20 +282,54 @@ public class EditionController {
 	 */
 	@GetMapping("/edition/{editionId}/participants")
 	@PreAuthorize("@permissionService.canManageParticipants(#editionId)")
-	public String getEditionParticipantsView(@PathVariable Long editionId, Model model,
-			@RequestParam(value = "student-search", required = false) String studentSearch,
+	public String getEditionParticipantsView(@AuthenticatedPerson Person person, @PathVariable Long editionId,
+			Model model,
+			@RequestParam(value = "search", required = false) String search,
+			@RequestParam(value = "roles", required = false) List<RolePersonDetailsDTO.TypeEnum> roleFilter,
 			@PageableDefault(size = 25) Pageable pageable) {
+		fillParticipantsModel(editionId, model, search, roleFilter, pageable);
+		model.addAttribute("courses", cApi.getAllCoursesByManager(person.getId()).collectList().block());
+		model.addAttribute("cohorts", cohortApi.getAllCohorts().collectList().block());
+		model.addAttribute("assignments",
+				mCache.get(eCache.getOrThrow(editionId).getModules().stream().map(ModuleSummaryDTO::getId))
+						.stream().flatMap(m -> m.getAssignments().stream()).toList());
+		return "edition/view/participants";
+	}
+
+	/**
+	 * Gets the course edition participants table.
+	 *
+	 * @param  editionId The id of the edition for which to display participants.
+	 * @param  model     The model to fill out for Thymeleaf template resolution.
+	 * @return           The Thymeleaf template to resolve.
+	 */
+	@GetMapping("/edition/{editionId}/participants/list")
+	@PreAuthorize("@permissionService.canManageParticipants(#editionId)")
+	public String getEditionParticipantsListOnly(@PathVariable Long editionId, Model model,
+			@RequestParam(value = "search", required = false) String search,
+			@RequestParam(value = "roles", required = false) List<RolePersonDetailsDTO.TypeEnum> roleFilter,
+			@PageableDefault(size = 25) Pageable pageable) {
+		fillParticipantsModel(editionId, model, search, roleFilter, pageable);
+		return "edition/view/participants_table";
+	}
+
+	private void fillParticipantsModel(Long editionId, Model model, String search,
+			List<RolePersonDetailsDTO.TypeEnum> roleFilter, Pageable pageable) {
 		var edition = eCache.getOrThrow(editionId);
-		var students = roleDTOService.students(edition);
+		var roles = erCache.getOrThrow(editionId).getRoles().stream()
+				.sorted(Comparator.comparing(r -> r.getPerson().getDisplayName()))
+				.sorted(Comparator.comparingInt((RolePersonDetailsDTO r) -> r.getType().ordinal()).reversed())
+				.toList();
 
-		if (studentSearch != null) {
-			students = es.studentsMatchingFilter(students, studentSearch);
+		if (roleFilter != null && !roleFilter.isEmpty()) {
+			roles = roles.stream().filter(r -> roleFilter.contains(r.getType())).toList();
+		}
+		if (search != null) {
+			roles = es.rolesMatchingFilter(roles, search);
 		}
 
 		model.addAttribute("edition", edition);
-		model.addAttribute("students", toPage(pageable, students));
-
-		return "edition/view/participants";
+		model.addAttribute("roles", toPage(pageable, roles));
 	}
 
 	/**
@@ -293,7 +343,8 @@ public class EditionController {
 	 */
 	@GetMapping("/edition/{editionId}/modules")
 	@PreAuthorize("@permissionService.canViewEdition(#editionId)")
-	public String getEditionModulesView(@PathVariable Long editionId, Model model) {
+	public String getEditionModulesView(@AuthenticatedPerson Person person, @PathVariable Long editionId,
+			Model model) {
 		var edition = eCache.getOrThrow(editionId);
 		var modules = mCache.get(edition.getModules().stream().map(ModuleSummaryDTO::getId));
 
@@ -311,6 +362,11 @@ public class EditionController {
 		model.addAttribute("groups", modules.stream()
 				.collect(Collectors.toMap(ModuleDetailsDTO::getId,
 						m -> sgCache.get(m.getGroups().stream().map(StudentGroupSmallSummaryDTO::getId)))));
+		model.addAttribute("courses", cApi.getAllCoursesByManager(person.getId()).collectList().block());
+		model.addAttribute("cohorts", cohortApi.getAllCohorts().collectList().block());
+		model.addAttribute("assignments",
+				mCache.get(eCache.getOrThrow(editionId).getModules().stream().map(ModuleSummaryDTO::getId))
+						.stream().flatMap(m -> m.getAssignments().stream()).toList());
 
 		return "edition/view/modules";
 	}
@@ -326,10 +382,42 @@ public class EditionController {
 	 */
 	@GetMapping("/edition/{editionId}/labs")
 	@PreAuthorize("@permissionService.canViewEdition(#editionId)")
-	public String getEditionSessionsView(@PathVariable Long editionId,
+	public String getEditionSessionsView(@AuthenticatedPerson Person person, @PathVariable Long editionId,
+			@RequestParam(required = false, defaultValue = "") List<QueueSessionType> queueSessionTypes,
+			@RequestParam(required = false, defaultValue = "") List<Long> modules,
+			Model model) {
+		fillSessionsModel(editionId, queueSessionTypes, modules, model);
+		model.addAttribute("courses", cApi.getAllCoursesByManager(person.getId()).collectList().block());
+		model.addAttribute("cohorts", cohortApi.getAllCohorts().collectList().block());
+		model.addAttribute("assignments",
+				mCache.get(eCache.getOrThrow(editionId).getModules().stream().map(ModuleSummaryDTO::getId))
+						.stream().flatMap(m -> m.getAssignments().stream()).toList());
+		model.addAttribute("buildings", bApi.getAllBuildings().collectList().block());
+		model.addAttribute("clusters", Edition.byId(editionId).getCohort().get().getClusters().value());
+
+		return "edition/view/labs";
+	}
+
+	/**
+	 * Gets the course edition labs tables.
+	 *
+	 * @param  editionId The id of the edition for which to display labs.
+	 * @param  model     The model to fill out for Thymeleaf template resolution.
+	 * @return           The Thymeleaf template to resolve.
+	 */
+	@GetMapping("/edition/{editionId}/labs/list")
+	@PreAuthorize("@permissionService.canViewEdition(#editionId)")
+	public String getEditionSessionsViewListOnly(@PathVariable Long editionId,
 			@RequestParam(required = false, defaultValue = "") List<QueueSessionType> queueSessionTypes,
 			@RequestParam(required = false, defaultValue = "") List<Long> modules,
 			Model model) {
+		fillSessionsModel(editionId, queueSessionTypes, modules, model);
+
+		return "edition/view/labs_tables";
+	}
+
+	private void fillSessionsModel(Long editionId, List<QueueSessionType> queueSessionTypes,
+			List<Long> modules, Model model) {
 		var edition = eCache.getOrThrow(editionId);
 
 		// Make sure that session details are cached.
@@ -337,21 +425,24 @@ public class EditionController {
 				.map(SessionSummaryDTO::getId));
 
 		// Find all labs from the sessions, convert to summaries, and filter.
-		var labs = es.filterLabs(
+		List<QueueSessionSummaryDTO> labs = es.filterLabs(
 				getLabSummariesFromSessions(sessions, s -> true),
-				queueSessionTypes, modules);
+				queueSessionTypes, modules)
+				.stream().sorted(Comparator.comparing(l -> l.getSlot().getOpensAt()))
+				.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
 
-		// Sort all labs
-		labs = es.sortLabs(labs);
+		var now = LocalDateTime.now();
 
 		model.addAttribute("edition", edition);
-		model.addAttribute("labs", labs);
-		model.addAttribute("allLabTypes", QueueSessionType.values());
+		model.addAttribute("currentSessions", labs.stream().filter(l -> l.getSlot().open()).toList());
+		model.addAttribute("upcomingSessions",
+				labs.stream().filter(l -> l.getSlot().getOpensAt().isAfter(now)).toList());
+		Collections.reverse(labs);
+		model.addAttribute("pastSessions",
+				labs.stream().filter(l -> l.getSlot().getClosesAt().isBefore(now)).toList());
 		model.addAttribute("queueSessionTypes", queueSessionTypes);
 		model.addAttribute("allModules", edition.getModules());
 		model.addAttribute("modules", modules);
-
-		return "edition/view/labs";
 	}
 
 	/**
@@ -363,129 +454,19 @@ public class EditionController {
 	 */
 	@GetMapping("/edition/{editionId}/questions")
 	@PreAuthorize("@permissionService.canManageQuestions(#editionId)")
-	public String getEditionQuestions(@PathVariable Long editionId, Model model) {
+	public String getEditionQuestions(@AuthenticatedPerson Person person, @PathVariable Long editionId,
+			Model model) {
 		model.addAttribute("edition", eCache.getOrThrow(editionId));
 		model.addAttribute("questions", qApi.getQuestionsByEdition(editionId).collectList().block());
+		model.addAttribute("courses", cApi.getAllCoursesByManager(person.getId()).collectList().block());
+		model.addAttribute("cohorts", cohortApi.getAllCohorts().collectList().block());
+		model.addAttribute("assignments",
+				mCache.get(eCache.getOrThrow(editionId).getModules().stream().map(ModuleSummaryDTO::getId))
+						.stream().flatMap(m -> m.getAssignments().stream()).toList());
 
 		return "edition/view/questions";
 	}
 
-	/**
-	 * Gets the participant add page. This page is used to add new participants to an edition.
-	 *
-	 * @param  editionId The id of the edition to which a participant should be added.
-	 * @param  model     The model to fill out for Thymeleaf template resolution.
-	 * @return           The Thymeleaf template to resolve.
-	 */
-	@GetMapping("/edition/{editionId}/participants/create")
-	@PreAuthorize("@permissionService.canManageParticipants(#editionId)")
-	public String getAddParticipantPage(@PathVariable Long editionId, Model model) {
-		model.addAttribute("edition", eCache.getOrThrow(editionId));
-		model.addAttribute("role", new QueueRoleCreateDTO(editionId));
-
-		return "edition/create/participant";
-	}
-
-	/**
-	 * Posts a single participant addition to the server. This action comes from the participant add page.
-	 *
-	 * @param  editionId The id of the edition to which a participant should be added.
-	 * @param  model     The model to fill out for Thymeleaf template resolution.
-	 * @return           The Thymeleaf template to resolve.
-	 */
-	@PostMapping("/edition/{editionId}/participants/create")
-	@PreAuthorize("@permissionService.canManageParticipants(#editionId)")
-	public String createParticipant(@PathVariable Long editionId, Model model,
-			QueueRoleCreateDTO dto, RedirectAttributes redirectAttributes) {
-		// TODO: Figure out a way to get rid of these additional sets
-		dto.setEditionId(editionId);
-
-		RoleCreateDTO create = dto.apply();
-		if (dto.hasErrors()) {
-			model.addAttribute("edition", eCache.getOrThrow(editionId));
-			model.addAttribute("role", dto);
-
-			return "edition/create/participant";
-		}
-
-		try {
-			pApi.getPersonByUsername(dto.getUsername())
-					.flatMap(person -> rApi.addRole(create.person(new PersonIdDTO().id(person.getId()))))
-					.block();
-		} catch (WebClientResponseException e) {
-			redirectAttributes.addFlashAttribute("error",
-					String.format("Person with username: %s cannot be found. " +
-							"Make sure this person has logged into our system before",
-							dto.getUsername()));
-			return "redirect:/edition/" + editionId + "/participants/create";
-		}
-
-		return "redirect:/edition/" + editionId + "/participants";
-	}
-
-	/**
-	 * Adds new participants to the course based on a CSV file that was uploaded by the user.
-	 *
-	 * @param  editionId  The id of the edition to which the users must be added.
-	 * @param  csv        A CSV file containing both the netId and their role in the course.
-	 * @param  attributes Used to pass information when redirecting to Thymeleaf.
-	 * @return            A redirect to either the participants page when successfull, otherwise redirect ot
-	 *                    create participants page
-	 */
-	@PostMapping("/edition/{editionId}/participants/import")
-	@PreAuthorize("@permissionService.canManageTeachers(#editionId)")
-	public String importParticipants(@PathVariable Long editionId,
-			@RequestParam("file") MultipartFile csv,
-			RedirectAttributes attributes) {
-		EditionDetailsDTO edition = eCache.getOrThrow(editionId);
-
-		try {
-			es.addCourseParticipants(csv, edition);
-			return "redirect:/edition/" + editionId + "/participants";
-		} catch (EmptyCsvException | InvalidCsvException e) {
-			attributes.addFlashAttribute("error", "We were unable to add parse the CSV file. ");
-		}
-
-		return "redirect:/edition/" + editionId + "/participants/create";
-	}
-
-	/**
-	 * Gets the participant remove page. This page is used to remove a specific participant from the given
-	 * edition.
-	 *
-	 * @param  editionId The id of the edition from which the participant should be removed.
-	 * @param  personId  The id of the person who should be removed.
-	 * @param  model     The model to fill out for Thymeleaf template resolution.
-	 * @return           The Thymeleaf template to resolve.
-	 */
-	@GetMapping("/edition/{editionId}/participants/{personId}/remove")
-	@PreAuthorize("@permissionService.canManageParticipants(#editionId)")
-	public String getRemoveParticipantPage(@PathVariable Long editionId, @PathVariable Long personId,
-			Model model) {
-		EditionDetailsDTO edition = eCache.getOrThrow(editionId);
-
-		model.addAttribute("edition", edition);
-		model.addAttribute("role", rCache.getOrThrow(new Id()
-				.personId(personId).editionId(editionId)));
-
-		return "edition/remove/participant";
-	}
-
-	/**
-	 * Removes a participant with the given personId from the given edition.
-	 *
-	 * @param  editionId The id of the edition from which the participant should be removed.
-	 * @param  personId  The id of the person who should be removed.
-	 * @return           A redirect to the edition participants overview.
-	 */
-	@PostMapping("/edition/{editionId}/participants/{personId}/remove")
-	@PreAuthorize("@permissionService.canManageParticipants(#editionId)")
-	public String removeParticipant(@PathVariable Long editionId, @PathVariable Long personId) {
-		eApi.removePersonFromEdition(editionId, personId).block();
-
-		return "redirect:/edition/" + editionId + "/participants";
-	}
-
 	/**
 	 * Blocks a person with the given id from the given edition.
 	 *
@@ -501,22 +482,6 @@ public class EditionController {
 		return "redirect:/edition/" + editionId + "/participants";
 	}
 
-	/**
-	 * Gets the page to confirm for a student that they want to unenrol from the given course edition.
-	 *
-	 * @param  editionId The id of the edition from which a student wants to unenrol.
-	 * @param  model     The model to fill out for Thymeleaf template resolution.
-	 * @return           The Thymeleaf template to resolve.
-	 */
-	@GetMapping("/edition/{editionId}/leave")
-	@PreAuthorize("@permissionService.canLeaveEdition(#editionId)")
-	public String getParticipantLeavePage(@PathVariable Long editionId,
-			Model model) {
-		model.addAttribute("edition", eCache.getOrThrow(editionId));
-
-		return "edition/view/leave";
-	}
-
 	/**
 	 * Processes a POST request from the user wanting to leave the given edition. Instead of deleting the role
 	 * of the user from the database entirely, we opt to block their role and thus disallow them access to the
@@ -526,10 +491,9 @@ public class EditionController {
 	 * @param  editionId The id of the edition that the student wants to leave.
 	 * @return           A redirect back to the edition view page.
 	 */
-	@PostMapping("/edition/{editionId}/leave")
+	@PostMapping("/edition/leave")
 	@PreAuthorize("@permissionService.canLeaveEdition(#editionId)")
-	public String participantLeave(@AuthenticatedPerson Person user,
-			@PathVariable Long editionId) {
+	public String participantLeave(@AuthenticatedPerson Person user, @RequestParam Long editionId) {
 		eApi.removePersonFromEdition(editionId, user.getId()).block();
 
 		return "redirect:/edition/" + editionId;
@@ -584,35 +548,15 @@ public class EditionController {
 		return "edition/view/status :: #assistant-table";
 	}
 
-	/**
-	 *
-	 * @param  model The model to fill out for Thymeleaf template resolution.
-	 * @return       The thymeleaf template to resolve.
-	 */
-	@GetMapping("/edition/add")
-	@PreAuthorize("@permissionService.canCreateEdition()")
-	public String viewAddEdition(@AuthenticatedPerson Person person, Model model) {
-		QueueEditionCreateDTO edition = QueueEditionCreateDTO.builder().build();
-		model.addAttribute("edition", edition);
-
-		List<CourseSummaryDTO> courses = cApi.getAllCoursesByManager(person.getId()).collectList().block();
-		model.addAttribute("courses", courses);
-
-		List<CohortSummaryDTO> cohorts = cohortApi.getAllCohorts().collectList().block();
-		model.addAttribute("cohorts", cohorts);
-
-		return "edition/create";
-	}
-
 	/**
 	 * Creates a new edition for a specific course.
 	 *
 	 * @param  create The DTO used to create the edition
 	 * @return        Redirect to the course page.
 	 */
-	@PostMapping("/edition/add")
+	@PostMapping("/edition")
 	@PreAuthorize("@permissionService.canCreateEdition(#create.course)")
-	public String addEdition(@Valid @ModelAttribute("edition") QueueEditionCreateDTO create) {
+	public String addEdition(@Valid QueueEditionCreateDTO create) {
 		create.setEnrollability(EditionCreateDTO.EnrollabilityEnum.OPEN);
 		EditionCreateDTO edition = create.apply();
 
@@ -621,6 +565,22 @@ public class EditionController {
 		return "redirect:/edition/" + id;
 	}
 
+	/**
+	 * Patches an edition.
+	 *
+	 * @param  patch The DTO used to patch the edition
+	 * @return       Redirect to the course page.
+	 */
+	@PatchMapping("/edition")
+	@PreAuthorize("@permissionService.canManageEdition(#patch.edition)")
+	public String editEdition(@Valid QueueEditionPatchDTO patch) {
+		EditionPatchDTO edition = patch.apply();
+
+		Long id = eApi.patchEdition(patch.getEdition(), edition).block();
+
+		return "redirect:/edition/" + id;
+	}
+
 	/**
 	 * Searches for questions when creating a request.
 	 *
@@ -649,31 +609,6 @@ public class EditionController {
 		return "redirect:/edition/{editionId}";
 	}
 
-	/**
-	 * Gets the page to edit a question.
-	 *
-	 * @param  editionId  The id of the edition the question is for
-	 * @param  questionId The id of the question
-	 * @param  model      The model to add details to
-	 * @return            The page to load
-	 */
-	@GetMapping("/edition/{editionId}/questions/{questionId}/edit")
-	@PreAuthorize("@permissionService.canManageQuestions(#editionId)")
-	public String getEditQuestionPage(@PathVariable Long editionId, @PathVariable Long questionId,
-			Model model) {
-		EditionDetailsDTO edition = eCache.getOrThrow(editionId);
-
-		model.addAttribute("edition", edition);
-		model.addAttribute("question", qApi.getQuestionById(questionId).block());
-		model.addAttribute("assignments",
-				mApi.getModulesById(edition.getModules().stream().map(ModuleSummaryDTO::getId)
-						.collect(Collectors.toList())).collectList().block().stream()
-						.flatMap(m -> m.getAssignments().stream()).collect(Collectors.toList()));
-		model.addAttribute("dto", new QuestionPatchDTO().assignment(new AssignmentIdDTO()));
-
-		return "edition/edit/question";
-	}
-
 	/**
 	 * Edits a question.
 	 *
@@ -691,26 +626,6 @@ public class EditionController {
 		return "redirect:/edition/{editionId}/questions";
 	}
 
-	/**
-	 * Gets the delete question page
-	 *
-	 * @param  editionId  The id of the edition the question is for
-	 * @param  questionId The id of the question
-	 * @param  model      The model to add details to
-	 * @return            The page to load
-	 */
-	@GetMapping("/edition/{editionId}/questions/{questionId}/remove")
-	@PreAuthorize("@permissionService.canManageQuestions(#editionId)")
-	public String getDeleteQuestionPage(@PathVariable Long editionId, @PathVariable Long questionId,
-			Model model) {
-		EditionDetailsDTO edition = eCache.getOrThrow(editionId);
-
-		model.addAttribute("edition", edition);
-		model.addAttribute("question", qApi.getQuestionById(questionId).block());
-
-		return "edition/remove/question";
-	}
-
 	/**
 	 * Deletes a question.
 	 *
diff --git a/src/main/java/nl/tudelft/queue/controller/HistoryController.java b/src/main/java/nl/tudelft/queue/controller/HistoryController.java
index 2dbfaf9b23370602199206c63d3bfe9a8db09481..e0842dd523a07222d0f8e92e7e55abee9f6d7a5c 100644
--- a/src/main/java/nl/tudelft/queue/controller/HistoryController.java
+++ b/src/main/java/nl/tudelft/queue/controller/HistoryController.java
@@ -26,12 +26,12 @@ import nl.tudelft.labracore.api.dto.EditionDetailsDTO;
 import nl.tudelft.labracore.api.dto.RoleEditionDetailsDTO;
 import nl.tudelft.labracore.api.dto.SessionSummaryDTO;
 import nl.tudelft.labracore.api.dto.StudentGroupSummaryDTO;
+import nl.tudelft.labracore.lib.cache.EditionCacheManager;
+import nl.tudelft.labracore.lib.cache.EditionRolesCacheManager;
+import nl.tudelft.labracore.lib.cache.PersonCacheManager;
+import nl.tudelft.labracore.lib.cache.SessionCacheManager;
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
-import nl.tudelft.labracore.lib.security.user.Person;
-import nl.tudelft.queue.cache.EditionCacheManager;
-import nl.tudelft.queue.cache.EditionRolesCacheManager;
-import nl.tudelft.queue.cache.PersonCacheManager;
-import nl.tudelft.queue.cache.SessionCacheManager;
 import nl.tudelft.queue.repository.LabRequestRepository;
 import nl.tudelft.queue.repository.QueueSessionRepository;
 import nl.tudelft.queue.service.RequestTableService;
diff --git a/src/main/java/nl/tudelft/queue/controller/HomeController.java b/src/main/java/nl/tudelft/queue/controller/HomeController.java
index 207c71a074e565b4b9bf4715d175a7f6427057d3..0edb7885f1b78674baad380897c99d593f52a75f 100644
--- a/src/main/java/nl/tudelft/queue/controller/HomeController.java
+++ b/src/main/java/nl/tudelft/queue/controller/HomeController.java
@@ -26,17 +26,15 @@ import java.util.*;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
-import nl.tudelft.labracore.api.EditionControllerApi;
-import nl.tudelft.labracore.api.PersonControllerApi;
-import nl.tudelft.labracore.api.SessionControllerApi;
+import nl.tudelft.labracore.api.*;
 import nl.tudelft.labracore.api.dto.*;
+import nl.tudelft.labracore.lib.cache.EditionCacheManager;
+import nl.tudelft.labracore.lib.cache.EditionCollectionCacheManager;
+import nl.tudelft.labracore.lib.cache.PersonCacheManager;
+import nl.tudelft.labracore.lib.cache.SessionCacheManager;
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
-import nl.tudelft.labracore.lib.security.user.Person;
 import nl.tudelft.librador.dto.view.View;
-import nl.tudelft.queue.cache.EditionCacheManager;
-import nl.tudelft.queue.cache.EditionCollectionCacheManager;
-import nl.tudelft.queue.cache.PersonCacheManager;
-import nl.tudelft.queue.cache.SessionCacheManager;
 import nl.tudelft.queue.dto.view.FeedbackViewDTO;
 import nl.tudelft.queue.dto.view.QueueSessionSummaryDTO;
 import nl.tudelft.queue.model.Feedback;
@@ -78,6 +76,12 @@ public class HomeController {
 	@Autowired
 	private PersonControllerApi pApi;
 
+	@Autowired
+	private CourseControllerApi cApi;
+
+	@Autowired
+	private CohortControllerApi cohortApi;
+
 	@Autowired
 	private EditionCacheManager eCache;
 
@@ -238,6 +242,8 @@ public class HomeController {
 			model.addAttribute("finishedRoles", finishedRoles);
 			model.addAttribute("archivedRoles", archivedRoles);
 			model.addAttribute("sessions", calendarEntries);
+			model.addAttribute("courses", cApi.getAllCoursesByManager(person.getId()).collectList().block());
+			model.addAttribute("cohorts", cohortApi.getAllCohorts().collectList().block());
 
 			model.addAttribute("page", "my-courses");
 
@@ -293,13 +299,13 @@ public class HomeController {
 	private void fillInFeedbackModel(Long assistantId, Person person, Model model, Pageable pageable) {
 		var assistant = pCache.getOrThrow(assistantId);
 
-		Page<Feedback> feedback = assistantId.equals(person.getId())
-				? fr.findByAssistantAnonymised(assistantId, pageable)
-				: fr.findByAssistant(assistantId, pageable);
+		Page<Feedback> allFeedback = fr.findByAssistant(assistantId, pageable);
+		Page<Feedback> anonymisedFeedback = fr.findByAssistantAnonymised(assistantId, pageable);
+		Page<Feedback> feedback = assistantId.equals(person.getId()) ? anonymisedFeedback : allFeedback;
 
 		model.addAttribute("assistant", assistant);
-		model.addAttribute("feedback",
-				feedback.map(fb -> View.convert(fb, FeedbackViewDTO.class)));
+		model.addAttribute("feedbackHidden", feedback.getTotalElements() < allFeedback.getTotalElements());
+		model.addAttribute("feedback", feedback.map(fb -> View.convert(fb, FeedbackViewDTO.class)));
 		model.addAttribute("stars", fs.countRatings(assistantId));
 	}
 
diff --git a/src/main/java/nl/tudelft/queue/controller/LabController.java b/src/main/java/nl/tudelft/queue/controller/LabController.java
index 2810c8ae39424ad7a51c4754a4281c35edb268f2..e8e3d442039871146376cdbb76400dfcb0e4055a 100644
--- a/src/main/java/nl/tudelft/queue/controller/LabController.java
+++ b/src/main/java/nl/tudelft/queue/controller/LabController.java
@@ -17,14 +17,12 @@
  */
 package nl.tudelft.queue.controller;
 
+import static nl.tudelft.labracore.api.dto.RolePersonDetailsDTO.TypeEnum.*;
 import static nl.tudelft.queue.service.LabService.SessionType.REGULAR;
 import static nl.tudelft.queue.service.LabService.SessionType.SHARED;
 
 import java.io.IOException;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
 import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -33,11 +31,15 @@ import java.util.stream.Collectors;
 import javax.servlet.http.HttpServletRequest;
 import javax.transaction.Transactional;
 
+import nl.tudelft.labracore.api.BuildingControllerApi;
+import nl.tudelft.labracore.api.CohortControllerApi;
 import nl.tudelft.labracore.api.dto.*;
+import nl.tudelft.labracore.lib.cache.*;
+import nl.tudelft.labracore.lib.data.CoreEntity;
+import nl.tudelft.labracore.lib.data.Person;
+import nl.tudelft.labracore.lib.data.Session;
 import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
-import nl.tudelft.labracore.lib.security.user.Person;
 import nl.tudelft.librador.resolver.annotations.PathEntity;
-import nl.tudelft.queue.cache.*;
 import nl.tudelft.queue.dto.create.labs.CapacitySessionCreateDTO;
 import nl.tudelft.queue.dto.create.labs.ExamLabCreateDTO;
 import nl.tudelft.queue.dto.create.labs.RegularLabCreateDTO;
@@ -50,11 +52,11 @@ import nl.tudelft.queue.dto.patch.labs.CapacitySessionPatchDTO;
 import nl.tudelft.queue.dto.patch.labs.ExamLabPatchDTO;
 import nl.tudelft.queue.dto.patch.labs.RegularLabPatchDTO;
 import nl.tudelft.queue.dto.patch.labs.SlottedLabPatchDTO;
+import nl.tudelft.queue.dto.view.QueueSessionViewDTO;
 import nl.tudelft.queue.dto.view.RequestViewDTO;
 import nl.tudelft.queue.model.QueueSession;
 import nl.tudelft.queue.model.Request;
 import nl.tudelft.queue.model.embeddables.AllowedRequest;
-import nl.tudelft.queue.model.enums.QueueSessionType;
 import nl.tudelft.queue.model.enums.RequestType;
 import nl.tudelft.queue.model.enums.SelectionProcedure;
 import nl.tudelft.queue.model.labs.*;
@@ -101,6 +103,9 @@ public class LabController {
 	@Autowired
 	private EditionCacheManager eCache;
 
+	@Autowired
+	private EditionRolesCacheManager erCache;
+
 	@Autowired
 	private EditionCollectionCacheManager ecCache;
 
@@ -116,6 +121,23 @@ public class LabController {
 	@Autowired
 	private RoleDTOService roleService;
 
+	@Autowired
+	private BuildingControllerApi bApi;
+
+	@Autowired
+	private CohortControllerApi cohortApi;
+
+	/**
+	 * Gets session data as a JSON object.
+	 *
+	 * @param  qSession The session
+	 * @return          The JSON representation of the session
+	 */
+	@GetMapping("/lab/json/{qSession}")
+	public @ResponseBody QueueSessionViewDTO<?> getSession(@PathEntity QueueSession<?> qSession) {
+		return qSession.toViewDTO();
+	}
+
 	/**
 	 * Gets the page with information on the lab with the given id. This page is different for TAs than for
 	 * students. For students, this page shows their history of requests and their current requests. For TAs,
@@ -133,13 +155,15 @@ public class LabController {
 	public String getSessionView(@PathEntity QueueSession<?> qSession,
 			@AuthenticatedPerson Person person,
 			Model model) {
+		var filter = rts.checkAndStoreFilterDTO(null, "lab/" + qSession.getId());
+
 		setEnqueuePageAttributes(qSession, model, person);
 
 		// Either get all requests for the lab if the person is an assistant or just those specific to the person.
-		//noinspection Convert2MethodRef
+		var requests = ps.canViewSessionRequests(qSession.getId()) ? qSession.getRequests()
+				: qSession.getAllRequestsForPerson(person.getId());
 		model.addAttribute("requests",
-				rts.convertRequestsToView(ps.canViewSessionRequests(qSession.getId()) ? qSession.getRequests()
-						: qSession.getAllRequestsForPerson(person.getId()))
+				rts.convertRequestsToView(requests.stream().filter(filter::apply).toList())
 						.stream()
 						.sorted(Comparator.comparing((RequestViewDTO<?> r) -> r.getCreatedAt()).reversed())
 						.collect(Collectors.toList()));
@@ -148,10 +172,6 @@ public class LabController {
 		Optional<?> currOptRequest = qSession.getOpenRequestForPerson(person.getId());
 		if (currOptRequest.isPresent()) {
 			Request<?> currRequest = (Request<?>) currOptRequest.get();
-			// If the user is currently being processed it should be looking at the request page
-			if (currRequest.getEventInfo().getStatus().isProcessing()) {
-				return "redirect:/request/" + currRequest.getId();
-			}
 			model.addAttribute("current", currRequest.toViewDTO());
 			model.addAttribute("currentDto", new ModelMapper().map(currRequest, RequestPatchDTO.class));
 		}
@@ -162,8 +182,28 @@ public class LabController {
 					.ifPresent(r -> model.addAttribute("selectionResult", r.toViewDTO()));
 		}
 
+		model.addAttribute("filter", filter);
 		model.addAttribute("modules", mCache.get(qSession.getModules().stream()));
 
+		model.addAttribute("assistants",
+				erCache.get(sCache.getOrThrow(qSession.getSession()).getEditions().stream()
+						.map(EditionSummaryDTO::getId))
+						.stream().flatMap(e -> e.getRoles().stream())
+						.filter(r -> Set.of(TEACHER, HEAD_TA, TA).contains(r.getType()))
+						.map(RolePersonDetailsDTO::getPerson).collect(Collectors.toList()));
+
+		var editions = Session.byId(qSession.getSession()).getEditions().get();
+		model.addAttribute("cohorts", cohortApi.getAllCohorts().collectList().block());
+		model.addAttribute("allModules", mCache
+				.get(editions.stream().flatMap(e -> e.getModules().value().stream().map(CoreEntity::getId))));
+		model.addAttribute("allAssignments",
+				mCache.get(editions.stream().flatMap(e -> e.getDto().getModules().stream())
+						.map(ModuleSummaryDTO::getId))
+						.stream().flatMap(m -> m.getAssignments().stream()).toList());
+		model.addAttribute("buildings", bApi.getAllBuildings().collectList().block());
+		model.addAttribute("clusters",
+				editions.stream().flatMap(e -> e.getCohort().get().getClusters().value().stream()).toList());
+
 		return "lab/view/" + qSession.getType().name().toLowerCase();
 	}
 
@@ -176,7 +216,7 @@ public class LabController {
 	 * @return     A redirect to the lab overview page.
 	 */
 	@Transactional
-	@PostMapping("/lab/{lab}/capacities")
+	@PatchMapping("/lab/{lab}/capacities")
 	@PreAuthorize("@permissionService.canManageSession(#lab)")
 	public String editLabCapacities(@PathEntity Lab lab,
 			SlottedLabTimeSlotCapacityPatchDTO dto) {
@@ -341,190 +381,115 @@ public class LabController {
 		return "redirect:/lab/" + qSession.getId();
 	}
 
-	/**
-	 * Gets the lab creation page. This page should be viewable by all teachers and managers (those that are
-	 * allowed to create labs). This page allows for editing a lab before final creation of that lab.
-	 *
-	 * @param  editionId The id of the edition to create the lab in.
-	 * @param  model     The model to fill out for Thymeleaf template resolution.
-	 * @return           The Thymeleaf template to resolve.
-	 */
-	@GetMapping("/edition/{editionId}/lab/create")
-	@PreAuthorize("@permissionService.canManageSessions(#editionId)")
-	public String getLabCreateView(@PathVariable Long editionId,
-			@RequestParam QueueSessionType type, Model model) {
-		model.addAttribute("dto", type.newCreateDto());
-		model.addAttribute("edition", eCache.getOrThrow(editionId));
-		model.addAttribute("lType", type);
-
-		setSessionEditingPageAttributes(List.of(editionId), model);
-
-		return "lab/create/" + type.name().toLowerCase();
-	}
-
-	/**
-	 * Gets the lab creation page. This page should be viewable by all teachers and managers (those that are
-	 * allowed to create labs). This page allows for editing a lab before final creation of that lab.
-	 *
-	 * @param  editionCollectionId The id of the edition collection to create lab in.
-	 * @param  model               The model to fill out for Thymeleaf template resolution.
-	 * @return                     The Thymeleaf template to resolve.
-	 */
-	@GetMapping("/shared-edition/{editionCollectionId}/lab/create")
-	@PreAuthorize("@permissionService.canManageSharedSessions(#editionCollectionId)")
-	public String getSharedLabCreateView(@PathVariable Long editionCollectionId,
-			@RequestParam QueueSessionType type, Model model) {
-		var editionCollection = ecCache.getOrThrow(editionCollectionId);
-
-		model.addAttribute("dto", type.newCreateDto());
-		model.addAttribute("ec", editionCollection);
-		model.addAttribute("lType", type);
-
-		setSessionEditingPageAttributes(editionCollection.getEditions().stream()
-				.map(EditionSummaryDTO::getId).collect(Collectors.toList()), model);
-
-		return "lab/create/" + type.name().toLowerCase();
-	}
-
 	/**
 	 * Creates a new capacity session using the session create DTO provided through the body of the POST
 	 * request. Creates a session of the capacity type.
 	 *
-	 * @param  editionId The id of the edition this session is part of.
-	 * @param  dto       The dto to apply to create a new session.
-	 * @return           A redirect to the created session page.
+	 * @param  dto The dto to apply to create a new session.
+	 * @return     A redirect to the created session page.
 	 */
-	@PostMapping("/edition/{editionId}/lab/create/capacity")
-	@PreAuthorize("@permissionService.canManageSessions(#editionId)")
-	public String createCapacityLab(@PathVariable Long editionId,
-			CapacitySessionCreateDTO dto) {
-		ls.createSessions(dto, editionId, REGULAR);
-		return "redirect:/edition/" + editionId + "/labs";
+	@PostMapping("/lab/capacity")
+	@PreAuthorize("@permissionService.canManageSessions(#dto.organisationalUnit)")
+	public String createCapacityLab(CapacitySessionCreateDTO dto) {
+		ls.createSessions(dto, REGULAR);
+		return "redirect:/edition/" + dto.getOrganisationalUnit() + "/labs";
 	}
 
 	/**
 	 * Creates a new lab using the lab create DTO provided through the body of the POST request. Creates a lab
 	 * of the regular type.
 	 *
-	 * @param  editionId The id of the edition this lab is part of.
-	 * @param  dto       The dto to apply to create a new lab.
-	 * @return           A redirect to the created lab page.
+	 * @param  dto The dto to apply to create a new lab.
+	 * @return     A redirect to the created lab page.
 	 */
-	@PostMapping("/edition/{editionId}/lab/create/regular")
-	@PreAuthorize("@permissionService.canManageSessions(#editionId)")
-	public String createRegularLab(@PathVariable Long editionId,
-			RegularLabCreateDTO dto) {
-		ls.createSessions(dto, editionId, REGULAR);
-		return "redirect:/edition/" + editionId + "/labs";
+	@PostMapping("/lab/regular")
+	@PreAuthorize("@permissionService.canManageSessions(#dto.organisationalUnit)")
+	public String createRegularLab(RegularLabCreateDTO dto) {
+		ls.createSessions(dto, REGULAR);
+		return "redirect:/edition/" + dto.getOrganisationalUnit() + "/labs";
 	}
 
 	/**
 	 * Creates a new lab using the lab create DTO provided through the body of the POST request. Creates a lab
 	 * of the slotted type.
 	 *
-	 * @param  editionId The id of the edition this lab is part of.
-	 * @param  dto       The dto to apply to create a new lab.
-	 * @return           A redirect to the created lab page.
+	 * @param  dto The dto to apply to create a new lab.
+	 * @return     A redirect to the created lab page.
 	 */
-	@PostMapping("/edition/{editionId}/lab/create/slotted")
-	@PreAuthorize("@permissionService.canManageSessions(#editionId)")
-	public String createSlottedLab(@PathVariable Long editionId,
-			SlottedLabCreateDTO dto) {
-		ls.createSessions(dto, editionId, REGULAR);
-		return "redirect:/edition/" + editionId + "/labs";
+	@PostMapping("/lab/slotted")
+	@PreAuthorize("@permissionService.canManageSessions(#dto.organisationalUnit)")
+	public String createSlottedLab(SlottedLabCreateDTO dto) {
+		ls.createSessions(dto, REGULAR);
+		return "redirect:/edition/" + dto.getOrganisationalUnit() + "/labs";
 	}
 
 	/**
 	 * Creates a new lab using the lab create DTO provided through the body of the POST request. Creates a lab
 	 * of the exam type.
 	 *
-	 * @param  editionId The id of the edition this lab is part of.
-	 * @param  dto       The dto to apply to create a new lab.
-	 * @return           A redirect to the created lab page.
+	 * @param  dto The dto to apply to create a new lab.
+	 * @return     A redirect to the created lab page.
 	 */
-	@PostMapping("/edition/{editionId}/lab/create/exam")
-	@PreAuthorize("@permissionService.canManageSessions(#editionId)")
-	public String createExamLab(@PathVariable Long editionId,
-			ExamLabCreateDTO dto) {
-		ls.createSessions(dto, editionId, REGULAR);
-		return "redirect:/edition/" + editionId + "/labs";
+	@PostMapping("/lab/exam")
+	@PreAuthorize("@permissionService.canManageSessions(#dto.organisationalUnit)")
+	public String createExamLab(ExamLabCreateDTO dto) {
+		ls.createSessions(dto, REGULAR);
+		return "redirect:/edition/" + dto.getOrganisationalUnit() + "/labs";
 	}
 
 	/**
 	 * Creates a new lab using the lab create DTO provided through the body of the POST request. This method
 	 * creates one specifically for a shared edition. Creates a lab of the regular type.
 	 *
-	 * @param  editionCollectionId The id of the edition collection the lab will be part of.
-	 * @param  dto                 The dto to apply to create a new lab.
-	 * @return                     A redirect to the created lab page.
+	 * @param  dto The dto to apply to create a new lab.
+	 * @return     A redirect to the created lab page.
 	 */
-	@PostMapping("/shared-edition/{editionCollectionId}/lab/create/regular")
-	@PreAuthorize("@permissionService.canManageSharedSessions(#editionCollectionId)")
-	public String createRegularSharedLab(@PathVariable Long editionCollectionId,
-			RegularLabCreateDTO dto) {
-		ls.createSessions(dto, editionCollectionId, SHARED);
-		return "redirect:/shared-edition/" + editionCollectionId;
+	@PostMapping("/shared-edition/lab/regular")
+	@PreAuthorize("@permissionService.canManageSharedSessions(#dto.organisationalUnit)")
+	public String createRegularSharedLab(RegularLabCreateDTO dto) {
+		ls.createSessions(dto, SHARED);
+		return "redirect:/shared-edition/" + dto.getOrganisationalUnit();
 	}
 
 	/**
 	 * Creates a new lab using the lab create DTO provided through the body of the POST request. This method
 	 * creates one specifically for a shared edition. Creates a lab of the slotted type.
 	 *
-	 * @param  editionCollectionId The id of the edition collection the lab will be part of.
-	 * @param  dto                 The dto to apply to create a new lab.
-	 * @return                     A redirect to the created lab page.
+	 * @param  dto The dto to apply to create a new lab.
+	 * @return     A redirect to the created lab page.
 	 */
-	@PostMapping("/shared-edition/{editionCollectionId}/lab/create/slotted")
-	@PreAuthorize("@permissionService.canManageSharedSessions(#editionCollectionId)")
-	public String createSlottedSharedLab(@PathVariable Long editionCollectionId,
-			SlottedLabCreateDTO dto) {
-		ls.createSessions(dto, editionCollectionId, SHARED);
-		return "redirect:/shared-edition/" + editionCollectionId;
+	@PostMapping("/shared-edition/lab/slotted")
+	@PreAuthorize("@permissionService.canManageSharedSessions(#dto.organisationalUnit)")
+	public String createSlottedSharedLab(SlottedLabCreateDTO dto) {
+		ls.createSessions(dto, SHARED);
+		return "redirect:/shared-edition/" + dto.getOrganisationalUnit();
 	}
 
 	/**
 	 * Creates a new lab using the lab create DTO provided through the body of the POST request. This method
 	 * creates one specifically for a shared edition. Creates a lab of the exam type.
 	 *
-	 * @param  editionCollectionId The id of the edition collection the lab will be part of.
-	 * @param  dto                 The dto to apply to create a new lab.
-	 * @return                     A redirect to the created lab page.
-	 */
-	@PostMapping("/shared-edition/{editionCollectionId}/lab/create/exam")
-	@PreAuthorize("@permissionService.canManageSharedSessions(#editionCollectionId)")
-	public String createExamSharedLab(@PathVariable Long editionCollectionId,
-			ExamLabCreateDTO dto) {
-		ls.createSessions(dto, editionCollectionId, SHARED);
-		return "redirect:/shared-edition/" + editionCollectionId;
-	}
-
-	/**
-	 * Gets the confirmation view for deleting a single lab.
-	 *
-	 * @param  qSession The lab to delete.
-	 * @param  model    The model to fill out for Thymeleaf template resolution.
-	 * @return          The Thymeleaf template to resolve.
+	 * @param  dto The dto to apply to create a new lab.
+	 * @return     A redirect to the created lab page.
 	 */
-	@GetMapping("/lab/{qSession}/remove")
-	@PreAuthorize("@permissionService.canManageSession(#qSession)")
-	public String getSessionDeleteView(@PathEntity QueueSession<?> qSession,
-			Model model) {
-		model.addAttribute("qSession", qSession);
-		ls.setOrganizationInModel(qSession, model);
-
-		return "lab/remove";
+	@PostMapping("/shared-edition/lab/exam")
+	@PreAuthorize("@permissionService.canManageSharedSessions(#dto.organisationalUnit)")
+	public String createExamSharedLab(ExamLabCreateDTO dto) {
+		ls.createSessions(dto, SHARED);
+		return "redirect:/shared-edition/" + dto.getOrganisationalUnit();
 	}
 
 	/**
 	 * Handles a POST request to the session delete endpoint by setting the deleted at date for the given
 	 * session.
 	 *
-	 * @param  qSession The session to delete.
-	 * @return          A redirect back to the edition lab overview.
+	 * @param  id The id of the session to delete.
+	 * @return    A redirect back to the edition lab overview.
 	 */
-	@PostMapping("/lab/{qSession}/remove")
-	@PreAuthorize("@permissionService.canManageSession(#qSession)")
-	public String deleteSession(@PathEntity QueueSession<?> qSession) {
+	@DeleteMapping("/lab")
+	@PreAuthorize("@permissionService.canManageSession(#id)")
+	public String deleteSession(@RequestParam Long id) {
+		QueueSession<?> qSession = qsRepository.findByIdOrThrow(id);
 		ls.deleteSession(qSession);
 		var session = sCache.getOrThrow(qSession.getSession());
 
@@ -532,108 +497,64 @@ public class LabController {
 		return "redirect:/edition/" + session.getEditions().get(0).getId() + "/labs";
 	}
 
-	/**
-	 * Gets the lab creation page copied from the lab targeted in the url of this request.
-	 *
-	 * @param  qSession The lab that is targeted for copying.
-	 * @param  model    The model to fill out for Thymeleaf template resolution.
-	 * @return          The Thymeleaf template to resolve.
-	 */
-	// TODO: Check whether this permission cannot be better: currently one can copy a lab if they can manage that lab,
-	//        but these permissions should be the same as creating a new lab.
-	@GetMapping("/lab/{qSession}/copy")
-	@PreAuthorize("@permissionService.canManageSession(#qSession)")
-	public String getLabCopyView(@PathEntity QueueSession<?> qSession,
-			Model model) {
-		var session = sCache.getOrThrow(qSession.getSession());
-
-		model.addAttribute("dto", qSession.copyLabCreateDTO(session));
-		model.addAttribute("lType", qSession.getType());
-
-		setSessionEditingPageAttributes(qSession, model);
-
-		return "lab/create/" + qSession.getType().name().toLowerCase();
-	}
-
-	/**
-	 * Gets the lab editing page.
-	 *
-	 * @param  qSession The lab that is to be edited by this page.
-	 * @param  model    The model to fill out for Thymeleaf template resolution.
-	 * @return          The Thymeleaf template to resolve.
-	 */
-	@GetMapping("/lab/{qSession}/edit")
-	@PreAuthorize("@permissionService.canManageSession(#qSession)")
-	public String getLabUpdateView(@PathEntity QueueSession<?> qSession, Model model) {
-		model.addAttribute("dto", qSession.newPatchDTO());
-		model.addAttribute("lab", qSession);
-
-		setSessionEditingPageAttributes(qSession, model);
-
-		return "lab/edit/" + qSession.getType().name().toLowerCase();
-	}
-
 	/**
 	 * Updates the given lab entity using the given lab patch DTO. The lab in question is of the regular type.
 	 *
-	 * @param  lab The lab to update.
 	 * @param  dto The dto representing the changes to apply to the lab.
 	 * @return     A redirect to the lab view page.
 	 */
 	@Transactional
-	@PostMapping("/lab/{lab}/edit/regular")
-	@PreAuthorize("@permissionService.canManageSession(#lab)")
-	public String updateSession(@PathEntity RegularLab lab, RegularLabPatchDTO dto) {
-		ls.updateSession(dto, lab);
-		return "redirect:/lab/" + lab.getId();
+	@PatchMapping("/lab/regular")
+	@PreAuthorize("@permissionService.canManageSession(#dto.id)")
+	public String updateSession(RegularLabPatchDTO dto) {
+		ls.updateSession(dto);
+		return "redirect:/lab/" + dto.getId();
 	}
 
 	/**
 	 * Updates the given lab entity using the given lab patch DTO. The lab in question is of the slotted type.
 	 *
-	 * @param  lab The lab to update.
 	 * @param  dto The dto representing the changes to apply to the lab.
 	 * @return     A redirect to the lab view page.
 	 */
 	@Transactional
-	@PostMapping("/lab/{lab}/edit/slotted")
-	@PreAuthorize("@permissionService.canManageSession(#lab)")
-	public String updateSession(@PathEntity SlottedLab lab, SlottedLabPatchDTO dto) {
-		ls.updateSession(dto, lab);
-		return "redirect:/lab/" + lab.getId();
+	@PatchMapping("/lab/slotted")
+	@PreAuthorize("@permissionService.canManageSession(#dto.id)")
+	public String updateSession(SlottedLabPatchDTO dto) {
+		ls.updateSession(dto);
+		return "redirect:/lab/" + dto.getId();
 	}
 
 	/**
 	 * Updates the given lab entity using the given lab patch DTO. The lab in question is of the exam type.
 	 *
-	 * @param  lab The lab to update.
 	 * @param  dto The dto representing the changes to apply to the lab.
 	 * @return     A redirect to the lab view page.
 	 */
 	@Transactional
-	@PostMapping("/lab/{lab}/edit/exam")
-	@PreAuthorize("@permissionService.canManageSession(#lab)")
-	public String updateSession(@PathEntity ExamLab lab, ExamLabPatchDTO dto) {
-		ls.updateSession(dto, lab);
-		return "redirect:/lab/" + lab.getId();
+	@PatchMapping("/lab/exam")
+	@PreAuthorize("@permissionService.canManageSession(#dto.id)")
+	public String updateSession(ExamLabPatchDTO dto) {
+		ls.updateSession(dto);
+		return "redirect:/lab/" + dto.getId();
 	}
 
 	/**
 	 * Updates the given session entity using the given session patch DTO. The session in question is of the
 	 * limited capacity type.
 	 *
-	 * @param  session The session to update.
-	 * @param  dto     The dto representing the changes to apply to the session.
-	 * @return         A redirect to the session view page.
+	 * @param  dto The dto representing the changes to apply to the session.
+	 * @return     A redirect to the session view page.
 	 */
 	@Transactional
-	@PostMapping("/lab/{session}/edit/capacity")
-	@PreAuthorize("@permissionService.canManageSession(#session)")
-	public String updateSession(@PathEntity CapacitySession session, CapacitySessionPatchDTO dto) {
+	@PatchMapping("/lab/capacity")
+	@PreAuthorize("@permissionService.canManageSession(#dto.id)")
+	public String updateSession(CapacitySessionPatchDTO dto) {
+		CapacitySession session = (CapacitySession) qsRepository.findByIdOrThrow(dto.getId());
 		SelectionProcedure oldProcedure = session.getCapacitySessionConfig().getProcedure();
-		ls.updateSession(dto, session);
+		ls.updateSession(dto);
 		ls.updateCapacitySessionRequests(session, oldProcedure);
-		return "redirect:/lab/" + session.getId();
+		return "redirect:/lab/" + dto.getId();
 	}
 
 	/**
@@ -724,11 +645,11 @@ public class LabController {
 
 		var divisions = modules.stream()
 				.flatMap(m -> m.getDivisions().stream()).collect(Collectors.toList());
-		var rooms = rCache.getAll();
+		//		var rooms = rCache.getAll();
 
 		modules.sort(Comparator.comparing(ModuleDetailsDTO::getName));
 		assignments.sort(Comparator.comparing(AssignmentDetailsDTO::getName));
-		rooms.sort(Comparator.comparing((RoomDetailsDTO r) -> r.getBuilding().getName() + r.getName()));
+		//		rooms.sort(Comparator.comparing((RoomDetailsDTO r) -> r.getBuilding().getName() + r.getName()));
 
 		model.addAttribute("editions", editions);
 		model.addAttribute("editionMap",
@@ -741,11 +662,11 @@ public class LabController {
 		model.addAttribute("clusters", clusters);
 		model.addAttribute("divisions", divisions);
 
-		model.addAttribute("rooms", rooms);
-		model.addAttribute("buildings", rooms.stream()
-				.map(RoomDetailsDTO::getBuilding).distinct()
-				.sorted(Comparator.comparing(BuildingSummaryDTO::getName))
-				.collect(Collectors.toList()));
+		//		model.addAttribute("rooms", rooms);
+		//		model.addAttribute("buildings", rooms.stream()
+		//				.map(RoomDetailsDTO::getBuilding).distinct()
+		//				.sorted(Comparator.comparing(BuildingSummaryDTO::getName))
+		//				.collect(Collectors.toList())); TODO
 	}
 
 	/**
diff --git a/src/main/java/nl/tudelft/queue/controller/MailController.java b/src/main/java/nl/tudelft/queue/controller/MailController.java
index 3f6f11cd5ed33e01df860aaf50b55adca6244b41..60477ea627d0d1940e3a40e70c686054d4bac541 100644
--- a/src/main/java/nl/tudelft/queue/controller/MailController.java
+++ b/src/main/java/nl/tudelft/queue/controller/MailController.java
@@ -17,8 +17,8 @@
  */
 package nl.tudelft.queue.controller;
 
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
-import nl.tudelft.labracore.lib.security.user.Person;
 import nl.tudelft.queue.dto.create.CourseRequestCreateDTO;
 import nl.tudelft.queue.service.MailService;
 
@@ -27,6 +27,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
 
 @Controller
 @ConditionalOnProperty(name = "queue.mail.enabled", havingValue = "true")
@@ -44,11 +45,13 @@ public class MailController {
 	 */
 	@PostMapping("/editions/request-course")
 	@PreAuthorize("@permissionService.isAdminOrTeacher()")
-	public String requestCourse(@AuthenticatedPerson Person person,
-			CourseRequestCreateDTO dto) {
+	public String requestCourse(@AuthenticatedPerson Person person, CourseRequestCreateDTO dto,
+			RedirectAttributes redirectAttributes) {
 		dto.validateOrThrow();
 		ms.sendCourseRequest(person, dto);
 
-		return "course/request-submitted";
+		redirectAttributes.addFlashAttribute("success", "Course successfully requested");
+
+		return "redirect:/";
 	}
 }
diff --git a/src/main/java/nl/tudelft/queue/controller/ModuleController.java b/src/main/java/nl/tudelft/queue/controller/ModuleController.java
index a86c0aed9e402b5c200de3660f1bbcaf1c33cf5c..6f661f19a3e339e163a406baf2875f5be79010e7 100644
--- a/src/main/java/nl/tudelft/queue/controller/ModuleController.java
+++ b/src/main/java/nl/tudelft/queue/controller/ModuleController.java
@@ -18,19 +18,18 @@
 package nl.tudelft.queue.controller;
 
 import nl.tudelft.labracore.api.ModuleControllerApi;
-import nl.tudelft.labracore.api.dto.EditionDetailsDTO;
-import nl.tudelft.labracore.api.dto.ModuleCreateDTO;
-import nl.tudelft.queue.cache.EditionCacheManager;
-import nl.tudelft.queue.cache.ModuleCacheManager;
+import nl.tudelft.labracore.api.dto.*;
+import nl.tudelft.labracore.lib.cache.EditionCacheManager;
+import nl.tudelft.labracore.lib.cache.ModuleCacheManager;
+import nl.tudelft.labracore.lib.data.Module;
 import nl.tudelft.queue.dto.create.QueueModuleCreateDTO;
+import nl.tudelft.queue.dto.patch.QueueModulePatchDTO;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Controller;
 import org.springframework.ui.Model;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.*;
 
 @Controller
 public class ModuleController {
@@ -45,42 +44,55 @@ public class ModuleController {
 	private ModuleControllerApi mApi;
 
 	/**
-	 * Gets the creation page for a module in the given edition. This sets up page attributes and returns the
-	 * thymeleaf template to resolve.
+	 * Gets the details of a module.
 	 *
-	 * @param  editionId The id of the edition the module should be in.
-	 * @param  model     The model to fill out for Thymeleaf template resolution.
-	 * @return           The Thymeleaf template to resolve.
+	 * @param  id The id of the module
+	 * @return    The details of the module
 	 */
-	@GetMapping("/edition/{editionId}/modules/create")
-	@PreAuthorize("@permissionService.canManageModules(#editionId)")
-	public String getModuleCreatePage(@PathVariable Long editionId, Model model) {
-		return addCreateModuleAttributes(editionId, new QueueModuleCreateDTO(editionId), model);
+	@GetMapping("/module/{id}")
+	public @ResponseBody ModuleDetailsDTO getModule(@PathVariable Long id) {
+		return Module.byId(id).getDto();
 	}
 
 	/**
 	 * Creates a module from the given DTO in the edition with the given id.
 	 *
-	 * @param  editionId The id of the edition to create the module in.
-	 * @param  dto       The creation DTO containing all information needed to create a new module.
-	 * @param  model     The model to fill out for Thymeleaf template resolution.
-	 * @return           A redirect back to the modules page or the module create page if something goes
-	 *                   wrong.
+	 * @param  dto The creation DTO containing all information needed to create a new module.
+	 * @return     A redirect back to the modules page or the module create page if something goes wrong.
 	 */
-	@PostMapping("/edition/{editionId}/modules/create")
-	@PreAuthorize("@permissionService.canManageModules(#editionId)")
-	public String createModule(@PathVariable Long editionId, QueueModuleCreateDTO dto, Model model) {
-		// Ensure the DTO contains the right edition ID, as malicious users can inject a different ID in the form
-		dto.setEditionId(editionId);
-
+	@PostMapping("/module")
+	@PreAuthorize("@permissionService.canManageModules(#dto.edition)")
+	public String createModule(QueueModuleCreateDTO dto) {
 		ModuleCreateDTO create = dto.apply();
-		if (dto.hasErrors()) {
-			return addCreateModuleAttributes(editionId, dto, model);
-		}
-
 		mApi.addModule(create).block();
+		return "redirect:/edition/" + dto.getEdition() + "/modules";
+	}
 
-		return "redirect:/edition/" + editionId + "/modules";
+	/**
+	 * Patches a module from the given DTO.
+	 *
+	 * @param  dto The creation DTO containing all information needed to create a new module.
+	 * @return     A redirect back to the modules page or the module create page if something goes wrong.
+	 */
+	@PatchMapping("/module")
+	@PreAuthorize("@permissionService.canManageModule(#dto.module)")
+	public String editModule(QueueModulePatchDTO dto) {
+		ModulePatchDTO patch = dto.apply();
+		mApi.patchModule(dto.getModule(), patch).block();
+		return "redirect:/edition/" + Module.byId(dto.getModule()).getEdition().value().getId() + "/modules";
+	}
+
+	/**
+	 * Removes the module with the given id.
+	 *
+	 * @param  id The id of the module to delete.
+	 * @return    A redirect back to the edition modules page.
+	 */
+	@DeleteMapping("/module")
+	@PreAuthorize("@permissionService.canManageModule(#id)")
+	public String removeModule(@RequestParam Long id) {
+		mApi.deleteModule(id).block();
+		return "redirect:/edition/" + Module.byId(id).getEdition().value().getId() + "/modules";
 	}
 
 	/**
@@ -104,23 +116,6 @@ public class ModuleController {
 		return "module/remove";
 	}
 
-	/**
-	 * Removes the module with the given id.
-	 *
-	 * @param  moduleId The id of the module to delete.
-	 * @return          A redirect back to the edition modules page.
-	 */
-	@PostMapping("/module/{moduleId}/remove")
-	@PreAuthorize("@permissionService.canManageModule(#moduleId)")
-	public String removeModule(@PathVariable Long moduleId) {
-		var module = mCache.getOrThrow(moduleId);
-		var edition = eCache.getOrThrow(module.getEdition().getId());
-
-		mApi.deleteModule(moduleId).block();
-
-		return "redirect:/edition/" + edition.getId() + "/modules";
-	}
-
 	/**
 	 * Fills in page attributes for the module creation page with the id of the edition and the initial module
 	 * creation DTO object.
diff --git a/src/main/java/nl/tudelft/queue/controller/ParticipantController.java b/src/main/java/nl/tudelft/queue/controller/ParticipantController.java
new file mode 100644
index 0000000000000000000000000000000000000000..d9af6fd21baf4d277ae2ebbf8a990726c87bb481
--- /dev/null
+++ b/src/main/java/nl/tudelft/queue/controller/ParticipantController.java
@@ -0,0 +1,125 @@
+/*
+ * Queue - A Queueing system that can be used to handle labs in higher education
+ * Copyright (C) 2016-2021  Delft University of Technology
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.queue.controller;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import nl.tudelft.labracore.api.EditionControllerApi;
+import nl.tudelft.labracore.api.PersonControllerApi;
+import nl.tudelft.labracore.api.RoleControllerApi;
+import nl.tudelft.labracore.api.dto.*;
+import nl.tudelft.queue.csv.EmptyCsvException;
+import nl.tudelft.queue.csv.InvalidCsvException;
+import nl.tudelft.queue.dto.create.QueueRoleCreateDTO;
+import nl.tudelft.queue.dto.create.QueueRolesCreateDTO;
+import nl.tudelft.queue.dto.patch.QueueRolePatchDTO;
+import nl.tudelft.queue.service.EditionService;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+@Controller
+@RequestMapping("/participant")
+public class ParticipantController {
+
+	@Autowired
+	private RoleControllerApi rApi;
+	@Autowired
+	private PersonControllerApi pApi;
+	@Autowired
+	private EditionControllerApi eApi;
+
+	@Autowired
+	private EditionService es;
+
+	@PostMapping("/single")
+	@PreAuthorize("@permissionService.canManageParticipants(#dto.edition)")
+	public String addSingleParticipant(QueueRoleCreateDTO dto, RedirectAttributes redirectAttributes) {
+		List<PersonSummaryDTO> person = pApi.searchForPeople(List.of(dto.getIdentifier())).block()
+				.getPeople();
+
+		if (person.isEmpty()) {
+			redirectAttributes.addFlashAttribute("error",
+					"User with username, email, or number " + dto.getIdentifier() + " cannot be found.");
+		} else if (!dto.hasErrors()) {
+			rApi.addRole(new RoleCreateDTO().person(new PersonIdDTO().id(person.get(0).getId()))
+					.edition(new EditionIdDTO().id(dto.getEdition())).type(dto.getRole())).block();
+		}
+
+		return "redirect:/edition/" + dto.getEdition() + "/participants";
+	}
+
+	@PostMapping("/multiple")
+	@PreAuthorize("@permissionService.canManageParticipants(#dto.edition)")
+	public String addMultipleParticipants(QueueRolesCreateDTO dto, RedirectAttributes redirectAttributes) {
+		PersonSearchDTO result = pApi
+				.searchForPeople(Arrays.stream(dto.getIdentifiers().split("\n")).map(String::strip).toList())
+				.block();
+		eApi.addParticipantsToEdition(dto.getEdition(),
+				new ParticipantsImportDTO().participants(result.getPeople().stream()
+						.collect(Collectors.toMap(PersonSummaryDTO::getUsername, __ -> dto.getRole()))),
+				false).block();
+
+		if (!result.getNotFound().isEmpty()) {
+			redirectAttributes.addFlashAttribute("error",
+					"The following users were not found: " + String.join(", ", result.getNotFound()));
+		}
+
+		return "redirect:/edition/" + dto.getEdition() + "/participants";
+	}
+
+	@PostMapping("/import")
+	@PreAuthorize("@permissionService.canManageParticipants(#edition)")
+	public String importParticipants(@RequestParam Long edition, @RequestParam MultipartFile file,
+			RedirectAttributes redirectAttributes) {
+		try {
+			List<String> notFound = es.addCourseParticipants(file, edition);
+			if (!notFound.isEmpty()) {
+				redirectAttributes.addFlashAttribute("error",
+						"The following users were not found: " + String.join(", ", notFound));
+			}
+		} catch (InvalidCsvException | EmptyCsvException e) {
+			redirectAttributes.addFlashAttribute("error", "Invalid CSV file");
+		}
+
+		return "redirect:/edition/" + edition + "/participants";
+	}
+
+	@PatchMapping
+	@PreAuthorize("@permissionService.canManageParticipant(#dto.edition, #dto.person)")
+	public String changeRole(QueueRolePatchDTO dto) {
+		if (!dto.hasErrors()) {
+			rApi.patchRole(dto.getPerson(), dto.getEdition(), new RolePatchDTO().type(dto.getRole())).block();
+		}
+		return "redirect:/edition/" + dto.getEdition() + "/participants";
+	}
+
+	@DeleteMapping
+	@PreAuthorize("@permissionService.canManageParticipant(#edition, #person)")
+	public String changeRole(@RequestParam Long edition, @RequestParam Long person) {
+		eApi.removePersonFromEdition(edition, person).block();
+		return "redirect:/edition/" + edition + "/participants";
+	}
+
+}
diff --git a/src/main/java/nl/tudelft/queue/controller/PushController.java b/src/main/java/nl/tudelft/queue/controller/PushController.java
index ce9623852f587e374dda4a57112a9f8913321d94..6513f39ada8854e67f1be9bb6527df6d6d148f5b 100644
--- a/src/main/java/nl/tudelft/queue/controller/PushController.java
+++ b/src/main/java/nl/tudelft/queue/controller/PushController.java
@@ -19,8 +19,8 @@ package nl.tudelft.queue.controller;
 
 import nl.martijndwars.webpush.PushService;
 import nl.martijndwars.webpush.Utils;
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
-import nl.tudelft.labracore.lib.security.user.Person;
 import nl.tudelft.queue.dto.create.SubscriptionCreateDTO;
 import nl.tudelft.queue.realtime.SubscriptionContainer;
 
diff --git a/src/main/java/nl/tudelft/queue/controller/QuestionController.java b/src/main/java/nl/tudelft/queue/controller/QuestionController.java
new file mode 100644
index 0000000000000000000000000000000000000000..a05be45b89296d7b57665a60fe9c7b3a2e721f7d
--- /dev/null
+++ b/src/main/java/nl/tudelft/queue/controller/QuestionController.java
@@ -0,0 +1,76 @@
+/*
+ * Queue - A Queueing system that can be used to handle labs in higher education
+ * Copyright (C) 2016-2021  Delft University of Technology
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.queue.controller;
+
+import nl.tudelft.labracore.api.QuestionControllerApi;
+import nl.tudelft.labracore.api.dto.QuestionCreateDTO;
+import nl.tudelft.labracore.api.dto.QuestionPatchDTO;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+@Controller
+@RequestMapping("/question")
+public class QuestionController {
+
+	@Autowired
+	private QuestionControllerApi qApi;
+
+	/**
+	 * Adds a question.
+	 *
+	 * @param  question The question to add
+	 * @return          The question page of the edition
+	 */
+	@PostMapping
+	@PreAuthorize("@permissionService.canManageQuestions(#question.edition.id)")
+	public String addQuestion(QuestionCreateDTO question) {
+		qApi.addQuestion(question).block();
+		return "/edition/" + question.getEdition().getId() + "/questions";
+	}
+
+	/**
+	 * Edits a question.
+	 *
+	 * @param  questionId The id of the question to edit
+	 * @param  question   The new question
+	 * @return            The question page of the edition
+	 */
+	@PatchMapping
+	@PreAuthorize("@permissionService.canManageQuestions(#question.edition.id)")
+	public String editQuestion(@RequestParam Long questionId, QuestionPatchDTO question) {
+		qApi.patchQuestion(questionId, question).block();
+		return "/edition/" + question.getEdition().getId() + "/questions";
+	}
+
+	/**
+	 * Deletes a question.
+	 *
+	 * @param  questionId The id of the question to edit
+	 * @return            The question page of the edition
+	 */
+	@DeleteMapping
+	@PreAuthorize("@permissionService.canManageQuestions(#editionId)")
+	public String editQuestion(@RequestParam Long editionId, @RequestParam Long questionId) {
+		qApi.deleteQuestion(questionId).block();
+		return "/edition/" + editionId + "/questions";
+	}
+
+}
diff --git a/src/main/java/nl/tudelft/queue/controller/RequestController.java b/src/main/java/nl/tudelft/queue/controller/RequestController.java
index 38e8139c8a436f192232db7189b145762b854f45..1b4b3ce0e319459831921018a9892b51aa403e6a 100644
--- a/src/main/java/nl/tudelft/queue/controller/RequestController.java
+++ b/src/main/java/nl/tudelft/queue/controller/RequestController.java
@@ -21,18 +21,16 @@ import static nl.tudelft.labracore.api.dto.RolePersonDetailsDTO.TypeEnum.*;
 
 import java.util.List;
 import java.util.Optional;
-import java.util.Set;
 import java.util.stream.Collectors;
 
 import javax.transaction.Transactional;
 
 import nl.tudelft.labracore.api.PersonControllerApi;
-import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
+import nl.tudelft.labracore.lib.cache.*;
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
-import nl.tudelft.labracore.lib.security.user.Person;
 import nl.tudelft.librador.dto.view.View;
 import nl.tudelft.librador.resolver.annotations.PathEntity;
-import nl.tudelft.queue.cache.*;
 import nl.tudelft.queue.dto.patch.FeedbackPatchDTO;
 import nl.tudelft.queue.dto.patch.RequestPatchDTO;
 import nl.tudelft.queue.dto.util.RequestTableFilterDTO;
@@ -43,17 +41,18 @@ import nl.tudelft.queue.model.Request;
 import nl.tudelft.queue.model.SelectionRequest;
 import nl.tudelft.queue.model.labs.Lab;
 import nl.tudelft.queue.repository.LabRequestRepository;
+import nl.tudelft.queue.repository.RequestRepository;
 import nl.tudelft.queue.service.*;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.web.PageableDefault;
+import org.springframework.http.ResponseEntity;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Controller;
 import org.springframework.ui.Model;
 import org.springframework.web.bind.annotation.*;
-import org.springframework.web.servlet.mvc.support.RedirectAttributes;
 
 @Controller
 public class RequestController {
@@ -61,6 +60,9 @@ public class RequestController {
 	@Autowired
 	private LabRequestRepository lrr;
 
+	@Autowired
+	private RequestRepository rr;
+
 	@Autowired
 	private RequestService rs;
 
@@ -146,10 +148,14 @@ public class RequestController {
 
 		model.addAttribute("page", "requests");
 		model.addAttribute("filter", filter);
-		model.addAttribute("requests", rts.convertRequestsToView(
-				lrr.findAllByFilter(labs, filter, pageable), lrr.countByFilter(labs, filter)));
-		model.addAttribute("requestCounts", rts.labRequestCounts(
-				labs, assistant, filter));
+		model.addAttribute("requests",
+				rts.convertRequestsToView(lrr.findAllByFilterForPerson(labs, filter, assistant, pageable),
+						lrr.countByFilterForPerson(labs, assistant, filter)));
+		model.addAttribute("currentRequests",
+				labs.stream().map(lab -> lrr.findCurrentlyProcessingRequest(assistant, lab))
+						.filter(Optional::isPresent).map(Optional::get).map(LabRequest::toViewDTO)
+						.collect(Collectors.toList()));
+		model.addAttribute("requestCounts", rts.labRequestCounts(labs, assistant, filter));
 
 		return "request/list";
 	}
@@ -183,18 +189,17 @@ public class RequestController {
 	/**
 	 * Updates the information on the location of a student within the given request.
 	 *
-	 * @param  request The request to update location information on.
-	 * @param  dto     The dto carrying the update information on the location.
-	 * @return         A redirect back to the lab view page.
+	 * @param request The request to update location information on.
+	 * @param dto     The dto carrying the update information on the location.
 	 */
 	@Transactional
 	@PostMapping("/request/{request}/update-request-info")
 	@PreAuthorize("@permissionService.canUpdateRequest(#request.id)")
-	public String updateRequestInfo(@PathEntity LabRequest request,
+	public ResponseEntity<Void> updateRequestInfo(@PathEntity LabRequest request,
 			RequestPatchDTO dto) {
 		dto.apply(request);
 
-		return "redirect:/lab/" + request.getSession().getId();
+		return ResponseEntity.ok().build();
 	}
 
 	/**
@@ -225,82 +230,46 @@ public class RequestController {
 		}
 	}
 
-	/**
-	 * Gets the request approval view. This page is used to submit a request approval.
-	 *
-	 * @param  request The request that is requested to be approved.
-	 * @param  model   The model to fill out for Thymeleaf template resolution.
-	 * @return         The Thymeleaf template to resolve.
-	 */
-	@GetMapping("/request/{request}/approve")
-	@PreAuthorize("@permissionService.canFinishRequest(#request.id)")
-	public String getRequestApproveView(@PathEntity LabRequest request, Model model) {
-		model.addAttribute("request", request);
-		ls.setOrganizationInModel(request.getSession(), model);
-
-		return "request/approve";
-	}
-
 	/**
 	 * Performs the 'approve request' action for a request. This controller method calls the service method to
 	 * handle a request approve command and redirects the user back to the requests page to grab their next
 	 * request.
 	 *
-	 * @param  request            The request that the user approves.
+	 * @param  requestId          The id of the request that the user approves.
 	 * @param  person             The user that is approving the given request.
 	 * @param  reasonForAssistant The reason the assistant gives other assistants for approving the request.
-	 * @param  redirectAttributes Attributes used to fill in a model message upon redirection.
 	 * @return                    A redirect to the requests table page.
 	 */
-	@PostMapping("/request/{request}/approve")
-	@PreAuthorize("@permissionService.canFinishRequest(#request.id)")
-	public String approveRequest(@PathEntity LabRequest request,
-			@AuthenticatedPerson Person person,
+	@PostMapping("/request/approve")
+	@PreAuthorize("@permissionService.canFinishRequest(#requestId)")
+	public String approveRequest(@AuthenticatedPerson Person person,
+			@RequestParam(value = "request") Long requestId,
 			@RequestParam(value = "reasonForAssistant") String reasonForAssistant,
-			@RequestParam(value = "answer", required = false) String answer,
-			RedirectAttributes redirectAttributes) {
+			@RequestParam(value = "answer", required = false) String answer) {
+		LabRequest request = lrr.findByIdOrThrow(requestId);
 		rs.approveRequest(request, person.getId(), reasonForAssistant, answer);
 
-		redirectAttributes.addFlashAttribute("message", "Request #" + request.getId() + " approved");
-
 		return "redirect:/requests";
 	}
 
-	/**
-	 * Gets the request reject view. This page is used to submit a request rejection.
-	 *
-	 * @param  request The request that is requested to be rejected.
-	 * @param  model   The model to fill out for Thymeleaf template resolution.
-	 * @return         The Thymeleaf template to resolve.
-	 */
-	@GetMapping("/request/{request}/reject")
-	@PreAuthorize("@permissionService.canFinishRequest(#request.id)")
-	public String getRequestRejectView(@PathEntity Request<?> request, Model model) {
-		model.addAttribute("request", request);
-		ls.setOrganizationInModel(request.getSession(), model);
-
-		return "request/reject";
-	}
-
 	/**
 	 * Performs the 'reject request' action for a request. This controller method calls the service method to
 	 * handle a request reject command and redirects the user back to the requests page to grab their next
 	 * request.
 	 *
-	 * @param  request            The request that the user rejects.
+	 * @param  requestId          The id of the request that the user rejects.
 	 * @param  person             The user that is rejecting the given request.
 	 * @param  reasonForAssistant The reason the assistant gives other assistants for rejecting the request.
 	 * @param  reasonForStudent   The reason the assistant gives the student(s) for rejecting the request.
-	 * @param  redirectAttributes Attributes used to fill in a model message upon redirection.
 	 * @return                    A redirect to the requests table page.
 	 */
-	@PostMapping("/request/{request}/reject")
-	@PreAuthorize("@permissionService.canFinishRequest(#request.id)")
-	public String rejectRequest(@PathEntity Request<?> request,
-			@AuthenticatedPerson Person person,
+	@PostMapping("/request/reject")
+	@PreAuthorize("@permissionService.canFinishRequest(#requestId)")
+	public String rejectRequest(@AuthenticatedPerson Person person,
+			@RequestParam(value = "request") Long requestId,
 			@RequestParam(value = "reasonForAssistant") String reasonForAssistant,
-			@RequestParam(value = "reasonForStudent") String reasonForStudent,
-			RedirectAttributes redirectAttributes) {
+			@RequestParam(value = "reasonForStudent") String reasonForStudent) {
+		Request<?> request = rr.findByIdOrThrow(requestId);
 		if (request instanceof LabRequest) {
 			rs.rejectRequest((LabRequest) request, person.getId(), reasonForAssistant, reasonForStudent);
 		} else {
@@ -308,63 +277,32 @@ public class RequestController {
 					reasonForAssistant, reasonForStudent);
 		}
 
-		redirectAttributes.addFlashAttribute("message", "Request #" + request.getId() + " rejected");
-
 		return "redirect:/requests";
 	}
 
-	/**
-	 * Gets the request forward view. This page is used to forward a request to a different TA.
-	 *
-	 * @param  request The request that is requested to be forwarded.
-	 * @param  model   The model to fill out for Thymeleaf template resolution.
-	 * @return         The Thymeleaf template to resolve.
-	 */
-	@GetMapping("/request/{request}/forward")
-	@PreAuthorize("@permissionService.canFinishRequest(#request.id)")
-	public String getRequestForwardView(@AuthenticatedPerson Person user,
-			@PathEntity LabRequest request, Model model) {
-		List<PersonSummaryDTO> assistants = rds
-				.roles(eCache.getOrThrow(
-						mCache.getOrThrow(aCache.getOrThrow(request.getAssignment()).getModule().getId())
-								.getEdition().getId()),
-						Set.of(TA, HEAD_TA, TEACHER))
-				.stream().filter(p -> !p.getId().equals(user.getId())).toList();
-
-		ls.setOrganizationInModel(request.getSession(), model);
-
-		model.addAttribute("request", request);
-		model.addAttribute("assistants", assistants);
-
-		return "request/forward";
-	}
-
 	/**
 	 * Performs the 'forward request' action for a request. This controller method calls the service method to
 	 * handle a request forward command and redirects the user back to the requests page to grab their next
 	 * request.
 	 *
-	 * @param  request            The request that the user forwards.
+	 * @param  requestId          The id of the request that the user forwards.
 	 * @param  assistant          The assistant the request should be forwarded to.
 	 * @param  reasonForAssistant The reason the assistant gives other assistants for forwarding the request.
-	 * @param  redirectAttributes Attributes used to fill in a model message upon redirection.
 	 * @return                    A redirect to the requests table page.
 	 */
-	@PostMapping("/request/{request}/forward")
-	@PreAuthorize("@permissionService.canFinishRequest(#request.id)")
-	public String forwardRequest(@PathEntity LabRequest request,
-			@AuthenticatedPerson Person assistant,
+	@PostMapping("/request/forward")
+	@PreAuthorize("@permissionService.canFinishRequest(#requestId)")
+	public String forwardRequest(@AuthenticatedPerson Person assistant,
+			@RequestParam(value = "request") Long requestId,
 			@RequestParam(value = "assistant") Long forwardedTo,
-			@RequestParam(value = "reasonForAssistant") String reasonForAssistant,
-			RedirectAttributes redirectAttributes) {
+			@RequestParam(value = "reasonForAssistant") String reasonForAssistant) {
+		LabRequest request = lrr.findByIdOrThrow(requestId);
 		if (forwardedTo == -1L) {
 			rs.forwardRequestToAnyone(request, assistant, reasonForAssistant);
 		} else {
 			rs.forwardRequestToPerson(request, assistant, pCache.getOrThrow(forwardedTo), reasonForAssistant);
 		}
 
-		redirectAttributes.addFlashAttribute("message", "Request #" + request.getId() + " forwarded");
-
 		return "redirect:/requests";
 	}
 
@@ -393,7 +331,7 @@ public class RequestController {
 	 * @return         The request the user is currently processing. Either the picked request, or the request
 	 *                 he/she was still processing.
 	 */
-	@GetMapping("/request/{request}/pick")
+	@PostMapping("/request/{request}/pick")
 	@PreAuthorize("@permissionService.canPickRequest(#request.id)")
 	public String pickRequest(@PathEntity LabRequest request, @AuthenticatedPerson Person person) {
 		LabRequest currRequest = rs.pickRequest(person, request);
@@ -414,7 +352,7 @@ public class RequestController {
 			FeedbackPatchDTO dto) {
 		fs.updateFeedback(request, assistantId, dto);
 
-		return "redirect:/request/" + request.getId();
+		return "redirect:/lab/" + request.getSession().getId();
 	}
 
 }
diff --git a/src/main/java/nl/tudelft/queue/controller/RequestFilterController.java b/src/main/java/nl/tudelft/queue/controller/RequestFilterController.java
index 3f8f6a728f65cd3a47cbb0d5e61bbfaf980ee4bc..37ce7c5b2f0921af1b0577f106c61c12971b09c4 100644
--- a/src/main/java/nl/tudelft/queue/controller/RequestFilterController.java
+++ b/src/main/java/nl/tudelft/queue/controller/RequestFilterController.java
@@ -18,6 +18,7 @@
 package nl.tudelft.queue.controller;
 
 import nl.tudelft.queue.dto.util.RequestTableFilterDTO;
+import nl.tudelft.queue.dto.util.RequestTableFilterUpdateDTO;
 import nl.tudelft.queue.service.RequestTableService;
 
 import org.springframework.beans.factory.annotation.Autowired;
@@ -51,6 +52,27 @@ public class RequestFilterController {
 		return "redirect:" + path;
 	}
 
+	/**
+	 * Updates filters for a request table page with a return-to-page path.
+	 *
+	 * @param  update             The DTO containing information about the filters that are to be updated.
+	 * @param  path               The path this method should redirect to after submitting filters.
+	 * @param  redirectAttributes Attributes used to fill in a model message upon redirection.
+	 * @return                    A redirect to the associated requests table page.
+	 */
+	@PostMapping(value = "/filter", params = "filter-update")
+	public String updateRequestTableFilters(RequestTableFilterUpdateDTO update,
+			@RequestParam("return-path") String path,
+			RedirectAttributes redirectAttributes) {
+		RequestTableFilterDTO filter = rts.checkAndStoreFilterDTO(null, path);
+		filter = update.updateFilter(filter);
+		rts.checkAndStoreFilterDTO(filter, path);
+
+		redirectAttributes.addAttribute("page", 0);
+
+		return "redirect:" + path;
+	}
+
 	/**
 	 * Clears filters for a request table under the given return-to-page path.
 	 *
diff --git a/src/main/java/nl/tudelft/queue/controller/SharedEditionController.java b/src/main/java/nl/tudelft/queue/controller/SharedEditionController.java
index b1fa235f51bff95ac358d54dfe1848cdc3dd7eb4..b9c04ed11296b608b45dcac301dfe94af8f979f2 100644
--- a/src/main/java/nl/tudelft/queue/controller/SharedEditionController.java
+++ b/src/main/java/nl/tudelft/queue/controller/SharedEditionController.java
@@ -18,23 +18,33 @@
 package nl.tudelft.queue.controller;
 
 import java.time.LocalDateTime;
+import java.util.Comparator;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 import javax.validation.Valid;
 
-import nl.tudelft.labracore.api.EditionCollectionControllerApi;
-import nl.tudelft.labracore.api.RoleControllerApi;
+import nl.tudelft.labracore.api.*;
 import nl.tudelft.labracore.api.dto.*;
+import nl.tudelft.labracore.lib.api.RoleType;
+import nl.tudelft.labracore.lib.cache.EditionCacheManager;
+import nl.tudelft.labracore.lib.cache.EditionCollectionCacheManager;
+import nl.tudelft.labracore.lib.cache.ModuleCacheManager;
+import nl.tudelft.labracore.lib.data.Edition;
+import nl.tudelft.labracore.lib.data.EditionCollection;
+import nl.tudelft.labracore.lib.data.Module;
 import nl.tudelft.librador.dto.view.View;
-import nl.tudelft.queue.cache.EditionCacheManager;
-import nl.tudelft.queue.cache.EditionCollectionCacheManager;
+import nl.tudelft.queue.PageUtil;
 import nl.tudelft.queue.dto.create.QueueEditionCollectionCreateDTO;
 import nl.tudelft.queue.dto.view.QueueSessionSummaryDTO;
 import nl.tudelft.queue.repository.LabRepository;
+import nl.tudelft.queue.service.EditionService;
 import nl.tudelft.queue.service.RoleDTOService;
 
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Controller;
 import org.springframework.ui.Model;
@@ -53,41 +63,54 @@ public class SharedEditionController {
 	@Autowired
 	private EditionCacheManager editionCacheManager;
 
+	@Autowired
+	private EditionCacheManager eCache;
+
+	@Autowired
+	private ModuleCacheManager mCache;
+
 	@Autowired
 	private RoleDTOService roleDTOService;
 
+	@Autowired
+	private EditionService es;
+
 	@Autowired
 	private LabRepository lr;
 
 	@Autowired
 	private RoleControllerApi rApi;
 
-	/**
-	 * Get the page for a shared edition which includes all the details of the specific shared edition.
-	 *
-	 * @param  id    The id of the shared edition to be displayed.
-	 * @param  model The model to fill out for Thymeleaf template resolution.
-	 * @return       The Thymeleaf template to resolve.
-	 */
+	@Autowired
+	private CourseControllerApi cApi;
+
+	@Autowired
+	private CohortControllerApi cohortApi;
+
+	@Autowired
+	private BuildingControllerApi bApi;
+
 	@GetMapping("/{id}")
 	@PreAuthorize("@permissionService.isAuthenticated()")
-	public String getSharedEdition(@PathVariable Long id, Model model) {
+	public String getSharedEdition(@PathVariable Long id) {
+		EditionCollection collection = EditionCollection.byId(id);
+		List<Module> modules = Module.byIds(collection.getEditions().get().stream()
+				.flatMap(e -> e.getModules().value().stream())
+				.map(Module::getId).toList());
+		boolean hasAssignments = modules.stream().anyMatch(m -> !m.getAssignments().value().isEmpty());
+		if (hasAssignments) {
+			return "redirect:/shared-edition/{id}/labs";
+		}
+		return "redirect:/shared-edition/{id}/editions";
+	}
+
+	@GetMapping("/{id}/labs")
+	@PreAuthorize("@permissionService.isAuthenticated()")
+	public String getSharedEditionLabs(@PathVariable Long id, Model model) {
 		var collection = editionCollectionCacheManager.getOrThrow(id);
 		var editions = editionCacheManager.get(
 				collection.getEditions().stream().map(EditionSummaryDTO::getId).toList());
 
-		var teachers = editions.stream().map(edition -> roleDTOService.teachers(edition))
-				.flatMap(List::stream).toList();
-
-		var headTas = editions.stream().map(edition -> roleDTOService.headTAs(edition)).flatMap(List::stream)
-				.toList();
-
-		var assistants = editions.stream().map(edition -> roleDTOService.assistants(edition))
-				.flatMap(List::stream).toList();
-
-		var students = editions.stream().map(edition -> roleDTOService.students(edition))
-				.flatMap(List::stream).toList();
-
 		var allSessions = editions.stream().map(EditionDetailsDTO::getSessions).flatMap(List::stream)
 				.toList();
 
@@ -106,22 +129,85 @@ public class SharedEditionController {
 		var pastSessions = lr.findAllBySessions(pastSessionsIds).stream()
 				.map(l -> View.convert(l, QueueSessionSummaryDTO.class)).toList();
 
-		var editionTeachers = editions.stream().collect(Collectors.toMap(EditionDetailsDTO::getId,
-				s -> roleDTOService.teacherNames(s)));
+		model.addAttribute("collection", collection);
+		model.addAttribute("currentSessions", currentSessions);
+		model.addAttribute("upcomingSessions", upComingSessions);
+		model.addAttribute("pastSessions", pastSessions);
+		model.addAttribute("allModules", editions.stream().flatMap(e -> e.getModules().stream()).toList());
+
+		model.addAttribute("cohorts", cohortApi.getAllCohorts().collectList().block());
+		model.addAttribute("assignments",
+				mCache.get(
+						editions.stream().flatMap(e -> e.getModules().stream()).map(ModuleSummaryDTO::getId))
+						.stream().flatMap(m -> m.getAssignments().stream()).toList());
+		model.addAttribute("buildings", bApi.getAllBuildings().collectList().block());
+		model.addAttribute("clusters", Edition.byIds(editions.stream().map(EditionDetailsDTO::getId).toList())
+				.stream().flatMap(e -> e.getCohort().get().getClusters().value().stream()).toList());
+
+		return "shared-edition/view/labs";
+	}
+
+	@GetMapping("/{id}/participants")
+	@PreAuthorize("@permissionService.isAuthenticated()")
+	public String getSharedEditionParticipants(@PathVariable Long id, Model model,
+			@RequestParam(value = "search", required = false) String search,
+			@RequestParam(value = "roles", required = false) List<RolePersonDetailsDTO.TypeEnum> roleFilter,
+			@PageableDefault(size = 25) Pageable pageable) {
+		fillParticipantsModel(id, model, search, roleFilter, pageable);
+		return "shared-edition/view/participants";
+	}
+
+	@GetMapping("/{id}/participants/list")
+	@PreAuthorize("@permissionService.isAuthenticated()")
+	public String getSharedEditionParticipantsListOnly(@PathVariable Long id, Model model,
+			@RequestParam(value = "search", required = false) String search,
+			@RequestParam(value = "roles", required = false) List<RolePersonDetailsDTO.TypeEnum> roleFilter,
+			@PageableDefault(size = 25) Pageable pageable) {
+		fillParticipantsModel(id, model, search, roleFilter, pageable);
+		return "shared-edition/view/participants_table";
+	}
 
-		var roles = rApi.getRolesByEditions(editions.stream().map(EditionDetailsDTO::getId).toList())
-				.filter(r -> r.getType() != RolePersonDetailsDTO.TypeEnum.STUDENT)
+	private void fillParticipantsModel(Long id, Model model, String search,
+			List<RolePersonDetailsDTO.TypeEnum> roleFilter, Pageable pageable) {
+		var collection = editionCollectionCacheManager.getOrThrow(id);
+		var editions = editionCacheManager.get(
+				collection.getEditions().stream().map(EditionSummaryDTO::getId).toList());
+
+		var rolesMap = rApi.getRolesByEditions(editions.stream().map(EditionDetailsDTO::getId).toList())
 				.collect(Collectors.groupingBy(RolePersonDetailsDTO::getPerson)).block();
+		var roles = rolesMap.entrySet().stream()
+				.map(e -> search == null ? e
+						: Map.entry(e.getKey(), es.rolesMatchingFilter(e.getValue(), search)))
+				.map(e -> roleFilter == null || roleFilter.isEmpty() ? e
+						: Map.entry(e.getKey(),
+								e.getValue().stream().filter(r -> roleFilter.contains(r.getType())).toList()))
+				.filter(e -> !e.getValue().isEmpty())
+				.sorted(Comparator.comparing(e -> e.getKey().getDisplayName()))
+				.sorted(Comparator.<Map.Entry<PersonSummaryDTO, List<RolePersonDetailsDTO>>>comparingInt(
+						e -> e.getValue().stream().map(RolePersonDetailsDTO::getType)
+								.max(Comparator.comparingInt(Enum::ordinal)).get().ordinal())
+						.reversed())
+				.toList();
 
 		model.addAttribute("collection", collection);
 		model.addAttribute("editions", editions);
-		model.addAttribute("editionTeachers", editionTeachers);
-		model.addAttribute("roles", roles);
-		model.addAttribute("currentSessions", currentSessions);
-		model.addAttribute("upComingSessions", upComingSessions);
-		model.addAttribute("pastSessions", pastSessions);
+		model.addAttribute("roles", PageUtil.toPage(pageable, roles));
+	}
+
+	@GetMapping("/{id}/editions")
+	@PreAuthorize("@permissionService.isAuthenticated()")
+	public String getSharedEditionEditions(@PathVariable Long id, Model model) {
+		EditionCollection collection = EditionCollection.byId(id);
+		List<Edition> editions = collection.getEditions().get();
+
+		model.addAttribute("collection", collection);
+		model.addAttribute("editions", editions);
+		model.addAttribute("teachers", editions.stream()
+				.map(e -> Map.entry(e.getId(),
+						e.getRoles().get().stream().filter(r -> r.getType() == RoleType.TEACHER).toList()))
+				.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
 
-		return "shared-edition/view";
+		return "shared-edition/view/editions";
 	}
 
 	/**
@@ -131,7 +217,7 @@ public class SharedEditionController {
 	 * @param  sharedEdition The dto containing the shared edition to be created.
 	 * @return               Redirect to the shared editions page.
 	 */
-	@PostMapping("/create")
+	@PostMapping
 	@PreAuthorize("@permissionService.isAdminOrTeacher()")
 	public String createSharedEdition(
 			@Valid QueueEditionCollectionCreateDTO sharedEdition) {
diff --git a/src/main/java/nl/tudelft/queue/controller/TimeSlotController.java b/src/main/java/nl/tudelft/queue/controller/TimeSlotController.java
index 5a479224b006599e92f1582a6c258c824b205d9a..103b9a589feb1606da6da05d4bfabe7e1cddb77d 100644
--- a/src/main/java/nl/tudelft/queue/controller/TimeSlotController.java
+++ b/src/main/java/nl/tudelft/queue/controller/TimeSlotController.java
@@ -21,8 +21,8 @@ import java.util.Optional;
 
 import javax.transaction.Transactional;
 
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
-import nl.tudelft.labracore.lib.security.user.Person;
 import nl.tudelft.librador.resolver.annotations.PathEntity;
 import nl.tudelft.queue.model.ClosableTimeSlot;
 import nl.tudelft.queue.model.LabRequest;
@@ -33,6 +33,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
 
 @Controller
 public class TimeSlotController {
@@ -51,7 +52,7 @@ public class TimeSlotController {
 	 * @return          A redirect to the lab overview page.
 	 */
 	@Transactional
-	@GetMapping("/time-slot/{timeSlot}/close")
+	@PostMapping("/time-slot/{timeSlot}/close")
 	@PreAuthorize("@permissionService.canCloseTimeSlot(#timeSlot)")
 	public String closeTimeSlot(@PathEntity ClosableTimeSlot timeSlot) {
 		tss.closeTimeSlot(timeSlot);
diff --git a/src/main/java/nl/tudelft/queue/csv/UserCsvHelper.java b/src/main/java/nl/tudelft/queue/csv/UserCsvHelper.java
index f09111678093f9eb141098617ffd06f5a2ab9448..f27d94c1e1cfd4c3dda81ff170b881af633e088a 100644
--- a/src/main/java/nl/tudelft/queue/csv/UserCsvHelper.java
+++ b/src/main/java/nl/tudelft/queue/csv/UserCsvHelper.java
@@ -20,7 +20,7 @@ package nl.tudelft.queue.csv;
 import java.util.List;
 
 import lombok.Getter;
-import nl.tudelft.labracore.api.dto.RoleCreateDTO;
+import nl.tudelft.labracore.api.dto.ParticipantsImportDTO;
 
 import org.springframework.web.multipart.MultipartFile;
 
@@ -29,27 +29,30 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.dataformat.csv.CsvSchema;
 
 @Getter
-public class UserCsvHelper extends RoleCreateDTO {
+public class UserCsvHelper {
 
-	private String netId;
+	private String identifier;
+	private ParticipantsImportDTO.InnerEnum role;
 
 	private static final CsvSchema schema = CsvSchema.builder()
-			.setSkipFirstDataRow(true)
-			.addColumn("netId")
+			.setSkipFirstDataRow(false)
+			.addColumn("identifier")
 			.addColumn("role")
 			.setColumnSeparator(',')
 			.build();
 
 	@JsonCreator
-	public UserCsvHelper(@JsonProperty("netId") String netId, @JsonProperty("role") String role)
+	public UserCsvHelper(@JsonProperty("identifier") String identifier, @JsonProperty("role") String role)
 			throws IllegalArgumentException, InvalidCsvException {
 		super();
-		TypeEnum type = TypeEnum.fromValue(role.replace(" ", "").replace("\"", ""));
-		if (type.equals(TypeEnum.ADMIN) || type.equals(TypeEnum.BLOCKED)) {
+		ParticipantsImportDTO.InnerEnum type = ParticipantsImportDTO.InnerEnum
+				.fromValue(role.replace(" ", "").replace("\"", ""));
+		if (type.equals(ParticipantsImportDTO.InnerEnum.ADMIN)
+				|| type.equals(ParticipantsImportDTO.InnerEnum.BLOCKED)) {
 			throw new InvalidCsvException("Admins or blocked people cannot be added this way.");
 		}
-		super.type(type);
-		this.netId = netId.replace(" ", "").replace("\"", "");
+		this.identifier = identifier.replace(" ", "").replace("\"", "");
+		this.role = type;
 	}
 
 	/**
diff --git a/src/main/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTO.java
index 7f329e5ad765c9e5017853c497e3e6541d5adaba..af5b27f592b64e558647a22e5c2783b9fd5e9f96 100644
--- a/src/main/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTO.java
@@ -36,7 +36,7 @@ import org.springframework.format.annotation.DateTimeFormat;
 @AllArgsConstructor
 @EqualsAndHashCode(callSuper = false)
 public class QueueAssignmentCreateDTO extends Create<AssignmentCreateDTO> {
-	private static final String TIME_FORMAT = "dd-MM-yyyy HH:mm";
+	private static final String TIME_FORMAT = "yyyy-MM-dd'T'HH:mm";
 
 	@NotBlank
 	private String name;
@@ -48,10 +48,10 @@ public class QueueAssignmentCreateDTO extends Create<AssignmentCreateDTO> {
 	private LocalDateTime deadline;
 
 	@NotNull
-	private Long moduleId;
+	private Long module;
 
-	public QueueAssignmentCreateDTO(Long moduleId) {
-		this.moduleId = moduleId;
+	public QueueAssignmentCreateDTO(Long module) {
+		this.module = module;
 	}
 
 	@Override
@@ -62,6 +62,6 @@ public class QueueAssignmentCreateDTO extends Create<AssignmentCreateDTO> {
 	@Override
 	protected void postApply(AssignmentCreateDTO data) {
 		data.description(new Description().text(description));
-		data.module(new ModuleIdDTO().id(moduleId));
+		data.module(new ModuleIdDTO().id(module));
 	}
 }
diff --git a/src/main/java/nl/tudelft/queue/dto/create/QueueEditionCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/QueueEditionCreateDTO.java
index 7b64c118a35f0d25c1d61e2b4e356da655eea81a..23afbbdd35f3414103c0e4950141626e5d653ab1 100644
--- a/src/main/java/nl/tudelft/queue/dto/create/QueueEditionCreateDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/create/QueueEditionCreateDTO.java
@@ -36,7 +36,7 @@ import org.springframework.format.annotation.DateTimeFormat;
 @AllArgsConstructor
 @EqualsAndHashCode(callSuper = false)
 public class QueueEditionCreateDTO extends Create<EditionCreateDTO> {
-	private static final String TIME_FORMAT = "dd-MM-yyyy HH:mm";
+	private static final String TIME_FORMAT = "yyyy-MM-dd'T'HH:mm";
 
 	@NotBlank
 	private String name;
diff --git a/src/main/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTO.java
index bc56b13233759aa4328c9e3e14a30ab24e015a25..7ca6cc10d9a49b5bddaf641e65314c58c3e780aa 100644
--- a/src/main/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTO.java
@@ -30,13 +30,13 @@ import nl.tudelft.librador.dto.create.Create;
 @AllArgsConstructor
 @EqualsAndHashCode(callSuper = false)
 public class QueueModuleCreateDTO extends Create<ModuleCreateDTO> {
-	private Long editionId;
+	private Long edition;
 
 	@NotBlank
 	private String name;
 
-	public QueueModuleCreateDTO(Long editionId) {
-		this.editionId = editionId;
+	public QueueModuleCreateDTO(Long edition) {
+		this.edition = edition;
 	}
 
 	@Override
@@ -46,6 +46,6 @@ public class QueueModuleCreateDTO extends Create<ModuleCreateDTO> {
 
 	@Override
 	protected void postApply(ModuleCreateDTO data) {
-		data.edition(new EditionIdDTO().id(editionId));
+		data.edition(new EditionIdDTO().id(edition));
 	}
 }
diff --git a/src/main/java/nl/tudelft/queue/dto/create/QueueRoleCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/QueueRoleCreateDTO.java
index 29a0e4409073e608f69db45f60b127c87438a32a..69ee65fe7160b0181d9f4f726e5770a76fb1dc50 100644
--- a/src/main/java/nl/tudelft/queue/dto/create/QueueRoleCreateDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/create/QueueRoleCreateDTO.java
@@ -22,38 +22,28 @@ import static nl.tudelft.labracore.api.dto.RoleCreateDTO.TypeEnum.*;
 import java.util.Set;
 
 import lombok.*;
-import nl.tudelft.labracore.api.dto.EditionIdDTO;
 import nl.tudelft.labracore.api.dto.RoleCreateDTO;
-import nl.tudelft.librador.dto.create.Create;
+import nl.tudelft.librador.dto.Validated;
 
 @Data
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
 @EqualsAndHashCode(callSuper = false)
-public class QueueRoleCreateDTO extends Create<RoleCreateDTO> {
-	private Long editionId;
-	private String username;
-	private RoleCreateDTO.TypeEnum type;
+public class QueueRoleCreateDTO extends Validated {
+	private Long edition;
+	private String identifier;
+	private RoleCreateDTO.TypeEnum role;
 
-	public QueueRoleCreateDTO(Long editionId) {
-		this.editionId = editionId;
-	}
-
-	@Override
-	public Class<nl.tudelft.labracore.api.dto.RoleCreateDTO> clazz() {
-		return RoleCreateDTO.class;
+	public QueueRoleCreateDTO(Long edition) {
+		this.edition = edition;
 	}
 
 	@Override
 	public void validate() {
-		if (Set.of(ADMIN, TEACHER_RO, BLOCKED).contains(type)) {
+		if (Set.of(ADMIN, TEACHER_RO, BLOCKED).contains(role)) {
 			errors.rejectValue("type", "The 'type' field for a role cannot be ADMIN, BLOCKED or TEACHER_RO");
 		}
 	}
 
-	@Override
-	protected void postApply(RoleCreateDTO data) {
-		data.edition(new EditionIdDTO().id(editionId));
-	}
 }
diff --git a/src/main/java/nl/tudelft/queue/cache/SessionCacheManager.java b/src/main/java/nl/tudelft/queue/dto/create/QueueRolesCreateDTO.java
similarity index 53%
rename from src/main/java/nl/tudelft/queue/cache/SessionCacheManager.java
rename to src/main/java/nl/tudelft/queue/dto/create/QueueRolesCreateDTO.java
index d6ca827405103f8a97d708f4780afbc512748628..003a12e857271598021edd31ea533d537a7e165f 100644
--- a/src/main/java/nl/tudelft/queue/cache/SessionCacheManager.java
+++ b/src/main/java/nl/tudelft/queue/dto/create/QueueRolesCreateDTO.java
@@ -15,34 +15,35 @@
  * You should have received a copy of the GNU Affero General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
-package nl.tudelft.queue.cache;
+package nl.tudelft.queue.dto.create;
 
-import java.util.List;
+import static nl.tudelft.labracore.api.dto.ParticipantsImportDTO.InnerEnum.*;
 
-import lombok.AllArgsConstructor;
-import lombok.NoArgsConstructor;
-import nl.tudelft.labracore.api.SessionControllerApi;
-import nl.tudelft.labracore.api.dto.SessionDetailsDTO;
+import java.util.Set;
 
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.web.context.annotation.RequestScope;
+import lombok.*;
+import nl.tudelft.labracore.api.dto.ParticipantsImportDTO;
+import nl.tudelft.librador.dto.Validated;
 
-@Component
-@RequestScope
+@Data
+@Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class SessionCacheManager extends CoreCacheManager<Long, SessionDetailsDTO> {
-	@Autowired
-	private SessionControllerApi api;
+@EqualsAndHashCode(callSuper = false)
+public class QueueRolesCreateDTO extends Validated {
+	private Long edition;
+	private String identifiers;
+	private ParticipantsImportDTO.InnerEnum role;
 
-	@Override
-	protected List<SessionDetailsDTO> fetch(List<Long> ids) {
-		return api.getSessionsById(ids).collectList().block();
+	public QueueRolesCreateDTO(Long edition) {
+		this.edition = edition;
 	}
 
 	@Override
-	protected Long getId(SessionDetailsDTO dto) {
-		return dto.getId();
+	public void validate() {
+		if (Set.of(ADMIN, TEACHER_RO, BLOCKED).contains(role)) {
+			errors.rejectValue("type", "The 'type' field for a role cannot be ADMIN, BLOCKED or TEACHER_RO");
+		}
 	}
+
 }
diff --git a/src/main/java/nl/tudelft/queue/dto/create/QueueSessionCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/QueueSessionCreateDTO.java
index 2ac52e1a5efd9ef78d0c80b4ddbeaaa5af6beaef..6536043b8c58dc91683932401f50ac0642d18645 100644
--- a/src/main/java/nl/tudelft/queue/dto/create/QueueSessionCreateDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/create/QueueSessionCreateDTO.java
@@ -25,8 +25,8 @@ import java.util.stream.Collectors;
 import lombok.*;
 import lombok.experimental.SuperBuilder;
 import nl.tudelft.labracore.api.dto.*;
+import nl.tudelft.labracore.lib.cache.RoomCacheManager;
 import nl.tudelft.librador.dto.create.Create;
-import nl.tudelft.queue.cache.RoomCacheManager;
 import nl.tudelft.queue.dto.create.embeddables.LabRequestConstraintsCreateDTO;
 import nl.tudelft.queue.model.QueueSession;
 import nl.tudelft.queue.model.embeddables.Slot;
@@ -37,6 +37,8 @@ import nl.tudelft.queue.model.embeddables.Slot;
 @AllArgsConstructor
 @EqualsAndHashCode(callSuper = false)
 public abstract class QueueSessionCreateDTO<D extends QueueSession<?>> extends Create<D> {
+	private Long organisationalUnit;
+
 	private String name;
 	private Slot slot;
 
diff --git a/src/main/java/nl/tudelft/queue/dto/create/embeddables/SlottedLabConfigCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/embeddables/SlottedLabConfigCreateDTO.java
index ca01ba2a216f5d32e368d4044824fd89faac2282..68b266dfa27c0d7565ca4e541ee49edb03464ea6 100644
--- a/src/main/java/nl/tudelft/queue/dto/create/embeddables/SlottedLabConfigCreateDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/create/embeddables/SlottedLabConfigCreateDTO.java
@@ -34,7 +34,7 @@ import org.springframework.format.annotation.DateTimeFormat;
 @AllArgsConstructor
 @EqualsAndHashCode(callSuper = false)
 public class SlottedLabConfigCreateDTO extends Create<SlottedLabConfig> {
-	private static final String TIME_FORMAT = "dd-MM-yyyy HH:mm";
+	private static final String TIME_FORMAT = "yyyy-MM-dd'T'HH:mm";
 
 	/**
 	 * The time that should be assigned to each of the timeslots. The slot duration is stored in minutes.
diff --git a/src/main/java/nl/tudelft/queue/dto/create/labs/LabCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/labs/LabCreateDTO.java
index 1845586e73a64f611464be03d3439474d0cecb26..8b77a7d5fee234abb60987d85922b2eb460e50c5 100644
--- a/src/main/java/nl/tudelft/queue/dto/create/labs/LabCreateDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/create/labs/LabCreateDTO.java
@@ -41,6 +41,7 @@ import nl.tudelft.queue.model.labs.Lab;
 @NoArgsConstructor
 @AllArgsConstructor
 @EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
 public abstract class LabCreateDTO<D extends Lab> extends QueueSessionCreateDTO<D> {
 	private CommunicationMethod communicationMethod;
 
diff --git a/src/main/java/nl/tudelft/queue/dto/create/labs/RegularLabCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/labs/RegularLabCreateDTO.java
index e7bd45e6e3f63b16cfb893d84e9aea92573c0cdd..0a7e1daf261a1d0a1cf8d19e433e8d5fa3cbf879 100644
--- a/src/main/java/nl/tudelft/queue/dto/create/labs/RegularLabCreateDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/create/labs/RegularLabCreateDTO.java
@@ -26,6 +26,7 @@ import nl.tudelft.queue.model.labs.RegularLab;
 @SuperBuilder
 @AllArgsConstructor
 @EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
 public class RegularLabCreateDTO extends LabCreateDTO<RegularLab> {
 
 	public RegularLabCreateDTO(SessionDetailsDTO session,
diff --git a/src/main/java/nl/tudelft/queue/dto/create/requests/LabRequestCreateDTO.java b/src/main/java/nl/tudelft/queue/dto/create/requests/LabRequestCreateDTO.java
index f09d6ff1c1d6e92e09b66be4ded7cf3d84eafd16..4fbfd12d629cee5cb0cd68a5a5607670f3949ab3 100644
--- a/src/main/java/nl/tudelft/queue/dto/create/requests/LabRequestCreateDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/create/requests/LabRequestCreateDTO.java
@@ -29,8 +29,8 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import lombok.experimental.SuperBuilder;
-import nl.tudelft.queue.cache.AssignmentCacheManager;
-import nl.tudelft.queue.cache.SessionCacheManager;
+import nl.tudelft.labracore.lib.cache.AssignmentCacheManager;
+import nl.tudelft.labracore.lib.cache.SessionCacheManager;
 import nl.tudelft.queue.dto.create.RequestCreateDTO;
 import nl.tudelft.queue.dto.id.TimeSlotIdDTO;
 import nl.tudelft.queue.model.LabRequest;
diff --git a/src/main/java/nl/tudelft/queue/dto/patch/QueueAssignmentPatchDTO.java b/src/main/java/nl/tudelft/queue/dto/patch/QueueAssignmentPatchDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..2f4822dd3f1f67eae7ce4ab8cbe3364b818f4f2f
--- /dev/null
+++ b/src/main/java/nl/tudelft/queue/dto/patch/QueueAssignmentPatchDTO.java
@@ -0,0 +1,61 @@
+/*
+ * Queue - A Queueing system that can be used to handle labs in higher education
+ * Copyright (C) 2016-2021  Delft University of Technology
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.queue.dto.patch;
+
+import java.time.LocalDateTime;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+import lombok.*;
+import nl.tudelft.labracore.api.dto.AssignmentPatchDTO;
+import nl.tudelft.labracore.api.dto.Description;
+import nl.tudelft.librador.dto.create.Create;
+
+import org.springframework.format.annotation.DateTimeFormat;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = false)
+public class QueueAssignmentPatchDTO extends Create<AssignmentPatchDTO> {
+	private static final String TIME_FORMAT = "yyyy-MM-dd'T'HH:mm";
+
+	@NotNull
+	private Long assignment;
+
+	@NotBlank
+	private String name;
+	@NotNull
+	@Builder.Default
+	private String description = "";
+
+	@DateTimeFormat(pattern = TIME_FORMAT)
+	private LocalDateTime deadline;
+
+	@Override
+	public Class<AssignmentPatchDTO> clazz() {
+		return AssignmentPatchDTO.class;
+	}
+
+	@Override
+	protected void postApply(AssignmentPatchDTO data) {
+		data.description(new Description().text(description));
+	}
+}
diff --git a/src/main/java/nl/tudelft/queue/dto/patch/QueueEditionPatchDTO.java b/src/main/java/nl/tudelft/queue/dto/patch/QueueEditionPatchDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..b16291cc3dc78d5f1a5dc08285a47ff6c7c51803
--- /dev/null
+++ b/src/main/java/nl/tudelft/queue/dto/patch/QueueEditionPatchDTO.java
@@ -0,0 +1,72 @@
+/*
+ * Queue - A Queueing system that can be used to handle labs in higher education
+ * Copyright (C) 2016-2021  Delft University of Technology
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.queue.dto.patch;
+
+import java.time.LocalDateTime;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+import lombok.*;
+import nl.tudelft.labracore.api.dto.CohortIdDTO;
+import nl.tudelft.labracore.api.dto.CourseIdDTO;
+import nl.tudelft.labracore.api.dto.EditionPatchDTO;
+import nl.tudelft.librador.dto.create.Create;
+
+import org.springframework.format.annotation.DateTimeFormat;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = false)
+public class QueueEditionPatchDTO extends Create<EditionPatchDTO> {
+	private static final String TIME_FORMAT = "yyyy-MM-dd'T'HH:mm";
+
+	@NotNull
+	private Long edition;
+
+	@NotBlank
+	private String name;
+
+	@NotNull
+	private Long course;
+
+	@NotNull
+	private Long cohort;
+
+	@NotNull
+	@DateTimeFormat(pattern = TIME_FORMAT)
+	private LocalDateTime startDate;
+
+	@NotNull
+	@DateTimeFormat(pattern = TIME_FORMAT)
+	private LocalDateTime endDate;
+
+	@Override
+	public Class<EditionPatchDTO> clazz() {
+		return EditionPatchDTO.class;
+	}
+
+	@Override
+	protected void postApply(EditionPatchDTO data) {
+		super.postApply(data);
+		data.course(new CourseIdDTO().id(course));
+		data.cohort(new CohortIdDTO().id(cohort));
+	}
+}
diff --git a/src/main/java/nl/tudelft/queue/cache/CohortCacheManager.java b/src/main/java/nl/tudelft/queue/dto/patch/QueueModulePatchDTO.java
similarity index 54%
rename from src/main/java/nl/tudelft/queue/cache/CohortCacheManager.java
rename to src/main/java/nl/tudelft/queue/dto/patch/QueueModulePatchDTO.java
index cde48d7f70da921b38b39c42eb3f372892b5a02b..0884a93a8b71e5637d484151d9f1f553455d868a 100644
--- a/src/main/java/nl/tudelft/queue/cache/CohortCacheManager.java
+++ b/src/main/java/nl/tudelft/queue/dto/patch/QueueModulePatchDTO.java
@@ -15,30 +15,30 @@
  * You should have received a copy of the GNU Affero General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
-package nl.tudelft.queue.cache;
+package nl.tudelft.queue.dto.patch;
 
-import java.util.List;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
 
-import nl.tudelft.labracore.api.CohortControllerApi;
-import nl.tudelft.labracore.api.dto.CohortDetailsDTO;
+import lombok.*;
+import nl.tudelft.labracore.api.dto.ModulePatchDTO;
+import nl.tudelft.librador.dto.create.Create;
 
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.web.context.annotation.RequestScope;
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = false)
+public class QueueModulePatchDTO extends Create<ModulePatchDTO> {
+	@NotNull
+	private Long module;
 
-@Component
-@RequestScope
-public class CohortCacheManager extends CoreCacheManager<Long, CohortDetailsDTO> {
-	@Autowired
-	private CohortControllerApi cApi;
+	@NotBlank
+	private String name;
 
 	@Override
-	protected List<CohortDetailsDTO> fetch(List<Long> ids) {
-		return cApi.getAllCohortsById(ids).collectList().block();
+	public Class<ModulePatchDTO> clazz() {
+		return ModulePatchDTO.class;
 	}
 
-	@Override
-	protected Long getId(CohortDetailsDTO dto) {
-		return dto.getId();
-	}
 }
diff --git a/src/main/java/nl/tudelft/queue/cache/EditionCacheManager.java b/src/main/java/nl/tudelft/queue/dto/patch/QueueRolePatchDTO.java
similarity index 54%
rename from src/main/java/nl/tudelft/queue/cache/EditionCacheManager.java
rename to src/main/java/nl/tudelft/queue/dto/patch/QueueRolePatchDTO.java
index b1ef979159d7339a4b8bcfedef4207692bc3fe84..b71daa5a8de1b9af2b2f4925992222e6dbab650e 100644
--- a/src/main/java/nl/tudelft/queue/cache/EditionCacheManager.java
+++ b/src/main/java/nl/tudelft/queue/dto/patch/QueueRolePatchDTO.java
@@ -15,30 +15,31 @@
  * You should have received a copy of the GNU Affero General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
-package nl.tudelft.queue.cache;
+package nl.tudelft.queue.dto.patch;
 
-import java.util.List;
+import static nl.tudelft.labracore.api.dto.RolePatchDTO.TypeEnum.*;
 
-import nl.tudelft.labracore.api.EditionControllerApi;
-import nl.tudelft.labracore.api.dto.EditionDetailsDTO;
+import java.util.Set;
 
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import org.springframework.web.context.annotation.RequestScope;
+import lombok.*;
+import nl.tudelft.labracore.api.dto.RolePatchDTO;
+import nl.tudelft.librador.dto.Validated;
 
-@Component
-@RequestScope
-public class EditionCacheManager extends CoreCacheManager<Long, EditionDetailsDTO> {
-	@Autowired
-	private EditionControllerApi api;
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = false)
+public class QueueRolePatchDTO extends Validated {
+	private Long edition;
+	private Long person;
+	private RolePatchDTO.TypeEnum role;
 
 	@Override
-	protected List<EditionDetailsDTO> fetch(List<Long> ids) {
-		return api.getEditionsById(ids).collectList().block();
+	public void validate() {
+		if (Set.of(ADMIN, TEACHER_RO, BLOCKED).contains(role)) {
+			errors.rejectValue("type", "The 'type' field for a role cannot be ADMIN, BLOCKED or TEACHER_RO");
+		}
 	}
 
-	@Override
-	protected Long getId(EditionDetailsDTO dto) {
-		return dto.getId();
-	}
 }
diff --git a/src/main/java/nl/tudelft/queue/dto/patch/QueueSessionPatchDTO.java b/src/main/java/nl/tudelft/queue/dto/patch/QueueSessionPatchDTO.java
index 631d5945cbec87bf6f7b240b8d7f895082dc5c39..dc0b24076728e78cdc80c9a5a70670088eefe539 100644
--- a/src/main/java/nl/tudelft/queue/dto/patch/QueueSessionPatchDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/patch/QueueSessionPatchDTO.java
@@ -37,6 +37,9 @@ import nl.tudelft.queue.model.embeddables.Slot;
 @AllArgsConstructor
 @EqualsAndHashCode(callSuper = false)
 public abstract class QueueSessionPatchDTO<D extends QueueSession<?>> extends Patch<D> {
+
+	private Long id;
+
 	private String name;
 	private Slot slot;
 
diff --git a/src/main/java/nl/tudelft/queue/dto/patch/RequestPatchDTO.java b/src/main/java/nl/tudelft/queue/dto/patch/RequestPatchDTO.java
index 25294984fcfc48b73e22c4faf92e534074aad135..28f2646ed508155cdc0ee2a83c214753a57dc38e 100644
--- a/src/main/java/nl/tudelft/queue/dto/patch/RequestPatchDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/patch/RequestPatchDTO.java
@@ -20,9 +20,9 @@ package nl.tudelft.queue.dto.patch;
 import java.util.Objects;
 
 import lombok.*;
+import nl.tudelft.labracore.lib.cache.SessionCacheManager;
 import nl.tudelft.librador.SpringContext;
 import nl.tudelft.librador.dto.patch.Patch;
-import nl.tudelft.queue.cache.SessionCacheManager;
 import nl.tudelft.queue.model.LabRequest;
 
 @Data
diff --git a/src/main/java/nl/tudelft/queue/dto/patch/SlottedLabConfigPatchDTO.java b/src/main/java/nl/tudelft/queue/dto/patch/SlottedLabConfigPatchDTO.java
index 07716b5f9e08b015ec71e5ecf16da0397b4e8c6d..207db1f57ec29e64d43ba5a7b0a2bbac25fccce3 100644
--- a/src/main/java/nl/tudelft/queue/dto/patch/SlottedLabConfigPatchDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/patch/SlottedLabConfigPatchDTO.java
@@ -31,7 +31,7 @@ import org.springframework.format.annotation.DateTimeFormat;
 @AllArgsConstructor
 @EqualsAndHashCode(callSuper = false)
 public class SlottedLabConfigPatchDTO extends Patch<SlottedLabConfig> {
-	private static final String TIME_FORMAT = "dd-MM-yyyy HH:mm";
+	private static final String TIME_FORMAT = "yyyy-MM-dd'T'HH:mm";
 
 	@DateTimeFormat(pattern = TIME_FORMAT)
 	private LocalDateTime selectionOpensAt;
diff --git a/src/main/java/nl/tudelft/queue/dto/util/RequestTableFilterDTO.java b/src/main/java/nl/tudelft/queue/dto/util/RequestTableFilterDTO.java
index 86ef532e4ccc81cfb7ba17eec0c75d60af72fb23..dd2c89878f3fb13f59ba62847ed5e3ed852a30ba 100644
--- a/src/main/java/nl/tudelft/queue/dto/util/RequestTableFilterDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/util/RequestTableFilterDTO.java
@@ -29,6 +29,8 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import nl.tudelft.librador.dto.Validated;
+import nl.tudelft.queue.model.LabRequest;
+import nl.tudelft.queue.model.Request;
 import nl.tudelft.queue.model.enums.RequestStatus;
 import nl.tudelft.queue.model.enums.RequestType;
 
@@ -70,8 +72,25 @@ public class RequestTableFilterDTO extends Validated implements Serializable {
 	 * @return A stream of Sets.
 	 */
 	private Stream<Set<?>> getAllFiltersAsStream() {
-		return Stream.of(labs, assigned, rooms, assigned, assignments, requestStatuses,
-				requestTypes);
+		return Stream.of(labs, assignments, rooms, assigned, requestStatuses, requestTypes);
+	}
+
+	/**
+	 * Checks whether a request matches this filter.
+	 *
+	 * @param  request The request to check
+	 * @return         True iff the request matches the filter
+	 */
+	public boolean apply(Request<?> request) {
+		return (labs.isEmpty() || labs.contains(request.getSession().getId())) &&
+				(assigned.isEmpty() || assigned.contains(request.getEventInfo().getAssignedTo())) &&
+				(rooms.isEmpty() || rooms.contains(request.getRoom())) &&
+				(assignments.isEmpty()
+						|| request instanceof LabRequest lr && assignments.contains(lr.getAssignment()))
+				&&
+				(requestStatuses.isEmpty() || requestStatuses.contains(request.getEventInfo().getStatus())) &&
+				(requestTypes.isEmpty()
+						|| request instanceof LabRequest lr && requestTypes.contains(lr.getRequestType()));
 	}
 
 	@Override
diff --git a/src/main/java/nl/tudelft/queue/dto/util/RequestTableFilterUpdateDTO.java b/src/main/java/nl/tudelft/queue/dto/util/RequestTableFilterUpdateDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..64fb81d87c016291181b032983f3f0b402b159f7
--- /dev/null
+++ b/src/main/java/nl/tudelft/queue/dto/util/RequestTableFilterUpdateDTO.java
@@ -0,0 +1,69 @@
+/*
+ * Queue - A Queueing system that can be used to handle labs in higher education
+ * Copyright (C) 2016-2021  Delft University of Technology
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.queue.dto.util;
+
+import java.io.Serializable;
+import java.util.Set;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import nl.tudelft.librador.dto.Validated;
+import nl.tudelft.queue.model.enums.RequestStatus;
+import nl.tudelft.queue.model.enums.RequestType;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = false)
+public class RequestTableFilterUpdateDTO extends Validated implements Serializable {
+
+	private Set<Long> labs;
+	private Set<Long> assignments;
+	private Set<Long> rooms;
+	private Set<Long> assigned;
+	private Set<RequestStatus> requestStatuses;
+	private Set<RequestType> requestTypes;
+
+	/**
+	 * Updates an existing filter with this DTO.
+	 *
+	 * @param  filter The filter to update
+	 * @return        The updated filter
+	 */
+	public RequestTableFilterDTO updateFilter(RequestTableFilterDTO filter) {
+		if (labs != null)
+			filter.setLabs(labs);
+		if (assignments != null)
+			filter.setAssignments(assignments);
+		if (rooms != null)
+			filter.setRooms(rooms);
+		if (assigned != null)
+			filter.setAssigned(assigned);
+		if (requestStatuses != null)
+			filter.setRequestStatuses(requestStatuses);
+		if (requestTypes != null)
+			filter.setRequestTypes(requestTypes);
+		return filter;
+	}
+
+	@Override
+	protected void validate() {
+	}
+}
diff --git a/src/main/java/nl/tudelft/queue/dto/view/CalendarEntryViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/CalendarEntryViewDTO.java
index 5fa5277ca295ae5bad77bd4ec078e2d0b43c343a..8b61ae319b3ead9507365b80e41d4ffc9020477a 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/CalendarEntryViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/CalendarEntryViewDTO.java
@@ -24,9 +24,9 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import nl.tudelft.labracore.api.dto.SessionDetailsDTO;
+import nl.tudelft.labracore.lib.cache.SessionCacheManager;
 import nl.tudelft.librador.SpringContext;
 import nl.tudelft.librador.dto.view.View;
-import nl.tudelft.queue.cache.SessionCacheManager;
 import nl.tudelft.queue.model.enums.QueueSessionType;
 import nl.tudelft.queue.model.labs.AbstractSlottedLab;
 import nl.tudelft.queue.model.labs.Lab;
diff --git a/src/main/java/nl/tudelft/queue/dto/view/ExamRequestViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/ExamRequestViewDTO.java
index 519bf9242b1325bbc981465c8965be749eb2e2e5..e6506a24e3e3d425921710e5b7582f5e577aa1dd 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/ExamRequestViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/ExamRequestViewDTO.java
@@ -23,8 +23,8 @@ import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
+import nl.tudelft.labracore.lib.cache.PersonCacheManager;
 import nl.tudelft.librador.dto.view.View;
-import nl.tudelft.queue.cache.PersonCacheManager;
 import nl.tudelft.queue.model.LabRequest;
 import nl.tudelft.queue.model.embeddables.RequestEventInfo;
 
diff --git a/src/main/java/nl/tudelft/queue/dto/view/ExamTimeSlotViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/ExamTimeSlotViewDTO.java
index 0c265b3d65b4d1cc5fa9da0525f7dd204e523720..4ca793457b7eca2320ad7ea03740fcf850e9926d 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/ExamTimeSlotViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/ExamTimeSlotViewDTO.java
@@ -27,12 +27,14 @@ import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
+import nl.tudelft.labracore.lib.cache.PersonCacheManager;
 import nl.tudelft.librador.dto.view.View;
-import nl.tudelft.queue.cache.PersonCacheManager;
 import nl.tudelft.queue.model.ClosableTimeSlot;
 import nl.tudelft.queue.model.LabRequest;
 import nl.tudelft.queue.model.embeddables.Slot;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
 @Data
 @NoArgsConstructor
 @AllArgsConstructor
@@ -48,6 +50,7 @@ public class ExamTimeSlotViewDTO extends View<ClosableTimeSlot> {
 
 	private Boolean active;
 
+	@JsonIgnore
 	private List<ExamRequestViewDTO> examRequests;
 
 	@Override
diff --git a/src/main/java/nl/tudelft/queue/dto/view/FeedbackViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/FeedbackViewDTO.java
index 820b8cf1f57f3d61a6f3661d5525ab7e5be89a6f..d4ec22042beb4a28f86517a6776ae8b1aaf02f06 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/FeedbackViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/FeedbackViewDTO.java
@@ -29,9 +29,9 @@ import lombok.NoArgsConstructor;
 import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
 import nl.tudelft.labracore.api.dto.RolePersonLayer1DTO;
 import nl.tudelft.labracore.api.dto.StudentGroupDetailsDTO;
+import nl.tudelft.labracore.lib.cache.SessionCacheManager;
+import nl.tudelft.labracore.lib.cache.StudentGroupCacheManager;
 import nl.tudelft.librador.dto.view.View;
-import nl.tudelft.queue.cache.SessionCacheManager;
-import nl.tudelft.queue.cache.StudentGroupCacheManager;
 import nl.tudelft.queue.model.Feedback;
 import nl.tudelft.queue.model.LabRequest;
 
diff --git a/src/main/java/nl/tudelft/queue/dto/view/LabViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/LabViewDTO.java
index e7799ea679224a2de128a21f86d69bf3e439156a..d1cacc992e68aeae50e08ab091dee68f6d30bdf4 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/LabViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/LabViewDTO.java
@@ -34,4 +34,6 @@ public abstract class LabViewDTO<D extends Lab> extends QueueSessionViewDTO<D> {
 	private Set<AllowedRequest> allowedRequests;
 
 	private Boolean enableExperimental;
+
+	private Integer eolGracePeriod;
 }
diff --git a/src/main/java/nl/tudelft/queue/dto/view/QueueSessionSummaryDTO.java b/src/main/java/nl/tudelft/queue/dto/view/QueueSessionSummaryDTO.java
index 16cad7b0f5aab595ee730635a90cb6af3fd6d672..8e1d8dbd30b588a32e2fb80cc2e36834a992f6ee 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/QueueSessionSummaryDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/QueueSessionSummaryDTO.java
@@ -26,8 +26,8 @@ import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
+import nl.tudelft.labracore.lib.cache.SessionCacheManager;
 import nl.tudelft.librador.dto.view.View;
-import nl.tudelft.queue.cache.SessionCacheManager;
 import nl.tudelft.queue.misc.QueueSessionStatus;
 import nl.tudelft.queue.model.QueueSession;
 import nl.tudelft.queue.model.TimeSlot;
diff --git a/src/main/java/nl/tudelft/queue/dto/view/QueueSessionViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/QueueSessionViewDTO.java
index efef4fad47d4c9e6928635b36b6a3d33a5749367..5a43fa2a46ba681ae3fbd4e0928e377161b5ea92 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/QueueSessionViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/QueueSessionViewDTO.java
@@ -31,9 +31,9 @@ import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import nl.tudelft.labracore.api.dto.ModuleDetailsDTO;
 import nl.tudelft.labracore.api.dto.SessionDetailsDTO;
+import nl.tudelft.labracore.lib.cache.ModuleCacheManager;
+import nl.tudelft.labracore.lib.cache.SessionCacheManager;
 import nl.tudelft.librador.dto.view.View;
-import nl.tudelft.queue.cache.ModuleCacheManager;
-import nl.tudelft.queue.cache.SessionCacheManager;
 import nl.tudelft.queue.misc.QueueSessionStatus;
 import nl.tudelft.queue.model.QueueSession;
 import nl.tudelft.queue.model.Request;
@@ -41,9 +41,7 @@ import nl.tudelft.queue.model.embeddables.LabRequestConstraints;
 import nl.tudelft.queue.model.enums.QueueSessionType;
 import nl.tudelft.queue.service.LabService;
 
-import org.commonmark.node.Node;
-import org.commonmark.parser.Parser;
-import org.commonmark.renderer.html.HtmlRenderer;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 
 @Data
 @NoArgsConstructor
@@ -63,6 +61,7 @@ public abstract class QueueSessionViewDTO<D extends QueueSession<?>> extends Vie
 
 	private Set<ModuleDetailsDTO> modules;
 
+	@JsonIgnore
 	private List<Request<?>> requests = new ArrayList<>();
 
 	private QueueSessionStatus status;
@@ -76,12 +75,12 @@ public abstract class QueueSessionViewDTO<D extends QueueSession<?>> extends Vie
 				getBean(ModuleCacheManager.class).get(data.getModules().stream()));
 		status = getBean(LabService.class).getLabStatus(data);
 
-		Parser parser = Parser.builder()
-				.build();
-		Node document = parser.parse(data.getExtraInfo() == null ? "" : data.getExtraInfo());
-		HtmlRenderer renderer = HtmlRenderer.builder().sanitizeUrls(true).escapeHtml(true)
-				.build();
-		extraInfo = renderer.render(document);
+		//		Parser parser = Parser.builder()
+		//				.build();
+		//		Node document = parser.parse(data.getExtraInfo() == null ? "" : data.getExtraInfo());
+		//		HtmlRenderer renderer = HtmlRenderer.builder().sanitizeUrls(true).escapeHtml(true)
+		//				.build(); TODO
+		extraInfo = data.getExtraInfo() == null ? "" : data.getExtraInfo(); //renderer.render(document);
 	}
 
 	/**
diff --git a/src/main/java/nl/tudelft/queue/dto/view/RequestEventInfoViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/RequestEventInfoViewDTO.java
index 26bf22ef2cf184adfe8fd058d671aff50d644984..95cf1362bc0485f0d0dfeb067f27b93fa03772fc 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/RequestEventInfoViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/RequestEventInfoViewDTO.java
@@ -25,9 +25,9 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
+import nl.tudelft.labracore.lib.cache.PersonCacheManager;
 import nl.tudelft.librador.SpringContext;
 import nl.tudelft.librador.dto.view.View;
-import nl.tudelft.queue.cache.PersonCacheManager;
 import nl.tudelft.queue.model.RequestEvent;
 import nl.tudelft.queue.model.embeddables.RequestEventInfo;
 import nl.tudelft.queue.model.enums.RequestStatus;
diff --git a/src/main/java/nl/tudelft/queue/dto/view/RequestViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/RequestViewDTO.java
index 7a9cfa5e03592f35bc49db0c9a4df96ae432d22b..799af77ac9407df68fa51973ecf6eb7e46e6e0bf 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/RequestViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/RequestViewDTO.java
@@ -31,8 +31,8 @@ import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import lombok.experimental.SuperBuilder;
 import nl.tudelft.labracore.api.dto.*;
+import nl.tudelft.labracore.lib.cache.*;
 import nl.tudelft.librador.dto.view.View;
-import nl.tudelft.queue.cache.*;
 import nl.tudelft.queue.csv.CsvAble;
 import nl.tudelft.queue.model.QueueSession;
 import nl.tudelft.queue.model.Request;
diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestApprovedEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestApprovedEventViewDTO.java
index 41dd0c9eafaa835e5a8a955507aa2beb68bd983d..61a4ae56befec140beda44848b7713bcf32b6306 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestApprovedEventViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestApprovedEventViewDTO.java
@@ -29,6 +29,6 @@ public class RequestApprovedEventViewDTO extends RequestHandledEventViewDTO<Requ
 
 	@Override
 	public String getIconClass() {
-		return "request-approved";
+		return "positive";
 	}
 }
diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestCreatedEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestCreatedEventViewDTO.java
index 8362d73d07984473be5d63412ad29baeebe60009..2580a71526201f32a4fb3e483ee60c083fb95153 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestCreatedEventViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestCreatedEventViewDTO.java
@@ -30,6 +30,6 @@ public class RequestCreatedEventViewDTO extends RequestEventViewDTO<RequestCreat
 
 	@Override
 	public String getIconClass() {
-		return "request-created";
+		return "neutral";
 	}
 }
diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestForwardedToAnyEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestForwardedToAnyEventViewDTO.java
index 4e3925c7640805b1a1a4421f1f1cfb4b10941ac5..82470d9b543cdcd49cb3d0b2f605aed196252be9 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestForwardedToAnyEventViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestForwardedToAnyEventViewDTO.java
@@ -43,6 +43,6 @@ public class RequestForwardedToAnyEventViewDTO extends RequestEventViewDTO<Reque
 
 	@Override
 	public String getIconClass() {
-		return "request-forwarded";
+		return "neutral";
 	}
 }
diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestForwardedToPersonEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestForwardedToPersonEventViewDTO.java
index 09201cbd923ba079af05d997c7ff624e153d306f..3793b01351119bd30cacf17bd853224fa14ccfd3 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestForwardedToPersonEventViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestForwardedToPersonEventViewDTO.java
@@ -24,7 +24,7 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
-import nl.tudelft.queue.cache.PersonCacheManager;
+import nl.tudelft.labracore.lib.cache.PersonCacheManager;
 import nl.tudelft.queue.dto.view.RequestEventViewDTO;
 import nl.tudelft.queue.model.events.RequestForwardedToPersonEvent;
 
@@ -53,6 +53,6 @@ public class RequestForwardedToPersonEventViewDTO extends RequestEventViewDTO<Re
 
 	@Override
 	public String getIconClass() {
-		return "request-forwarded";
+		return "neutral";
 	}
 }
diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestHandledEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestHandledEventViewDTO.java
index 22c6aa8200526e83b6136cccf42c663d3d4bd1c0..8654afac0ebc1b2228a93fafab380485c3265722 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestHandledEventViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestHandledEventViewDTO.java
@@ -24,7 +24,7 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
-import nl.tudelft.queue.cache.PersonCacheManager;
+import nl.tudelft.labracore.lib.cache.PersonCacheManager;
 import nl.tudelft.queue.dto.view.RequestEventViewDTO;
 import nl.tudelft.queue.model.events.RequestHandledEvent;
 
diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestNotPickedEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestNotPickedEventViewDTO.java
index e9ef3b90fc0bbc305c91d9d5b2b5dbcf3f10f3de..6ed6ca9c2a33b28d898faca14e0cd7ef2657925a 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestNotPickedEventViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestNotPickedEventViewDTO.java
@@ -28,6 +28,6 @@ public class RequestNotPickedEventViewDTO extends RequestEventViewDTO<RequestNot
 
 	@Override
 	public String getIconClass() {
-		return "request-not-picked";
+		return "greyed-out";
 	}
 }
diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestNotSelectedEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestNotSelectedEventViewDTO.java
index 3c371829c6b447b51220e29897a6b9f37fc08bc3..784d224cbb77048c226849e604fbeb892de723c3 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestNotSelectedEventViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestNotSelectedEventViewDTO.java
@@ -28,6 +28,6 @@ public class RequestNotSelectedEventViewDTO extends RequestEventViewDTO<RequestN
 
 	@Override
 	public String getIconClass() {
-		return "request-not-selected";
+		return "negative";
 	}
 }
diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestPickedEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestPickedEventViewDTO.java
index 67db9917b086ef851a923104ef34e7f4ec14c241..809834a5f6d99d3d3d6d9e1482524f6206f8381f 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestPickedEventViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestPickedEventViewDTO.java
@@ -24,7 +24,7 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
-import nl.tudelft.queue.cache.PersonCacheManager;
+import nl.tudelft.labracore.lib.cache.PersonCacheManager;
 import nl.tudelft.queue.dto.view.RequestEventViewDTO;
 import nl.tudelft.queue.model.events.RequestPickedEvent;
 
@@ -47,6 +47,6 @@ public class RequestPickedEventViewDTO extends RequestEventViewDTO<RequestPicked
 
 	@Override
 	public String getIconClass() {
-		return "request-taken";
+		return "neutral";
 	}
 }
diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestRejectedEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestRejectedEventViewDTO.java
index 22411d2dd27ae8b5660ecf3854f7e64f47ae013d..bd0a709bdbe8145deade8b1325f5ad4f9d410ad6 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestRejectedEventViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestRejectedEventViewDTO.java
@@ -29,6 +29,6 @@ public class RequestRejectedEventViewDTO extends RequestHandledEventViewDTO<Requ
 
 	@Override
 	public String getIconClass() {
-		return "request-rejected";
+		return "negative";
 	}
 }
diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestRevokedEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestRevokedEventViewDTO.java
index 25ec29c058b58b9f314ff82b131acf1b55a9be82..5cfc4576257642b6f209cd4755a66a598d5efc6e 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestRevokedEventViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestRevokedEventViewDTO.java
@@ -30,6 +30,6 @@ public class RequestRevokedEventViewDTO extends RequestEventViewDTO<RequestRevok
 
 	@Override
 	public String getIconClass() {
-		return "request-revoked";
+		return "greyed-out";
 	}
 }
diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestSelectedEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestSelectedEventViewDTO.java
index 7b50ae0d332bad13b62667203d5dca488c2ac609..8eab9e6ae447356bdda7793cb1d214eea7149bfb 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestSelectedEventViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestSelectedEventViewDTO.java
@@ -28,6 +28,6 @@ public class RequestSelectedEventViewDTO extends RequestEventViewDTO<RequestSele
 
 	@Override
 	public String getIconClass() {
-		return "request-selected";
+		return "positive";
 	}
 }
diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/RequestTakenEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/RequestTakenEventViewDTO.java
index 2054839fef59804d8cdd412fc1eaeb027b356279..a371c4a84d5ba53e6b28394c9bb7ed592ccb8830 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/events/RequestTakenEventViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/events/RequestTakenEventViewDTO.java
@@ -24,7 +24,7 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
-import nl.tudelft.queue.cache.PersonCacheManager;
+import nl.tudelft.labracore.lib.cache.PersonCacheManager;
 import nl.tudelft.queue.dto.view.RequestEventViewDTO;
 import nl.tudelft.queue.model.events.RequestTakenEvent;
 
@@ -47,6 +47,6 @@ public class RequestTakenEventViewDTO extends RequestEventViewDTO<RequestTakenEv
 
 	@Override
 	public String getIconClass() {
-		return "request-taken";
+		return "neutral";
 	}
 }
diff --git a/src/main/java/nl/tudelft/queue/dto/view/events/StudentNotFoundEventViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/events/StudentNotFoundEventViewDTO.java
index cae29aee2f293b9fa4f7164c5c8f3fa44a6123bc..feb2728fd506ccbbc4a50cf50771dc1c2eaacd87 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/events/StudentNotFoundEventViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/events/StudentNotFoundEventViewDTO.java
@@ -53,6 +53,6 @@ public class StudentNotFoundEventViewDTO extends RequestEventViewDTO<StudentNotF
 
 	@Override
 	public String getIconClass() {
-		return "request-not-found";
+		return "neutral";
 	}
 }
diff --git a/src/main/java/nl/tudelft/queue/dto/view/labs/ExamLabViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/labs/ExamLabViewDTO.java
index 04e867692357053f791cdc9fe04670b942022cbc..b97805e1fa46cb86a49097e3bad4367a146f6da7 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/labs/ExamLabViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/labs/ExamLabViewDTO.java
@@ -18,7 +18,6 @@
 package nl.tudelft.queue.dto.view.labs;
 
 import java.util.List;
-import java.util.stream.Collectors;
 
 import lombok.AllArgsConstructor;
 import lombok.Data;
@@ -30,8 +29,6 @@ import nl.tudelft.queue.model.embeddables.ExamLabConfig;
 import nl.tudelft.queue.model.labs.ExamLab;
 
 import org.springframework.data.domain.Page;
-import org.springframework.data.domain.PageImpl;
-import org.springframework.data.domain.PageRequest;
 
 @Data
 @NoArgsConstructor
@@ -45,7 +42,7 @@ public class ExamLabViewDTO extends AbstractSlottedLabViewDTO<ExamLab> {
 	private int page;
 	private int pages;
 
-	private List<Page<ExamTimeSlotViewDTO>> timeSlotPages;
+	private List<ExamTimeSlotViewDTO> examTimeSlots;
 
 	private int totalHandled;
 	private int totalNeeded;
@@ -58,13 +55,7 @@ public class ExamLabViewDTO extends AbstractSlottedLabViewDTO<ExamLab> {
 		page = currentPage();
 		pages = data.getTimeSlots().size() / SLOTS_PER_PAGE + 1;
 
-		List<ExamTimeSlotViewDTO> timeSlots = View.convert(data.getTimeSlots(), ExamTimeSlotViewDTO.class);
-		timeSlotPages = timeSlots.stream()
-				.collect(Collectors.groupingBy(t -> timeSlots.indexOf(t) / 4))
-				.entrySet().stream()
-				.map(e -> new PageImpl<>(e.getValue(), PageRequest.of(e.getKey(), pages),
-						data.getTimeSlots().size()))
-				.collect(Collectors.toList());
+		examTimeSlots = View.convert(data.getTimeSlots(), ExamTimeSlotViewDTO.class);
 		totalHandled = data.getHandled().size();
 		totalNeeded = (int) Math
 				.ceil(data.getRequests().size() * ((double) examLabConfig.getPercentage() / 100.0));
diff --git a/src/main/java/nl/tudelft/queue/dto/view/requests/LabRequestViewDTO.java b/src/main/java/nl/tudelft/queue/dto/view/requests/LabRequestViewDTO.java
index 67072fb3a5e71543b59b758ed1dbdc8f73aaab2b..769eca6b4655da57f4a0c6dbc5d074d16d976789 100644
--- a/src/main/java/nl/tudelft/queue/dto/view/requests/LabRequestViewDTO.java
+++ b/src/main/java/nl/tudelft/queue/dto/view/requests/LabRequestViewDTO.java
@@ -27,9 +27,9 @@ import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import lombok.experimental.SuperBuilder;
 import nl.tudelft.labracore.api.dto.AssignmentDetailsDTO;
-import nl.tudelft.queue.cache.AssignmentCacheManager;
-import nl.tudelft.queue.cache.EditionCacheManager;
-import nl.tudelft.queue.cache.ModuleCacheManager;
+import nl.tudelft.labracore.lib.cache.AssignmentCacheManager;
+import nl.tudelft.labracore.lib.cache.EditionCacheManager;
+import nl.tudelft.labracore.lib.cache.ModuleCacheManager;
 import nl.tudelft.queue.dto.view.RequestViewDTO;
 import nl.tudelft.queue.model.Feedback;
 import nl.tudelft.queue.model.LabRequest;
diff --git a/src/main/java/nl/tudelft/queue/model/TimeSlot.java b/src/main/java/nl/tudelft/queue/model/TimeSlot.java
index 18b412308db8f6d581df869126698c6ae43e021d..e4c154d58d271d6b08fd257a953f154edc07829a 100644
--- a/src/main/java/nl/tudelft/queue/model/TimeSlot.java
+++ b/src/main/java/nl/tudelft/queue/model/TimeSlot.java
@@ -33,6 +33,8 @@ import nl.tudelft.queue.model.enums.RequestStatus;
 import nl.tudelft.queue.model.labs.AbstractSlottedLab;
 import nl.tudelft.queue.model.labs.Lab;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
 /**
  * Entity class representing a time-slot created for a certain lab. Labs create time-slots when the lab is set
  * to be a slotted lab.
@@ -62,6 +64,7 @@ public class TimeSlot {
 	 */
 	@NotNull
 	@ManyToOne
+	@JsonIgnore
 	@JoinColumn(name = "lab_id", updatable = false)
 	private Lab lab;
 
@@ -88,6 +91,7 @@ public class TimeSlot {
 	 * The requests that currently claim this slot.
 	 */
 	@NotNull
+	@JsonIgnore
 	@ToString.Exclude
 	@EqualsAndHashCode.Exclude
 	@OneToMany(mappedBy = "timeSlot")
diff --git a/src/main/java/nl/tudelft/queue/model/constraints/ClusterConstraint.java b/src/main/java/nl/tudelft/queue/model/constraints/ClusterConstraint.java
index 66e9b63acddec2c5d230aa9747bd14be4b6664f0..3888c3f38686c981bdbfc243995cfe2f8537fd8a 100644
--- a/src/main/java/nl/tudelft/queue/model/constraints/ClusterConstraint.java
+++ b/src/main/java/nl/tudelft/queue/model/constraints/ClusterConstraint.java
@@ -34,12 +34,14 @@ import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import lombok.experimental.SuperBuilder;
 import nl.tudelft.labracore.api.dto.ClusterDetailsDTO;
+import nl.tudelft.labracore.lib.cache.ClusterCacheManager;
 import nl.tudelft.librador.SpringContext;
-import nl.tudelft.queue.cache.ClusterCacheManager;
 import nl.tudelft.queue.dto.create.constraints.ClusterConstraintCreateDTO;
 import nl.tudelft.queue.model.LabRequestConstraint;
 import nl.tudelft.queue.model.QueueSession;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
 @Data
 @Entity
 @SuperBuilder
@@ -59,6 +61,7 @@ public class ClusterConstraint extends LabRequestConstraint {
 	 */
 	@NotNull
 	@OneToOne
+	@JsonIgnore
 	private QueueSession<?> session;
 
 	@Override
diff --git a/src/main/java/nl/tudelft/queue/model/constraints/ModuleDivisionConstraint.java b/src/main/java/nl/tudelft/queue/model/constraints/ModuleDivisionConstraint.java
index b21b5ac18e32071216a4c5434d2b00e9c8af1c04..cce0f7bbda32a1494c02b60a8bbf246d3a833313 100644
--- a/src/main/java/nl/tudelft/queue/model/constraints/ModuleDivisionConstraint.java
+++ b/src/main/java/nl/tudelft/queue/model/constraints/ModuleDivisionConstraint.java
@@ -31,15 +31,16 @@ import javax.validation.constraints.NotNull;
 import lombok.*;
 import lombok.experimental.SuperBuilder;
 import nl.tudelft.labracore.api.ModuleDivisionControllerApi;
-import nl.tudelft.labracore.api.dto.StudentGroupDetailsDTO;
+import nl.tudelft.labracore.lib.cache.PersonCacheManager;
+import nl.tudelft.labracore.lib.cache.StudentGroupCacheManager;
 import nl.tudelft.librador.SpringContext;
-import nl.tudelft.queue.cache.PersonCacheManager;
-import nl.tudelft.queue.cache.StudentGroupCacheManager;
 import nl.tudelft.queue.dto.create.constraints.ModuleDivisionConstraintCreateDTO;
 import nl.tudelft.queue.model.LabRequestConstraint;
 import nl.tudelft.queue.model.QueueSession;
 import nl.tudelft.queue.model.Request;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
 @Data
 @Entity
 @SuperBuilder
@@ -59,14 +60,15 @@ public class ModuleDivisionConstraint extends LabRequestConstraint {
 	 */
 	@NotNull
 	@OneToOne
+	@JsonIgnore
 	private QueueSession<?> session;
 
 	@Override
 	public boolean canCreateRequest(Long personId) {
 		// Go through every one of the student groups the person is in and check whether they are in one that is part
 		// of an allowed module division.
-		Stream<Long> studentGroups = SpringContext.getBean(StudentGroupCacheManager.class)
-				.getByPerson(personId).stream().map(StudentGroupDetailsDTO::getId);
+		Stream<Long> studentGroups = null; // TODO SpringContext.getBean(StudentGroupCacheManager.class)
+		//				.getByPerson(personId).stream().map(StudentGroupDetailsDTO::getId);
 		return studentInDivision(personId)
 				|| studentGroupInDivision(studentGroups);
 	}
diff --git a/src/main/java/nl/tudelft/queue/model/embeddables/Slot.java b/src/main/java/nl/tudelft/queue/model/embeddables/Slot.java
index 6f2fd10a2816711704b278b38e593995d7dcf7f0..7db85f7b1a6137506889c8206d6ce9a253893906 100644
--- a/src/main/java/nl/tudelft/queue/model/embeddables/Slot.java
+++ b/src/main/java/nl/tudelft/queue/model/embeddables/Slot.java
@@ -39,7 +39,7 @@ import org.springframework.format.annotation.DateTimeFormat;
 @NoArgsConstructor
 @AllArgsConstructor
 public class Slot {
-	private static final String TIME_FORMAT = "dd-MM-yyyy HH:mm";
+	private static final String TIME_FORMAT = "yyyy-MM-dd'T'HH:mm";
 	private static final String SIMPLE_SLOT_FORMAT = "HH:mm";
 
 	/**
diff --git a/src/main/java/nl/tudelft/queue/model/enums/RequestStatus.java b/src/main/java/nl/tudelft/queue/model/enums/RequestStatus.java
index 667fe0a7f1f2cd588cd52611749031c43f453a77..8462471eda2ac6ec96ea54e48ae23dfa4f5b06c1 100644
--- a/src/main/java/nl/tudelft/queue/model/enums/RequestStatus.java
+++ b/src/main/java/nl/tudelft/queue/model/enums/RequestStatus.java
@@ -111,11 +111,16 @@ public enum RequestStatus {
 	}
 
 	/**
-	 * Gets the CSS class that determines the colour that this status will be displayed as.
+	 * Gets the data-type that determines the colour that this status will be displayed as.
 	 *
-	 * @return The name of the class that determines the colour for this status.
+	 * @return The name of the data-type that determines the colour for this status.
 	 */
 	public String getColourClass() {
-		return "bg-" + name().toLowerCase();
+		return switch (this) {
+			case APPROVED, SELECTED -> "positive";
+			case REJECTED, NOTSELECTED -> "negative";
+			case REVOKED -> "greyed-out";
+			default -> "regular";
+		};
 	}
 }
diff --git a/src/main/java/nl/tudelft/queue/model/labs/CapacitySession.java b/src/main/java/nl/tudelft/queue/model/labs/CapacitySession.java
index a419aeaaa21cc2d4436aae8dd32f418fdd2d1f4e..f80b3ebc6c1e4f536ee7a8f903dfafa9f429ef07 100644
--- a/src/main/java/nl/tudelft/queue/model/labs/CapacitySession.java
+++ b/src/main/java/nl/tudelft/queue/model/labs/CapacitySession.java
@@ -33,8 +33,8 @@ import lombok.*;
 import lombok.experimental.SuperBuilder;
 import nl.tudelft.labracore.api.dto.RoomDetailsDTO;
 import nl.tudelft.labracore.api.dto.SessionDetailsDTO;
+import nl.tudelft.labracore.lib.cache.SessionCacheManager;
 import nl.tudelft.librador.dto.view.View;
-import nl.tudelft.queue.cache.SessionCacheManager;
 import nl.tudelft.queue.dto.create.QueueSessionCreateDTO;
 import nl.tudelft.queue.dto.create.labs.CapacitySessionCreateDTO;
 import nl.tudelft.queue.dto.patch.QueueSessionPatchDTO;
diff --git a/src/main/java/nl/tudelft/queue/model/labs/Lab.java b/src/main/java/nl/tudelft/queue/model/labs/Lab.java
index 5d88cf2213aa1e7ad40bef1dc2c8ecdbe372c9bf..dc49e41f8acd236ab33e075ecca1ca84c0e72131 100644
--- a/src/main/java/nl/tudelft/queue/model/labs/Lab.java
+++ b/src/main/java/nl/tudelft/queue/model/labs/Lab.java
@@ -35,7 +35,7 @@ import javax.validation.constraints.NotNull;
 
 import lombok.*;
 import lombok.experimental.SuperBuilder;
-import nl.tudelft.queue.cache.SessionCacheManager;
+import nl.tudelft.labracore.lib.cache.SessionCacheManager;
 import nl.tudelft.queue.model.LabRequest;
 import nl.tudelft.queue.model.QueueSession;
 import nl.tudelft.queue.model.Request;
diff --git a/src/main/java/nl/tudelft/queue/realtime/SubscriptionContainer.java b/src/main/java/nl/tudelft/queue/realtime/SubscriptionContainer.java
index e8263e41c93aea1d37a4c17e8787b5907eb3bee5..15c8f77a383b3919c9d61c6f997b29ef4eaa4a87 100644
--- a/src/main/java/nl/tudelft/queue/realtime/SubscriptionContainer.java
+++ b/src/main/java/nl/tudelft/queue/realtime/SubscriptionContainer.java
@@ -22,7 +22,7 @@ import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 
 import lombok.NoArgsConstructor;
-import nl.tudelft.labracore.lib.security.user.Person;
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.queue.dto.create.SubscriptionCreateDTO;
 import nl.tudelft.queue.properties.PushProperties;
 
diff --git a/src/main/java/nl/tudelft/queue/realtime/messages/RequestCreatedMessage.java b/src/main/java/nl/tudelft/queue/realtime/messages/RequestCreatedMessage.java
index c0736c9bf9881553c33a3d161a0725368b889f4b..cd0ae241ec9b304b2523c9e3df93fd8a213bff5b 100644
--- a/src/main/java/nl/tudelft/queue/realtime/messages/RequestCreatedMessage.java
+++ b/src/main/java/nl/tudelft/queue/realtime/messages/RequestCreatedMessage.java
@@ -19,14 +19,19 @@ package nl.tudelft.queue.realtime.messages;
 
 import static nl.tudelft.librador.SpringContext.getBean;
 
+import java.util.Locale;
+
 import lombok.*;
+import nl.tudelft.labracore.lib.cache.ModuleCacheManager;
+import nl.tudelft.librador.SpringContext;
 import nl.tudelft.librador.dto.view.View;
-import nl.tudelft.queue.cache.ModuleCacheManager;
 import nl.tudelft.queue.dto.view.requests.LabRequestViewDTO;
 import nl.tudelft.queue.model.LabRequest;
 import nl.tudelft.queue.model.enums.RequestStatus;
 import nl.tudelft.queue.model.enums.RequestType;
 
+import org.springframework.context.MessageSource;
+
 @Data
 @Builder
 @NoArgsConstructor
@@ -54,9 +59,11 @@ public class RequestCreatedMessage extends View<LabRequest> implements Message {
 	private String assignmentName;
 	private Long moduleId;
 	private String moduleName;
+	private String courseName;
 
 	private Long labId;
 	private RequestStatus status;
+	private String statusDisplayName;
 
 	private RequestType requestType;
 	private String requestTypeDisplayName;
@@ -93,10 +100,14 @@ public class RequestCreatedMessage extends View<LabRequest> implements Message {
 		assignmentName = view.getAssignment().getName();
 		moduleId = view.getAssignment().getModule().getId();
 		moduleName = view.getAssignment().getModule().getName();
+		courseName = view.getEdition().getCourse().getName();
 
 		labId = view.getQSession().getId();
 		status = RequestStatus.PENDING;
 
+		statusDisplayName = SpringContext.getBean(MessageSource.class).getMessage(
+				"request.status." + status.name().toLowerCase(), new String[0], Locale.getDefault());
+
 		requestTypeDisplayName = view.getRequestType().displayName();
 	}
 }
diff --git a/src/main/java/nl/tudelft/queue/realtime/messages/RequestStatusUpdateMessage.java b/src/main/java/nl/tudelft/queue/realtime/messages/RequestStatusUpdateMessage.java
index 0e56818f0878827223601ca8932bd8932dec883a..b8e814dbe38e61b8aa8a5ed42ec19e5f122550a1 100644
--- a/src/main/java/nl/tudelft/queue/realtime/messages/RequestStatusUpdateMessage.java
+++ b/src/main/java/nl/tudelft/queue/realtime/messages/RequestStatusUpdateMessage.java
@@ -17,14 +17,19 @@
  */
 package nl.tudelft.queue.realtime.messages;
 
+import java.util.Locale;
+
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
+import nl.tudelft.librador.SpringContext;
 import nl.tudelft.librador.dto.view.View;
 import nl.tudelft.queue.model.RequestEvent;
 import nl.tudelft.queue.model.enums.RequestStatus;
 
+import org.springframework.context.MessageSource;
+
 @Data
 @NoArgsConstructor
 @AllArgsConstructor
@@ -37,6 +42,11 @@ public abstract class RequestStatusUpdateMessage<T extends RequestEvent> extends
 
 	public abstract RequestStatus getStatus();
 
+	public String getStatusDisplayName() {
+		return SpringContext.getBean(MessageSource.class).getMessage(
+				"request.status." + getStatus().name().toLowerCase(), new String[0], Locale.getDefault());
+	}
+
 	@Override
 	public void postApply() {
 		id = data.getRequest().getId();
diff --git a/src/main/java/nl/tudelft/queue/realtime/messages/RequestTakenMessage.java b/src/main/java/nl/tudelft/queue/realtime/messages/RequestTakenMessage.java
index b269bb099835c8b16b12e04fd9b137f1048b1b94..51a9e315498d9a120291dff76cac901c85e6140c 100644
--- a/src/main/java/nl/tudelft/queue/realtime/messages/RequestTakenMessage.java
+++ b/src/main/java/nl/tudelft/queue/realtime/messages/RequestTakenMessage.java
@@ -20,7 +20,8 @@ package nl.tudelft.queue.realtime.messages;
 import static nl.tudelft.librador.SpringContext.getBean;
 
 import lombok.*;
-import nl.tudelft.queue.cache.PersonCacheManager;
+import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
+import nl.tudelft.labracore.lib.cache.PersonCacheManager;
 import nl.tudelft.queue.model.enums.RequestStatus;
 import nl.tudelft.queue.model.events.RequestTakenEvent;
 
@@ -32,13 +33,16 @@ import nl.tudelft.queue.model.events.RequestTakenEvent;
 public class RequestTakenMessage extends RequestStatusUpdateMessage<RequestTakenEvent> {
 	private static final long serialVersionUID = -2710218413754443043L;
 
+	private Long takenById;
 	private String takenBy;
 
 	@Override
 	public void postApply() {
 		super.postApply();
 
-		takenBy = getBean(PersonCacheManager.class).getOrThrow(data.getAssistant()).getDisplayName();
+		PersonSummaryDTO assistant = getBean(PersonCacheManager.class).getOrThrow(data.getAssistant());
+		takenById = assistant.getId();
+		takenBy = assistant.getDisplayName();
 	}
 
 	@Override
diff --git a/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java b/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java
index 658c90981b4ea0f34eae60521f6ce2acf0853746..4a742a7f160a3665616c422cce89d403c7ee6062 100644
--- a/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java
+++ b/src/main/java/nl/tudelft/queue/repository/LabRequestRepository.java
@@ -27,7 +27,7 @@ import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import nl.tudelft.labracore.api.dto.AssignmentSummaryDTO;
-import nl.tudelft.labracore.lib.security.user.Person;
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.queue.dto.util.RequestTableFilterDTO;
 import nl.tudelft.queue.model.*;
 import nl.tudelft.queue.model.enums.QueueSessionType;
@@ -35,14 +35,13 @@ import nl.tudelft.queue.model.enums.RequestStatus;
 import nl.tudelft.queue.model.labs.AbstractSlottedLab;
 import nl.tudelft.queue.model.labs.Lab;
 
-import org.springframework.data.domain.Page;
-import org.springframework.data.domain.PageRequest;
-import org.springframework.data.domain.Pageable;
-import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.*;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.querydsl.QuerydslPredicateExecutor;
+import org.springframework.data.rest.webmvc.ResourceNotFoundException;
 import org.springframework.lang.NonNull;
 
+import com.google.common.collect.ImmutableList;
 import com.querydsl.core.types.Predicate;
 import com.querydsl.core.types.dsl.BooleanExpression;
 
@@ -50,6 +49,10 @@ public interface LabRequestRepository
 		extends JpaRepository<LabRequest, Long>, QuerydslPredicateExecutor<LabRequest> {
 	QLabRequest qlr = QLabRequest.labRequest;
 
+	default LabRequest findByIdOrThrow(Long id) {
+		return findById(id).orElseThrow(() -> new ResourceNotFoundException("Lab request not found"));
+	}
+
 	@NonNull
 	@Override
 	List<LabRequest> findAll(@NonNull Predicate predicate);
@@ -328,6 +331,28 @@ public interface LabRequestRepository
 										.and(qlr.timeSlot.slot.opensAt.before(now().plusMinutes(10)))))))));
 	}
 
+	/**
+	 * Counts all requests that pass the given filter for a person. This filter is a DTO transferred from the
+	 * page requesting to see these requests. The filter contains all labs, rooms, assignments, etc. that need
+	 * to be displayed. If a field in the filter is left as an empty set, the filter ignores it.
+	 *
+	 * @param  labs   The labs that the should also be kept in the filter.
+	 * @param  person The person
+	 * @param  filter The filter to apply to the boolean expression.
+	 * @return        The amount of requests.
+	 */
+	default long countByFilterForPerson(List<Lab> labs, Person person, RequestTableFilterDTO filter) {
+		return count(qlr.in(select(qlr).from(qlr)
+				.leftJoin(qlr.timeSlot, QTimeSlot.timeSlot).on(qlr.timeSlot.id.eq(QTimeSlot.timeSlot.id))
+				.where(createFilterBooleanExpression(filter, qlr.session.in(labs).and(
+						qlr.session.type.eq(QueueSessionType.REGULAR)
+								.or(qlr.session.type.in(QueueSessionType.SLOTTED, QueueSessionType.EXAM)
+										.and(qlr.timeSlot.slot.opensAt.before(now().plusMinutes(10))))))
+												.and(isStatusOrForwardedToAny(RequestStatus.PENDING, person)
+														.or(isStatusOrForwarded(RequestStatus.PENDING,
+																person))))));
+	}
+
 	/**
 	 * Finds all requests that pass the given filter. This filter is a DTO transferred from the page
 	 * requesting to see these requests. The filter contains all labs, rooms, assignments, etc. that need to
@@ -340,13 +365,45 @@ public interface LabRequestRepository
 	 */
 	default Page<LabRequest> findAllByFilter(List<Lab> labs, RequestTableFilterDTO filter,
 			Pageable pageable) {
-		return findAll(qlr.in(select(qlr).from(qlr)
+		BooleanExpression predicate = qlr.in(select(qlr).from(qlr)
+				.leftJoin(qlr.timeSlot, QTimeSlot.timeSlot).on(qlr.timeSlot.id.eq(QTimeSlot.timeSlot.id))
+				.where(createFilterBooleanExpression(filter, qlr.session.in(labs).and(
+						qlr.session.type.eq(QueueSessionType.REGULAR)
+								.or(qlr.session.type.in(QueueSessionType.SLOTTED, QueueSessionType.EXAM)
+										.and(qlr.timeSlot.slot.opensAt.before(now().plusMinutes(10))))))));
+		List<LabRequest> requests = ImmutableList.copyOf(findAll(predicate, qlr.createdAt.asc()));
+		var page = requests.subList(pageable.getPageNumber() * pageable.getPageSize(),
+				Math.min(requests.size(), (pageable.getPageNumber() + 1) * pageable.getPageSize()));
+		return new PageImpl<>(page, pageable, count(predicate));
+	}
+
+	/**
+	 * Finds all requests that pass the given filter for a person. This filter is a DTO transferred from the
+	 * page requesting to see these requests. The filter contains all labs, rooms, assignments, etc. that need
+	 * to be displayed. If a field in the filter is left as an empty set, the filter ignores it. Only requests
+	 * the person is able to handle are returned.
+	 *
+	 * @param  labs     The labs that the should also be kept in the filter.
+	 * @param  filter   The filter to apply to the boolean expression.
+	 * @param  person   The person
+	 * @param  pageable The pageable object representing the page.
+	 * @return          The filtered list of requests.
+	 */
+	default Page<LabRequest> findAllByFilterForPerson(List<Lab> labs, RequestTableFilterDTO filter,
+			Person person, Pageable pageable) {
+		BooleanExpression predicate = qlr.in(select(qlr).from(qlr)
 				.leftJoin(qlr.timeSlot, QTimeSlot.timeSlot).on(qlr.timeSlot.id.eq(QTimeSlot.timeSlot.id))
 				.where(createFilterBooleanExpression(filter, qlr.session.in(labs).and(
 						qlr.session.type.eq(QueueSessionType.REGULAR)
 								.or(qlr.session.type.in(QueueSessionType.SLOTTED, QueueSessionType.EXAM)
-										.and(qlr.timeSlot.slot.opensAt.before(now().plusMinutes(10)))))))),
-				pageable);
+										.and(qlr.timeSlot.slot.opensAt.before(now().plusMinutes(10))))))
+												.and(isStatusOrForwarded(RequestStatus.PENDING, person)
+														.or(isStatusOrForwardedToAny(RequestStatus.PENDING,
+																person)))));
+		List<LabRequest> requests = ImmutableList.copyOf(findAll(predicate, qlr.createdAt.asc()));
+		var page = requests.subList(pageable.getPageNumber() * pageable.getPageSize(),
+				Math.min(requests.size(), (pageable.getPageNumber() + 1) * pageable.getPageSize()));
+		return new PageImpl<>(page, pageable, count(predicate));
 	}
 
 	/**
diff --git a/src/main/java/nl/tudelft/queue/repository/QueueSessionRepository.java b/src/main/java/nl/tudelft/queue/repository/QueueSessionRepository.java
index de06984c4e0f2b70aa06a85b769c80cf255a5cbb..270b8ceefada757a06aa74cc10383ae0000cffe7 100644
--- a/src/main/java/nl/tudelft/queue/repository/QueueSessionRepository.java
+++ b/src/main/java/nl/tudelft/queue/repository/QueueSessionRepository.java
@@ -22,6 +22,8 @@ import java.util.List;
 import nl.tudelft.queue.model.QQueueSession;
 import nl.tudelft.queue.model.QueueSession;
 
+import org.springframework.data.rest.webmvc.ResourceNotFoundException;
+
 public interface QueueSessionRepository extends QueueRepository<QueueSession<?>, Long> {
 	QQueueSession qs = QQueueSession.queueSession;
 
@@ -33,4 +35,7 @@ public interface QueueSessionRepository extends QueueRepository<QueueSession<?>,
 		return findAll(qs.session.in(sessions));
 	}
 
+	default QueueSession<?> findByIdOrThrow(Long id) {
+		return findById(id).orElseThrow(() -> new ResourceNotFoundException("Queue Session not found"));
+	}
 }
diff --git a/src/main/java/nl/tudelft/queue/repository/RequestRepository.java b/src/main/java/nl/tudelft/queue/repository/RequestRepository.java
index 0afcabb9bd783df1a8ebb7d93fac08679da6e510..f09c8679e0cbc0b5375285331a2fd1458006ab44 100644
--- a/src/main/java/nl/tudelft/queue/repository/RequestRepository.java
+++ b/src/main/java/nl/tudelft/queue/repository/RequestRepository.java
@@ -19,5 +19,12 @@ package nl.tudelft.queue.repository;
 
 import nl.tudelft.queue.model.Request;
 
+import org.springframework.data.rest.webmvc.ResourceNotFoundException;
+
 public interface RequestRepository extends QueueRepository<Request<?>, Long> {
+
+	default Request<?> findByIdOrThrow(Long id) {
+		return findById(id).orElseThrow(() -> new ResourceNotFoundException("Request not found"));
+	}
+
 }
diff --git a/src/main/java/nl/tudelft/queue/security/DevUserDetailsProvider.java b/src/main/java/nl/tudelft/queue/security/DevUserDetailsProvider.java
index b3b0c90d7580c2dec9ecafa4bcb5b822e33835c8..4d21e0bd69086e83448f084b95da37615458f41d 100644
--- a/src/main/java/nl/tudelft/queue/security/DevUserDetailsProvider.java
+++ b/src/main/java/nl/tudelft/queue/security/DevUserDetailsProvider.java
@@ -20,8 +20,8 @@ package nl.tudelft.queue.security;
 import javax.transaction.Transactional;
 
 import nl.tudelft.labracore.api.PersonControllerApi;
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.labracore.lib.security.memory.InMemoryUserProvider;
-import nl.tudelft.labracore.lib.security.user.Person;
 
 import org.modelmapper.ModelMapper;
 import org.springframework.beans.factory.annotation.Autowired;
diff --git a/src/main/java/nl/tudelft/queue/security/QueueUserHandler.java b/src/main/java/nl/tudelft/queue/security/QueueUserHandler.java
index 224d16275cfe788bfa8b4edded982764b6ce095a..c04e252fa9c01f7587de5428c8d3be914f2af71b 100644
--- a/src/main/java/nl/tudelft/queue/security/QueueUserHandler.java
+++ b/src/main/java/nl/tudelft/queue/security/QueueUserHandler.java
@@ -17,8 +17,8 @@
  */
 package nl.tudelft.queue.security;
 
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.labracore.lib.security.LabradorUserHandler;
-import nl.tudelft.labracore.lib.security.user.Person;
 
 import org.springframework.stereotype.Service;
 
diff --git a/src/main/java/nl/tudelft/queue/service/CapacitySessionService.java b/src/main/java/nl/tudelft/queue/service/CapacitySessionService.java
index b7817b290e1e5f5d05ff36d8c93bcb3561138b0c..fc63b2e97697ed745c737ffeef619f04d70f0434 100644
--- a/src/main/java/nl/tudelft/queue/service/CapacitySessionService.java
+++ b/src/main/java/nl/tudelft/queue/service/CapacitySessionService.java
@@ -29,9 +29,9 @@ import nl.tudelft.labracore.api.StudentGroupControllerApi;
 import nl.tudelft.labracore.api.dto.RolePersonLayer1DTO;
 import nl.tudelft.labracore.api.dto.RoomDetailsDTO;
 import nl.tudelft.labracore.api.dto.SessionDetailsDTO;
-import nl.tudelft.queue.cache.PersonCacheManager;
-import nl.tudelft.queue.cache.SessionCacheManager;
-import nl.tudelft.queue.cache.StudentGroupCacheManager;
+import nl.tudelft.labracore.lib.cache.PersonCacheManager;
+import nl.tudelft.labracore.lib.cache.SessionCacheManager;
+import nl.tudelft.labracore.lib.cache.StudentGroupCacheManager;
 import nl.tudelft.queue.model.Request;
 import nl.tudelft.queue.model.SelectionRequest;
 import nl.tudelft.queue.model.embeddables.RequestEventInfo;
@@ -41,7 +41,6 @@ import nl.tudelft.queue.repository.CapacitySessionRepository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 
 @Service
@@ -270,8 +269,8 @@ public class CapacitySessionService {
 	 * @param labs The labs that selections will be run over.
 	 */
 	private void initializeCaches(List<CapacitySession> labs) {
-		sCache = new SessionCacheManager(sApi);
-		sgCache = new StudentGroupCacheManager(sgApi, pCache);
+		//		sCache = new SessionCacheManager(sApi); TODO
+		//		sgCache = new StudentGroupCacheManager(sgApi, pCache);
 
 		sCache.get(labs.stream().map(CapacitySession::getSession));
 		sgCache.get(labs.stream().flatMap(lab -> lab.getRequests().stream())
@@ -335,7 +334,7 @@ public class CapacitySessionService {
 	 * that are available in the lab.
 	 */
 	@Transactional
-	@Scheduled(cron = "3 * * * * ?")
+	//	@Scheduled(cron = "3 * * * * ?") TODO
 	public void selectStudentsInCapacityLab() {
 		// Find the labs that selection should be done for.
 		List<CapacitySession> labs = csr.findAllThatShouldPickStudents();
diff --git a/src/main/java/nl/tudelft/queue/service/EditionService.java b/src/main/java/nl/tudelft/queue/service/EditionService.java
index 3e00f24752d6179158fd4e53116e2a660e85a6f9..f95545dd7121a5d12b08919db4e95d26ca25b3f3 100644
--- a/src/main/java/nl/tudelft/queue/service/EditionService.java
+++ b/src/main/java/nl/tudelft/queue/service/EditionService.java
@@ -20,6 +20,7 @@ package nl.tudelft.queue.service;
 import static java.time.LocalDateTime.now;
 import static java.time.temporal.ChronoUnit.MINUTES;
 import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.toMap;
 
 import java.io.IOException;
 import java.util.*;
@@ -34,7 +35,7 @@ import nl.tudelft.labracore.api.EditionControllerApi;
 import nl.tudelft.labracore.api.PersonControllerApi;
 import nl.tudelft.labracore.api.RoleControllerApi;
 import nl.tudelft.labracore.api.dto.*;
-import nl.tudelft.queue.cache.AssignmentCacheManager;
+import nl.tudelft.labracore.lib.cache.AssignmentCacheManager;
 import nl.tudelft.queue.csv.CsvHelper;
 import nl.tudelft.queue.csv.EmptyCsvException;
 import nl.tudelft.queue.csv.InvalidCsvException;
@@ -86,28 +87,29 @@ public class EditionService {
 	private AssignmentCacheManager acm;
 
 	/**
-	 * Filters a list of people on their username, display name and student number. If any of the
+	 * Filters a list of roles on their username, display name and student number. If any of the
 	 * aforementioned values match the given search term, the person is included. If not, the person is
 	 * excluded.
 	 *
-	 * @param  people     The people through which we need to filter.
+	 * @param  roles      The people through which we need to filter.
 	 * @param  searchTerm The original search term that was typed by the user.
 	 * @return            The filtered list of people (this could be empty).
 	 */
-	public List<PersonSummaryDTO> studentsMatchingFilter(List<PersonSummaryDTO> people,
+	public List<RolePersonDetailsDTO> rolesMatchingFilter(List<RolePersonDetailsDTO> roles,
 			String searchTerm) {
 		var search = searchTerm.toLowerCase();
 
-		return people.stream()
-				.filter(person -> person.getDisplayName() != null
-						&& StringUtils.stripAccents(person.getDisplayName())
+		return roles.stream()
+				.filter(r -> r.getPerson().getDisplayName() != null
+						&& StringUtils.stripAccents(r.getPerson().getDisplayName())
 								.toLowerCase().contains(search)
 						||
-						person.getUsername() != null && StringUtils.stripAccents(person.getUsername())
-								.toLowerCase().contains(search)
+						r.getPerson().getUsername() != null
+								&& StringUtils.stripAccents(r.getPerson().getUsername())
+										.toLowerCase().contains(search)
 						||
-						Objects.toString(person.getNumber()).contains(search))
-				.collect(Collectors.toList());
+						Objects.toString(r.getPerson().getNumber()).contains(search))
+				.toList();
 	}
 
 	/**
@@ -156,19 +158,21 @@ public class EditionService {
 	 * Add participants according a CSV file to a specific editions.
 	 *
 	 * @param  csv         CSF file containing netid's and roles.
-	 * @param  edition     The edition to which these people need to be added.
+	 * @param  editionId   The edition id to which these people need to be added.
+	 * @return             The list of not found users
 	 * @throws IOException
 	 */
-	public void addCourseParticipants(MultipartFile csv, EditionDetailsDTO edition)
+	public List<String> addCourseParticipants(MultipartFile csv, Long editionId)
 			throws EmptyCsvException, InvalidCsvException {
-		List<UserCsvHelper> users = UserCsvHelper.readCsv(csv);
-
-		for (UserCsvHelper csvUser : users) {
-			pApi.getPersonByUsername(csvUser.getNetId())
-					.flatMap(person -> rApi.addRole(csvUser.person(new PersonIdDTO().id(person.getId()))
-							.edition(new EditionIdDTO().id(edition.getId()))))
-					.block();
-		}
+		Map<String, ParticipantsImportDTO.InnerEnum> users = UserCsvHelper.readCsv(csv).stream()
+				.collect(toMap(UserCsvHelper::getIdentifier, UserCsvHelper::getRole));
+		PersonSearchDTO result = pApi.searchForPeople(new ArrayList<>(users.keySet())).block();
+		Map<String, ParticipantsImportDTO.InnerEnum> roleMap = result.getPeople().stream()
+				.collect(toMap(PersonSummaryDTO::getUsername, p -> users.getOrDefault(p.getUsername(),
+						users.getOrDefault(p.getEmail(), users.get(p.getNumber().toString())))));
+		eca.addParticipantsToEdition(editionId, new ParticipantsImportDTO().participants(roleMap), false)
+				.block();
+		return result.getNotFound();
 	}
 
 	/**
diff --git a/src/main/java/nl/tudelft/queue/service/EditionStatusService.java b/src/main/java/nl/tudelft/queue/service/EditionStatusService.java
index b2163bb56226d3903ea748a0ea772b49d82b88e6..29227c3b90efc706a2929745941020a248a71f00 100644
--- a/src/main/java/nl/tudelft/queue/service/EditionStatusService.java
+++ b/src/main/java/nl/tudelft/queue/service/EditionStatusService.java
@@ -29,10 +29,10 @@ import java.util.stream.StreamSupport;
 import nl.tudelft.labracore.api.dto.AssignmentDetailsDTO;
 import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
 import nl.tudelft.labracore.api.dto.RoomDetailsDTO;
-import nl.tudelft.queue.cache.AssignmentCacheManager;
-import nl.tudelft.queue.cache.PersonCacheManager;
-import nl.tudelft.queue.cache.RoomCacheManager;
-import nl.tudelft.queue.cache.SessionCacheManager;
+import nl.tudelft.labracore.lib.cache.AssignmentCacheManager;
+import nl.tudelft.labracore.lib.cache.PersonCacheManager;
+import nl.tudelft.labracore.lib.cache.RoomCacheManager;
+import nl.tudelft.labracore.lib.cache.SessionCacheManager;
 import nl.tudelft.queue.model.LabRequest;
 import nl.tudelft.queue.model.QLabRequest;
 import nl.tudelft.queue.model.enums.RequestType;
diff --git a/src/main/java/nl/tudelft/queue/service/FeedbackService.java b/src/main/java/nl/tudelft/queue/service/FeedbackService.java
index dab0b51811f6e6f36c2b9f3f1d0555acd5dbd581..9df09b16e00305a965f8571ae65366cc3cdd5a44 100644
--- a/src/main/java/nl/tudelft/queue/service/FeedbackService.java
+++ b/src/main/java/nl/tudelft/queue/service/FeedbackService.java
@@ -26,7 +26,7 @@ import javax.transaction.Transactional;
 import javax.validation.ValidationException;
 
 import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
-import nl.tudelft.queue.cache.PersonCacheManager;
+import nl.tudelft.labracore.lib.cache.PersonCacheManager;
 import nl.tudelft.queue.dto.patch.FeedbackPatchDTO;
 import nl.tudelft.queue.model.Feedback;
 import nl.tudelft.queue.model.LabRequest;
diff --git a/src/main/java/nl/tudelft/queue/service/JitsiService.java b/src/main/java/nl/tudelft/queue/service/JitsiService.java
index 9bd2d5990803272eb10f718561d8ec97c369a6b4..5d36e6ef6cbd2da25ae7d2d77d196b43c6d82db1 100644
--- a/src/main/java/nl/tudelft/queue/service/JitsiService.java
+++ b/src/main/java/nl/tudelft/queue/service/JitsiService.java
@@ -19,7 +19,7 @@ package nl.tudelft.queue.service;
 
 import java.util.UUID;
 
-import nl.tudelft.queue.cache.PersonCacheManager;
+import nl.tudelft.labracore.lib.cache.PersonCacheManager;
 import nl.tudelft.queue.model.LabRequest;
 import nl.tudelft.queue.properties.JitsiProperties;
 
diff --git a/src/main/java/nl/tudelft/queue/service/LabService.java b/src/main/java/nl/tudelft/queue/service/LabService.java
index 3e4a2d972ad2370198af4cf17cf8b66c176986f8..0ae2f69e12906d52e9597e3cead719c922f782a7 100644
--- a/src/main/java/nl/tudelft/queue/service/LabService.java
+++ b/src/main/java/nl/tudelft/queue/service/LabService.java
@@ -23,7 +23,6 @@ import static nl.tudelft.queue.misc.QueueSessionStatus.*;
 import java.io.IOException;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
 import java.util.stream.Collectors;
@@ -34,11 +33,11 @@ import nl.tudelft.labracore.api.EditionControllerApi;
 import nl.tudelft.labracore.api.ModuleControllerApi;
 import nl.tudelft.labracore.api.SessionControllerApi;
 import nl.tudelft.labracore.api.dto.*;
-import nl.tudelft.labracore.lib.security.user.Person;
+import nl.tudelft.labracore.lib.cache.EditionCacheManager;
+import nl.tudelft.labracore.lib.cache.EditionCollectionCacheManager;
+import nl.tudelft.labracore.lib.cache.SessionCacheManager;
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.librador.dto.view.View;
-import nl.tudelft.queue.cache.EditionCacheManager;
-import nl.tudelft.queue.cache.EditionCollectionCacheManager;
-import nl.tudelft.queue.cache.SessionCacheManager;
 import nl.tudelft.queue.csv.CsvHelper;
 import nl.tudelft.queue.dto.create.QueueSessionCreateDTO;
 import nl.tudelft.queue.dto.create.labs.AbstractSlottedLabCreateDTO;
@@ -249,15 +248,15 @@ public class LabService {
 	}
 
 	@Transactional
-	public <D extends QueueSession<?>> List<D> createSessions(QueueSessionCreateDTO<D> dto, Long ouId,
+	public <D extends QueueSession<?>> List<D> createSessions(QueueSessionCreateDTO<D> dto,
 			SessionType sessionType) {
 		ArrayList<D> sessions = new ArrayList<>();
-		sessions.add(createOneSession(dto, ouId, sessionType));
+		sessions.add(createOneSession(dto, dto.getOrganisationalUnit(), sessionType));
 
 		if (dto.getRepeatForXWeeks() != null && dto.getRepeatForXWeeks() > 0) {
 			for (int i = 0; i < dto.getRepeatForXWeeks(); i++) {
 				dto.plusWeeks(1L);
-				sessions.add(createOneSession(dto, ouId, sessionType));
+				sessions.add(createOneSession(dto, dto.getOrganisationalUnit(), sessionType));
 			}
 		}
 
@@ -317,20 +316,21 @@ public class LabService {
 	 * @param session The lab that is to be edited.
 	 */
 	@Transactional
-	public <D extends QueueSession<?>> void updateSession(QueueSessionPatchDTO<D> dto, D session) {
+	public <D extends QueueSession<?>> void updateSession(QueueSessionPatchDTO<D> dto) {
 		// Get the assignments and rooms as ID DTOs.
 		List<AssignmentIdDTO> assignments = dto.assignmentIdDTOs();
 		List<RoomIdDTO> rooms = dto.roomIdDTOs();
+		@SuppressWarnings("unchecked") D qSession = (D) qsr.findByIdOrThrow(dto.getId());
 
 		// Patch the session on the Labracore side of things.
-		sApi.patchSession(session.getSession(), new SessionPatchDTO()
+		sApi.patchSession(qSession.getSession(), new SessionPatchDTO()
 				.name(dto.getName())
 				.start((dto.getSlot() != null) ? dto.getSlot().getOpensAt() : null)
 				.end((dto.getSlot() != null) ? dto.getSlot().getClosesAt() : null)
 				.assignments(assignments)
 				.rooms(rooms)).block();
-		session.setExtraInfo(dto.getExtraInfo());
-		dto.apply(session);
+		qSession.setExtraInfo(dto.getExtraInfo());
+		dto.apply(qSession);
 	}
 
 	public void updateCapacitySessionRequests(CapacitySession session,
@@ -477,11 +477,11 @@ public class LabService {
 			var collection = ecCache.getOrThrow(sessionDetails.getEditionCollection().getId());
 			var roles = collection.getEditions().stream().map(e -> rService.rolesForPersonInEdition(e,
 					assistant)).flatMap(List::stream).filter(rService::isStaff).toList();
-			allowedAssignments = roles.stream()
-					.map(r -> mApi.getModuleByEdition(r.getEdition().getId()).collectList().block())
-					.filter(Objects::nonNull)
-					.flatMap(Collection::stream).map(ModuleDetailsDTO::getAssignments)
-					.flatMap(List::stream).collect(Collectors.toList());
+			//			allowedAssignments = roles.stream()
+			//					.map(r -> mApi.getModuleByEdition(r.getEdition().getId()).collectList().block())
+			//					.filter(Objects::nonNull)
+			//					.flatMap(Collection::stream).map(ModuleDetailsDTO::getAssignments)
+			//					.flatMap(List::stream).collect(Collectors.toList());
 		}
 		return allowedAssignments;
 	}
diff --git a/src/main/java/nl/tudelft/queue/service/MailService.java b/src/main/java/nl/tudelft/queue/service/MailService.java
index 359a63237975791afb6d29505b84e0b87e45d34e..3f446246c17756f852296b61b48fbac736770756 100644
--- a/src/main/java/nl/tudelft/queue/service/MailService.java
+++ b/src/main/java/nl/tudelft/queue/service/MailService.java
@@ -17,7 +17,7 @@
  */
 package nl.tudelft.queue.service;
 
-import nl.tudelft.labracore.lib.security.user.Person;
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.queue.dto.create.CourseRequestCreateDTO;
 import nl.tudelft.queue.properties.MailProperties;
 import nl.tudelft.queue.properties.QueueProperties;
diff --git a/src/main/java/nl/tudelft/queue/service/PermissionService.java b/src/main/java/nl/tudelft/queue/service/PermissionService.java
index bdc8d156162c1bf148afdb7dd43e11e5e89014ac..01b2b7b68c15a1b7e46673544b5094190dc2a3bf 100644
--- a/src/main/java/nl/tudelft/queue/service/PermissionService.java
+++ b/src/main/java/nl/tudelft/queue/service/PermissionService.java
@@ -28,11 +28,12 @@ import java.util.stream.Collectors;
 
 import nl.tudelft.labracore.api.AuthorizationControllerApi;
 import nl.tudelft.labracore.api.CourseControllerApi;
+import nl.tudelft.labracore.api.StudentGroupControllerApi;
 import nl.tudelft.labracore.api.dto.*;
+import nl.tudelft.labracore.lib.api.DefaultRole;
+import nl.tudelft.labracore.lib.cache.*;
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.labracore.lib.security.LabradorUserDetails;
-import nl.tudelft.labracore.lib.security.user.DefaultRole;
-import nl.tudelft.labracore.lib.security.user.Person;
-import nl.tudelft.queue.cache.*;
 import nl.tudelft.queue.model.*;
 import nl.tudelft.queue.model.enums.QueueSessionType;
 import nl.tudelft.queue.model.enums.RequestStatus;
@@ -96,6 +97,9 @@ public class PermissionService {
 	@Autowired
 	private CourseControllerApi cApi;
 
+	@Autowired
+	private StudentGroupControllerApi sgApi;
+
 	/**
 	 * Gets the Person associated to the currently authenticated entity and passes it to the given callback.
 	 * If no API Key with user could be found, this function just outputs {@code false}.
@@ -426,7 +430,8 @@ public class PermissionService {
 	public boolean canLeaveEdition(Long editionId) {
 		return withRole(editionId, (person, role) -> STAFF_ROLES.contains(role) ||
 				(role == STUDENT && withEdition(editionId, edition -> {
-					var groups = sgCache.getByPerson(person.getId());
+					List<StudentGroupDetailsDTO> groups = sgCache.get(sgApi.getGroupsForPerson(person.getId())
+							.map(StudentGroupSummaryDTO::getId).collectList().block());
 					var modules = edition.getModules().stream()
 							.map(ModuleSummaryDTO::getId).collect(Collectors.toSet());
 
@@ -494,6 +499,14 @@ public class PermissionService {
 		return isAdmin() || withRole(editionId, (person, role) -> TEACHER == role);
 	}
 
+	/**
+	 * @param  qSessionId The id of the session that the user wants to manage.
+	 * @return            Whether the authenticated user can change properties of the given session.
+	 */
+	public boolean canManageSession(Long qSessionId) {
+		return isAdmin() || withQueueSession(qSessionId, this::canManageSession);
+	}
+
 	/**
 	 * @param  qSession The session that the user wants to manage.
 	 * @return          Whether the authenticated user can change properties of the given session.
@@ -592,6 +605,18 @@ public class PermissionService {
 		return isAdmin() || hasManagerRole(editionId);
 	}
 
+	/**
+	 * @param  editionCollectionId The id of the edition collection the user wants to manage.
+	 * @return                     Whether the authenticated user can manage the edition collection with the
+	 *                             given id.
+	 */
+	public boolean canManageSharedEdition(Long editionCollectionId) {
+		return isAdmin() || withEditionCollection(editionCollectionId,
+				ec -> withAnyRole(
+						ec.getEditions().stream().map(EditionSummaryDTO::getId).collect(Collectors.toList()),
+						(person, role) -> role == TEACHER));
+	}
+
 	/**
 	 * @param  editionCollectionId The id of the edition collection the user wants to manage sessions for.
 	 * @return                     Whether the authenticated user can add/remove/change sessions within the
@@ -604,6 +629,18 @@ public class PermissionService {
 						(person, role) -> MANAGER_ROLES.contains(role)));
 	}
 
+	/**
+	 * @param  editionCollectionId The id of the edition collection the user wants to manage participants for.
+	 * @return                     Whether the authenticated user can add/remove/change participants within
+	 *                             the edition collection with the given id.
+	 */
+	public boolean canManageSharedParticipants(Long editionCollectionId) {
+		return isAdmin() || withEditionCollection(editionCollectionId,
+				ec -> withAnyRole(
+						ec.getEditions().stream().map(EditionSummaryDTO::getId).collect(Collectors.toList()),
+						(person, role) -> MANAGER_ROLES.contains(role)));
+	}
+
 	/**
 	 * @param  timeSlot The time slot the user wants to take a next request from.
 	 * @return          Whether the authenticated user can take a next request from the given time slot.
diff --git a/src/main/java/nl/tudelft/queue/service/QueuePushService.java b/src/main/java/nl/tudelft/queue/service/QueuePushService.java
index b5861dedc2fc35e52f88aafd8daed8d05202a5f1..7ac1b29b336712d1c84c0ad5b241dd3af644ca1a 100644
--- a/src/main/java/nl/tudelft/queue/service/QueuePushService.java
+++ b/src/main/java/nl/tudelft/queue/service/QueuePushService.java
@@ -25,7 +25,7 @@ import lombok.SneakyThrows;
 import nl.martijndwars.webpush.Notification;
 import nl.martijndwars.webpush.PushService;
 import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
-import nl.tudelft.labracore.lib.security.user.Person;
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.queue.model.LabRequest;
 import nl.tudelft.queue.realtime.NotificationPayload;
 import nl.tudelft.queue.realtime.Subscription;
diff --git a/src/main/java/nl/tudelft/queue/service/RequestService.java b/src/main/java/nl/tudelft/queue/service/RequestService.java
index 6bc340bbbe2f09ff06e19f2509a7c304c5eb6a8b..b86deb2a89edb1534ca8ea94a468c0bb9e413426 100644
--- a/src/main/java/nl/tudelft/queue/service/RequestService.java
+++ b/src/main/java/nl/tudelft/queue/service/RequestService.java
@@ -26,8 +26,8 @@ import nl.tudelft.labracore.api.ModuleControllerApi;
 import nl.tudelft.labracore.api.QuestionControllerApi;
 import nl.tudelft.labracore.api.StudentGroupControllerApi;
 import nl.tudelft.labracore.api.dto.*;
-import nl.tudelft.labracore.lib.security.user.Person;
-import nl.tudelft.queue.cache.*;
+import nl.tudelft.labracore.lib.cache.*;
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.queue.dto.create.RequestCreateDTO;
 import nl.tudelft.queue.dto.util.RequestTableFilterDTO;
 import nl.tudelft.queue.model.ClosableTimeSlot;
diff --git a/src/main/java/nl/tudelft/queue/service/RequestTableService.java b/src/main/java/nl/tudelft/queue/service/RequestTableService.java
index 29e49ba429f28150cacbc1ee4b3d942500309551..29887a324b1785372eae771ea7ac6bb73298cc28 100644
--- a/src/main/java/nl/tudelft/queue/service/RequestTableService.java
+++ b/src/main/java/nl/tudelft/queue/service/RequestTableService.java
@@ -28,9 +28,9 @@ import javax.servlet.http.HttpSession;
 import nl.tudelft.labracore.api.EditionControllerApi;
 import nl.tudelft.labracore.api.SessionControllerApi;
 import nl.tudelft.labracore.api.dto.*;
-import nl.tudelft.labracore.lib.security.user.Person;
+import nl.tudelft.labracore.lib.cache.*;
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.librador.dto.view.View;
-import nl.tudelft.queue.cache.*;
 import nl.tudelft.queue.dto.util.RequestTableFilterDTO;
 import nl.tudelft.queue.dto.view.QueueSessionSummaryDTO;
 import nl.tudelft.queue.dto.view.RequestViewDTO;
diff --git a/src/main/java/nl/tudelft/queue/service/RoleDTOService.java b/src/main/java/nl/tudelft/queue/service/RoleDTOService.java
index 0fb97e106c57afefa27548b9a928b0c5b7de0d48..7c2f5114c202029495cfbf131a442e4ac3ebdd50 100644
--- a/src/main/java/nl/tudelft/queue/service/RoleDTOService.java
+++ b/src/main/java/nl/tudelft/queue/service/RoleDTOService.java
@@ -26,8 +26,8 @@ import java.util.stream.Collectors;
 
 import nl.tudelft.labracore.api.RoleControllerApi;
 import nl.tudelft.labracore.api.dto.*;
-import nl.tudelft.labracore.lib.security.user.Person;
-import nl.tudelft.queue.cache.EditionRolesCacheManager;
+import nl.tudelft.labracore.lib.cache.EditionRolesCacheManager;
+import nl.tudelft.labracore.lib.data.Person;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
diff --git a/src/main/resources/application.yml.template b/src/main/resources/application.yml.template
index 5dbcc5abbe79af0f65b490ff633e66cc223f8df7..1b967f53bc7d5357830e638b76e5f945d9ca329e 100644
--- a/src/main/resources/application.yml.template
+++ b/src/main/resources/application.yml.template
@@ -50,7 +50,10 @@ spring:
     username:
     password:
     test-connection: true
-
+  mvc:
+    hiddenmethod:
+      filter:
+        enabled: true
 
 opentracing:
   jaeger:
diff --git a/src/main/resources/scss/_variables.scss b/src/main/resources/archive/_variables.scss
similarity index 100%
rename from src/main/resources/scss/_variables.scss
rename to src/main/resources/archive/_variables.scss
diff --git a/src/main/resources/templates/admin/announcements.html b/src/main/resources/archive/admin/announcements.html
similarity index 100%
rename from src/main/resources/templates/admin/announcements.html
rename to src/main/resources/archive/admin/announcements.html
diff --git a/src/main/resources/templates/admin/create/announcement.html b/src/main/resources/archive/admin/create/announcement.html
similarity index 100%
rename from src/main/resources/templates/admin/create/announcement.html
rename to src/main/resources/archive/admin/create/announcement.html
diff --git a/src/main/resources/templates/admin/create/course.html b/src/main/resources/archive/admin/create/course.html
similarity index 100%
rename from src/main/resources/templates/admin/create/course.html
rename to src/main/resources/archive/admin/create/course.html
diff --git a/src/main/resources/templates/admin/view.html b/src/main/resources/archive/admin/view.html
similarity index 100%
rename from src/main/resources/templates/admin/view.html
rename to src/main/resources/archive/admin/view.html
diff --git a/src/main/resources/templates/admin/view/calendar.html b/src/main/resources/archive/admin/view/calendar.html
similarity index 100%
rename from src/main/resources/templates/admin/view/calendar.html
rename to src/main/resources/archive/admin/view/calendar.html
diff --git a/src/main/resources/templates/admin/view/course.html b/src/main/resources/archive/admin/view/course.html
similarity index 100%
rename from src/main/resources/templates/admin/view/course.html
rename to src/main/resources/archive/admin/view/course.html
diff --git a/src/main/resources/templates/admin/view/courseList.html b/src/main/resources/archive/admin/view/courseList.html
similarity index 100%
rename from src/main/resources/templates/admin/view/courseList.html
rename to src/main/resources/archive/admin/view/courseList.html
diff --git a/src/main/resources/archive/assignment/create.html b/src/main/resources/archive/assignment/create.html
new file mode 100644
index 0000000000000000000000000000000000000000..eae1b1452609bd975860036fd8259dffad4cdd27
--- /dev/null
+++ b/src/main/resources/archive/assignment/create.html
@@ -0,0 +1,102 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{edition/view}">
+<head>
+    <title th:text="|Create Assignment for ${'#' + edition.id}|"></title>
+
+    <script src="/webjars/momentjs/min/moment.min.js"></script>
+
+    <script type="application/javascript"
+            src="/webjars/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.min.js"></script>
+    <link rel="stylesheet" href="/webjars/tempusdominus-bootstrap-4/css/tempusdominus-bootstrap-4.min.css"/>
+
+    <script type="application/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
+    <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"/>
+</head>
+
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+<!--@thymesVar id="_module" type="nl.tudelft.labracore.api.dto.ModuleSummaryDTO"-->
+
+<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.QueueAssignmentCreateDTO"-->
+
+<body>
+<section layout:fragment="subcontent">
+    <div class="page-header">
+        <h3>Create Assignment</h3>
+    </div>
+
+    <form th:with="action = @{/module/{mId}/assignment/create(id=${edition.id}, mId=${_module.id})}"
+          th:action="${action}" th:object="${dto}"
+          class="form-horizontal" method="post">
+        <input type="hidden" name="moduleId" th:value="${_module.id}">
+
+        <div class="form-group form-row">
+            <label for="name-input" class="col-sm-2 col-form-label">Name:</label>
+            <div class="col-sm-4">
+                <input id="name-input"
+                       type="text" class="form-control"
+                       required th:field="*{name}"
+                       placeholder="The name of the assignment"/>
+            </div>
+        </div>
+
+        <div class="form-group form-row">
+            <label for="description-input" class="col-sm-2 col-form-label">Description:</label>
+            <div class="col-sm-10">
+                <input id="description-input"
+                       type="text" class="form-control"
+                       th:field="*{description}"
+                       placeholder="(Optionally) a description of the assignment"/>
+            </div>
+        </div>
+
+        <div class="form-group form-row">
+            <label for="deadline-input" class="col-sm-2 col-form-label">Deadline:</label>
+            <div class="col-sm-10 input-group" id="deadline-input" data-target-input="nearest">
+                <input type="text" th:field="*{deadline}"
+                       class="form-control datetimepicker-input"
+                       data-target="#deadline-input"/>
+                <div class="input-group-append">
+                    <span class="input-group-text"
+                          data-target="#deadline-input"
+                          data-toggle="datetimepicker">
+                        <i class="fa fa-calendar"></i>
+                    </span>
+                </div>
+            </div>
+        </div>
+
+        <div class="form-group">
+            <button type="submit" class="btn btn-primary ctrl-enter-submit float-right">
+                Create new Assignment
+            </button>
+            <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script>
+        </div>
+    </form>
+
+    <script type="application/javascript">
+        $("#deadline-input").datetimepicker({format: "DD-MM-YYYY HH:mm"})
+    </script>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/archive/assignment/remove.html b/src/main/resources/archive/assignment/remove.html
new file mode 100644
index 0000000000000000000000000000000000000000..e08c73676030e1508252021595325097b134647e
--- /dev/null
+++ b/src/main/resources/archive/assignment/remove.html
@@ -0,0 +1,47 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{edition/view}">
+
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+<!--@thymesVar id="assignment" type="nl.tudelft.labracore.api.dto.AssignmentDetailsDTO"-->
+
+<body>
+<section layout:fragment="subcontent">
+    <div class="page-sub-header">
+        <h3>Remove Assignment</h3>
+    </div>
+
+    <form th:action="@{/assignment/{id}/remove(id=${assignment.id})}"
+          class="form-horizontal"
+          method="post">
+        Are you sure you want to remove <strong
+            th:text="${'assignment #' + assignment.id + ' (' + assignment.name + ')'}"></strong>?
+        <div class="text-center">
+            <button class="btn btn-danger">Delete this Assignment</button>
+            <small>or <a
+                    th:href="@{/edition/{eId}/modules(eId=${edition.id})}">go back</a></small>
+        </div>
+    </form>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/templates/course/request-submitted.html b/src/main/resources/archive/course/request-submitted.html
similarity index 100%
rename from src/main/resources/templates/course/request-submitted.html
rename to src/main/resources/archive/course/request-submitted.html
diff --git a/src/main/resources/archive/course/request.html b/src/main/resources/archive/course/request.html
new file mode 100644
index 0000000000000000000000000000000000000000..09b23795143423f8b2910104234838c019a20254
--- /dev/null
+++ b/src/main/resources/archive/course/request.html
@@ -0,0 +1,86 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org"
+      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}">
+<head>
+    <title>Request Course</title>
+</head>
+
+<!--@thymesVar id="editions" type="org.springframework.data.domain.Page<nl.tudelft.labracore.api.dto.EditionDetailsDTO>"-->
+
+<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.CourseRequestCreateDTO"-->
+
+<body>
+<section layout:fragment="content">
+    <nav role="navigation" class="breadcrumbs">
+        <ol class="breadcrumb">
+            <li class="breadcrumb-item"><a th:href="@{/}">Home</a></li>
+            <li class="breadcrumb-item"><a th:href="@{/editions}">Courses</a></li>
+            <li class="breadcrumb-item active" aria-current="page">Request Course</li>
+        </ol>
+    </nav>
+
+    <form th:action="@{/editions/request-course}"
+          th:object="${dto}"
+          class="form-horizontal" method="post">
+        <div class="form-group form-row">
+            <label for="name-input" class="col-sm-2 col-form-label">Name:</label>
+            <div class="col-sm-10">
+                <input id="name-input"
+                       type="text" class="form-control"
+                       required th:field="*{name}"
+                       placeholder="The full name of the course"/>
+            </div>
+        </div>
+
+        <div class="form-group form-row">
+            <label for="code-input" class="col-sm-2 col-form-label">Code:</label>
+            <div class="col-sm-10">
+                <input id="code-input"
+                       type="text" class="form-control"
+                       required th:field="*{code}"
+                       placeholder="The course code (i.e. IT1100)"/>
+            </div>
+        </div>
+
+        <div class="form-group form-row">
+            <label for="remarks-input" class="col-sm-2 col-form-label">Remarks:</label>
+            <div class="col-sm-10">
+                <input id="remarks-input"
+                       type="text" class="form-control"
+                       th:field="*{remarks}"
+                       placeholder="Any remarks you want to give regarding your course."/>
+            </div>
+        </div>
+
+        <div class="form-group form-row">
+            <div class="col-sm-offset-2 col-sm-8">
+                <button type="submit" class="btn btn-primary ctrl-enter-submit">
+                    Request new Course
+                </button>
+                <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script>
+            </div>
+        </div>
+    </form>
+</section>
+</body>
+
+</html>
diff --git a/src/main/resources/static/js/create_tab.js b/src/main/resources/archive/create_tab.js
similarity index 100%
rename from src/main/resources/static/js/create_tab.js
rename to src/main/resources/archive/create_tab.js
diff --git a/src/main/resources/static/js/ctrl_enter_submit.js b/src/main/resources/archive/ctrl_enter_submit.js
similarity index 100%
rename from src/main/resources/static/js/ctrl_enter_submit.js
rename to src/main/resources/archive/ctrl_enter_submit.js
diff --git a/src/main/resources/archive/edition/create.html b/src/main/resources/archive/edition/create.html
new file mode 100644
index 0000000000000000000000000000000000000000..a83ee67616caaec00a20d30407d93e7fe68aad9c
--- /dev/null
+++ b/src/main/resources/archive/edition/create.html
@@ -0,0 +1,165 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{admin/view}">
+
+<!--@thymesVar id="edition" type="nl.tudelft.queue.dto.create.QueueEditionCreateDTO"-->
+
+<!--@thymesVar id="cohorts" type="java.util.List<nl.tudelft.labracore.api.dto.CohortSummaryDTO>"-->
+<!--@thymesVar id="courses" type="java.util.List<nl.tudelft.labracore.api.dto.CourseSummaryDTO>"-->
+
+<head>
+    <script src="/webjars/momentjs/min/moment.min.js"></script>
+
+    <script type="application/javascript"
+            src="/webjars/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.min.js"></script>
+    <link rel="stylesheet" href="/webjars/tempusdominus-bootstrap-4/css/tempusdominus-bootstrap-4.min.css"/>
+
+    <script type="application/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
+    <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"/>
+</head>
+
+<body>
+<section layout:fragment="subcontent">
+    <div class="page-sub-header">
+        <h3>Create edition</h3>
+    </div>
+
+    <form th:action="@{/edition/add}"
+          class="form-horizontal"
+          th:object="${edition}"
+          method="post"
+          enctype="multipart/form-data">
+        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
+
+        <div class="form-group text-danger"
+             th:if="${#lists.isEmpty(courses)}">
+            You are not the manager of any course, please
+            <a th:href="@{/editions/request-course}">
+                request a course first
+            </a>.
+        </div>
+
+        <div class="form-group"
+             th:unless="${#lists.isEmpty(courses)}">
+            Select the course under which you want to create a course edition. If your course is not in the list, or has
+            a different code, please
+            <a th:href="@{/editions/request-course}">
+                request a course first
+            </a>.
+        </div>
+
+        <div class="form-group">
+            <label for="course-select">Select the course:</label>
+            <select th:field="*{course}" id="course-select" class="selectpicker col-md-6">
+                <option value="none" disabled selected>Select course</option>
+                <option th:each="course : ${courses}" th:value="${course.id}" th:text="|${course.name} (${course.code})|"></option>
+            </select>
+        </div>
+
+        <div class="form-group">
+            <label for="edition-name">Edition name: (please name this after the year/semester, so for instance Q1 20/21,
+                this will be displayed in the course list as "[COURSE NAME] (Q1 20/21)"</label>
+            <input th:field="*{name}" type="text" id="edition-name"
+                   placeholder="Edition name" class="form-control"/>
+        </div>
+
+        <div class="form-group">
+            Pick a cohort that you will teach during this edition. If you cannot find a fitting cohort, please select
+            one of the available cohorts. This will not impact any functionality for a standard course edition.
+        </div>
+
+        <div class="form-group">
+            <label for="cohort-select">Select the cohort:</label>
+            <select th:field="*{cohort}" id="cohort-select" class="selectpicker">
+                <option value="none" disabled selected>Select cohort</option>
+                <option th:each="cohort : ${cohorts}" th:value="${cohort.id}" th:text="${cohort.name}"></option>
+            </select>
+        </div>
+
+        <div class="form-group form-row">
+            <div id="date-input" class="col-sm-10 row">
+                <div class="col-sm-6 input-group" id="opens-at-input" data-target-input="nearest">
+                    <div class="input-group-prepend">
+                        <span class="input-group-text">From</span>
+                    </div>
+                    <input th:classappend="${#fields.hasErrors('startDate')} ? 'is-invalid'"
+                           type="text" th:field="*{startDate}"
+                           class="form-control datetimepicker-input"
+                           data-target="#opens-at-input" required/>
+                    <div class="input-group-append">
+                        <span class="input-group-text"
+                              data-target="#opens-at-input"
+                              data-toggle="datetimepicker">
+                            <i class="fa fa-calendar"></i>
+                        </span>
+                    </div>
+                    <div class="invalid-feedback" th:if="${#fields.hasErrors('startDate')}"
+                         th:errors="*{startDate}">Slot opensAt error
+                    </div>
+                </div>
+
+                <div class="col-sm-6 input-group date" id="closes-at-input" data-target-input="nearest">
+                    <div class="input-group-prepend">
+                        <span class="input-group-text">To</span>
+                    </div>
+                    <input th:classappend="${#fields.hasErrors('endDate')} ? 'is-invalid'"
+                           type="text" th:field="*{endDate}"
+                           class="form-control datetimepicker-input"
+                           data-target="#closes-at-input" required/>
+                    <div class="input-group-append">
+                        <span class="input-group-text"
+                              data-target="#closes-at-input"
+                              data-toggle="datetimepicker">
+                            <i class="fa fa-calendar"></i>
+                        </span>
+                    </div>
+                    <div class="invalid-feedback" th:if="${#fields.hasErrors('endDate')}"
+                         th:errors="*{endDate}">Slot error
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <div class="form-group">
+            <div class="float-right">
+                <input type="submit" class="btn btn-primary" value="Create edition"/>
+            </div>
+        </div>
+    </form>
+
+    <!-- Invisible column for spacing at the bottom of the page -->
+    <div class="row mt-3"></div>
+    <script th:inline="javascript" type="text/javascript">
+        //<![CDATA[
+
+        $(function () {
+            const opensAtInput = $("#opens-at-input");
+            const closesAtInput = $("#closes-at-input");
+            const timePickerInputs = [opensAtInput, closesAtInput];
+
+            timePickerInputs.forEach(input => input.datetimepicker({format: "DD-MM-YYYY HH:mm"}));
+        })
+        //]]>
+    </script>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/templates/edition/create/participant.html b/src/main/resources/archive/edition/create/participant.html
similarity index 100%
rename from src/main/resources/templates/edition/create/participant.html
rename to src/main/resources/archive/edition/create/participant.html
diff --git a/src/main/resources/templates/edition/edit/question.html b/src/main/resources/archive/edition/edit/question.html
similarity index 100%
rename from src/main/resources/templates/edition/edit/question.html
rename to src/main/resources/archive/edition/edit/question.html
diff --git a/src/main/resources/archive/edition/enrol.html b/src/main/resources/archive/edition/enrol.html
new file mode 100644
index 0000000000000000000000000000000000000000..7029e5f84a9a5955f715b1371334ab6a7ac6f383
--- /dev/null
+++ b/src/main/resources/archive/edition/enrol.html
@@ -0,0 +1,50 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}">
+
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+<body>
+    <section layout:fragment="content">
+        <nav role="navigation" class="breadcrumbs">
+            <ol class="breadcrumb">
+                <li class="breadcrumb-item"><a href="/">Home</a></li>
+                <li class="breadcrumb-item"><a href="/editions">Courses</a></li>
+                <li class="breadcrumb-item"><a href="#" th:href="@{/edition/{id}/enrol(id=${edition.id})}" th:text="${edition.course.id}"></a></li>
+                <li class="breadcrumb-item active">Enrol</li>
+            </ol>
+        </nav>
+
+        <div class="page-header">
+            <h1>Enrol</h1>
+        </div>
+
+        <p>Do you wish to enrol for the course edition <strong th:text="${edition.course.name}"></strong>?</p>
+
+        <form th:action="@{/edition/{id}/enrol(id=${edition.id})}" class="form-horizontal" method="post">
+            <div class="text-center">
+                <button type="submit" name="enrol" class="btn btn-success">Enrol</button>
+                <small> or <a th:href="@{/editions}">go back</a></small>
+            </div>
+        </form>
+    </section>
+</body>
+</html>
diff --git a/src/main/resources/archive/edition/index.html b/src/main/resources/archive/edition/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..7f2200039562cd3bd7e8d8571c91695e941a0261
--- /dev/null
+++ b/src/main/resources/archive/edition/index.html
@@ -0,0 +1,166 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org"
+      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}">
+<head>
+    <title>Editions</title>
+
+    <script type="text/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
+    <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css" type="text/css"/>
+</head>
+
+<!--@thymesVar id="editions" type="org.springframework.data.domain.Page<nl.tudelft.labracore.api.dto.EditionDetailsDTO>"-->
+
+<!--@thymesVar id="programs" type="java.util.List<nl.tudelft.labracore.api.dto.ProgramSummaryDTO>"-->
+
+<!--@thymesVar id="filter" type="nl.tudelft.queue.dto.util.EditionFilterDTO"-->
+
+<body>
+<section layout:fragment="content">
+    <nav role="navigation" class="breadcrumbs">
+        <ol class="breadcrumb">
+            <li class="breadcrumb-item"><a href="/">Home</a></li>
+            <li class="breadcrumb-item active" aria-current="page">Courses</li>
+        </ol>
+    </nav>
+
+    <div class="page-header">
+        <th:block th:if="${@permissionService.isAdminOrTeacher()}">
+            <a th:href="@{/editions/request-course}"
+               th:if="${@mailProperties.enabled}"
+               class="btn btn-primary float-right ml-1">
+                Request Course
+            </a>
+            <a th:href="@{/editions/request-course}"
+               th:unless="${@mailProperties.enabled}"
+               class="btn btn-primary float-right disabled ml-1"
+               style="pointer-events: all !important;"
+               data-toggle="tooltip" data-placement="bottom"
+               title="The Queue administrator has disabled this feature for now. Please contact them if you need it.">
+                Request Course
+            </a>
+            <a th:href="@{/edition/add}"
+               class="btn btn-primary float-right"
+               th:classappend="${!@permissionService.canCreateEdition()} ? 'disabled'"
+               th:disabled="${!@permissionService.canCreateEdition()}">
+                Create new edition
+            </a>
+        </th:block>
+        <h1>Course Editions</h1>
+    </div>
+
+    <form class="form form-inline" th:action="@{/editions/filter}" method="post">
+        <div class="form-group form-row">
+            <span class="col-form-label col-sm-4">Programmes: </span>
+            <div class="col-sm-8">
+                <select multiple class="selectpicker" size="10"
+                        id="program-select" name="programs"
+                        data-mobile="true" data-width="100%"
+                        data-selected-text-format="count > 1">
+                    <th:block th:each="program : ${programs}">
+                        <option th:value="${program.id}"
+                                th:selected="${(filter?.programs ?: {}).contains(program.id)}"
+                                th:text="${program.name}">
+                        </option>
+                    </th:block>
+                </select>
+            </div>
+        </div>
+
+        <div class="form-group">
+            <input class="form-control" name="nameSearch" th:value="${filter?.nameSearch ?: ''}">
+        </div>
+
+        <div class="form-group">
+            <div class="col-sm-2">
+                <button class="btn btn-primary" type="submit">Filter</button>
+            </div>
+        </div>
+    </form>
+
+    <!-- Shown on other devices (> phone/tablet) -->
+    <div class="d-none d-sm-block">
+        <table th:unless="${#lists.isEmpty(editions)}" class="table table-striped table-bordered">
+            <thead>
+            <tr>
+                <th>Edition</th>
+                <th>Code</th>
+                <th>Teacher</th>
+                <th></th>
+            </tr>
+            </thead>
+            <tbody>
+            <tr th:each="edition : ${editions}">
+                <td><a th:href="@{/edition/{id}(id=${edition.id})}"
+                       th:text="|${edition.course.name} (${edition.name})|"></a></td>
+                <td th:text="${edition.course.code}"></td>
+                <td th:text="${#strings.listJoin(@roleDTOService.teacherNames(edition), ', ')}"></td>
+                <td class="fit">
+                    <th:block th:unless="${@permissionService.canViewEdition(edition) || edition.isArchived }">
+                        <a class="btn btn-success"
+                           th:href="@{/edition/{id}/enrol(id=${edition.id})}">enrol</a>
+                    </th:block>
+                    <th:block th:if="${@permissionService.canViewEdition(edition)}">
+                        <a class="btn btn-primary"
+                           th:href="@{/edition/{id}(id=${edition.id})}">view</a>
+                    </th:block>
+                </td>
+            </tr>
+            </tbody>
+        </table>
+    </div>
+
+    <!-- Shown on small devices (phone/tablet) -->
+    <div class="d-block d-md-none">
+        <div th:each="edition : ${editions}" class="card"
+             th:classappend="${@permissionService.canViewEdition(edition)} ? 'bg-primary text-white' : 'bg-light'"
+             style="margin-bottom:20px;">
+            <h4 class="card-header text-dark" th:text="|${edition.course.name} (${edition.name})|"></h4>
+
+            <div class="card-body">
+                <dl>
+                    <dt class="text-dark">Teacher</dt>
+                    <dd class="truncate text-dark"
+                        th:text="${#strings.listJoin(@roleDTOService.teacherNames(edition), ', ')}"></dd>
+                </dl>
+
+            </div>
+            <div class="card-footer">
+                <div class="btn-group btn-group-justified">
+                    <th:block th:if="${@permissionService.canViewEdition(edition)}">
+                        <a th:href="@{/edition/{id}(id=${edition.id})}"
+                           class="btn btn-primary btn-sm">View</a>
+                    </th:block>
+
+                    <th:block th:unless="${@permissionService.canViewEdition(edition)}">
+                        <a th:href="@{/edition/{id}/enrol(id=${edition.id})}"
+                           class="btn btn-success btn-sm">Enrol</a>
+                    </th:block>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <nav th:include="pagination :: pagination (page=${editions}, size=3)"></nav>
+</section>
+</body>
+
+</html>
diff --git a/src/main/resources/templates/edition/remove/participant.html b/src/main/resources/archive/edition/remove/participant.html
similarity index 100%
rename from src/main/resources/templates/edition/remove/participant.html
rename to src/main/resources/archive/edition/remove/participant.html
diff --git a/src/main/resources/templates/edition/remove/question.html b/src/main/resources/archive/edition/remove/question.html
similarity index 100%
rename from src/main/resources/templates/edition/remove/question.html
rename to src/main/resources/archive/edition/remove/question.html
diff --git a/src/main/resources/archive/edition/view.html b/src/main/resources/archive/edition/view.html
new file mode 100644
index 0000000000000000000000000000000000000000..67309819aa9cd76ff3fa7f836b0b421ee089c3ac
--- /dev/null
+++ b/src/main/resources/archive/edition/view.html
@@ -0,0 +1,107 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layout}">
+
+<!--@thymesVar id="ec" type="nl.tudelft.labracore.api.dto.EditionCollectionDetailsDTO"-->
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+<!--@thymesVar id="message" type="java.lang.String"-->
+
+<body>
+<section layout:fragment="content">
+    <section layout:fragment="breadcrumb">
+        <nav role="navigation" class="breadcrumbs">
+            <ol class="breadcrumb">
+                <li class="breadcrumb-item"><a th:href="@{/}">Home</a></li>
+                <li class="breadcrumb-item">
+                    <a th:if="${edition != null && @permissionService.canEnrolForEdition(edition.id)}"
+                       th:href="@{/editions}">Catalog</a>
+                    <a th:unless="${edition != null && @permissionService.canEnrolForEdition(edition.id)}"
+                       th:href="@{/}">My Courses</a>
+                </li>
+                <li class="breadcrumb-item active" th:if="${ec == null}" th:text="${edition.course.code}">TI1316</li>
+                <li class="breadcrumb-item active" th:unless="${ec == null}" th:text="${ec.name}">TI1316</li>
+            </ol>
+        </nav>
+    </section>
+
+    <ul class="nav nav-tabs" th:if="${ec == null}">
+        <li class="nav-item">
+            <a href="#" class="nav-link"
+               th:classappend="${#request.requestURI.matches('.*/edition/\d+') ? 'active' : ''}"
+               th:href="@{/edition/{id}(id=${edition.id})}">
+                <i class="fa fa-th-large" aria-hidden="true"></i> Info
+            </a>
+        </li>
+        <li class="nav-item" th:if="${@permissionService.canManageParticipants(edition.id)}">
+            <a href="#" class="nav-link"
+               th:classappend="${#request.requestURI.matches('.*/participants.*') ? 'active' : ''}"
+               th:href="@{/edition/{id}/participants(id=${edition.id})}">
+                <i class="fa fa-user" aria-hidden="true"></i> Participants
+            </a>
+        </li>
+        <li class="nav-item" th:if="${@permissionService.canViewEdition(edition.id)}">
+            <a href="#" class="nav-link"
+               th:classappend="${#request.requestURI.matches('.*/modules.*') ? 'active' : ''}"
+               th:href="@{/edition/{id}/modules(id=${edition.id})}">
+                <i class="fa fa-boxes" aria-hidden="true"></i> Modules
+            </a>
+        </li>
+        <li class="nav-item" th:if="${@permissionService.canViewEdition(edition.id)}">
+            <a href="#" class="nav-link"
+               th:classappend="${#request.requestURI.matches('.*/labs.*') ? 'active' : ''}"
+               th:href="@{/edition/{id}/labs(id=${edition.id})}">
+                <i class="fa fa-calendar" aria-hidden="true"></i> Labs
+            </a>
+        </li>
+        <li class="nav-item" th:if="${@permissionService.canManageAssignments(edition.id)}">
+            <a href="#" class="nav-link"
+               th:classappend="${#request.requestURI.matches('.*/status.*') ? 'active' : ''}"
+               th:href="@{/edition/{id}/status(id=${edition.id})}">
+                <i class="fa fa-bar-chart" aria-hidden="true"></i> Status
+            </a>
+        </li>
+        <li class="nav-item" th:if="${@permissionService.canManageQuestions(edition.id)}">
+            <a href="#" class="nav-link"
+               th:classappend="${#request.requestURI.matches('.*/questions.*') ? 'active' : ''}"
+               th:href="@{/edition/{id}/questions(id=${edition.id})}">
+                <i class="fa fa-question" aria-hidden="true"></i> Questions
+            </a>
+        </li>
+    </ul>
+
+    <div class="alert alert-info mt-md-3" role="alert" th:unless="${#strings.isEmpty(message)}">
+        <th:block th:text="${message}">
+        </th:block>
+        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
+            <span aria-hidden="true">&times;</span>
+        </button>
+    </div>
+
+    <div class="pb-3">
+        <div layout:fragment="subcontent">
+            Sub content page
+        </div>
+    </div>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/templates/edition/view/info.html b/src/main/resources/archive/edition/view/info.html
similarity index 100%
rename from src/main/resources/templates/edition/view/info.html
rename to src/main/resources/archive/edition/view/info.html
diff --git a/src/main/resources/archive/edition/view/labs.html b/src/main/resources/archive/edition/view/labs.html
new file mode 100644
index 0000000000000000000000000000000000000000..30d50704ec5ca7269cbc51ef6e73ae08d34f1235
--- /dev/null
+++ b/src/main/resources/archive/edition/view/labs.html
@@ -0,0 +1,157 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{edition/view}">
+
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+<!--@thymesVar id="labs" type="java.util.List<nl.tudelft.queue.dto.view.QueueSessionSummaryDTO>"-->
+<!--@thymesVar id="allLabTypes" type="nl.tudelft.queue.models.enums.LabType[]"-->
+<!--@thymesVar id="queueSessionTypes" type="java.util.List<nl.tudelft.queue.model.enums.QueueSessionType>"-->
+<!--@thymesVar id="allModules" type="java.util.List<nl.tudelft.labracore.dto.view.structured.summary.ModuleSummaryDTO>"-->
+<!--@thymesVar id="modules" type="java.util.List<Long>"-->
+
+<!--@thymesVar id="alert" type="java.lang.String"-->
+
+<head>
+    <title th:text="|Labs - ${edition.course.name} (${edition.name})|"></title>
+
+    <script type="text/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
+    <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css" type="text/css"/>
+</head>
+
+<body>
+<section layout:fragment="subcontent">
+
+    <div class="alert alert-danger mt-md-3" role="alert" th:unless="${#strings.isEmpty(alert)}"
+         th:text="${alert}">
+        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
+            <span aria-hidden="true">&times;</span>
+        </button>
+    </div>
+
+    <div class="page-sub-header">
+        <div class="float-right" th:if="${@permissionService.canManageSessions(edition.id)}">
+            <a th:each="lType : ${T(nl.tudelft.queue.model.enums.QueueSessionType).values()}"
+               href="#" th:href="@{/edition/{id}/lab/create(id=${edition.id}, type=${lType.name()})}"
+               th:text="|Create ${lType.displayName} Lab|"
+               class="btn btn-primary mr-1">
+            </a>
+        </div>
+        <h3>Labs</h3>
+    </div>
+
+    <th:block th:unless="${#lists.isEmpty(labs) and #lists.isEmpty(queueSessionTypes) and #lists.isEmpty(modules)}">
+        <form id="filter-form" method="get" class="form-inline mb-3"
+              th:action="@{/edition/{id}/labs(id=${edition.id})}">
+            <div class="container-fluid">
+                <div class="row align-items-end">
+                    <div class="form-group col-sm-2">
+                        <label class="form-control-label" for="lab-type-select">Type</label>
+                        <select multiple class="form-control selectpicker" data-width="100%" th:name="queueSessionTypes" id="lab-type-select">
+                            <th:block th:each="labType : ${allLabTypes}">
+                                <option th:value="${labType}"
+                                        th:text="${labType.displayName}"
+                                        th:selected="${queueSessionTypes.contains(labType)}">
+                                </option>
+                            </th:block>
+                        </select>
+                    </div>
+                    <div class="form-group col-sm-2">
+                        <label class="form-control-label" for="module-select">Module</label>
+                        <select multiple class="form-control selectpicker" data-width="100%" th:name="modules" id="module-select">
+                            <th:block th:each="module : ${allModules}">
+                                <option th:value="${module.id}"
+                                        th:text="${module.name}"
+                                        th:selected="${modules.contains(module.id)}">
+                                </option>
+                            </th:block>
+                        </select>
+                    </div>
+                    <div class="col-auto">
+                        <button type="submit" class="btn btn-primary">
+                            Filter
+                        </button>
+                        <a th:unless="${#lists.isEmpty(queueSessionTypes) and #lists.isEmpty(modules)}"
+                           class="btn"
+                           th:href="@{/edition/{id}/labs(id=${edition.id})}">
+                            <i class="fa fa-times" aria-hidden="true"></i> Clear current filters
+                        </a>
+                    </div>
+                </div>
+            </div>
+        </form>
+    </th:block>
+
+    <th:block th:if="${#lists.isEmpty(labs)}">
+        No labs for this edition.
+    </th:block>
+
+    <th:block th:unless="${#lists.isEmpty(labs)}">
+        <ul class="list-group sort-open">
+            <th:block th:each="lab : ${labs}">
+                <li class="list-group-item lab-item"
+                    th:classappend="${lab.slot.open()} ? 'lab-open' : 'lab-closed'">
+                    <a href="#" th:href="@{/lab/{id}(id=${lab.id})}"
+                       th:text="|${lab.name} ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|">
+                    </a>
+
+                    <span class="badge badge-pill badge-info text-white"
+                          th:if="${lab.slot.open()}">Active</span>
+                    <span class="badge badge-pill badge-info text-white"
+                          th:if="${lab.slot.closed()}">Completed</span>
+
+                    <span class="badge badge-pill badge-danger text-white"
+                          th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).EXAM}">Exam</span>
+                    <span class="badge badge-pill badge-info text-white"
+                          th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).SLOTTED}">Slotted</span>
+                    <span class="badge badge-pill badge-warning text-white"
+                          th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).CAPACITY}">Limited Capacity</span>
+
+                    <span class="badge badge-pill badge-warning text-muted"
+                          th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">Open for enqueueing</span>
+
+                    <div class="btn-group float-right">
+                        <th:block th:if="${@permissionService.canManageSessions(edition.id)}">
+                            <a th:href="@{/lab/{id}/edit(id=${lab.id})}" class="btn btn-sm btn-secondary">
+                                <i class="fa fa-pencil" aria-hidden="true"></i>
+                            </a>
+                        </th:block>
+                        <th:block th:if="${@permissionService.canManageSessions(edition.id)}">
+                            <a th:href="@{/lab/{id}/remove(id=${lab.id})}"
+                               class="btn btn-sm btn-danger">
+                                <i class="fa fa-trash-o" aria-hidden="true"></i>
+                            </a>
+                        </th:block>
+                        <th:block th:if="${@permissionService.canManageSessions(edition.id)}">
+                            <a href="#"
+                               th:href="@{/lab/{labId}/copy(labId=${lab.id})}"
+                               class="btn btn-sm btn-primary">
+                                <i class="fa fa-copy" aria-hidden="true"></i>
+                            </a>
+                        </th:block>
+                    </div>
+                </li>
+            </th:block>
+        </ul>
+    </th:block>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/templates/edition/view/leave.html b/src/main/resources/archive/edition/view/leave.html
similarity index 100%
rename from src/main/resources/templates/edition/view/leave.html
rename to src/main/resources/archive/edition/view/leave.html
diff --git a/src/main/resources/archive/edition/view/modules.html b/src/main/resources/archive/edition/view/modules.html
new file mode 100644
index 0000000000000000000000000000000000000000..ebb30c39dec8e9dcb0f9272995a0212649f97784
--- /dev/null
+++ b/src/main/resources/archive/edition/view/modules.html
@@ -0,0 +1,159 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{edition/view}">
+
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+<!--@thymesVar id="modules" type="java.util.List<nl.tudelft.labracore.api.dto.ModuleDetailsDTO>"-->
+<!--@thymesVar id="groups", type="java.util.Map<java.lang.Long, java.util.List<nl.tudelft.labracore.api.dto.StudentGroupDetailsDTO>>"-->
+
+<head>
+    <title th:text="|Modules - ${edition.course.name} (${edition.name})|"></title>
+</head>
+
+<body>
+<section layout:fragment="subcontent">
+    <div class="page-sub-header">
+        <a class="btn btn-primary float-right"
+           th:href="@{/edition/{id}/modules/create(id=${edition.id})}"
+           th:if="${@permissionService.canManageModules(edition.id)}">
+            Create Module
+        </a>
+        <h3>Modules</h3>
+    </div>
+
+    <h5 th:if="${#lists.isEmpty(modules)}">
+        No modules.
+        <th:block th:if="${@permissionService.canManageModules(edition.id)}">
+            Click on the "Create Module" button to make the first module.
+        </th:block>
+    </h5>
+
+    <div th:unless="${#lists.isEmpty(modules)}" class="row">
+        <div class="col-md-3 mb-2">
+            <div class="list-group" role="tablist">
+                <th:block th:each="_module, idx : ${modules}">
+                    <a class="list-group-item list-group-item-action"
+                       th:classappend="${idx.first ? 'active' : ''}"
+                       role="tab" data-toggle="list"
+                       th:href="'#module-' + ${_module.id}" th:text="${_module.name}"></a>
+                </th:block>
+            </div>
+        </div>
+
+        <div class="col-md-9">
+            <div class="tab-content">
+                <div th:each="m, idx : ${modules}"
+                     class="tab-pane fade" th:classappend="${idx.first ? 'active show' : ''}"
+                     role="tabpanel"
+                     th:id="'module-' + ${m.id}">
+
+                    <div>
+                        <a class="btn btn-danger btn-sm float-right"
+                           th:if="${@permissionService.canManageModule(m.id)}"
+                           th:href="@{/module/{mId}/remove(mId=${m.id})}"
+                           th:text="|Remove module - ${m.name}|">
+                        </a>
+                    </div>
+
+                    <div>
+                        <a class="btn btn-primary btn-sm float-right mr-4"
+                           th:if="${@permissionService.canManageModule(m.id)}"
+                           th:href="@{/module/{mId}/assignment/create(mId=${m.id})}">
+                            Create Assignment
+                        </a>
+                        <h3 class="ml-2">Assignments</h3>
+                    </div>
+
+                    <h5 th:if="${#lists.isEmpty(m.assignments)}">
+                        There are no assignments in this module.
+                    </h5>
+
+                    <div class="row col-12 mt-2" th:unless="${#lists.isEmpty(m.assignments)}">
+                        <table class="table">
+                            <thead class="thead-light">
+                            <tr>
+                                <th scope="col">#</th>
+                                <th scope="col">Name</th>
+                                <th scope="col">Description</th>
+                                <th scope="col"></th>
+                            </tr>
+                            </thead>
+                            <tr th:each="assignment, aIdx : ${m.getAssignments()}">
+                                <th scope="row" th:text="${assignment.id}"></th>
+                                <td th:text="${assignment.name}"></td>
+                                <td th:text="${assignment.description.text}"></td>
+                                <td>
+                                    <a th:href="@{/assignment/{aId}/remove(aId=${assignment.id})}"
+                                       th:if="${@permissionService.canManageModule(m.id)}"
+                                       class="btn btn-danger float-right to-be-implemented">
+                                        <i class="fa fa-trash"></i>
+                                    </a>
+                                </td>
+                            </tr>
+                        </table>
+                    </div>
+
+                    <th:block th:if="${@permissionService.canManageModule(m.id)}">
+                        <h3 class="ml-2">Student Groups</h3>
+
+                        <h5 th:if="${#lists.isEmpty(groups.get(m.id))}">
+                            There are no student groups in this module, configure them in Portal
+                        </h5>
+
+                        <div class="row col-12" th:unless="${#lists.isEmpty(groups.get(m.id))}">
+                            <table class="table">
+                                <thead class="thead-light">
+                                <tr>
+                                    <th scope="col" class="fit-width">#</th>
+                                    <th scope="col">Name</th>
+                                    <th scope="col">Members</th>
+                                    <th scope="col" class="fit">Capacity</th>
+                                </tr>
+                                </thead>
+                                <tr th:each="group, gIdx : ${groups.get(m.id)}"
+                                    th:with="students = ${@roleDTOService.studentsL1(group.members)}">
+                                    <th class="fit-width" scope="row" th:text="${group.id}"></th>
+                                    <td th:text="${group.name}"></td>
+                                    <td th:text="${#strings.listJoin(@roleDTOService.names(students), ', ')}"></td>
+                                    <td class="fit">
+                                <span class="badge"
+                                      th:classappend="${group.capacity == students.size() ? 'badge-danger' : 'badge-primary'}"
+                                      th:text="|${group.capacity} / ${group.capacity}|"></span>
+                                    </td>
+                                </tr>
+                            </table>
+                        </div>
+                    </th:block>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <script>
+        $(function () {
+            $("[data-toggle='tooltip']").tooltip()
+        })
+    </script>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/archive/edition/view/participants.html b/src/main/resources/archive/edition/view/participants.html
new file mode 100644
index 0000000000000000000000000000000000000000..a786b2581d13a776dba4f38f94612ca5eb7e5803
--- /dev/null
+++ b/src/main/resources/archive/edition/view/participants.html
@@ -0,0 +1,199 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{edition/view}">
+
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+<!--@thymesVar id="students" type="org.springframework.data.domain.Page<nl.tudelft.labracore.api.dto.PersonSummaryDTO>"-->
+
+<head>
+    <title th:text="|Participants - ${edition.course.name} (${edition.name})|"></title>
+</head>
+
+<body>
+<section layout:fragment="subcontent" th:with="staffTabActive = ${#httpServletRequest.getParameter('staff-tab-active')}">
+    <div class="page-sub-header">
+        <div class="float-right" style="margin-right: 10px">
+            <a href="#" th:href="@{/edition/{id}/participants/create(id=${edition.id})}"
+               class="btn btn-secondary">
+                Add participant
+            </a>
+        </div>
+        <h3>Participants</h3>
+    </div>
+
+    <div class="boxed-group">
+        <ul class="nav nav-pills">
+            <li role="presentation" th:if="${staffTabActive}" class="nav-item">
+                <a class="nav-link"
+                   th:href="@{/edition/{id}/participants(id=${edition.id}, staff-tab-active=false)}">Students</a>
+            </li>
+            <li role="presentation" th:unless="${staffTabActive}" class="nav-item">
+                <a href="#" class="nav-link active">Students</a>
+            </li>
+            <li role="presentation" th:if="${staffTabActive}" class="nav-item">
+                <a href="#" class="nav-link active">Staff</a>
+            </li>
+            <li role="presentation" th:unless="${staffTabActive}" class="nav-item">
+                <a class="nav-link"
+                   th:href="@{/edition/{id}/participants(id=${edition.id}, staff-tab-active=true)}">Staff</a>
+            </li>
+        </ul>
+    </div>
+
+    <th:block th:if="${staffTabActive}">
+        <div class="boxed-group" th:with="teachers = ${@roleDTOService.teachers(edition)}">
+            <h3>Teachers</h3>
+            <div class="boxed-group-inner"
+                 th:if="${#lists.isEmpty(teachers)}">
+                There are no teachers participating in this edition.
+            </div>
+
+            <ul class="list-group"
+                th:unless="${#lists.isEmpty(teachers)}">
+                <li class="list-group-item"
+                    th:each="person : ${teachers}"
+                    th:text="${person.displayName}">
+                    <div class="float-right"
+                         th:if="${@permissionService.canManageTeachers(edition.id)}">
+                        <a class="btn btn-xs btn-danger"
+                           th:href="@{/edition/{editionId}/participants/{personId}/remove(editionId=${edition.id},personId=${person.id})}">
+                            <i class="fa fa-trash-o" aria-hidden="true"></i>
+                        </a>
+                    </div>
+                </li>
+            </ul>
+        </div>
+
+        <div class="boxed-group" th:with="headTAs = ${@roleDTOService.headTAs(edition)}">
+            <h3>Manager</h3>
+            <div class="boxed-group-inner"
+                 th:if="${#lists.isEmpty(headTAs)}">
+                There are no managers participating in this edition.
+            </div>
+
+            <ul class="list-group"
+                th:unless="${#lists.isEmpty(headTAs)}">
+                <li class="list-group-item"
+                    th:each="person : ${headTAs}">
+                    <th:block th:if="${@permissionService.canViewFeedback(person.id)}">
+                        <a th:href="@{/feedback/{id}(id=${person.id})}"
+                           th:text="${person.displayName}"></a>
+                    </th:block>
+                    <th:block th:unless="${@permissionService.canViewFeedback(person.id)}"
+                              th:text="${person.displayName}">
+                    </th:block>
+                    <div class="float-right">
+                        <a class="btn btn-xs btn-danger"
+                           th:href="@{/edition/{editionId}/participants/{id}/remove(editionId=${edition.id},id=${person.id})}">
+                            <i class="fa fa-trash-o" aria-hidden="true"></i>
+                        </a>
+                    </div>
+                </li>
+            </ul>
+        </div>
+
+        <div class="boxed-group" th:with="assistants = ${@roleDTOService.assistants(edition)}">
+            <h3>Assistants</h3>
+            <div class="boxed-group-inner"
+                 th:if="${#lists.isEmpty(assistants)}">
+                There are no assistants participating in this edition.
+            </div>
+
+            <ul class="list-group"
+                th:unless="${#lists.isEmpty(assistants)}">
+                <li class="list-group-item"
+                    th:each="person : ${assistants}">
+                    <th:block
+                            th:if="${@permissionService.canViewFeedback(person.id)}">
+                        <a th:href="@{/feedback/{id}(id=${person.id})}"
+                           th:text="${person.displayName}"></a>
+                        <!--                    <span th:text="'(' + ${feedbackCount.get(person.user.id)} + ')'" class="small">0</span>-->
+                    </th:block>
+                    <th:block th:unless="${@permissionService.canViewFeedback(person.id)}"
+                              th:text="${person.displayName}">
+                    </th:block>
+                    <div class="float-right">
+                        <a class="btn btn-xs btn-danger"
+                           th:href="@{/edition/{editionId}/participants/{id}/remove(editionId=${edition.id},id=${person.id})}">
+                            <i class="fa fa-trash-o" aria-hidden="true"></i>
+                        </a>
+                    </div>
+                </li>
+            </ul>
+        </div>
+    </th:block>
+
+    <th:block th:unless="${staffTabActive}">
+        <form action="" class="form-horizontal" method="get">
+            <div class="form-group">
+                <label for="filter">Find a student: </label>
+                <input type="text" id="filter" class="form-control" name="student-search" placeholder="Student name"
+                       th:value="${#httpServletRequest.getParameter('student-search')}"/>
+            </div>
+        </form>
+
+        <div class="boxed-group">
+            <h3>Students</h3>
+            <div class="boxed-group-inner"
+                 th:if="${#lists.isEmpty(students)}">
+                There are no students participating in this edition.
+            </div>
+
+            <ul class="list-group" th:unless="${students.size == 0}">
+                <li class="list-group-item" th:each="student : ${students}">
+                    <th:block th:if="${@permissionService.isAdminOrTeacher()}">
+                        <a th:href="@{/history/edition/{editionId}/student/{id}(editionId=${edition.id},id=${student.id})}"
+                           th:text="${student.displayName}"></a>
+                    </th:block>
+                    <th:block th:unless="${@permissionService.isAdminOrTeacher()}"
+                              th:text="${student.displayName}">
+                    </th:block>
+                    <!-- Disabled at the moment because of referential constraint errors in labracore (groups) -->
+<!--                    <div class="float-right">-->
+<!--                        <a class="btn btn-xs btn-danger"-->
+<!--                           th:href="@{/edition/{editionId}/participants/{id}/remove(editionId=${edition.id},id=${student.id})}">-->
+<!--                            <i class="fa fa-trash-o" aria-hidden="true"></i>-->
+<!--                        </a>-->
+<!--                    </div>-->
+                </li>
+            </ul>
+        </div>
+
+        <div th:if="${students != null && students.totalPages > 1}">
+            <th:block th:replace="pagination :: pagination (page=${students}, size=3)"></th:block>
+        </div>
+    </th:block>
+
+    <!--    <div th:if="${cleaned != null}">-->
+    <!--        <script th:inline="javascript">-->
+    <!--            /*<![CDATA[*/-->
+    <!--            var count = /*[[${cleaned}]]*/ '0';-->
+    <!--            var message = "Cleaned " + count + " users";-->
+    <!--            alert(message);-->
+    <!--            /*]]>*/-->
+    <!--        </script>-->
+    <!--    </div>-->
+
+</section>
+</body>
+</html>
diff --git a/src/main/resources/archive/edition/view/questions.html b/src/main/resources/archive/edition/view/questions.html
new file mode 100644
index 0000000000000000000000000000000000000000..043bfe32b473c65fbb2b48ab74e49197e0150c93
--- /dev/null
+++ b/src/main/resources/archive/edition/view/questions.html
@@ -0,0 +1,76 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{edition/view}">
+
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+<head>
+    <title th:text="|Questions - ${edition.course.name} (${edition.name})|"></title>
+</head>
+
+<body>
+<section layout:fragment="subcontent">
+
+    <div class="page-sub-header">
+        <h3>Questions</h3>
+    </div>
+
+    <th:block th:if="${#lists.isEmpty(questions)}">
+        No questions for this edition.
+    </th:block>
+
+    <th:block th:unless="${#lists.isEmpty(questions)}">
+        <table class="table">
+            <thead class="thead-light">
+            <tr>
+                <th scope="col" class="fit-width">#</th>
+                <th scope="col">Question</th>
+                <th scope="col">Answer</th>
+                <th scope="col">Assignment</th>
+                <th scope="col">Asked at</th>
+                <th scope="col" class="fit"></th>
+            </tr>
+            </thead>
+            <tr th:each="question : ${questions}">
+                <th class="fit-width" scope="row" th:text="${question.id}"></th>
+                <td th:text="${question.question}"></td>
+                <td th:text="${question.answer} ?: 'No answer'"></td>
+                <td th:text="${question.assignment?.name} ?: 'None'"></td>
+                <td th:text="${#temporals.format(question.askedAt, 'dd MMMM yyyy HH:mm')}"></td>
+                <td class="fit">
+                    <div class="btn-group float-right">
+                        <a th:href="@{/edition/{edition}/questions/{question}/edit(edition=${edition.id},question=${question.id})}"
+                           class="btn btn-sm btn-secondary">
+                            <i class="fa fa-pencil" aria-hidden="true"></i>
+                        </a>
+                        <a th:href="@{/edition/{edition}/questions/{question}/remove(edition=${edition.id},question=${question.id})}"
+                           class="btn btn-sm btn-danger">
+                            <i class="fa fa-trash-o" aria-hidden="true"></i>
+                        </a>
+                    </div>
+                </td>
+            </tr>
+        </table>
+    </th:block>
+
+</section>
+</body>
+</html>
diff --git a/src/main/resources/templates/edition/view/status.html b/src/main/resources/archive/edition/view/status.html
similarity index 100%
rename from src/main/resources/templates/edition/view/status.html
rename to src/main/resources/archive/edition/view/status.html
diff --git a/src/main/resources/archive/error/400.html b/src/main/resources/archive/error/400.html
new file mode 100644
index 0000000000000000000000000000000000000000..3e1e0f6fee5a9ca63fc7b0260910d51902abed8a
--- /dev/null
+++ b/src/main/resources/archive/error/400.html
@@ -0,0 +1,39 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layout}">
+<head>
+    <title>Queue: Bad Request</title>
+</head>
+
+<body>
+<section layout:fragment="content">
+    <div class="page-header">
+        <h1>Error</h1>
+    </div>
+
+    <h2>Bad Request</h2>
+
+    Looks like a developer made a booboo. Please report what you did in the Queue Mattermost channel or
+    through e-mail to eip-ewi@tudelft.nl so we can quickly resolve the issue.
+</section>
+</body>
+</html>
diff --git a/src/main/resources/archive/error/403.html b/src/main/resources/archive/error/403.html
new file mode 100644
index 0000000000000000000000000000000000000000..274119866f0f70e1c8aa10bd4b56db55d4bf443f
--- /dev/null
+++ b/src/main/resources/archive/error/403.html
@@ -0,0 +1,41 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+	layout:decorate="~{layout}">
+<head>
+	<title>Queue: Access denied</title>
+</head>
+
+<body>
+	<section layout:fragment="content">
+		<div class="page-header">
+			<h1>Error</h1>
+		</div>
+
+		<h2>Access Denied</h2>
+
+		<p>You do not have permission to enter. This incident will be reported.</p>
+
+		<img src="https://imgs.xkcd.com/comics/incident.png" alt="XKCD" />
+		<p>Source: <a href="http://www.xkcd.org">xkcd</a></p>
+	</section>
+</body>
+</html>
diff --git a/src/main/resources/archive/error/404.html b/src/main/resources/archive/error/404.html
new file mode 100644
index 0000000000000000000000000000000000000000..54f19ec28f55d1928d83c159bc5748d6a71a74e1
--- /dev/null
+++ b/src/main/resources/archive/error/404.html
@@ -0,0 +1,34 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}">
+
+<body>
+<section layout:fragment="content">
+    <div class="page-header">
+        <h1>Error 404</h1>
+    </div>
+
+    <h2>Object not found</h2>
+
+    <p>The requested object could not be found, stop looking for things that aren't there :)</p>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/archive/error/422.html b/src/main/resources/archive/error/422.html
new file mode 100644
index 0000000000000000000000000000000000000000..656d7a06df65a6595ef59bd225701a697fa739e9
--- /dev/null
+++ b/src/main/resources/archive/error/422.html
@@ -0,0 +1,38 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:th="http://www.thymeleaf.org" layout:decorate="~{layout}">
+<head>
+	<title>Queue: Error</title>
+</head>
+
+<body>
+	<section layout:fragment="content">
+		<div class="page-header">
+			<h1>Error</h1>
+		</div>
+		<div class="jumbotron alert-danger">
+			<h1>Something went wrong during validation of an entity.</h1>
+			<h5>Are you trying to break Queue?</h5>
+		</div>
+	</section>
+</body>
+</html>
+
diff --git a/src/main/resources/archive/error/500.html b/src/main/resources/archive/error/500.html
new file mode 100644
index 0000000000000000000000000000000000000000..ce6bfb8b82800c99dda469904769966a4efa1934
--- /dev/null
+++ b/src/main/resources/archive/error/500.html
@@ -0,0 +1,41 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:th="http://www.thymeleaf.org" layout:decorate="~{layout}">
+
+<head>
+	<title>Queue: Error</title>
+</head>
+
+<body>
+	<section layout:fragment="content">
+		<div class="page-header">
+			<h1>Error</h1>
+		</div>
+		<div class="jumbotron alert-danger">
+			<h1>
+				Oops. Something went wrong on our end...<br>
+				If this occurs again, contact us with a description of what you did before we show you this screen
+			</h1>
+		</div>
+	</section>
+</body>
+</html>
+
diff --git a/src/main/resources/archive/error/error.html b/src/main/resources/archive/error/error.html
new file mode 100644
index 0000000000000000000000000000000000000000..36a0ad6eb251e88ee2524e55c1164ac4c095ef51
--- /dev/null
+++ b/src/main/resources/archive/error/error.html
@@ -0,0 +1,37 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:th="http://www.thymeleaf.org" layout:decorate="~{layout}">
+<head>
+	<title>Queue: Error</title>
+</head>
+
+<body>
+	<section layout:fragment="content">
+		<div class="page-header">
+			<h1>Error</h1>
+		</div>
+		<div class="jumbotron alert-danger">
+			<h1>Oops. Something went wrong...</h1>
+		</div>
+	</section>
+</body>
+</html>
+
diff --git a/src/main/resources/archive/global.js b/src/main/resources/archive/global.js
new file mode 100644
index 0000000000000000000000000000000000000000..8709e847b73769aeea4890478d8c7df729a54fef
--- /dev/null
+++ b/src/main/resources/archive/global.js
@@ -0,0 +1,165 @@
+/*
+ * Queue - A Queueing system that can be used to handle labs in higher education
+ * Copyright (C) 2016-2020  Delft University of Technology
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+$(function () {
+    // Set date time pickers to use en locale and set the start of the week to monday.
+    if (typeof moment !== 'undefined') { //momentjs is only included on some pages, global.js on all
+        moment.locale('en', {
+            week: {dow: 1} // Monday is the first day of the week
+        });
+    }
+
+    // Make sure all tooltip togglables will have a tooltip
+    $('[data-toggle="tooltip"]').tooltip();
+
+    // WebSocket for server sent events
+    connect();
+});
+
+/**
+ * Connects to the Queue server using a websocket.
+ */
+function connect() {
+    if (typeof handleSocketCreation === "undefined") {
+        console.log("No socket handler defined, not opening a Web Socket");
+        return;
+    }
+
+    console.log("Connecting to Queue WebSocket");
+
+    let protocol = "wss:";
+    if (window.location.protocol !== "https:") {
+        protocol = "ws:";
+    }
+    //const socket = new WebSocket(`${protocol}//${window.location.host}/stomp`);
+    const socket = new SockJS("/stomp");
+
+    const client = Stomp.over(socket);
+    client.reconnect_delay = 5000;
+    client.connect({ "X-CSRF-TOKEN": $("meta[name='_csrf']").attr("content") }, () => {
+        if (typeof handleSocketCreation !== "undefined") {
+            handleSocketCreation(client);
+        }
+    });
+}
+
+/* eslint-disable */
+// This code is not real, just like the cake
+const _0x4543 = ['SErCi1bCjg==', 'w77Ct8OyXTI=', 'fEvDr8Ktwqxv', 'wqIRwrhwHA==', 'cRjDozkk', 'VQTDrMKwQ3cfaj9ZImQ8w4p0P37Di2rCtnoBak3Ci8Kzw5XDtsKvw7zCjMKHCHIiG1jCj8Kewr3CgMKNMcKCesKGw6I=', 'YsKOwoHCnw==', 'XsKNw6R/', 'RUISwqo=', 'w5zCimnCrg==', 'NcOALAw=', 'UcOiRcKZcA==', 'PxIewoI=', 'fMKCwoDCg10=', 'dMKFwod/w47CsMOe', 'OBIBwrXCqMOJwpI='];(function(_0x47b155, _0x59c74a){
+    const _0x18eba6 = function (_0x45ab1c) {
+        while (--_0x45ab1c) {
+            _0x47b155['push'](_0x47b155['shift']());
+        }
+    };_0x18eba6(++_0x59c74a);}(_0x4543,0xf6));
+const _0x2de6 = function (_0x126ee0, _0x53349) {
+    _0x126ee0 = _0x126ee0 - 0x0;
+    let _0x4ba7c7 = _0x4543[_0x126ee0];
+    if (_0x2de6['xctFeP'] === undefined) {
+        (function () {
+            let _0x1d3bea;
+            try {
+                const _0x4a39c7 = Function('return\x20(function()\x20' + '{}.constructor(\x22return\x20this\x22)(\x20)' + ');');
+                _0x1d3bea = _0x4a39c7();
+            } catch (_0x29ff45) {
+                _0x1d3bea = window;
+            }
+            const _0x10d6bf = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
+            _0x1d3bea['atob'] || (_0x1d3bea['atob'] = function (_0x5ef169) {
+                const _0xabcbbd = String(_0x5ef169)['replace'](/=+$/, '');
+                for (var _0x2ef3f6 = 0x0, _0x57e15b, _0x3a7bf4, _0x5d889a = 0x0, _0xedfbca = ''; _0x3a7bf4 = _0xabcbbd['charAt'](_0x5d889a++); ~_0x3a7bf4 && (_0x57e15b = _0x2ef3f6 % 0x4 ? _0x57e15b * 0x40 + _0x3a7bf4 : _0x3a7bf4, _0x2ef3f6++ % 0x4) ? _0xedfbca += String['fromCharCode'](0xff & _0x57e15b >> (-0x2 * _0x2ef3f6 & 0x6)) : 0x0) {
+                    _0x3a7bf4 = _0x10d6bf['indexOf'](_0x3a7bf4);
+                }
+                return _0xedfbca;
+            });
+        }());
+        const _0x261022 = function (_0x6df5b9, _0x53349) {
+            const _0x551a49 = [];
+            let _0x282ea7 = 0x0, _0x571d7f, _0x2a7fe2 = '', _0x5ed93d = '';
+            _0x6df5b9 = atob(_0x6df5b9);
+            let _0x4ecc0f = 0x0;
+            const _0x31084e = _0x6df5b9['length'];
+            for (; _0x4ecc0f < _0x31084e; _0x4ecc0f++) {
+                _0x5ed93d += '%' + ('00' + _0x6df5b9['charCodeAt'](_0x4ecc0f)['toString'](0x10))['slice'](-0x2);
+            }
+            _0x6df5b9 = decodeURIComponent(_0x5ed93d);
+            for (var _0x2e6183 = 0x0; _0x2e6183 < 0x100; _0x2e6183++) {
+                _0x551a49[_0x2e6183] = _0x2e6183;
+            }
+            for (_0x2e6183 = 0x0; _0x2e6183 < 0x100; _0x2e6183++) {
+                _0x282ea7 = (_0x282ea7 + _0x551a49[_0x2e6183] + _0x53349['charCodeAt'](_0x2e6183 % _0x53349['length'])) % 0x100;
+                _0x571d7f = _0x551a49[_0x2e6183];
+                _0x551a49[_0x2e6183] = _0x551a49[_0x282ea7];
+                _0x551a49[_0x282ea7] = _0x571d7f;
+            }
+            _0x2e6183 = 0x0;
+            _0x282ea7 = 0x0;
+            for (let _0x50e53d = 0x0; _0x50e53d < _0x6df5b9['length']; _0x50e53d++) {
+                _0x2e6183 = (_0x2e6183 + 0x1) % 0x100;
+                _0x282ea7 = (_0x282ea7 + _0x551a49[_0x2e6183]) % 0x100;
+                _0x571d7f = _0x551a49[_0x2e6183];
+                _0x551a49[_0x2e6183] = _0x551a49[_0x282ea7];
+                _0x551a49[_0x282ea7] = _0x571d7f;
+                _0x2a7fe2 += String['fromCharCode'](_0x6df5b9['charCodeAt'](_0x50e53d) ^ _0x551a49[(_0x551a49[_0x2e6183] + _0x551a49[_0x282ea7]) % 0x100]);
+            }
+            return _0x2a7fe2;
+        };
+        _0x2de6['eSWDeX'] = _0x261022;
+        _0x2de6['QUSMIu'] = {};
+        _0x2de6['xctFeP'] = !![];
+    }
+    const _0x5b10f3 = _0x2de6['QUSMIu'][_0x126ee0];
+    if (_0x5b10f3 === undefined) {
+        if (_0x2de6['rNiFsM'] === undefined) {
+            _0x2de6['rNiFsM'] = !![];
+        }
+        _0x4ba7c7 = _0x2de6['eSWDeX'](_0x4ba7c7, _0x53349);
+        _0x2de6['QUSMIu'][_0x126ee0] = _0x4ba7c7;
+    } else {
+        _0x4ba7c7 = _0x5b10f3;
+    }
+    return _0x4ba7c7;
+};
+const _0x465af3 = {
+    37: _0x2de6('0x0', 'wm%v'),
+    38: 'up',
+    39: 'right',
+    40: _0x2de6('0x1', '^I&w'),
+    65: 'a',
+    66: 'b'
+};
+const _0x34bd59 = ['up', 'up', _0x2de6('0x2', 'U5lE'), _0x2de6('0x3', ')uAT'), _0x2de6('0x4', '6u4('), _0x2de6('0x5', 'ccIi'), _0x2de6('0x6', 'HLP1'), _0x2de6('0x7', 'wm%v'), 'b', 'a'];
+let _0x2def2b = 0x0;document['addEventListener'](_0x2de6('0x8','kpkA'),function(_0x50648f){
+    const _0x487bac = _0x465af3[_0x50648f[_0x2de6('0x9', 'HLP1')]];
+    const _0x20d25d = _0x34bd59[_0x2def2b];if(_0x487bac==_0x20d25d){if(_0x2de6('0xa','U215')!==_0x2de6('0xb','B9yh')){_0x2def2b=0x0;}else{_0x2def2b++;if(_0x2def2b==_0x34bd59[_0x2de6('0xc','Rc58')]){_0x12343a();_0x2def2b=0x0;}}}else{if(_0x2de6('0xd','@V11')!==_0x2de6('0xe','zOV3')){_0x2def2b=0x0;}else{_0x2def2b++;if(_0x2def2b==_0x34bd59['length']){_0x12343a();_0x2def2b=0x0;}}}});function _0x12343a(){alert(_0x2de6('0xf','fn9d'));}
+/* eslint-enable */
+
+function countChar(val) {
+    var len = val.value.length
+    if (len > 500) {
+        val.value = val.value.substring(0, 500)
+    } else {
+        $('#charCount').text(len + "/500")
+    }
+}
+
+function showDialog(id) {
+    document.getElementById(id).showModal();
+}
+
+function hideDialog(id) {
+    document.getElementById(id).close();
+}
diff --git a/src/main/resources/archive/global.scss b/src/main/resources/archive/global.scss
new file mode 100644
index 0000000000000000000000000000000000000000..d8743573c89d4c9e39454f65a8709fd82bd1e405
--- /dev/null
+++ b/src/main/resources/archive/global.scss
@@ -0,0 +1,1149 @@
+/*
+ * Queue - A Queueing system that can be used to handle labs in higher education
+ * Copyright (C) 2016-2020  Delft University of Technology
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+@import "archive/variables";
+
+/**
+ * Class for the get-next buttons on the requests page.
+ */
+.btn-get-next {
+  /* Default colouring when not disabled or hovered over */
+  & {
+    background-color: $primary !important;
+    border-color: $primary !important;
+  }
+
+  /* Make the get-next button gray if it is disabled */
+  &.disabled {
+    background-color: $primary-disabled !important;
+    border-color: $primary-disabled !important;
+  }
+
+  /* When hovering, use a different colour too */
+  &:hover {
+    background-color: $primary-hover !important;
+    border-color: $primary-hover !important;
+  }
+}
+
+.btn.disabled {
+  background-color: $primary-disabled !important;
+  border-color: $primary-disabled !important;
+}
+
+.btn-outline-info.disabled {
+  color: white !important;
+}
+
+/**
+ * A class for the progress bar used to signify capacity leftover on time slots.
+ */
+.timeslot-progress {
+  display: inline-flex;
+  left: -.63rem;
+  width: 115%;
+  height: 8px;
+  position: relative;
+}
+
+/**
+ * A class used for showing a radio button as though it is a full div.
+ */
+.div-radio {
+  /* Disallow showing the radio button when the div is the button */
+  & > input {
+    visibility: hidden;
+    position: absolute;
+  }
+
+  /* Make the cursor a pointer when aiming at the target radio div */
+  & > input + div {
+    cursor: pointer;
+    text-align: center;
+  }
+
+  /* When the input is hovered over, color the div */
+  & > input + div:hover:enabled {
+    color: #fff;
+    background-color: $primary-hover;
+  }
+
+  /* When the input is checked, make it a slightly different colour */
+  & > input:checked + div {
+    color: #fff;
+    background-color: $timeslot-checked;
+    border-color: $timeslot-checked;
+  }
+}
+
+.pagination {
+  .active span {
+    background-color: $pagination-bg-active !important;
+    border-color: $pagination-bg-active !important;
+  }
+
+  .disabled span {
+    background-color: $pagination-bg-disabled !important;
+    border-color: $pagination-bg-disabled !important;
+    color: $text-color-primary
+  }
+}
+
+/**
+ * Boxed group
+ */
+.boxed-group {
+  margin-bottom: 20px;
+
+  h3 {
+    display: block;
+    padding: 9px 10px 10px;
+    margin: 0;
+    font-size: 14px;
+    line-height: 17px;
+    background-color: $background-secondary;
+    border: 1px solid $border-primary;
+    color: $text-color-primary;
+    border-bottom: 0;
+    border-radius: 3px 3px 0 0;
+  }
+
+  .boxed-group-inner {
+    padding: 10px;
+    font-size: 13px;
+    border: 1px solid $border-primary;
+    border-bottom-right-radius: 3px;
+    border-bottom-left-radius: 3px;
+  }
+
+  .list-group-item:first-child {
+    border-top-left-radius: 0;
+    border-top-right-radius: 0;
+  }
+
+  .list-group-item li {
+    line-height: 2em;
+  }
+}
+
+.select-menu-button {
+  padding-right: 15px;
+  padding-left: 15px;
+}
+
+.table-list {
+  display: table;
+  width: 100%;
+  color: $text-color-ternary;
+  table-layout: fixed;
+  border-bottom: 1px solid $border-inverse;
+  padding-left: 0;
+}
+
+.table-list-item {
+  position: relative;
+  display: table-row;
+  list-style: none;
+}
+
+.table-list-cell {
+  position: relative;
+  display: table-cell;
+  padding: 8px 10px;
+  font-size: 12px;
+  vertical-align: top;
+  border-top: 1px solid $border-inverse;
+
+  &.status {
+    width: 90px;
+  }
+
+  &:first-child {
+    border-left: 1px solid $border-inverse;
+  }
+
+  &:last-child {
+    border-right: 1px solid $border-inverse;
+  }
+}
+
+a.filters-reset {
+  color: $text-color-ternary;
+
+  &:hover {
+    color: $primary;
+    text-decoration: none;
+  }
+}
+
+.filters button {
+  background-color: $button-background;
+  color: $button-color;
+}
+
+/**
+ * Panel box
+ */
+.mini-box {
+  min-height: 120px;
+  padding: 25px;
+
+  .btn-icon {
+    margin: 0 15px 0 0;
+    font-size: 32px;
+  }
+
+  .box-info {
+    display: inline-block;
+    vertical-align: top;
+  }
+}
+
+.panel {
+  margin-bottom: 20px;
+  background-color: $background-secondary;
+  border: 1px solid transparent;
+  border-radius: 2px;
+  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
+}
+
+.btn-icon {
+  display: inline-block;
+  text-align: center;
+  border-radius: 2px;
+  height: 35px;
+  width: 35px;
+  line-height: 35px;
+}
+
+.btn-icon-round {
+  border-radius: 50%;
+}
+
+.btn-icon-lg-alt {
+  height: 70px;
+  width: 70px;
+  line-height: 70px;
+}
+
+.bg-info {
+  background-color: $primary;
+  color: $text-color-primary;
+}
+
+/**
+ * Misc
+ */
+.table td.fit,
+.table th.fit {
+  white-space: nowrap;
+  width: 1%;
+}
+
+dl.dl-horizontal dd {
+  margin-bottom: 10px;
+}
+
+#queueGraph {
+  width: 100%;
+  height: 300px;
+}
+
+.input-daterange .input-group-addon {
+  border-left: none;
+  border-right: none;
+}
+
+div.row-xs {
+  padding: 15px;
+
+  &:nth-of-type(odd) {
+    background-color: $background-primary;
+  }
+}
+
+.time {
+  font-size: 12px;
+  color: $text-color-ternary;
+  margin: 5px 0 0;
+}
+
+/**
+ * Timeline
+ */
+.info-offset-left {
+  margin-left: 50px;
+}
+
+.timeline-header {
+  text-transform: uppercase;
+  font-size: 12px;
+  color: $text-color-ternary;
+}
+
+.timeline {
+  clear: both;
+  list-style: none;
+  padding: 0;
+  margin: 0;
+  font-size: 13px;
+
+  li {
+    padding-bottom: 24px;
+    margin-left: 50px;
+    position: relative;
+
+    &::before {
+      top: 0;
+      bottom: 0;
+      position: absolute;
+      content: " ";
+      width: 1px;
+      background-color: $border-primary;
+      left: -40px;
+      margin-left: 1px;
+    }
+
+    &:first-child::before {
+      top: 10px;
+    }
+
+    &:last-child:before {
+      top: 0;
+      bottom: 0;
+      height: 10px;
+    }
+  }
+
+  .timeline-icon {
+    position: absolute;
+    top: 6px;
+    left: -46px;
+    z-index: 100;
+    margin: 0;
+    padding: 0;
+
+    &::before {
+      content: " ";
+      position: absolute;
+      display: block;
+      width: 15px;
+      height: 15px;
+      border-radius: 50%;
+      border: 3px solid $background-primary;
+      margin: 0;
+      top: 0;
+      left: 0;
+    }
+
+    // Request Colours
+    &.request-created::before {
+      background-color: $background-pending;
+    }
+
+    &.request-revoked::before {
+      background-color: $background-revoked;
+    }
+
+    &.request-approved::before {
+      background-color: $background-approved;
+    }
+
+    &.request-selected::before {
+      background-color: $background-approved;
+    }
+
+    &.request-taken::before {
+      background-color: $background-processing;
+    }
+
+    &.request-forwarded::before {
+      background-color: $background-forwarded;
+    }
+
+    &.request-rejected::before {
+      background-color: $background-rejected;
+    }
+
+    &.request-not-selected::before {
+      background-color: $background-rejected;
+    }
+
+    &.request-notfound::before {
+      background-color: $background-notfound;
+    }
+  }
+
+  .timeline-description {
+    color: $text-color-secondary;
+  }
+
+  .timeline-time {
+    color: $text-color-ternary;
+  }
+}
+
+/**
+ * Table
+ */
+.table-body {
+  border-radius: 4px;
+  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .12);
+}
+
+.table-group {
+  .table-row {
+    border-bottom: 1px solid #e5e5e5;
+    border-left: 1px solid #bfbfc1;
+    border-right: 1px solid #bfbfc1;
+    line-height: 44px;
+    font-size: 15px;
+    position: relative;
+    word-wrap: break-word;
+
+    &:first-child {
+      border-top: 1px solid #bfbfc1;
+    }
+
+    &:first-child,
+    &:first-child a,
+    &:last-child {
+      border-top-right-radius: 4px;
+      border-top-left-radius: 4px;
+    }
+
+    &:last-child {
+      border-bottom: 1px solid $border-primary;
+    }
+  }
+}
+
+/**
+ * Unread indicator as overlay on hamburger menu
+ */
+.unread-indicator {
+  position: absolute;
+  display: block;
+  width: 14px;
+  height: 14px;
+  background-color: $primary;
+  border: 2px solid #333;
+  border-radius: 50%;
+}
+
+.navbar-inverse {
+  background-color: $navbar-background;
+  border-color: $border-inverse;
+  color: $navbar-text;
+
+  .navbar-brand,
+  .navbar-nav li a {
+    color: $navbar-text;
+  }
+}
+
+.navbar-header .unread-indicator {
+  top: 0;
+  right: 0;
+}
+
+.navbar-collapse .unread-indicator {
+  top: 10px;
+  right: 9px;
+}
+
+/**
+ * List group
+ */
+.list-group .item-icon {
+  padding-left: 3em;
+
+  span.icon {
+    position: absolute;
+    top: 25px;
+    left: 1em;
+  }
+}
+
+.course-filter {
+  margin-bottom: 20px;
+
+  .count {
+    float: right;
+    font-weight: bold;
+  }
+}
+
+/**
+ * Truncation
+ */
+.truncate {
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+/**
+ * Icon
+ */
+.icon.unread {
+  width: 12px;
+  height: 12px;
+  color: $text-color-inverse;
+  text-align: center;
+  background-image: -webkit-linear-gradient(lighten($primary, 20%), $primary);
+  background-image: linear-gradient(lighten($primary, 20%), $primary);
+  background-clip: padding-box;
+  border-radius: 50%;
+}
+
+/**
+ * Pagehead
+ */
+.pagehead {
+  position: relative;
+  padding-top: 20px;
+  margin-top: -20px;
+  margin-bottom: 20px;
+  padding-bottom: 0;
+  background-color: $background-primary;
+  border-bottom: 1px solid $border-primary;
+  color: $text-color-primary;
+
+  .nav-tabs {
+    border-bottom: 0;
+  }
+}
+
+/**
+ * Page
+ */
+.page-sub-header {
+  padding-bottom: 9px;
+  margin: 20px 0 20px;
+}
+
+.nav {
+  color: $navbar-text;
+}
+
+.nav-tabs {
+  border-color: $navbar-tabs-border;
+
+  .nav-link {
+    color: $navbar-tabs-color;
+
+    &:hover, &:focus-visible {
+      border-color: $navbar-tabs-border;
+    }
+
+    &.active {
+      color: $navbar-tabs-color;
+      border-color: $navbar-tabs-border;
+      background-color: $navbar-tabs-background;
+    }
+  }
+
+  .fa {
+    margin-right: 5px;
+  }
+}
+
+/**
+ * Lab list
+ */
+.list-group-item {
+  background-color: $background-primary;
+  border-color: $border-primary;
+
+  &.lab-item {
+    line-height: 30px;
+  }
+}
+
+/**
+ * Badge
+ */
+.badge {
+  color: #fff;
+}
+
+/**
+ * Blankslate
+ */
+.blankslate {
+  position: relative;
+  padding: 30px;
+  text-align: center;
+  color: $text-color-primary;
+  background-color: $background-secondary;
+  border: 1px solid $border-primary;
+  border-radius: 3px;
+  box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05);
+
+  h3 {
+    margin-top: 0;
+    margin-bottom: 0;
+    font-size: 1.5em;
+    line-height: 1.43;
+  }
+
+  p {
+    margin: 0;
+  }
+}
+
+.breadcrumbs {
+  padding-top: 1em;
+}
+
+.breadcrumb {
+  background: $background-secondary;
+}
+
+.breadcrumb-item {
+  a {
+    color: $text-color-secondary;
+  }
+
+  &.active {
+    color: $text-color-primary;
+  }
+}
+
+.container-alert {
+  padding-top: 1em;
+}
+
+.page-header {
+  margin-top: 1em;
+  margin-bottom: 1em;
+}
+
+.bootstrap-datetimepicker-widget.dropdown-menu {
+  display: block;
+  margin: 2px 0;
+  padding: 4px;
+  width: 300px !important;
+}
+
+.nav-pills .menu-link.active {
+  background: $navbar-selected !important;
+}
+
+.nav-pills, .show > .nav-pills .nav-link {
+  background: none;
+}
+
+.no-border-top th {
+  border-top: none !important;
+}
+
+.sort-open {
+  display: flex;
+}
+
+.lab-closed {
+  order: 1;
+}
+
+.lab-open {
+  order: 0;
+}
+
+/**
+ * Tables
+ */
+.table {
+  background-color: $background-primary;
+  color: $text-color-primary;
+
+  th, td {
+    border-color: $background-secondary !important;
+  }
+}
+
+/*
+ * Cards
+ */
+.card {
+  border-color: $border-primary;
+
+  .card-header, .card-footer {
+    background-color: $background-primary;
+  }
+
+  .card-body {
+    background-color: $background-secondary;
+  }
+
+  &.queue-info {
+    color: $text-color-queue;
+
+    .card-header, .card-footer {
+      background-color: $background-queue;
+    }
+
+    .card-body {
+      background-color: $background-queue-secondary;
+    }
+  }
+
+  &.location-info {
+    color: $text-color-location;
+
+    .card-header, .card-footer {
+      background-color: $background-location;
+    }
+
+    .card-body {
+      background-color: $background-location-secondary;
+    }
+  }
+
+  &.lab-info {
+    color: $text-color-lab;
+
+    .card-header, .card-footer {
+      background-color: $background-lab;
+    }
+
+    .card-body {
+      background-color: $background-lab-secondary;
+    }
+  }
+
+  &.more-info {
+    color: $text-color-more;
+
+    .card-header, .card-footer {
+      background-color: $background-more;
+    }
+
+    .card-body {
+      background-color: $background-more-secondary;
+    }
+  }
+
+  &.approved {
+    color: $approved-text;
+
+    .card-header, .card-footer {
+      background-color: $approved-primary;
+    }
+
+    .card-body {
+      background-color: $approved-secondary;
+    }
+  }
+
+  &.not-selected {
+    color: $not-selected-text;
+
+    .card-header, .card-footer {
+      background-color: $not-selected-primary;
+    }
+
+    .card-body {
+      background-color: $not-selected-secondary;
+    }
+  }
+}
+
+/**
+ * Nav Tabs
+ */
+
+/**
+ * Hacky solution to broken bootstrap styles when upgrading from 4.0.0 to 4.3.1
+ */
+.btn-buttonface {
+  background-color: buttonface;
+}
+
+/**
+ * Background colours
+ */
+.bg-processing {
+  background-color: $background-processing !important;
+}
+
+.bg-rejected {
+  background-color: $background-rejected !important;
+}
+
+.bg-not-selected, .bg-notselected {
+  background-color: $background-rejected !important;
+}
+
+.bg-pending {
+  background-color: $background-pending !important;
+}
+
+.bg-revoked {
+  background-color: $background-revoked !important;
+}
+
+.bg-approved {
+  background-color: $background-approved !important;
+}
+
+.bg-selected {
+  background-color: $background-approved !important;
+}
+
+.bg-notfound {
+  background-color: $background-notfound !important;
+}
+
+.bg-notpicked {
+  background-color: $background-notpicked !important;
+}
+
+.bg-forwarded {
+  background-color: $background-forwarded !important;
+}
+
+.btn-time-slot {
+  position: relative;
+  min-height: 50px;
+}
+
+.progress-ts {
+  position: absolute;
+  bottom: 0;
+  height: 10px;
+  left: 0;
+  width: 100%;
+}
+
+.table-tst {
+  & {
+    table-layout: fixed;
+  }
+
+  & > thead > tr > td {
+    width: 25%;
+  }
+}
+
+.tst-container {
+  position: relative;
+
+  height: 120px;
+}
+
+.tst-container-start {
+  @extend .empty-content;
+  top: 5%;
+  left: 0;
+  height: 90%;
+  width: 0.25rem;
+
+  border-left-width: 1.5px;
+  border-left-style: solid;
+  border-left-color: #d7d7d7;
+}
+
+.tst-container-end {
+  @extend .empty-content;
+  position: relative !important;
+  top: 5%;
+  left: 0;
+  height: 90%;
+  width: 0.25rem;
+
+  border-right-width: 1.5px;
+  border-right-style: solid;
+  border-right-color: #d7d7d7;
+}
+
+.tst-element {
+  &::after {
+    @extend .empty-content;
+    top: 50%;
+    left: 0;
+    height: 1px;
+    width: 100%;
+  }
+
+  &:not(:first-of-type)::before {
+    @extend .empty-content;
+    top: 30%;
+    left: 0;
+    height: 40%;
+    width: 0.1rem;
+  }
+}
+
+.tst-element-top {
+  position: absolute;
+  left: 5%;
+  top: 2%;
+  width: 90%;
+}
+
+.tst-element-middle {
+  z-index: 1;
+
+  position: absolute;
+  left: 10%;
+  top: 46.5%;
+  height: 10px;
+  width: 80%;
+}
+
+.tst-element-bottom {
+  position: absolute;
+  left: 5%;
+  bottom: 9%;
+  width: 90%;
+}
+
+.empty-content {
+  content: "";
+  position: absolute;
+  background-color: #adadad;
+}
+
+.text-hidden {
+  color: transparent;
+}
+
+.active-filter {
+  background-color: $primary;
+}
+
+dialog {
+  border: 1px solid var(--lt-color-gray-400);
+}
+
+@mixin horizontal-bar($colour) {
+  background-color: $colour;
+  content: '';
+  height: 1px;
+  left: 0;
+  position: absolute;
+  width: 100%;
+}
+
+.tabs {
+  border-bottom: 1px solid var(--primary-grey);
+  display: flex;
+  font-size: var(--regular-size);
+  width: 100%;
+}
+
+.tab {
+  background: none;
+  border: none;
+  color: var(--primary-dark);
+  cursor: pointer;
+  display: block;
+  padding-top: 1.8rem;
+  padding-bottom: 1.2rem;
+  position: relative;
+  text-align: center;
+  text-decoration: none;
+  width: 12.5rem;
+
+  &::after {
+    @include horizontal-bar(var(--primary-blue));
+    bottom: -2px;
+    height: 3px;
+    transform: scaleX(0);
+    z-index: 1;
+  }
+  &.active::after, &:hover::after {
+    transform: scaleX(1);
+  }
+  & + .tab {
+    margin-left: .5rem;
+  }
+}
+
+.hidden {
+  display: none !important;
+}
+
+.table {
+  border-collapse: collapse;
+  font-size: var(--table-size);
+  position: relative;
+  text-align: left;
+  width: 100%;
+
+  --header-height: 1.4rem;
+
+  box-shadow: 0 1px 3px rgb(0 0 0 / 25%);
+
+  & > thead {
+    background-color: #EAEAEA;
+    th, td {
+      border-bottom: 3px solid var(--primary-dark);
+      border-top: 1px solid var(--primary-grey);
+      padding: var(--header-height) 0.5rem var(--header-height) 0;
+    }
+    th:first-child, td:first-child {
+      padding: var(--header-height) 0.5rem var(--header-height) var(--header-height);
+    }
+    th:last-child, td:last-child {
+      padding: var(--header-height) var(--header-height) var(--header-height) 0;
+    }
+  }
+
+  tbody {
+    tr {
+      border-top: 1.5px solid var(--primary-grey);
+      border-radius: 2px;
+      gap: 6px;
+
+      td {
+        vertical-align: initial;
+      }
+
+    }
+  }
+
+  &_actions {
+    display: flex;
+    justify-content: flex-end;
+    > * + * {
+      margin-left: 2rem;
+    }
+    > .icon-button + .icon-button {
+      margin-left: .5rem;
+    }
+  }
+
+  &_cell-with-buttons {
+    display: flex;
+    > span {
+      align-items: center;
+      display: flex;
+    }
+    > * + * {
+      gap: .5rem;
+    }
+  }
+
+  td {
+    padding-right: 0.5rem;
+  }
+  td:first-child {
+    padding: 1rem 0.5rem 1rem var(--header-height);
+  }
+  td:last-child {
+    padding: 1rem var(--header-height) 1rem 0;
+  }
+
+}
+.subtitle + .table {
+  margin-top: 1rem;
+}
+
+.controls {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: flex-start;
+  padding: 0.3rem 0 0.3rem 0;
+
+  &_buttons {
+    margin-left: auto;
+  }
+}
+
+.help-tip {
+  position: absolute;
+  display: inline-block;
+  text-align: center;
+  background-color: var(--primary-blue);
+  border-radius: 50%;
+  width: 24px;
+  height: 24px;
+  font-size: 12px;
+  line-height: 26px;
+  cursor: default;
+  transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
+  margin-top: 0.4rem;
+  margin-left: 0.4rem;
+  outline: none;
+  &:hover, &:focus-visible {
+    cursor: pointer;
+    background-color: #ccc;
+    p {
+      cursor: default;
+      visibility: visible;
+      opacity: 1;
+      transform: scale(1.0);
+    }
+  }
+  .help-icon {
+    font-weight: 700;
+    color: #fff;
+  }
+  p {
+    visibility: hidden;
+    opacity: 0;
+    text-align: left;
+    background-color: var(--primary-blue);
+    padding: 20px;
+    width: 300px;
+    position: absolute;
+    border-radius: 4px;
+    right: -4px;
+    color: #fff;
+    font-size: 13px;
+    line-height: normal;
+    transform: scale(0.7);
+    transform-origin: 100% 0%;
+    transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
+    z-index: 100;
+    &:before {
+      position: absolute;
+      content: '';
+      width: 0;
+      height: 0;
+      border: 6px solid transparent;
+      border-bottom-color: var(--primary-blue);
+      right: 10px;
+      top: -12px;
+    }
+    &::after {
+      width: 100%;
+      height: 40px;
+      content: '';
+      position: absolute;
+      top: -5px;
+      left: 0;
+    }
+  }
+  a {
+    color: #fff;
+    font-weight: 700;
+    &:hover, &:focus-visible {
+      color: #fff;
+      text-decoration: underline;
+    }
+    &:focus, &:focus-visible {
+      color: #fff;
+      text-decoration: underline;
+    }
+  }
+}
+
diff --git a/src/main/resources/archive/history/index.html b/src/main/resources/archive/history/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..14f072e6dcd2c2c502892eeb1054bad53b77fa14
--- /dev/null
+++ b/src/main/resources/archive/history/index.html
@@ -0,0 +1,58 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layout}">
+
+<!--@thymesVar id="requests" type="org.springframework.data.domain.Page<nl.tudelft.queue.dto.view.RequestViewDTO>"-->
+
+<body>
+<section layout:fragment="content">
+    <nav role="navigation" class="breadcrumbs">
+        <ol class="breadcrumb">
+            <li class="breadcrumb-item"><a href="/">Home</a></li>
+            <li class="breadcrumb-item active">History</li>
+        </ol>
+    </nav>
+
+    <div class="page-header">
+        <h1>My Requests</h1>
+    </div>
+</section>
+
+<section layout:fragment="outside-content">
+    <div class="row no-gutters">
+        <div class="col-lg-3 col-xl-2 pl-lg-3 pr-lg-1">
+            <th:block th:replace="request/list/filters :: filters (returnPath=@{/history})">
+            </th:block>
+        </div>
+        <div class="col-lg-9 col-xl-10 pl-lg-1 pr-lg-3">
+            <th:block th:replace="request/list/request-table :: request-table">
+            </th:block>
+        </div>
+    </div>
+
+    <div class="justify-content-center">
+        <th:block th:replace="pagination :: pagination (page=${requests}, size=3)">
+        </th:block>
+    </div>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/templates/history/student.html b/src/main/resources/archive/history/student.html
similarity index 100%
rename from src/main/resources/templates/history/student.html
rename to src/main/resources/archive/history/student.html
diff --git a/src/main/resources/archive/home/about.html b/src/main/resources/archive/home/about.html
new file mode 100644
index 0000000000000000000000000000000000000000..8cd0a24c96b9a3f2e9b6e63d22649709ade67ad5
--- /dev/null
+++ b/src/main/resources/archive/home/about.html
@@ -0,0 +1,94 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}">
+
+<body>
+<section layout:fragment="content">
+    <div class="page-header">
+        <h1> About Queue </h1>
+    </div>
+    <div class="row">
+        <div class="col-sm-12 col-md-6">
+            <p class="h3"> What is Queue? </p>
+            <p>
+                Queue is a system that serves as a replacement for a physical queue. It was originally intended for
+                lab sessions, where students work on their own and can ask questions to student assistants. As the number
+                of students grows, it becomes harder and harder to keep track of who has questions, and who has the first turn.
+
+                In its simplest form, Queue allows students to create a request and allows student assistants to pick up these
+                requests, in order.
+            </p>
+        </div>
+
+        <div class="col-sm-12 col-md-6">
+            <p class="h3"> Why Queue? </p>
+            <p>
+                Queue is a simple to use tool with many advanced capabilities and customisation options included. With the problem
+                of managing and keeping track of questions out of the way, the student assistants can do what they do best: teach.
+            </p>
+        </div>
+    </div>
+
+    <div class="row">
+        <div class="col-sm-12 col-md-6">
+            <p class="h3"> How does Queue work? </p>
+            <p>
+                Queue transforms a physical queue into a virtual one by taking all the requests of the students and putting them
+                in the right order. Student assistants can then, one by one, take those requests and handle them individually.
+                Additionally, (the) Queue is also highly customisable and allows teachers to create labs for specific situations
+                such as exams or feedback sessions.
+            </p>
+        </div>
+
+        <div class="col-sm-12 col-md-6">
+            <p class="h3"> History of Queue </p>
+            <p>
+                Queue started as a literal physical queue that eventually evolved into a management system with a spreadsheet.
+                This short-term solution wasn't the best and thus the idea of Queue was born. Shortly after, a student was hired
+                and Queue 0.1 was not only a dream anymore. After that, a Surf grant helped to further development and the team
+                grew to a size of 8 students.
+            </p>
+        </div>
+    </div>
+
+    <div class="row">
+        <div class="col-sm-12 col-md-6">
+            <p class="h3"> How do I report bugs? </p>
+            <p>
+                Because Queue is an open-source project, one can go to the <a href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue" target="_blank">
+                Queue repository</a>, choose Issues in the menu and open a new issue. There are templates for submitting
+                features and bugs and it should be straightforward from there.
+            </p>
+        </div>
+
+        <div class="col-sm-12 col-md-6">
+            <p class="h3"> How do I contribute? </p>
+            <p>
+                There are two main ways of contributing: working on an open-standing issue or tackling a bug bounty and
+                earn something extra. The Queue contains an <a href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue/-/issues" target="_blank">
+                issues list</a> and a filter for all the <a href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue/-/issues?label_name[]=BugBounty" target="_blank">bug bounties</a>.
+                For reading the rules of contributing check the <a href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue/-/blob/development/CONTRIBUTING.md" target="_blank">CONTRIBUTING.md</a>.
+            </p>
+        </div>
+    </div>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/archive/home/dashboard.html b/src/main/resources/archive/home/dashboard.html
new file mode 100644
index 0000000000000000000000000000000000000000..0a5a9dbd25d8ce91dffd71cb3955319a2f9d713b
--- /dev/null
+++ b/src/main/resources/archive/home/dashboard.html
@@ -0,0 +1,275 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layout}">
+    <head>
+        <link href='/webjars/fullcalendar/main.css' rel='stylesheet' />
+        <script src='/webjars/fullcalendar/main.js'></script>
+        <script src="/webjars/fullcalendar/locales/nl.js"></script>
+    </head>
+
+<!--@thymesVar id="user" type="nl.tudelft.labracore.api.dto.PersonDetailsDTO"-->
+<!--@thymesVar id="editions" type="java.util.Map<java.lang.Long, nl.tudelft.labracore.api.dto.EditionDetailsDTO>"-->
+<!--@thymesVar id="runningEditions" type="java.util.List<nl.tudelft.labracore.api.dto.EditionSummaryDTO>"-->
+
+<!--@thymesVar id="activeRoles" type="java.util.List<nl.tudelft.labracore.api.dto.RoleEditionDetailsDTO>"-->
+<!--@thymesVar id="finishedRoles" type="java.util.List<nl.tudelft.labracore.api.dto.RoleEditionDetailsDTO>"-->
+<!--@thymesVar id="archivedRoles" type="java.util.List<nl.tudelft.labracore.api.dto.RoleEditionDetailsDTO>"-->
+
+<body>
+    <section layout:fragment="content">
+        <nav role="navigation" class="breadcrumbs">
+            <ol class="breadcrumb">
+                <li class="breadcrumb-item active" aria-current="page">Home</li>
+            </ol>
+        </nav>
+
+        <div class="page-header">
+            <h1>Home</h1>
+        </div>
+        <div style="margin-bottom: 1rem" th:if="${@permissionService.isAdminOrTeacher()}">
+            <button class="btn btn-outline-primary" onclick="showDialog('create-shared-edition-dialog')">
+                Create course collection
+            </button>
+            <div tabindex="0" class="help-tip">
+                <div class="help-icon">?</div>
+                <p>A course collection is a collection of courses that acts in the queue as one
+                    course. This can be used for sessions in which multiple courses take place at
+                    the same time. This avoids having to create a session for every
+                    course, as sessions can be shared.</p>
+            </div>
+            <div th:replace="~{shared-edition/create/create-shared-edition :: overlay}"></div>
+        </div>
+
+        <ul class="nav nav-tabs" id="overview-tabs" role="tablist">
+            <li class="nav-item">
+                <a class="nav-link active" id="overview-tab" data-toggle="tab" href="#overview"
+                   role="tab" aria-controls="overview" aria-selected="true">Course overview</a>
+            </li>
+            <li class="nav-item">
+                <a class="nav-link" id="calendar-tab" data-toggle="tab" href="#calendar" role="tab"
+                   aria-controls="calendar" aria-selected="false">Calendar</a>
+            </li>
+            <li class="nav-item" th:if="${@permissionService.isAdmin()}">
+                <a class="nav-link" id="admin-tab" data-toggle="tab" href="#admin" role="tab"
+                   aria-controls="admin" aria-selected="false">Admin</a>
+            </li>
+        </ul>
+
+        <div class="tab-content mt-2">
+            <div class="tab-pane fade" id="calendar" role="tabpanel"
+                 aria-labelledby="calendar-tab">
+                <h2>Calendar</h2>
+                <div id="calendar-view"></div>
+                <script th:inline="javascript">
+                    document.addEventListener('DOMContentLoaded', function () {
+                        let calendarEl = document.getElementById('calendar-view');
+                        let calendar = new FullCalendar.Calendar(calendarEl, {
+                            // plugins: [listPlugin],
+                            headerToolbar: {
+                                left: 'timeGridWeek,timeGridDay,listDay',
+                                center: 'title',
+                                right: 'today prev,next'
+                            },
+                            initialView: 'timeGridWeek',
+                            nowIndicator: true,
+                            businessHours: {
+                                daysOfWeek: [1, 2, 3, 4, 5],
+                                startTime: '08:45',
+                                endTime: '17:45'
+                            },
+                            eventDisplay: 'block',
+                        });
+                        let sessions = /*[[${sessions}]]*/;
+                        sessions.forEach(dto => {
+                            const event = {
+                                title: dto.session.name,
+                                start: dto.session.start,
+                                end: dto.session.end,
+                                description: dto.session.description,
+                                url: "/lab/" + dto.id
+                            }
+                            switch (dto.type) {
+                                case "SLOTTED":
+                                    event['backgroundColor'] = "yellow"
+                                    event['textColor'] = "black"
+                                    calendar.addEvent({
+                                        title: dto.session.name + " slot selection",
+                                        start: dto.selectionOpensAt,
+                                        backgroundColor: "yellow",
+                                        textColor: "black"
+                                    })
+                                    break;
+                                case "EXAM":
+                                    event['backgroundColor'] = "red"
+                                    calendar.addEvent({
+                                        title: dto.session.name + " slot selection",
+                                        start: dto.selectionOpensAt,
+                                        backgroundColor: "red",
+                                    })
+                                    break;
+                            }
+                            calendar.addEvent(event)
+                        })
+                        calendar.render();
+                        $(document).on('shown.bs.tab', 'a[data-toggle="tab"]', function (e) {
+                            calendar.updateSize();
+                        })
+                    })
+                </script>
+            </div>
+
+            <div class="tab-pane fade show active" id="overview" role="tabpanel"
+                 aria-labelledby="overview-tabs">
+                <div class="boxed-group">
+                    <h3>Active courses you participate in</h3>
+                    <div class="boxed-group-inner" th:if="${#lists.isEmpty(activeRoles)}">
+                        You do not participate in any courses. Why don't you <a th:href="@{/editions}">enrol for your first course</a>?
+                    </div>
+
+                    <ul class="list-group">
+                        <li class="list-group-item" th:each="role : ${activeRoles}"
+                            th:with="edition = ${editions.get(role.edition.id)}">
+                            <a th:href="@{/edition/{id}(id=${edition.id})}"
+                               th:text="|${edition.course.name} (${edition.name})|"></a>
+                            <span th:text="${'(' + @roleDTOService.typeDisplayName(role.type.toString()) + ')'}"></span>
+                            <ul th:unless="${#lists.isEmpty(labs)}">
+                                <th:block th:each="sess : ${edition.sessions}">
+                                    <li th:each="lab : ${labs[sess.id]}" th:classappend="${lab.slot.open()} ? 'lab-open' : 'lab-closed'">
+                                        <a href="#" th:href="@{/lab/{id}(id=${lab.id})}"
+                                           th:text="|${lab.name} ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|">
+                                        </a>
+                                        <span th:if="${lab.isShared}" class="badge badge-pill badge-info text-white">Shared lab</span>
+                                        <span class="badge badge-pill badge-info text-white"
+                                              th:if="${lab.slot.open()}">Active</span>
+                                        <span class="badge badge-pill badge-info text-white"
+                                              th:if="${lab.slot.closed()}">Completed</span>
+
+                                        <span class="badge badge-pill badge-danger text-white"
+                                              th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).EXAM}">Exam</span>
+                                        <span class="badge badge-pill badge-info text-white"
+                                              th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).SLOTTED}">Slotted</span>
+                                        <span class="badge badge-pill badge-warning text-white"
+                                              th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).CAPACITY}">Limited Capacity</span>
+
+                                        <span class="badge badge-pill badge-warning text-muted"
+                                              th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">Open for enqueueing</span>
+                                    </li>
+                                </th:block>
+                            </ul>
+                        </li>
+                        <li class="list-group-item" th:each="shared : ${sharedEditions}">
+                            <a th:href="@{/shared-edition/{id}(id=${shared.id})}"
+                               th:text="|${shared.getName()}|"></a>
+                            <span class="badge badge-pill badge-info text-white">
+                                Shared edition
+                            </span>
+                            <ul th:unless="${#lists.isEmpty(labs)}">
+                                    <li th:each="lab : ${sharedLabs[shared.id]}"
+                                        th:classappend="${lab.slot.open()} ? 'lab-open' : 'lab-closed'">
+                                        <a href="#" th:href="@{/lab/{id}(id=${lab.id})}"
+                                           th:text="|${lab.name} ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|">
+                                        </a>
+                                        <span th:if="${lab.isShared}" class="badge badge-pill badge-info text-white">Shared lab</span>
+                                        <span class="badge badge-pill badge-info text-white"
+                                              th:if="${lab.slot.open()}">Active</span>
+                                        <span class="badge badge-pill badge-info text-white"
+                                              th:if="${lab.slot.closed()}">Completed</span>
+
+                                        <span class="badge badge-pill badge-danger text-white"
+                                              th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).EXAM}">Exam</span>
+                                        <span class="badge badge-pill badge-info text-white"
+                                              th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).SLOTTED}">Slotted</span>
+                                        <span class="badge badge-pill badge-warning text-white"
+                                              th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).CAPACITY}">Limited Capacity</span>
+
+                                        <span class="badge badge-pill badge-warning text-muted"
+                                              th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">Open for enqueueing</span>
+                                    </li>
+                            </ul>
+                        </li>
+                    </ul>
+                </div>
+
+                <div class="boxed-group" th:if="${!#lists.isEmpty(finishedRoles)}">
+                    <h3>Finished courses</h3>
+
+                    <ul class="list-group">
+                        <li class="list-group-item" th:each="role : ${finishedRoles}"
+                            th:with="edition = ${editions.get(role.edition.id)}">
+                            <a th:href="@{/edition/{id}(id=${edition.id})}"
+                               th:text="|${edition.course.name} (${edition.name})|"></a>
+                            <span th:text="${'(' + @roleDTOService.typeDisplayName(role.type.toString()) + ')'}"></span>
+                        </li>
+                    </ul>
+                </div>
+
+                <div class="boxed-group" th:if="${!#lists.isEmpty(archivedRoles)}">
+                    <h3>Archived courses</h3>
+
+                    <ul class="list-group">
+                        <th:block th:each="role : ${archivedRoles}" th:with="edition = ${editions.get(role.edition.id)}">
+                            <li class="list-group-item">
+                                <a th:href="@{/edition/{id}(id=${edition.id})}"
+                                   th:text="|${edition.course.name} (${edition.name})|"></a>
+                                <span th:text="${'(' + @roleDTOService.typeDisplayName(role.type.toString()) + ')'}"></span>
+                            </li>
+                        </th:block>
+                    </ul>
+                </div>
+            </div>
+
+            <div class="tab-pane fade" id="admin" role="tabpanel" aria-labelledby="admin-tab"
+                 th:if="${@permissionService.isAdmin()}">
+                <div class="boxed-group" >
+                    <h3>Admin overview - Labs running today:</h3>
+                    <div class="boxed-group-inner" th:if="${#lists.isEmpty(runningLabs)}">
+                        None.
+                    </div>
+
+                    <ul class="list-group">
+                        <li th:each="lab : ${runningLabs}" th:classappend="${lab.slot.open()} ? 'lab-open' : 'lab-closed'" class="list-group-item">
+                            <a href="#" th:href="@{/lab/{id}(id=${lab.id})}"
+                               th:text="|${lab.name} ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|">
+                            </a>
+
+                            <span class="badge badge-pill badge-info text-white"
+                                  th:if="${lab.slot.open()}">Active</span>
+                            <span class="badge badge-pill badge-info text-white"
+                                  th:if="${lab.slot.closed()}">Completed</span>
+
+                            <span class="badge badge-pill badge-danger text-white"
+                                  th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).EXAM}">Exam</span>
+                            <span class="badge badge-pill badge-info text-white"
+                                  th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).SLOTTED}">Slotted</span>
+                            <span class="badge badge-pill badge-warning text-white"
+                                  th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).CAPACITY}">Limited Capacity</span>
+
+                            <span class="badge badge-pill badge-warning text-muted"
+                                  th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">Open for enqueueing</span>
+                        </li>
+                    </ul>
+                </div>
+            </div>
+        </div>
+    </section>
+</body>
+</html>
diff --git a/src/main/resources/archive/home/feedback.html b/src/main/resources/archive/home/feedback.html
new file mode 100644
index 0000000000000000000000000000000000000000..14a34db2ce22fa4340b28517f36e78971e2eeb36
--- /dev/null
+++ b/src/main/resources/archive/home/feedback.html
@@ -0,0 +1,135 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layout}">
+
+<!--@thymesVar id="#authenticatedP" type="nl.tudelft.labracore.lib.security.user.Person"-->
+
+<!--@thymesVar id="assistant" type="nl.tudelft.labracore.api.dto.PersonDetailsDTO"-->
+<!--@thymesVar id="feedback" type="org.springframework.data.domain.Page<nl.tudelft.queue.dto.view.FeedbackViewDTO>"-->
+
+<head>
+    <title th:text="|Feedback for ${assistant.displayName}|"></title>
+
+    <script src="/webjars/chartjs/Chart.min.js"></script>
+</head>
+
+<body>
+<section layout:fragment="content">
+    <div class="page-sub-header">
+        <h3 th:text="|Feedback for ${assistant.displayName}|"></h3>
+    </div>
+
+    <div th:if="${feedback.isEmpty()}">
+        <h4 th:if="${#authenticatedP.id == assistant.id}">
+            You have no feedback yet.
+        </h4>
+        <h4 th:if="${#authenticatedP.id != assistant.id}">
+            There is no feedback for this assistant yet.
+        </h4>
+    </div>
+
+    <div class="row" th:unless="${feedback.isEmpty()}">
+        <div class="col-md-6">
+            <div class="panel panel-default container">
+                <h4 class="mt-2">Feedback comments</h4>
+
+                <blockquote class="blockquote"
+                            th:each="fb : ${feedback}"
+                            th:if="${fb.feedback != null && !fb.feedback.isEmpty()}">
+                    <p th:text="${fb.feedback}"></p>
+                    <footer class="blockquote-footer"
+                            th:if="${fb.rating != null}"
+                            th:unless="${@permissionService.canViewDeanonimizedFeedback()}">
+                        <span th:text="|Feedback of ${fb.rating + '/5'}|"></span>
+                    </footer>
+                    <footer class="blockquote-footer" th:if="${@permissionService.canViewDeanonimizedFeedback()}">
+                        <span>Feedback </span>
+                        <span th:if="${fb.rating != null}" th:text="|of ${fb.rating + '/5'} |"></span>
+                        <span th:text="|by ${fb.groupName} on Request|"></span>
+                        <a th:text="${'#' + fb.request.id}"
+                           th:href="@{/request/{id}(id = ${fb.request.id})}"></a><br>
+                        <span th:text="|Last updated on ${#temporals.format(fb.lastUpdatedAt, 'dd-MM-yyyy')}, created ${#temporals.format(fb.createdAt, 'dd-MM-yyyy')}|"></span>
+                    </footer>
+                </blockquote>
+            </div>
+
+            <th:block th:replace="pagination :: pagination (page=${feedback}, size=3)">
+            </th:block>
+        </div>
+
+        <div class="col-md-6">
+            <div class="container panel panel-default">
+                <h4 class="mt-2">Overview (5 is most positive)</h4>
+
+                <canvas id="feedback-histogram"></canvas>
+
+                <script th:inline="javascript" type="text/javascript">
+                    //<![CDATA[
+                    const stars = /*[[${stars}]]*/ [0, 0, 0, 0, 0]
+
+                    const ctx = document.getElementById("feedback-histogram").getContext('2d');
+                    new Chart(ctx, {
+                        type: 'bar',
+                        data: {
+                            labels: ['1', '2', '3', '4', '5'],
+                            datasets: [{
+                                label: '# of Votes',
+                                data: stars,
+                                backgroundColor: [
+                                    'rgba(255, 99, 132, 0.2)',
+                                    'rgba(54, 162, 235, 0.2)',
+                                    'rgba(255, 206, 86, 0.2)',
+                                    'rgba(75, 192, 192, 0.2)',
+                                    'rgba(153, 102, 255, 0.2)',
+                                    'rgba(255, 159, 64, 0.2)'
+                                ],
+                                borderColor: [
+                                    'rgba(255, 99, 132, 1)',
+                                    'rgba(54, 162, 235, 1)',
+                                    'rgba(255, 206, 86, 1)',
+                                    'rgba(75, 192, 192, 1)',
+                                    'rgba(153, 102, 255, 1)',
+                                    'rgba(255, 159, 64, 1)'
+                                ],
+                                borderWidth: 1
+                            }]
+                        },
+                        options: {
+                            scales: {
+                                yAxes: [{
+                                    ticks: {
+                                        beginAtZero: true,
+                                        precision: 0,
+                                        stepSize: 1
+                                    }
+                                }]
+                            }
+                        }
+                    });
+                    //]]>
+                </script>
+            </div>
+        </div>
+    </div>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/archive/home/index.html b/src/main/resources/archive/home/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..c0786acc19f909ffa73e2cb7998ae441748f63a0
--- /dev/null
+++ b/src/main/resources/archive/home/index.html
@@ -0,0 +1,47 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}">
+
+<body>
+    <section layout:fragment="content">
+
+        <div class="page-header">
+            <h1>Home</h1>
+        </div>
+
+        <th:block th:unless="${@thymeleafConfig.isTheDay()}">
+            Welcome to the TU Delft queue system!<br><br>
+        </th:block>
+
+        <th:block th:if="${@thymeleafConfig.isTheDay()}">
+            Welcome to the TU Delft stack system! First one in is still first one out, have a nice day.<br><br>
+        </th:block>
+
+
+        <h2>How to use this system?</h2>
+
+        After you login, you can <em>enrol</em> for a particular course. Once enrolled you can navigate to a specific
+        lab. If the lab is opened, you can <em>enqueue</em> for a specific assignment. The assistants process this
+        queue. You will receive a notification when an assistant is assigned to your request. The notification
+        instructs you to either visit the TA or wait for the TA to visit you.
+    </section>
+</body>
+</html>
diff --git a/src/main/resources/archive/home/privacy.html b/src/main/resources/archive/home/privacy.html
new file mode 100644
index 0000000000000000000000000000000000000000..c91e75064a404f53bc958e4f4d215fce0fc2080b
--- /dev/null
+++ b/src/main/resources/archive/home/privacy.html
@@ -0,0 +1,145 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}">
+
+<body>
+<section layout:fragment="content">
+    <div class="page-header">
+        <h1>Privacy statement</h1>
+    </div>
+    <p><strong>This privacy statement is a first version. Some aspects in this privacy statement might be changed in the
+            future to be in line with the law and best practices</strong></p>
+    
+    <p>The Queue project is a project from the TU Delft. In this privacy statement, it will be explained what personal data
+        the Queue collects when using the website. In this document the term Labrador will also be mentioned. Labrador is a
+        future project of the TU Delft, which will include the Queue.</p>
+    <h2 id="topics">Topics</h2>
+    <ul>
+        <li>What data do we collect?</li>
+        <li>How do we collect your data?</li>
+        <li>How will we use your data?</li>
+        <li>How will we store your data?</li>
+        <li>Other matters</li>
+    </ul>
+    <h2 id="what-data-do-we-collect">What data do we collect</h2>
+    <p>To support your education at the <th:block th:text="${@instituteProperties.name}"></th:block>,
+        the university has a legal ground to store your data. To be more specific, it is a contractual necessity: the data
+        processing is necessary for the performance of the contract between the student and
+        <th:block th:text="${@instituteProperties.name}"></th:block>.
+        Because of this, the Queue collects the following data:</p>
+    <ul>
+        <li>Name</li>
+        <li>Email</li>
+        <li>Student number</li>
+        <li>NetID</li>
+        <li>affiliation (if the user is a student or teacher)</li>
+        <li>which department/faculty the user is part of</li>
+        <li>all notifications the user has received</li>
+    </ul>
+    <p>For the purpose of providing notifications, we collect some metadata about the device used.</p>
+    <p>Furthermore, there exist multiple subgroups in the system. We will describe for each subgroup the extra data we
+        store.</p>
+    <h3 id="student">Student</h3>
+    <p>We collect the following information about students:</p>
+    <ul>
+        <li>The mentor group they are enrolled in</li>
+        <li>Courses they are enrolled in</li>
+        <li>The groups the student is/was part of</li>
+    </ul>
+    <p>We also collect every request the student makes (asked a question or asked to be signed off). This contains the
+        following items:</p>
+    <ul>
+        <li>The course the student is following</li>
+        <li>The lab the student is participating in</li>
+        <li>The assignment the student wants to hand in or has a question about</li>
+        <li>The room they are sitting in</li>
+        <li>The comment the student made on the request</li>
+        <li>The time the request was made</li>
+        <li>The timeslot the user has chosen if applicable</li>
+        <li>The question the student asked</li>
+        <li>The feedback on the the TA the student has written down</li>
+    </ul>
+    <h3 id="ta">TA</h3>
+    <p>For a TA (teaching assistant), we collect the following data:</p>
+    <ul>
+        <li>The requests handled by the TA</li>
+        <li>The time the request was assigned</li>
+        <li>The time the request was handled by the TA</li>
+    </ul>
+    <p>This data then enables us to see at which labs the TA attended.</p>
+    <p>Furthermore, feedback about the TA is stored.</p>
+    <h3 id="teacher">Teacher</h3>
+    <p>For a teacher, we collect the following data:</p>
+    <ul>
+        <li>all courses the teacher teaches</li>
+    </ul>
+    <h2 id="how-do-we-collect-your-data">How do we collect your data</h2>
+    <p>All data we collect is collected when using the Queue website.</p>
+    
+    <h2 id="how-will-we-use-your-data">How will we use your data</h2>
+    <p>At this moment, we only use the data given to support the functionality the Queue promises, giving students an easy
+        way to ask questions and sign off work and giving teaching staff an easy to use system to support signing off and
+        answering questions.</p>
+    <p>Data is being used on a need to know basis. This means that student-assistants only see, what they need to see to
+        fulfil their duties as teaching assistant and the same for teachers. Only administrators have access to the
+        database.</p>
+    <p>For example, student assistants cannot view each others feedback. Also teaching assistants can only see the name of
+        the student, not the NetID, email or student number of the student.</p>
+    <p>A teacher might export data to use for purposes to calculate final grades or import it into other tools, namely
+        BrightSpace and Osiris. Furthermore, admins are able to run a script to gather aggregate data about lab sessions and
+        specific people, who spent a lot of time with a teaching assistant. If these numbers are a cause for concern, they
+        will be relayed to the study advisor.</p>
+    <p>Teachers will look at the feedback of TA's to evaluate their performance and if action needs to be taken. The teacher
+        is able to determine the student that posted the feedback and is allowed to contact the student to inquire more
+        information.</p>
+    <p>Data from the Queue is is also used for research purposes. This research is done to explore the possibilities of
+        increasing efficiency and quality of the support given to students and teachers. Questions students asked will be
+        stored and used to figure out if there is a way to be able to answer easy questions right from the Queue, without a
+        need for a TA.</p>
+    <p>For this research the following data will be stored:</p>
+    <ul>
+        <li>The user id of the user that asked the question. This will help us to determine if a user asked a question
+            multiple times and if there is a statistically relevant correlation between multiple types of questions. We are
+            storing the user id as we do not need the name of the student</li>
+        <li>The content of the question. This content will be manually sorted through to remove all data that contains
+            revealing information about the user or a student assistant. For instance: "I'm the one with the red hair" or
+            "Yell Jack when looking for me as John, the previous TA couldn't find me"</li>
+    </ul>
+    <p>This data will only be accessible to people doing the experiment and will be responsibly handled in such a way that
+        no unauthorised person or persons can get access to this data.</p>
+    <h2 id="how-do-we-store-your-data">How do we store your data</h2>
+    <p>To comply with GDPR regulations, all data is stored on site at the <th:block th:text="${@instituteProperties.name}"></th:block>.
+        The data stored on our servers will be deleted after 5 years.</p>
+    <p>Access to data is strictly regulated with only select admins having access to the database. All other entities need
+        to use the website itself, where, as mentioned previously, information is provided on a need to know basis.</p>
+    <h1 id="other-matters">Other matters</h1>
+    <p>For matters like:</p>
+    <ul>
+        <li>What are your data protection rights</li>
+        <li>Which cookies we set and why</li>
+    </ul>
+    <p>Please refer to the <a href="https://www.tudelft.nl/privacy-statement/"
+            title="https://www.tudelft.nl/privacy-statement/">Privacy Statement of the TU Delft</a></p>
+    <p>If you have any questions about the privacy policy or want to make use of your rights, please contact <a
+            href="mailto:privacy-tud@tudelft.nl" title="mailto:privacy-tud@tudelft.nl">the following email address</a></p>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/archive/lab/create.html b/src/main/resources/archive/lab/create.html
new file mode 100644
index 0000000000000000000000000000000000000000..e71d7e3589deb060f76be1f7540100b893c205d0
--- /dev/null
+++ b/src/main/resources/archive/lab/create.html
@@ -0,0 +1,73 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{edition/view}">
+<head>
+    <title th:text="|Create Lab for ${'#' + (ec == null ? edition.id : ec.id)}|"></title>
+
+    <script src="/webjars/momentjs/min/moment.min.js"></script>
+
+    <script type="application/javascript"
+            src="/webjars/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.min.js"></script>
+    <link rel="stylesheet" href="/webjars/tempusdominus-bootstrap-4/css/tempusdominus-bootstrap-4.min.css"/>
+
+    <script type="application/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
+    <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"/>
+
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
+    <script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
+</head>
+
+<!--@thymesVar id="ec" type="nl.tudelft.labracore.api.dto.EditionCollectionDetailsDTO"-->
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.labs.LabCreateDTO"-->
+<!--@thymesVar id="lType" type="nl.tudelft.queue.model.enums.QueueSessionType"-->
+
+<body>
+<section layout:fragment="subcontent">
+    <div class="page-header">
+        <h3>Create lab</h3>
+    </div>
+    <form th:with="action = @{/{ou}/{id}/lab/create/{lTy}(id=${ec == null ? edition.id : ec.id}, ou=${ec == null ? 'edition' : 'shared-edition'}, lTy=${lType.name().toLowerCase()})}"
+          th:action="${action}" th:object="${dto}"
+          class="form-horizontal" method="post" id="form">
+
+        <section layout:fragment="general-config"></section>
+
+        <section layout:fragment="slot-config"></section>
+        <section layout:fragment="exam-config"></section>
+        <section layout:fragment="capacity-config"></section>
+
+        <section layout:fragment="advanced-config"></section>
+
+        <div class="form-group form-row">
+            <div class="col-sm-offset-2 col-sm-8">
+                <button type="submit" class="btn btn-primary ctrl-enter-submit">
+                    Create new lab
+                </button>
+                <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script>
+            </div>
+        </div>
+    </form>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/templates/lab/create/capacity.html b/src/main/resources/archive/lab/create/capacity.html
similarity index 100%
rename from src/main/resources/templates/lab/create/capacity.html
rename to src/main/resources/archive/lab/create/capacity.html
diff --git a/src/main/resources/templates/lab/create/components/advanced-lab.html b/src/main/resources/archive/lab/create/components/advanced-lab.html
similarity index 100%
rename from src/main/resources/templates/lab/create/components/advanced-lab.html
rename to src/main/resources/archive/lab/create/components/advanced-lab.html
diff --git a/src/main/resources/templates/lab/create/components/advanced-slotted.html b/src/main/resources/archive/lab/create/components/advanced-slotted.html
similarity index 100%
rename from src/main/resources/templates/lab/create/components/advanced-slotted.html
rename to src/main/resources/archive/lab/create/components/advanced-slotted.html
diff --git a/src/main/resources/templates/lab/create/components/advanced.html b/src/main/resources/archive/lab/create/components/advanced.html
similarity index 100%
rename from src/main/resources/templates/lab/create/components/advanced.html
rename to src/main/resources/archive/lab/create/components/advanced.html
diff --git a/src/main/resources/templates/lab/create/components/advanced/lab.html b/src/main/resources/archive/lab/create/components/advanced/lab.html
similarity index 100%
rename from src/main/resources/templates/lab/create/components/advanced/lab.html
rename to src/main/resources/archive/lab/create/components/advanced/lab.html
diff --git a/src/main/resources/templates/lab/create/components/advanced/session.html b/src/main/resources/archive/lab/create/components/advanced/session.html
similarity index 100%
rename from src/main/resources/templates/lab/create/components/advanced/session.html
rename to src/main/resources/archive/lab/create/components/advanced/session.html
diff --git a/src/main/resources/templates/lab/create/components/advanced/slotted.html b/src/main/resources/archive/lab/create/components/advanced/slotted.html
similarity index 100%
rename from src/main/resources/templates/lab/create/components/advanced/slotted.html
rename to src/main/resources/archive/lab/create/components/advanced/slotted.html
diff --git a/src/main/resources/templates/lab/create/components/lab-general.html b/src/main/resources/archive/lab/create/components/lab-general.html
similarity index 100%
rename from src/main/resources/templates/lab/create/components/lab-general.html
rename to src/main/resources/archive/lab/create/components/lab-general.html
diff --git a/src/main/resources/templates/lab/create/components/session-general.html b/src/main/resources/archive/lab/create/components/session-general.html
similarity index 100%
rename from src/main/resources/templates/lab/create/components/session-general.html
rename to src/main/resources/archive/lab/create/components/session-general.html
diff --git a/src/main/resources/archive/lab/create/exam.html b/src/main/resources/archive/lab/create/exam.html
new file mode 100644
index 0000000000000000000000000000000000000000..cbee1acae5416603bfc85a0ae1e19f2f6fa33573
--- /dev/null
+++ b/src/main/resources/archive/lab/create/exam.html
@@ -0,0 +1,55 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{lab/create/slotted}">
+<head>
+    <title>Create Exam Lab</title>
+</head>
+
+<!--@thymesVar id="ec" type="nl.tudelft.labracore.api.dto.EditionCollectionDetailsDTO"-->
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+<!--@thymesVar id="modules" type="java.util.List<nl.tudelft.labracore.api.dto.ModuleDetailsDTO>"-->
+<!--@thymesVar id="assignments" type="java.util.List<nl.tudelft.labracore.api.dto.AssignmentDetailsDTO>"-->
+<!--@thymesVar id="rooms" type="java.util.List<nl.tudelft.labracore.api.dto.RoomSummaryDTO>"-->
+
+<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.labs.ExamLabCreateDTO"-->
+<!--@thymesVar id="lType" type="nl.tudelft.queue.model.enums.QueueSessionType"-->
+
+<body>
+<th:block th:object="${dto}">
+    <section layout:fragment="exam-config">
+        <h4>Exam</h4>
+        <hr/>
+
+        <div class="form-group form-row">
+            <label for="exam-percentage-input" class="col-sm-2 col-form-label">Percentage for Exam lab</label>
+            <div class="col-sm-4">
+                <input id="exam-percentage-input" th:field="*{examLabConfig.percentage}"
+                       min="10" max="100" type="number" placeholder="15" class="form-control"
+                       required/>
+            </div>
+        </div>
+    </section>
+</th:block>
+
+</body>
+</html>
diff --git a/src/main/resources/templates/lab/create/regular.html b/src/main/resources/archive/lab/create/regular.html
similarity index 100%
rename from src/main/resources/templates/lab/create/regular.html
rename to src/main/resources/archive/lab/create/regular.html
diff --git a/src/main/resources/templates/lab/create/slotted.html b/src/main/resources/archive/lab/create/slotted.html
similarity index 100%
rename from src/main/resources/templates/lab/create/slotted.html
rename to src/main/resources/archive/lab/create/slotted.html
diff --git a/src/main/resources/templates/lab/edit.html b/src/main/resources/archive/lab/edit.html
similarity index 100%
rename from src/main/resources/templates/lab/edit.html
rename to src/main/resources/archive/lab/edit.html
diff --git a/src/main/resources/templates/lab/edit/capacity.html b/src/main/resources/archive/lab/edit/capacity.html
similarity index 100%
rename from src/main/resources/templates/lab/edit/capacity.html
rename to src/main/resources/archive/lab/edit/capacity.html
diff --git a/src/main/resources/templates/lab/edit/components/advanced-lab.html b/src/main/resources/archive/lab/edit/components/advanced-lab.html
similarity index 100%
rename from src/main/resources/templates/lab/edit/components/advanced-lab.html
rename to src/main/resources/archive/lab/edit/components/advanced-lab.html
diff --git a/src/main/resources/templates/lab/edit/components/advanced-slotted.html b/src/main/resources/archive/lab/edit/components/advanced-slotted.html
similarity index 100%
rename from src/main/resources/templates/lab/edit/components/advanced-slotted.html
rename to src/main/resources/archive/lab/edit/components/advanced-slotted.html
diff --git a/src/main/resources/templates/lab/edit/components/advanced.html b/src/main/resources/archive/lab/edit/components/advanced.html
similarity index 100%
rename from src/main/resources/templates/lab/edit/components/advanced.html
rename to src/main/resources/archive/lab/edit/components/advanced.html
diff --git a/src/main/resources/templates/lab/edit/components/advanced/lab.html b/src/main/resources/archive/lab/edit/components/advanced/lab.html
similarity index 100%
rename from src/main/resources/templates/lab/edit/components/advanced/lab.html
rename to src/main/resources/archive/lab/edit/components/advanced/lab.html
diff --git a/src/main/resources/templates/lab/edit/components/advanced/session.html b/src/main/resources/archive/lab/edit/components/advanced/session.html
similarity index 100%
rename from src/main/resources/templates/lab/edit/components/advanced/session.html
rename to src/main/resources/archive/lab/edit/components/advanced/session.html
diff --git a/src/main/resources/templates/lab/edit/components/advanced/slotted.html b/src/main/resources/archive/lab/edit/components/advanced/slotted.html
similarity index 100%
rename from src/main/resources/templates/lab/edit/components/advanced/slotted.html
rename to src/main/resources/archive/lab/edit/components/advanced/slotted.html
diff --git a/src/main/resources/templates/lab/edit/components/lab-general.html b/src/main/resources/archive/lab/edit/components/lab-general.html
similarity index 100%
rename from src/main/resources/templates/lab/edit/components/lab-general.html
rename to src/main/resources/archive/lab/edit/components/lab-general.html
diff --git a/src/main/resources/templates/lab/edit/components/session-general.html b/src/main/resources/archive/lab/edit/components/session-general.html
similarity index 100%
rename from src/main/resources/templates/lab/edit/components/session-general.html
rename to src/main/resources/archive/lab/edit/components/session-general.html
diff --git a/src/main/resources/templates/lab/edit/exam.html b/src/main/resources/archive/lab/edit/exam.html
similarity index 100%
rename from src/main/resources/templates/lab/edit/exam.html
rename to src/main/resources/archive/lab/edit/exam.html
diff --git a/src/main/resources/templates/lab/edit/regular.html b/src/main/resources/archive/lab/edit/regular.html
similarity index 100%
rename from src/main/resources/templates/lab/edit/regular.html
rename to src/main/resources/archive/lab/edit/regular.html
diff --git a/src/main/resources/templates/lab/edit/slotted.html b/src/main/resources/archive/lab/edit/slotted.html
similarity index 100%
rename from src/main/resources/templates/lab/edit/slotted.html
rename to src/main/resources/archive/lab/edit/slotted.html
diff --git a/src/main/resources/archive/lab/enqueue.html b/src/main/resources/archive/lab/enqueue.html
new file mode 100644
index 0000000000000000000000000000000000000000..cc899c729411eb37f2fabb168fcefd5b4490da2c
--- /dev/null
+++ b/src/main/resources/archive/lab/enqueue.html
@@ -0,0 +1,66 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is di•••••••••••stributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{edition/view}">
+
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+<!--@thymesVar id="qSession" type="nl.tudelft.queue.model.QueueSession"-->
+<!--@thymesVar id="request" type="nl.tudelft.queue.dto.create.RequestCreateDTO"-->
+
+<!--@thymesVar id="rooms" type="java.util.List<nl.tudelft.labracore.api.dto.RoomSummaryDTO>"-->
+<!--@thymesVar id="assignments" type="java.util.List<nl.tudelft.labracore.api.dto.AssignmentSummaryDTO>"-->
+<!--@thymesVar id="types" type="java.util.Map<java.lang.Long, java.util.Set<java.lang.String>>"-->
+
+<!--@thymesVar id="rType" type="java.lang.String"-->
+
+<!--@thymesVar id="message" type="java.lang.String"-->
+
+<body>
+<section layout:fragment="subcontent">
+    <script type="text/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
+    <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css" type="text/css"/>
+
+    <div class="page-sub-header">
+        <h3 th:text="${'Session #' + qSession.id}"></h3>
+    </div>
+
+    <div class="alert alert-info mt-md-3" role="alert" th:unless="${#strings.isEmpty(message)}">
+        <span th:text="${message}"></span>
+    </div>
+
+    <form th:action="@{/lab/{id}/enqueue/{rType}(id=${qSession.id}, rType=${rType})}" th:object="${request}" method="post"
+          class="form-horizontal">
+        <th:block layout:fragment="enqueue-body">
+        </th:block>
+
+        <div class="form-group">
+            <div class="col-sm-offset-2 col-sm-10">
+                <button type="submit" name="enqueue" id="enqueue"
+                        class="btn btn-success ctrl-enter-submit">Enqueue
+                </button>
+                <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script>
+            </div>
+        </div>
+    </form>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/templates/lab/enqueue/capacity.html b/src/main/resources/archive/lab/enqueue/capacity.html
similarity index 100%
rename from src/main/resources/templates/lab/enqueue/capacity.html
rename to src/main/resources/archive/lab/enqueue/capacity.html
diff --git a/src/main/resources/templates/lab/enqueue/lab.html b/src/main/resources/archive/lab/enqueue/lab.html
similarity index 100%
rename from src/main/resources/templates/lab/enqueue/lab.html
rename to src/main/resources/archive/lab/enqueue/lab.html
diff --git a/src/main/resources/templates/lab/enqueue/question.html b/src/main/resources/archive/lab/enqueue/question.html
similarity index 100%
rename from src/main/resources/templates/lab/enqueue/question.html
rename to src/main/resources/archive/lab/enqueue/question.html
diff --git a/src/main/resources/templates/lab/question.html b/src/main/resources/archive/lab/question.html
similarity index 100%
rename from src/main/resources/templates/lab/question.html
rename to src/main/resources/archive/lab/question.html
diff --git a/src/main/resources/archive/lab/remove.html b/src/main/resources/archive/lab/remove.html
new file mode 100644
index 0000000000000000000000000000000000000000..7223294af27cf7865ec7da8130505883a9310824
--- /dev/null
+++ b/src/main/resources/archive/lab/remove.html
@@ -0,0 +1,51 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{edition/view}">
+
+<!--@thymesVar id="ec" type="nl.tudelft.labracore.api.dto.EditionCollectionDetailsDTO"-->
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+<!--@thymesVar id="qSession" type="nl.tudelft.queue.model.QueueSession"-->
+
+<body>
+<section layout:fragment="subcontent">
+    <div class="page-sub-header">
+        <h3>Remove lab</h3>
+    </div>
+
+    <form action="#" th:action="@{/lab/{id}/remove(id=${qSession.id})}" th:object="${qSession}"
+          class="form-horizontal"
+          method="post">
+        <input type="hidden" th:field="*{id}"/>
+
+        Are you sure you want to remove <strong th:text="${'session #' + qSession.id}"></strong>?
+
+        <div class="text-center">
+            <button class="btn btn-danger">Delete this session</button>
+            <small>or <a
+                    th:href="@{/{ou}/{id}/labs(id=${ec == null ? edition.id : ec.id}, ou=${ec == null ? 'edition' : 'shared-edition'})}">go
+                back</a></small>
+        </div>
+    </form>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/archive/lab/view.html b/src/main/resources/archive/lab/view.html
new file mode 100644
index 0000000000000000000000000000000000000000..84dcdb866379ddeeff9c89b0ea9a5c92af796944
--- /dev/null
+++ b/src/main/resources/archive/lab/view.html
@@ -0,0 +1,136 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org"
+      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{edition/view}">
+
+<!--@thymesVar id="#authenticatedP" type="nl.tudelft.labracore.api.dto.Person"-->
+
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+<!--@thymesVar id="qSession" type="nl.tudelft.queue.dto.view.QueueSessionViewDTO"-->
+
+<!--@thymesVar id="rooms" type="java.util.List<nl.tudelft.labracore.api.dto.RoomSummaryDTO>"-->
+<!--@thymesVar id="assignments" type="java.util.List<nl.tudelft.labracore.api.dto.AssignmentSummaryDTO>"-->
+<!--@thymesVar id="modules" type="java.util.List<nl.tudelft.labracore.api.dto.ModuleDetailsDTO>"-->
+
+<!--@thymesVar id="requests" type="java.util.List<nl.tudelft.queue.dto.view.RequestViewDTO>"-->
+
+<!--@thymesVar id="current" type="nl.tudelft.queue.dto.view.RequestViewDTO"-->
+<!--@thymesVar id="currentDto" type="nl.tudelft.queue.dto.patch.RequestPatchDTO"-->
+
+<!--@thymesVar id="types" type="java.util.Map<java.lang.Long, java.util.Set<java.lang.String>>"-->
+
+<head>
+    <title th:text="|Lab ${qSession.id}|"></title>
+
+    <script th:inline="javascript" type="text/javascript">
+        //<![CDATA[
+        const labId = /*[[${qSession.id}]]*/ -1;
+        //]]>
+    </script>
+    <script type="text/javascript" src="/js/lab_view.js"></script>
+
+    <th:block th:if="${@permissionService.canManageSession(qSession.data)}">
+        <script type="text/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
+        <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"
+              type="text/css"/>
+    </th:block>
+</head>
+
+<body>
+<th:block layout:fragment="subcontent">
+    <div class="page-sub-header">
+        <div class="float-right">
+            <th:block th:if="${current == null && @permissionService.canEnqueueSelf(qSession.id)}">
+                <a href="#" th:href="@{/lab/{id}/enqueue(id=${qSession.id})}"
+                   class="btn btn-success" style="margin: 0.25em"
+                   th:classappend="${!@permissionService.canEnqueueSelfNow(qSession.id)} ? 'disabled'"
+                    th:disabled="${!@permissionService.canEnqueueSelfNow(qSession.id)}">Enqueue</a>
+            </th:block>
+
+            <div class="row"
+                 th:if="${@permissionService.canManageSession(qSession.data)}">
+                <a class="btn btn-primary" style="margin: 0.25em"
+                   th:href="@{/lab/{id}/export(id=${qSession.id})}" download>Export lab</a>
+                <form class="form-inline"
+                      th:action="@{/lab/{id}/close-enqueue/{bool}(id=${qSession.id}, bool=${!qSession.enqueueClosed})}"
+                      method="post">
+                    <button th:unless="${qSession.enqueueClosed}" class="btn btn-danger" style="margin: 0.25em"
+                            type="submit">Close enqueueing
+                    </button>
+                    <button th:if="${qSession.enqueueClosed}" class="btn btn-success" style="margin: 0.25em"
+                            type="submit">Open enqueueing
+                    </button>
+                </form>
+
+                <a th:href="@{/lab/{id}/edit(id=${qSession.id})}" style="margin: 0.25em" class="btn btn-secondary">
+                    <i class="fa fa-pencil" aria-hidden="true"></i>
+                </a>
+            </div>
+        </div>
+
+        <h3 th:text="'Lab #' + ${qSession.id} + ': ' + ${qSession.session.name}"></h3>
+    </div>
+
+    <div class="row">
+        <th:block th:replace="lab/view/components/selection-result :: selection-result">
+        </th:block>
+    </div>
+
+    <div th:if="${current != null}"
+         class="row">
+        <th:block th:replace="lab/view/components/current-request :: current-request">
+        </th:block>
+
+        <th:block layout:fragment="current-request-edit">
+            <th:block th:replace="lab/view/components/current-request-edit :: current-request-edit">
+            </th:block>
+        </th:block>
+    </div>
+
+    <div class="card-deck mt-3">
+        <th:block layout:fragment="session-info">
+        </th:block>
+    </div>
+
+    <div class="mt-3">
+        <th:block layout:fragment="request-table">
+        </th:block>
+    </div>
+
+    <script src="/js/map_loader.js"></script>
+    <script type="text/javascript">
+        $(function () {
+            const roomId = $('select').val();
+            if (roomId) {
+                updateRequestInfo(roomId);
+            }
+        });
+    </script>
+</th:block>
+
+<th:block layout:fragment="outside-content">
+    <th:block layout:fragment="exam-request-table">
+    </th:block>
+</th:block>
+</body>
+</html>
diff --git a/src/main/resources/templates/lab/view/capacity.html b/src/main/resources/archive/lab/view/capacity.html
similarity index 100%
rename from src/main/resources/templates/lab/view/capacity.html
rename to src/main/resources/archive/lab/view/capacity.html
diff --git a/src/main/resources/templates/lab/view/components/capacity-session-info.html b/src/main/resources/archive/lab/view/components/capacity-session-info.html
similarity index 100%
rename from src/main/resources/templates/lab/view/components/capacity-session-info.html
rename to src/main/resources/archive/lab/view/components/capacity-session-info.html
diff --git a/src/main/resources/templates/lab/view/components/current-request-edit.html b/src/main/resources/archive/lab/view/components/current-request-edit.html
similarity index 100%
rename from src/main/resources/templates/lab/view/components/current-request-edit.html
rename to src/main/resources/archive/lab/view/components/current-request-edit.html
diff --git a/src/main/resources/templates/lab/view/components/current-request.html b/src/main/resources/archive/lab/view/components/current-request.html
similarity index 100%
rename from src/main/resources/templates/lab/view/components/current-request.html
rename to src/main/resources/archive/lab/view/components/current-request.html
diff --git a/src/main/resources/templates/lab/view/components/exam-lab-info.html b/src/main/resources/archive/lab/view/components/exam-lab-info.html
similarity index 100%
rename from src/main/resources/templates/lab/view/components/exam-lab-info.html
rename to src/main/resources/archive/lab/view/components/exam-lab-info.html
diff --git a/src/main/resources/templates/lab/view/components/exam-lab-slots.html b/src/main/resources/archive/lab/view/components/exam-lab-slots.html
similarity index 100%
rename from src/main/resources/templates/lab/view/components/exam-lab-slots.html
rename to src/main/resources/archive/lab/view/components/exam-lab-slots.html
diff --git a/src/main/resources/templates/lab/view/components/full-request-table.html b/src/main/resources/archive/lab/view/components/full-request-table.html
similarity index 100%
rename from src/main/resources/templates/lab/view/components/full-request-table.html
rename to src/main/resources/archive/lab/view/components/full-request-table.html
diff --git a/src/main/resources/templates/lab/view/components/lab-info.html b/src/main/resources/archive/lab/view/components/lab-info.html
similarity index 100%
rename from src/main/resources/templates/lab/view/components/lab-info.html
rename to src/main/resources/archive/lab/view/components/lab-info.html
diff --git a/src/main/resources/templates/lab/view/components/selection-result.html b/src/main/resources/archive/lab/view/components/selection-result.html
similarity index 100%
rename from src/main/resources/templates/lab/view/components/selection-result.html
rename to src/main/resources/archive/lab/view/components/selection-result.html
diff --git a/src/main/resources/templates/lab/view/components/slots-info.html b/src/main/resources/archive/lab/view/components/slots-info.html
similarity index 100%
rename from src/main/resources/templates/lab/view/components/slots-info.html
rename to src/main/resources/archive/lab/view/components/slots-info.html
diff --git a/src/main/resources/templates/lab/view/components/slotted-lab-info.html b/src/main/resources/archive/lab/view/components/slotted-lab-info.html
similarity index 100%
rename from src/main/resources/templates/lab/view/components/slotted-lab-info.html
rename to src/main/resources/archive/lab/view/components/slotted-lab-info.html
diff --git a/src/main/resources/archive/lab/view/exam.html b/src/main/resources/archive/lab/view/exam.html
new file mode 100644
index 0000000000000000000000000000000000000000..79eba6e8ac877e7ab63ae95e6c943af03f4bcf8c
--- /dev/null
+++ b/src/main/resources/archive/lab/view/exam.html
@@ -0,0 +1,48 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{lab/view/lab}">
+
+<!--@thymesVar id="#authenticatedP" type="nl.tudelft.labracore.api.dto.Person"-->
+
+<!--@thymesVar id="qSession" type="nl.tudelft.queue.dto.view.labs.ExamLabViewDTO"-->
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+<!--@thymesVar id="requests" type="java.util.List<nl.tudelft.queue.model.TimeSlot>"-->
+
+<!--@thymesVar id="current" type="nl.tudelft.queue.dto.view.RequestViewDTO"-->
+
+<head>
+    <title th:text="'Exam Lab #' + ${qSession.id}"></title>
+</head>
+
+<body>
+<th:block layout:fragment="additional-lab-info">
+    <th:block th:replace="lab/view/components/exam-lab-info :: exam-lab-info">
+    </th:block>
+</th:block>
+
+<th:block th:if="${@permissionService.canTakeRequest(qSession.id)}" layout:fragment="exam-request-table">
+    <th:block th:replace="lab/view/components/exam-lab-slots :: exam-lab-slots">
+    </th:block>
+</th:block>
+</body>
+</html>
diff --git a/src/main/resources/templates/lab/view/lab.html b/src/main/resources/archive/lab/view/lab.html
similarity index 100%
rename from src/main/resources/templates/lab/view/lab.html
rename to src/main/resources/archive/lab/view/lab.html
diff --git a/src/main/resources/archive/lab/view/regular.html b/src/main/resources/archive/lab/view/regular.html
new file mode 100644
index 0000000000000000000000000000000000000000..4121caae9cfcacbbc5b051057bf84200ee4f4418
--- /dev/null
+++ b/src/main/resources/archive/lab/view/regular.html
@@ -0,0 +1,35 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{lab/view/lab}">
+
+<!--@thymesVar id="qSession" type="nl.tudelft.queue.dto.view.LabViewDTO"-->
+
+<head>
+    <title th:text="'Lab #' + ${qSession.id}"></title>
+</head>
+
+<body>
+<th:block layout:fragment="request-table">
+    <th:block th:replace="lab/view/components/full-request-table :: full-request-table">
+    </th:block>
+</th:block>
+</body>
+</html>
diff --git a/src/main/resources/archive/lab/view/slotted.html b/src/main/resources/archive/lab/view/slotted.html
new file mode 100644
index 0000000000000000000000000000000000000000..85479108743166b35bbbecaab1d866d9cf9e0200
--- /dev/null
+++ b/src/main/resources/archive/lab/view/slotted.html
@@ -0,0 +1,45 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{lab/view/lab}">
+
+<!--@thymesVar id="qSession" type="nl.tudelft.queue.dto.view.labs.SlottedLabViewDTO"-->
+
+<head>
+    <title th:text="'Lab #' + ${qSession.id}"></title>
+</head>
+
+<body>
+<th:block layout:fragment="additional-lab-info">
+    <th:block th:replace="lab/view/components/slotted-lab-info :: slotted-lab-info">
+    </th:block>
+</th:block>
+
+<th:block layout:fragment="request-table">
+    <th:block th:replace="lab/view/components/slots-info :: slots-info">
+    </th:block>
+
+    <div class="mt-3"></div>
+
+    <th:block th:replace="lab/view/components/full-request-table :: full-request-table">
+    </th:block>
+</th:block>
+</body>
+</html>
diff --git a/src/main/resources/static/js/lab_status.js b/src/main/resources/archive/lab_status.js
similarity index 100%
rename from src/main/resources/static/js/lab_status.js
rename to src/main/resources/archive/lab_status.js
diff --git a/src/main/resources/static/js/lab_status_charts.js b/src/main/resources/archive/lab_status_charts.js
similarity index 100%
rename from src/main/resources/static/js/lab_status_charts.js
rename to src/main/resources/archive/lab_status_charts.js
diff --git a/src/main/resources/archive/lab_view.js b/src/main/resources/archive/lab_view.js
new file mode 100644
index 0000000000000000000000000000000000000000..1192b88ea51d2d3f48f5a87d7b693b7a25013b16
--- /dev/null
+++ b/src/main/resources/archive/lab_view.js
@@ -0,0 +1,34 @@
+/*
+ * Queue - A Queueing system that can be used to handle labs in higher education
+ * Copyright (C) 2016-2020  Delft University of Technology
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+"use strict"
+
+/**
+ * Handles the creation of a websocket connection by subscribing to the relevant topics.
+ * @param client The STOMP Client object to connect with.
+ */
+function handleSocketCreation(client) {
+    client.subscribe(`/user/topic/lab/${labId}/position`, msg => {
+        const event = JSON.parse(msg.body);
+
+        if (event["type"] === "position-update") {
+            $("#position").text(event["position"]);
+        } else if (event["type"] === "request-taken") {
+            location.reload();
+        }
+    });
+}
diff --git a/src/main/resources/archive/layout.html b/src/main/resources/archive/layout.html
new file mode 100644
index 0000000000000000000000000000000000000000..7722dc4fd877670ad6e2cd3b31f33a460ab18a19
--- /dev/null
+++ b/src/main/resources/archive/layout.html
@@ -0,0 +1,241 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+<head>
+    <meta charset="utf-8"/>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
+    <meta http-equiv="Content-Language" content="en_GB"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1"/>
+
+    <meta name="description" content="Queue system"/>
+    <meta name="author" content=""/>
+    <meta name="_csrf_header" th:content="${_csrf.headerName}"/>
+    <meta name="_csrf" th:content="${_csrf.token}"/>
+
+    <link rel="icon" href="/favicon.ico"/>
+
+    <title th:unless="${@thymeleafConfig.isTheDay()}">Queue</title>
+    <title th:if="${@thymeleafConfig.isTheDay()}">Stack</title>
+    <title>Queue</title>
+
+    <link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css"/>
+    <link rel="stylesheet" href="/webjars/font-awesome/css/all.css"/>
+    <link rel="stylesheet" href="/webjars/font-awesome/css/v4-shims.css"/>
+    <link rel="stylesheet" type="text/css" href="/css/global.css"/>
+
+    <script src="/webjars/jquery/jquery.min.js"></script>
+    <script src="/webjars/jquery-cookie/jquery.cookie.js"></script>
+    <script src="/webjars/bootstrap/js/bootstrap.bundle.min.js"></script>
+
+	<script src="/webjars/sockjs-client/sockjs.min.js"></script>
+    <script src="/webjars/stomp-websocket/stomp.min.js"></script>
+
+    <script src="/js/global.js"></script>
+
+    <script type="application/javascript">
+        $.ajaxSetup({
+            headers: {
+                "X-CSRF-TOKEN": $("meta[name='_csrf']").attr("content")
+            }
+        });
+    </script>
+
+    <script th:if="${#authenticatedP != null}" th:inline="javascript" type="application/javascript">
+        //<![CDATA[
+        const authenticatedId = /*[[${#authenticatedP.id}]]*/ -1;
+        //]]>
+    </script>
+</head>
+
+<!--@thymesVar id="page" type="java.lang.String"-->
+
+<!--@thymesVar id="#authenticatedP" type="nl.tudelft.labracore.lib.security.user.Person"-->
+
+<body class="d-flex flex-column" style="min-height: 100vh">
+<nav class="navbar navbar-expand-lg navbar-inverse text-white">
+    <!-- Navigation bar with courses, requests, admin, login, request history etc. -->
+    <a class="navbar-brand" href="/" th:unless="${@thymeleafConfig.isTheDay()}">Queue</a>
+    <a class="navbar-brand" href="/" th:if="${@thymeleafConfig.isTheDay()}">Stack</a>
+    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar-nav"
+            aria-controls="navbar-nav" aria-expanded="false" aria-label="Toggle navigation">
+        <i class="fa fa-bars" style="color:white;" aria-hidden="true"></i>
+    </button>
+    <div class="collapse navbar-collapse" id="navbar-nav">
+        <ul class="navbar-nav nav-pills mr-auto mt-2 mt-lg-0">
+            <th:block th:if="${#authenticatedP != null}">
+                <li class="nav-item">
+                    <a class="nav-link"
+                       th:classappend="${page == 'my-courses' ? 'active' : ''}"
+                       th:href="@{/}">My Courses</a></li>
+                <li class="nav-item">
+                    <a class="nav-link"
+                       th:classappend="${page == 'catalog' ? 'active' : ''}"
+                       th:href="@{/editions}">Catalog</a></li>
+                <li class="nav-item" th:if="${@permissionService.canViewRequests()}">
+                    <a class="nav-link"
+                       th:href="@{/requests}"
+                       th:classappend="${page == 'requests' ? 'active' : ''}">Requests</a></li>
+                <li class="nav-item" th:if="${@permissionService.isAdmin()}">
+                    <a class="nav-link"
+                       th:href="@{/admin}"
+                       th:classappend="${page == 'admin' ? 'active' : ''}">Admin</a>
+                </li>
+            </th:block>
+        </ul>
+        <!-- Specifically for logged in users to get to personal pages -->
+        <ul class="navbar-nav">
+            <th:block th:if="${#authenticatedP != null}">
+                <li class="nav-item dropdown">
+                    <a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown"
+                       role="button" aria-haspopup="true" aria-expanded="false"
+                       th:text="${#authenticatedP.displayName}">
+                        <span class="caret"></span>
+                    </a>
+                    <div class="dropdown-menu dropdown-menu-right">
+                        <a class="dropdown-item text-dark" th:href="@{/history}">Request history</a>
+                        <a class="dropdown-item text-dark"
+                           th:href="@{/feedback/{id}(id=${#authenticatedP.id})}"
+                           th:if="${@permissionService.canViewOwnFeedback()}">Feedback</a>
+                        <form th:action="@{/logout}" method="post">
+                            <button class="dropdown-item" type="submit">Logout</button>
+                        </form>
+                    </div>
+                </li>
+            </th:block>
+            <li class="nav-item" th:unless="${#authenticatedP != null}">
+                <a class="nav-link" th:href="@{/login}">Login</a>
+            </li>
+        </ul>
+    </div>
+</nav>
+
+<main class="flex-fill fluid-container">
+    <!-- Announcement banners -->
+    <div class="fluid-container justify-content-center mt-3 mb-3 ml-3 mr-3">
+        <th:block th:each="announcement : ${@announcementRepository.findActiveAnnouncements()}">
+            <div class="alert fade d-none" role="alert" th:data-announcement-id="|alert-${announcement.id}|"
+                 th:styleappend="|color: ${'#' + announcement.hexTextColour()}; background-color: ${'#' + announcement.hexBackgroundColour()};|"
+                 th:classappend="${announcement.isDismissible ? 'alert-dismissible' : ''}">
+                <strong th:text="${announcement.message}"></strong>
+                <button type="button" class="close" data-dismiss="alert" aria-label="Close"
+                        th:if="${announcement.isDismissible}">
+                    <span aria-hidden="true">&times;</span>
+                </button>
+            </div>
+        </th:block>
+    </div>
+
+    <div class="row no-gutters justify-content-center mb-3">
+        <!-- Page content -->
+        <div class="pl-3 pr-3 col-12 col-sm-11 col-md-10 col-lg-9 col-xl-8">
+            <th:block layout:fragment="content" class="content">
+                <p>Page content goes here</p>
+            </th:block>
+        </div>
+    </div>
+    <th:block layout:fragment="outside-content">
+    </th:block>
+    <div class="modal fade" id="sessionExpiredModal" tabindex="-1" role="dialog">
+        <div class="modal-dialog modal-dialog-centered" role="document">
+            <div class="modal-content">
+                <div class="modal-body">
+                    <h3>Your session has expired</h3>
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn btn-primary" data-dismiss="modal">Close
+                    </button>
+                    <a th:href="@{/login}" class="btn btn-primary">Login</a>
+                </div>
+            </div>
+        </div>
+    </div>
+</main>
+
+<footer class="bg-light">
+    <div class="container-fluid">
+        <div class="row no-gutters justify-content-center">
+            <div class="col-12 col-lg-9 col-xl-8">
+                <div class="row justify-content-center">
+                    <div class="d-none d-sm-block col-sm-6">
+                        <p class="text-muted btn mb-0">&copy; Delft University of Technology</p>
+                    </div>
+                    <div class="col-5 col-sm-2">
+                        <a class="text-muted btn" th:href="@{/privacy}">Privacy</a>
+                    </div>
+                    <div class="col-4 col-sm-2">
+                        <a class="text-muted btn" th:href="@{/about}">About</a>
+                    </div>
+                    <div class="col-3 col-sm-2">
+                        <img th:src="@{${@instituteProperties.logo}}"
+                             class="float-right pr-2 mr-0"
+                             height="40px"
+                             alt="Logo"/>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <th:block th:if="${#authenticatedP != null}">
+        <!-- Import push.js -->
+        <script src="/js/push.js"></script>
+        <script th:inline="javascript">
+            /*<![CDATA[*/
+
+                let timeout = /*[[${#httpSession.getMaxInactiveInterval()}]]*/ '10';
+                setTimeout(function() {
+                    $('#sessionExpiredModal').modal('show')
+                }, parseInt(timeout) * 1000)
+
+            /*]]>*/
+        </script>
+    </th:block>
+    <script type="application/javascript">
+        // Initialize the dismissed announcements cookie when it does not yet exist
+        if (typeof $.cookie("dismissed-announcements") === "undefined") {
+            $.cookie("dismissed-announcements", "[]", { path: "/" })
+        }
+
+        // For every alert, check whether it is dismissed. If an alert is not dismissed, show it
+        $(() => {
+            const dismissedAnnouncements = JSON.parse($.cookie("dismissed-announcements"));
+
+            $(".alert").each(function() {
+                const id = $(this).data("announcement-id");
+                if (!dismissedAnnouncements.includes(id)) {
+                    $(this).removeClass("d-none").addClass("show");
+                }
+            });
+        });
+
+        // Register an event listener that adds a dismissed announcement to the dedicated cookie
+        $(".alert").on("closed.bs.alert", function() {
+            const id = $(this).data("announcement-id");
+
+            const dismissedAnnouncements = JSON.parse($.cookie("dismissed-announcements"));
+            dismissedAnnouncements.push(id);
+
+            $.cookie("dismissed-announcements", JSON.stringify(dismissedAnnouncements), { path: "/" });
+        });
+    </script>
+</footer>
+</body>
+</html>
diff --git a/src/main/resources/archive/login.html b/src/main/resources/archive/login.html
new file mode 100644
index 0000000000000000000000000000000000000000..128efa600bbae86661b3ba162c0c75992d039d63
--- /dev/null
+++ b/src/main/resources/archive/login.html
@@ -0,0 +1,63 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}">
+<head>
+</head>
+
+<body>
+<section layout:fragment="content">
+    <div class="page-header">
+        <h1>Login</h1>
+    </div>
+
+    <div class="alert alert-danger" role="alert" th:if="${param.error}">
+        <span>Invalid username and password.</span>
+    </div>
+
+    <div class="alert alert-info" role="alert" th:if="${param.logout}">
+        <span>You have been logged out.</span>
+    </div>
+
+    <form action="#" th:action="@{/perform-login}" class="form-horizontal" method="post" id="login-form">
+        <div class="form-group">
+            <label for="input-username" class="col-sm-2 control-label">Username</label>
+
+            <div class="col-sm-8">
+                <input type="text" id="input-username" name="username" class="form-control" placeholder="Username"/>
+            </div>
+        </div>
+        <div class="form-group">
+            <label for="input-password" class="col-sm-2 control-label">Password</label>
+
+            <div class="col-sm-8">
+                <input type="password" id="input-password" name="password" class="form-control" placeholder="Password"/>
+            </div>
+        </div>
+        <input type="hidden" name="password" value=""/>
+        <div class="form-group">
+            <div class="col-sm-offset-2 col-sm-10">
+                <input type="submit" class="btn btn-primary" value="Login"/>
+            </div>
+        </div>
+    </form>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/static/js/map_loader.js b/src/main/resources/archive/map_loader.js
similarity index 100%
rename from src/main/resources/static/js/map_loader.js
rename to src/main/resources/archive/map_loader.js
diff --git a/src/main/resources/archive/module/create.html b/src/main/resources/archive/module/create.html
new file mode 100644
index 0000000000000000000000000000000000000000..6185fae1159f959de09f38a634bea3f3f77b3505
--- /dev/null
+++ b/src/main/resources/archive/module/create.html
@@ -0,0 +1,64 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{edition/view}">
+<head>
+    <title th:text="|Create Module for ${'#' + edition.id}|"></title>
+
+    <script type="application/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
+    <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"/>
+</head>
+
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.QueueModuleCreateDTO"-->
+
+<body>
+<section layout:fragment="subcontent">
+    <div class="page-header">
+        <h3>Create Module</h3>
+    </div>
+
+    <form th:with="action = @{/edition/{id}/modules/create(id=${edition.id})}"
+          th:action="${action}" th:object="${dto}"
+          class="form-horizontal" method="post">
+        <input type="hidden" name="id" th:value="${edition.id}">
+
+        <div class="form-group form-row">
+            <label for="title-input" class="col-md-2 col-form-label">Name:</label>
+            <div class="col-md-4">
+                <input id="title-input"
+                       type="text" class="form-control"
+                       required="required" th:field="*{name}"
+                       placeholder="Name of the module. For instance: Assignments"/>
+            </div>
+
+            <div class="col-md-6">
+                <button type="submit" class="btn btn-primary ctrl-enter-submit float-right">
+                    Create new module
+                </button>
+                <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script>
+            </div>
+        </div>
+    </form>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/archive/module/remove.html b/src/main/resources/archive/module/remove.html
new file mode 100644
index 0000000000000000000000000000000000000000..e2ad302af292b3b86984c7ea78c14c1ec57da098
--- /dev/null
+++ b/src/main/resources/archive/module/remove.html
@@ -0,0 +1,47 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{edition/view}">
+
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+
+<!--@thymesVar id="_module" type="nl.tudelft.labracore.api.dto.ModuleDetailsDTO"-->
+
+<body>
+<section layout:fragment="subcontent">
+    <div class="page-sub-header">
+        <h3>Remove Module</h3>
+    </div>
+
+    <form th:action="@{/module/{id}/remove(id=${_module.id})}"
+          class="form-horizontal"
+          method="post">
+        Are you sure you want to remove <strong
+            th:text="${'module #' + _module.id + ' (' + _module.name + ')'}"></strong>?
+        <div class="text-center">
+            <button class="btn btn-danger">Delete this Module</button>
+            <small>or <a
+                    th:href="@{/edition/{eId}/modules(eId=${edition.id})}">go back</a></small>
+        </div>
+    </form>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/templates/notification/index.html b/src/main/resources/archive/notification/index.html
similarity index 100%
rename from src/main/resources/templates/notification/index.html
rename to src/main/resources/archive/notification/index.html
diff --git a/src/main/resources/archive/pagination.html b/src/main/resources/archive/pagination.html
new file mode 100644
index 0000000000000000000000000000000000000000..e9ff0a1e557e26b4e498ef5ab927a6ba707c0360
--- /dev/null
+++ b/src/main/resources/archive/pagination.html
@@ -0,0 +1,74 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org">
+
+<!--@thymesVar id="page" type="org.springframework.data.domain.Page<java.lang.Object>"-->
+<!--@thymesVar id="size" type="java.lang.Integer"-->
+<!--@thymesVar id="url" type="java.lang.String"-->
+
+<body>
+<nav th:if="${page.getTotalPages() > 1}" aria-label="Page navigation" th:fragment="pagination (page, size)" th:with="url=${#httpServletRequest.requestURI}">
+    <ul class="pagination">
+        <li th:classappend="${not page.isFirst()} ? '' : 'disabled'" class="page-item">
+            <span th:if="${page.isFirst()}" class="page-link">&laquo;</span>
+
+            <a th:if="${!page.isFirst()}" th:href="@{${url}(page=0, size=${page.getSize()})}" class="page-link">&laquo;</a>
+        </li>
+
+        <th:block th:if="${page.getTotalPages() < size}">
+            <li th:each="pageNumber : ${#numbers.sequence(0, page.getTotalPages()-1)}"
+                th:classappend="${pageNumber == page.getNumber()} ? 'active' : ''" class="page-item">
+                <span th:if="${pageNumber == page.getNumber()}" th:text="${pageNumber+1}" class="page-link">1</span>
+                <a th:if="${pageNumber != page.getNumber()}" th:text="${pageNumber+1}"
+                   th:href="@{${url}(page=${pageNumber}, size=${page.getSize()})}" class="page-link">1</a>
+            </li>
+        </th:block>
+
+        <th:block th:unless="${page.getTotalPages() < size}">
+            <th:block th:if="${page.getNumber() > size}">
+                <li class="page-item"><a href="#" class="page-link">...</a></li>
+            </th:block>
+
+            <th:block th:each="pageNumber : ${#numbers.sequence(page.getNumber() - size + 1, page.getNumber() + size - 1)}">
+                <th:block th:if="${pageNumber >= 0 and pageNumber <= page.getTotalPages()-1}">
+                    <li th:classappend="${pageNumber == page.getNumber()} ? 'active' : ''"  class="page-item">
+                        <span th:if="${pageNumber == page.getNumber()}" th:text="${pageNumber+1}" class="page-link">1</span>
+                        <a th:if="${pageNumber != page.getNumber()}" th:text="${pageNumber+1}"
+                           th:href="@{${url}(page=${pageNumber}, size=${page.getSize()})}" class="page-link">1</a>
+                    </li>
+                </th:block>
+            </th:block>
+
+            <th:block th:if="${page.getNumber() + size <= page.getTotalPages()-1}">
+                <li class="page-item"><a href="#" class="page-link">...</a></li>
+            </th:block>
+        </th:block>
+
+        <li th:classappend="${not page.isLast()} ? '' : 'disabled'" class="page-item">
+            <span th:if="${page.isLast()}" class="page-link">&raquo;</span>
+
+            <a th:if="${!page.isLast()}"
+               th:href="@{${url}(page=${page.getTotalPages()-1}, size=${page.getSize()})}" class="page-link">&raquo;</a>
+        </li>
+    </ul>
+</nav>
+</body>
+</html>
diff --git a/src/main/resources/templates/request/approve.html b/src/main/resources/archive/request/approve.html
similarity index 100%
rename from src/main/resources/templates/request/approve.html
rename to src/main/resources/archive/request/approve.html
diff --git a/src/main/resources/templates/request/forward.html b/src/main/resources/archive/request/forward.html
similarity index 100%
rename from src/main/resources/templates/request/forward.html
rename to src/main/resources/archive/request/forward.html
diff --git a/src/main/resources/archive/request/list.html b/src/main/resources/archive/request/list.html
new file mode 100644
index 0000000000000000000000000000000000000000..9a1e67069eec968122872aef9c834d6ca01a0f8c
--- /dev/null
+++ b/src/main/resources/archive/request/list.html
@@ -0,0 +1,118 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layout}">
+
+<!--@thymesVar id="filter" type="nl.tudelft.queue.dto.util.RequestTableFilterDTO"-->
+
+<!--@thymesVar id="editions" type="java.util.List<nl.tudelft.labracore.api.dto.EditionDetailsDTO>"-->
+<!--@thymesVar id="labs" type="java.util.List<nl.tudelft.queue.dto.view.QueueSessionSummaryDTO>"-->
+<!--@thymesVar id="assignments" type="java.util.List<nl.tudelft.labracore.api.dto.AssignmentSummaryDTO>"-->
+<!--@thymesVar id="rooms" type="java.util.List<nl.tudelft.labracore.api.dto.RoomSummaryDTO>"-->
+<!--@thymesVar id="assistants" type="java.util.List<nl.tudelft.labracore.api.dto.PersonSummaryDTO>"-->
+
+<!--@thymesVar id="requests" type="org.springframework.data.domain.Page<nl.tudelft.queue.dto.view.RequestViewDTO>"-->
+<!--@thymesVar id="requestCounts" type="java.util.Map<java.lang.Long, java.lang.Long>"-->
+
+<head>
+    <title>Requests</title>
+
+    <script type="text/javascript" src="/js/request_table.js"></script>
+    <script type="text/javascript" src="/webjars/handlebars/handlebars.min.js"></script>
+
+    <link th:if="${@thymeleafConfig.isTheDay() && @requestTableService.partakes()}" rel="stylesheet" type="text/css" href="/css/stack.css"/>
+</head>
+
+<body>
+<section layout:fragment="content">
+    <nav role="navigation" class="breadcrumbs">
+        <ol class="breadcrumb">
+            <li class="breadcrumb-item"><a href="/">Home</a></li>
+            <li class="breadcrumb-item active">Requests</li>
+        </ol>
+    </nav>
+
+    <div class="row">
+        <h1 class="col-12">Requests</h1>
+    </div>
+
+    <div class="row mb-2">
+        <div class="col-12 btn-toolbar float-right" role="toolbar">
+            <th:block th:each="lab : ${labs}">
+                <a type="submit" class="btn btn-sm btn-get-next text-white mr-1 float-right"
+                   th:id="|get-next-${lab.id}|"
+                   th:if="${lab.isActiveOrGracePeriod}"
+                   th:classappend="${requestCounts.getOrDefault(lab.id, 0) == 0} ? 'disabled' : ''"
+                   th:href="@{/requests/next/{labId}(labId = ${lab.id})}">
+                    <span th:text="${@thymeleafConfig.isTheDay()} ? 'Pop from' : 'Get next for'"></span>
+                    <th:block th:text="${lab.name}">Lab name</th:block>
+                    <span th:id="|span-${lab.id}|" th:text="|(${requestCounts.getOrDefault(lab.id, 0)})|">(0)</span>
+                </a>
+            </th:block>
+            <a class="btn btn-sm btn-secondary text-white float-right"
+               onclick='location.reload();'>Refresh</a>
+            <form th:if="${@thymeleafConfig.isTheDay() && @requestTableService.partakes()}"
+                  method="post" th:action="@{/requests/ilikemyeyesthankyou}">
+                <button type="submit"
+                        class="btn btn-sm btn-success ml-4">Unstack me please</button>
+            </form>
+            <form th:if="${@thymeleafConfig.isTheDay() && !@requestTableService.partakes()}"
+                  method="post" th:action="@{/requests/iloveallqueuedevelopers}">
+                <button type="submit"
+                        class="btn btn-sm btn-success ml-4">Stack me please</button>
+            </form>
+        </div>
+    </div>
+
+    <form class="form" method="get">
+        <div class="form-row">
+            <input class="form-control col-sm-5 mr-0 mr-sm-2 mb-2 mb-sm-0"
+                   id="pagesize" name="size" type="number"
+                   th:placeholder="${requests.size} + ' requests'"
+                   required/>
+
+            <button type="submit" value="Submit"
+                    class="btn btn-success col-sm-auto">Submit
+            </button>
+        </div>
+    </form>
+</section>
+
+<section layout:fragment="outside-content">
+    <div class="row no-gutters">
+        <div class="col-lg-3 col-xl-2 pl-lg-3 pr-lg-1">
+            <th:block th:replace="request/list/filters :: filters (returnPath='/requests')">
+            </th:block>
+        </div>
+        <div class="col-lg-9 col-xl-10 pl-lg-1 pr-lg-3">
+            <th:block th:replace="request/list/request-table :: request-table">
+            </th:block>
+        </div>
+    </div>
+
+    <div class="justify-content-center">
+        <th:block th:replace="pagination :: pagination (page=${requests}, size=3)">
+        </th:block>
+    </div>
+</section>
+</body>
+</html>
+
diff --git a/src/main/resources/templates/request/list/filters.html b/src/main/resources/archive/request/list/filters.html
similarity index 100%
rename from src/main/resources/templates/request/list/filters.html
rename to src/main/resources/archive/request/list/filters.html
diff --git a/src/main/resources/templates/request/list/request-table.html b/src/main/resources/archive/request/list/request-table.html
similarity index 100%
rename from src/main/resources/templates/request/list/request-table.html
rename to src/main/resources/archive/request/list/request-table.html
diff --git a/src/main/resources/templates/request/reject.html b/src/main/resources/archive/request/reject.html
similarity index 100%
rename from src/main/resources/templates/request/reject.html
rename to src/main/resources/archive/request/reject.html
diff --git a/src/main/resources/templates/request/status.html b/src/main/resources/archive/request/status.html
similarity index 100%
rename from src/main/resources/templates/request/status.html
rename to src/main/resources/archive/request/status.html
diff --git a/src/main/resources/archive/request/view.html b/src/main/resources/archive/request/view.html
new file mode 100644
index 0000000000000000000000000000000000000000..800a77656b39bf1a318975a15204c6912921e307
--- /dev/null
+++ b/src/main/resources/archive/request/view.html
@@ -0,0 +1,132 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layout}">
+
+<!--@thymesVar id="request" type="nl.tudelft.queue.dto.view.RequestViewDTO"-->
+<!--@thymesVar id="prevRequests" type="java.util.List<nl.tudelft.queue.dto.view.RequestViewDTO>"-->
+
+<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+<!--@thymesVar id="assistant" type="nl.tudelft.labracore.api.dto.PersonDetailsDTO"-->
+
+<head>
+    <title th:text="${'Request #' + request.id}"></title>
+
+    <script type="text/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
+    <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css" type="text/css"/>
+</head>
+
+<body>
+<section layout:fragment="~{breadcrumb}">
+    <nav role="navigation" class="breadcrumbs">
+        <ol class="breadcrumb">
+            <li class="breadcrumb-item"><a th:href="@{/}">Home</a></li>
+            <li class="breadcrumb-item"><a th:href="@{/editions}">Courses</a></li>
+            <li class="breadcrumb-item active" th:text="${'Request #' + request.id}"></li>
+        </ol>
+    </nav>
+</section>
+
+<section layout:fragment="content">
+    <th:block layout:fragment="claim-button">
+    </th:block>
+
+    <div class="row">
+        <div class="col-sm-4">
+            <th:block layout:fragment="feedback">
+            </th:block>
+
+            <th:block layout:fragment="history">
+            </th:block>
+        </div>
+
+        <div class="col-sm-8">
+            <th:block layout:fragment="request-info">
+            </th:block>
+        </div>
+
+        <script src="/js/map_loader.js"></script>
+        <script type="text/javascript" th:inline="javascript">
+            const roomId = /*[[${request.room}]]*/ 0;
+            updateRequestInfo(roomId);
+        </script>
+
+        <style>
+            .btn-group.feedback > .btn.focus {
+                transition: .5s;
+                outline: none !important;
+                box-shadow: none;
+            }
+
+            .btn-group.feedback > .btn.active:nth-child(1) {
+                background: #C23A2C !important;
+                border: 1px solid #AD3427 !important;
+            }
+
+            .btn-group.feedback > .btn.active:nth-child(2) {
+                background: #F78921 !important;
+                border: 1px solid #F67C09 !important;
+            }
+
+            .btn-group.feedback > .btn.active:nth-child(3) {
+                background: #EAD234 !important;
+                border: 1px solid #E8CD1D !important;
+            }
+
+            .btn-group.feedback > .btn.active:nth-child(4) {
+                background: #76D142 !important;
+                border: 1px solid #68C931 !important;
+            }
+
+            .btn-group.feedback > .btn.active:nth-child(5) {
+                background: #489B52 !important;
+                border: 1px solid #408949 !important;
+            }
+
+            .btn-group.feedback > .btn:hover:nth-child(1) {
+                background: #C23A2C !important;
+                border: 1px solid #AD3427 !important;
+            }
+
+            .btn-group.feedback > .btn:hover:nth-child(2) {
+                background: #F78921 !important;
+                border: 1px solid #F67C09 !important;
+            }
+
+            .btn-group.feedback > .btn:hover:nth-child(3) {
+                background: #EAD234 !important;
+                border: 1px solid #E8CD1D !important;
+            }
+
+            .btn-group.feedback > .btn:hover:nth-child(4) {
+                background: #76D142 !important;
+                border: 1px solid #68C931 !important;
+            }
+
+            .btn-group.feedback > .btn:hover:nth-child(5) {
+                background: #489B52 !important;
+                border: 1px solid #408949 !important;
+            }
+        </style>
+    </div>
+</section>
+</body>
+</html>
diff --git a/src/main/resources/templates/request/view/components/claim-button.html b/src/main/resources/archive/request/view/components/claim-button.html
similarity index 100%
rename from src/main/resources/templates/request/view/components/claim-button.html
rename to src/main/resources/archive/request/view/components/claim-button.html
diff --git a/src/main/resources/templates/request/view/components/feedback.html b/src/main/resources/archive/request/view/components/feedback.html
similarity index 100%
rename from src/main/resources/templates/request/view/components/feedback.html
rename to src/main/resources/archive/request/view/components/feedback.html
diff --git a/src/main/resources/templates/request/view/components/history.html b/src/main/resources/archive/request/view/components/history.html
similarity index 100%
rename from src/main/resources/templates/request/view/components/history.html
rename to src/main/resources/archive/request/view/components/history.html
diff --git a/src/main/resources/templates/request/view/components/lab-request-info.html b/src/main/resources/archive/request/view/components/lab-request-info.html
similarity index 100%
rename from src/main/resources/templates/request/view/components/lab-request-info.html
rename to src/main/resources/archive/request/view/components/lab-request-info.html
diff --git a/src/main/resources/templates/request/view/components/selection-request-info.html b/src/main/resources/archive/request/view/components/selection-request-info.html
similarity index 100%
rename from src/main/resources/templates/request/view/components/selection-request-info.html
rename to src/main/resources/archive/request/view/components/selection-request-info.html
diff --git a/src/main/resources/archive/request/view/lab.html b/src/main/resources/archive/request/view/lab.html
new file mode 100644
index 0000000000000000000000000000000000000000..08ed3184beb6f95db14558a202765ff8d420ddbf
--- /dev/null
+++ b/src/main/resources/archive/request/view/lab.html
@@ -0,0 +1,45 @@
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{request/view}">
+
+<body>
+<th:block layout:fragment="claim-button">
+    <th:block th:replace="request/view/components/claim-button :: claim-button">
+    </th:block>
+</th:block>
+
+<th:block layout:fragment="feedback">
+    <th:block th:replace="request/view/components/feedback :: feedback">
+    </th:block>
+</th:block>
+
+<th:block layout:fragment="history">
+    <th:block th:replace="request/view/components/history :: history">
+    </th:block>
+</th:block>
+
+<th:block layout:fragment="request-info">
+    <th:block th:replace="request/view/components/lab-request-info :: request-info">
+    </th:block>
+</th:block>
+</body>
+</html>
diff --git a/src/main/resources/templates/request/view/selection.html b/src/main/resources/archive/request/view/selection.html
similarity index 100%
rename from src/main/resources/templates/request/view/selection.html
rename to src/main/resources/archive/request/view/selection.html
diff --git a/src/main/resources/archive/request_table.js b/src/main/resources/archive/request_table.js
new file mode 100644
index 0000000000000000000000000000000000000000..df87bf80b77b1b01a714f1cdecdd6aefb14f4a5a
--- /dev/null
+++ b/src/main/resources/archive/request_table.js
@@ -0,0 +1,219 @@
+/*
+ * Queue - A Queueing system that can be used to handle labs in higher education
+ * Copyright (C) 2016-2020  Delft University of Technology
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Finds the selected elements or the options that are selected.
+ * @param select The select box to find options of.
+ * @returns {*} The selected options as an array.
+ */
+function findSelected(select) {
+    const selected = select.val();
+    if (typeof selected === "undefined" || selected.length === 0) {
+        return select.find("option").map(function () {
+            return this.value;
+        }).toArray();
+    }
+    return selected;
+}
+
+/**
+ * Checks the info of a request to see whether it is conform to the currently active filtering options.
+ * This constant is created by directly applying a function context finding the currently selected options and
+ * creating a function to actually filter an incoming request.
+ * @type {function(any): boolean} A function to check incoming requests on whether they should be displayed.
+ */
+const inFilter = (() => {
+    let selectedLabs;
+    let selectedAssignments;
+    let selectedRooms;
+    let selectedStatuses;
+    let selectedTypes;
+
+    $(() => {
+        // Look up the currently selected filter options
+        selectedLabs = findSelected($("#lab-select"));
+        selectedAssignments = findSelected($("#assignment-select"));
+        selectedRooms = findSelected($("#room-select"));
+        selectedStatuses = findSelected($("#status-select"));
+        selectedTypes = findSelected($("#request-type-select"));
+    });
+
+    // Return a function checking whether an incoming request passes the filters
+    return event => {
+        return selectedLabs.includes(event["labId"].toString()) &&
+            selectedAssignments.includes(event["assignmentId"].toString()) &&
+            selectedRooms.includes(event["roomId"].toString()) &&
+            selectedStatuses.includes(event["status"]) &&
+            selectedTypes.includes(event["requestType"]);
+    }
+}).apply()
+
+/**
+ * Handles the creation of a websocket connection by subscribing to the relevant topics.
+ * @param client The STOMP Client object to connect with.
+ */
+function handleSocketCreation(client) {
+    client.subscribe("/user/topic/request-table", msg => {
+        const event = JSON.parse(msg.body);
+        if (event.type === "request-created" && inFilter(event)) {
+            prependToRequestTable(event);
+            increaseGetNextCounter(event["labId"])
+        } else if (event.type !== "request-created") {
+            updateStatus(event["id"], event["status"]);
+            switch (event.type) {
+                case "request-taken":
+                    decreaseGetNextCounter(event["labId"]);
+                    updateAssigned(event["id"], event["takenBy"]);
+                    break;
+                case "request-revoked":
+                    decreaseGetNextCounter(event["labId"]);
+                    break;
+                case "request-forwarded-to-any":
+                    increaseGetNextCounter(event["labId"]);
+                    break;
+                case "request-forwarded-to-person":
+                    if (event["forwardedTo"] === authenticatedId) {
+                        increaseGetNextCounter(event["labId"]);
+                    }
+                    break;
+                default:
+                    console.log(`Could not classify message from web socket: ${event}`);
+                    return;
+            }
+        }
+    });
+}
+
+/**
+ * Use Handlebars to compile a template for each request and fill in the template directly using the request
+ * info sent through the web socket message.
+ * @param event The event that occured with information on the created request.
+ */
+function prependToRequestTable(event) {
+    // Get the request template and fill it in
+    const source = $("#request-entry-template").html();
+    const template = Handlebars.compile(source);
+    const html = template(event);
+
+    // Add the compiled HTML to the request table with a fade in effect.
+    $(html).hide().prependTo("#request-table tbody").fadeIn();
+}
+
+/**
+ * Updates the status of a request with the given id in the request table to the given status.
+ * @param id {number} The id of the request to update.
+ * @param status {string} The status to update the request to.
+ */
+function updateStatus(id, status) {
+    // Change the status badge to display the new status
+    const statusSelector = selectStatus(id);
+    statusSelector.text(status);
+    statusSelector.removeClass();
+    statusSelector.addClass("badge badge-pill bg-info text-white");
+
+    // Change the background color of the row based on the status
+    const rowSelector = selectRow(id);
+    rowSelector.removeClass();
+    rowSelector.addClass("text-white");
+    rowSelector.addClass(bgColorDict[status]);
+}
+
+/**
+ * Updates the text saying who is currently assigned to a request with the given id.
+ * @param id {number} The id of the request to change.
+ * @param assigned {string} The name of the person who is now assigned to the request.
+ */
+function updateAssigned(id, assigned) {
+    const assignedSelector = selectAssigned(id);
+    assignedSelector.text(assigned);
+}
+
+/**
+ * Decreases the lab get-next button counter and disables it if the counter drops to 0.
+ * @param labId The id of the lab to update the get-next button for.
+ */
+function decreaseGetNextCounter(labId) {
+    const lab = $(`#get-next-${labId}`);
+    const span = $(`#span-${labId}`);
+
+    // language=RegExp
+    let counter = parseInt(span.text().match(/\d+/)[0]);
+    if (counter !== 0) {
+        counter = counter - 1;
+        span.text(`(${counter})`);
+        if (counter === 0) {
+            lab.addClass("disabled");
+            span.hide();
+        }
+    }
+}
+
+/**
+ * Increases the lab get-next button counter and enables it if necessary.
+ * @param labId {number} The id of the lab to update the get-next button for.
+ */
+function increaseGetNextCounter(labId) {
+    const lab = $(`#get-next-${labId}`);
+    const span = $(`#span-${labId}`);
+    const count = parseInt(span.text().match(/\d+/)[0]) + 1;
+
+    lab.removeClass("disabled");
+    span.text(`(${count})`);
+    span.show();
+}
+
+/**
+ * Selects the row with the request with the given id on it.
+ * @param id {number} The id of the request to select.
+ * @returns {*|jQuery.fn.init|jQuery|HTMLElement}
+ */
+function selectRow(id) {
+    return $("#request-" + id);
+}
+
+/**
+ * Selects the status span of the request with the given id.
+ * @param id {number} The id of the request to select.
+ * @returns {*|jQuery.fn.init|jQuery|HTMLElement}
+ */
+function selectStatus(id) {
+    return $("#status-" + id);
+}
+
+/**
+ * Selects the assigned data of the request with the given id.
+ * @param id {number} The id of the request to select.
+ * @returns {*|jQuery.fn.init|jQuery|HTMLElement}
+ */
+function selectAssigned(id) {
+    return $("#assigned-" + id);
+}
+
+/**
+ * A dictionary mapping statuses to a colour.
+ * @type {{REVOKED: string, NOTFOUND: string, FORWARDED: string, PROCESSING: string, PENDING: string, APPROVED: string, REJECTED: string}}
+ */
+bgColorDict = {
+    "PENDING": "bg-pending",
+    "PROCESSING": "bg-processing",
+    "APPROVED": "bg-approved",
+    "REJECTED": "bg-rejected",
+    "FORWARDED": "bg-forwarded",
+    "REVOKED": "bg-revoked",
+    "NOTFOUND": "bg-notfound"
+};
diff --git a/src/main/resources/templates/shared-edition/create/create-shared-edition.html b/src/main/resources/archive/shared-edition/create/create-shared-edition.html
similarity index 100%
rename from src/main/resources/templates/shared-edition/create/create-shared-edition.html
rename to src/main/resources/archive/shared-edition/create/create-shared-edition.html
diff --git a/src/main/resources/archive/shared-edition/view.html b/src/main/resources/archive/shared-edition/view.html
new file mode 100644
index 0000000000000000000000000000000000000000..6ba75a80df706ad97aaff8a645d3d82eb100ae55
--- /dev/null
+++ b/src/main/resources/archive/shared-edition/view.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+      layout:decorate="~{layout}">
+<body>
+    <section layout:fragment="content">
+        <section layout:fragment="breadcrum">
+            <nav role="navigation" class="breadcrumbs">
+                <ol class="breadcrumb">
+                    <li class="breadcrumb-item"><a th:href="@{/}">Home</a></li>
+                    <li class="breadcrumb-item">
+                        <a th:href="@{/}">My Courses</a>
+                    </li>
+                    <li class="breadcrumb-item active" th:text="${collection.getName()}">TI1316</li>
+                </ol>
+            </nav>
+        </section>
+
+        <div class="controls" th:if="${@permissionService.canManageSharedSessions(collection.id)}">
+            <div class="controls_buttons">
+                <button class="btn btn-primary"
+                        onclick="showDialog('create-shared-session-dialog')">Create shared
+                    session</button>
+            </div>
+            <div th:replace="~{shared-edition/view/create-session-dialog :: overlay}"></div>
+        </div>
+
+        <div class="tabs">
+            <button id="sessions-tab" class="active tab" onclick="switchTab('sessions')">Sessions
+            </button>
+            <button id="editions-tab" class="tab" onclick="switchTab('editions')">Editions</button>
+            <button id="staff-tab" class="tab" onclick="switchTab('staff')">Staff</button>
+        </div>
+
+        <div th:replace="~{shared-edition/view/session-list :: tab}"></div>
+
+        <div id="editions" class="hidden">
+            <table class="table">
+                <thead>
+                    <tr>
+                        <th>Course edition</th>
+                        <th>Course code</th>
+                        <th>Teachers</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <tr th:each="edition : ${editions}">
+                        <td><a th:href="@{/edition/{id}(id=${edition.id})}"
+                               th:text="|${edition.course.name} - ${edition.name}|"></a></td>
+                        <td th:text="${edition.course.code}"></td>
+                        <td th:with="teacher = ${editionTeachers[edition.id].toString()}"
+                            th:text="${teacher.substring(1, teacher.length() - 1)}"></td>
+                    </tr>
+                </tbody>
+            </table>
+        </div>
+
+        <div id="staff" class="hidden">
+            <table class="table">
+                <thead>
+                <tr class="table_header">
+                    <th>Name</th>
+                    <th>Role</th>
+                </tr>
+                </thead>
+                <tbody>
+                <tr th:each="instance : ${roles}">
+                    <td th:text="${instance.key.displayName}"></td>
+                    <td>
+                        <ul>
+                            <li th:each="r : ${instance.getValue()}"
+                                th:text="|${r.getType().getValue()} (${@editionCacheManager.getOrThrow(r.id.editionId).course.name})|">
+                            </li>
+                        </ul>
+                    </td>
+                </tr>
+                </tbody>
+            </table>
+        </div>
+
+        <script>
+            let currentTab = "sessions"
+            function switchTab(to) {
+                if (to !== currentTab) {
+                    document.getElementById(to).classList.toggle("hidden");
+                    document.getElementById(currentTab).classList.toggle("hidden");
+                    document.getElementById(to + "-tab").classList.toggle("active");
+                    document.getElementById(currentTab + "-tab").classList.toggle("active");
+                }
+                currentTab = to;
+            }
+        </script>
+    </section>
+</body>
+</html>
diff --git a/src/main/resources/templates/shared-edition/view/create-session-dialog.html b/src/main/resources/archive/shared-edition/view/create-session-dialog.html
similarity index 100%
rename from src/main/resources/templates/shared-edition/view/create-session-dialog.html
rename to src/main/resources/archive/shared-edition/view/create-session-dialog.html
diff --git a/src/main/resources/templates/shared-edition/view/session-list.html b/src/main/resources/archive/shared-edition/view/session-list.html
similarity index 100%
rename from src/main/resources/templates/shared-edition/view/session-list.html
rename to src/main/resources/archive/shared-edition/view/session-list.html
diff --git a/src/main/resources/scss/stack.scss b/src/main/resources/archive/stack.scss
similarity index 100%
rename from src/main/resources/scss/stack.scss
rename to src/main/resources/archive/stack.scss
diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties
index 9d08c83823ad46c58a84dceb4575923832c62708..e35bdf3568b32cab639899fd9c7bd9a554ee861c 100644
--- a/src/main/resources/messages.properties
+++ b/src/main/resources/messages.properties
@@ -20,3 +20,30 @@ queue.slot.order = The slot must open before it closes.
 
 user.username.notFound = The given username could not be found.
 user.username.participates = The user already participates in this course.
+
+role.teacher = Teacher
+role.head_ta = Head TA
+role.ta = TA
+role.student = Student
+role.teacher_ro = Teacher (read only)
+
+lab.type.regular = Regular
+lab.type.exam = Exam
+lab.type.slotted = Slotted
+lab.type.capacity = Limited capacity
+
+request.status.pending = Pending
+request.status.processing = Processing
+request.status.approved = Approved
+request.status.rejected = Rejected
+request.status.forwarded = Forwarded
+request.status.revoked = Revoked
+request.status.selected = Selected
+request.status.notselected = Not selected
+request.status.notfound = Not found
+request.status.notpicked = Not picked
+
+session.selection.procedure.fcfs = First come, first serve
+session.selection.procedure.random = Purely random
+session.selection.procedure.weighted_by_count = Random, but people that got selected less often are prioritised
+session.selection.procedure.weighted_by_least_recent = Random, but people that got selected least recently are prioritised
diff --git a/src/main/resources/scss/_components.scss b/src/main/resources/scss/_components.scss
new file mode 100644
index 0000000000000000000000000000000000000000..8a562942a127ab0cbe16301d0ad6a20b4ab58bef
--- /dev/null
+++ b/src/main/resources/scss/_components.scss
@@ -0,0 +1,1043 @@
+.title {
+    color: var(--title-colour);
+    font-family: var(--title-font);
+    font-size: clamp(var(--font-size-800), 6vw, var(--font-size-900));
+    font-weight: 100;
+}
+
+.subtitle {
+    color: var(--subtitle-colour);
+    font-family: var(--title-font);
+    font-size: clamp(var(--font-size-700), 4vw, var(--font-size-800));
+    font-weight: 100;
+}
+
+.subsubtitle {
+    color: var(--subtitle-colour);
+    font-family: var(--title-font);
+    font-size: clamp(var(--font-size-600), 3vw, var(--font-size-700));
+    font-weight: 100;
+}
+
+.tabs {
+    display: flex;
+    font-size: clamp(var(--font-size-300), 2vw, var(--font-size-500));
+    overflow-x: auto;
+    position: relative;
+
+    &::after {
+        background-color: var(--divider-colour);
+        bottom: 1px;
+        content: "";
+        height: 1px;
+        position: absolute;
+        width: 100%;
+    }
+
+    & > button,
+    & > a {
+        background: none;
+        border: none;
+        color: var(--text-colour);
+        cursor: pointer;
+        display: flex;
+        flex-direction: column;
+        min-width: clamp(8rem, 20vw, 12rem);
+        padding-block: 1rem;
+        position: relative;
+        text-align: center;
+        text-decoration: none;
+
+        &::before {
+            background-color: var(--divider-colour);
+            bottom: 1px;
+            content: "";
+            height: 1px;
+            position: absolute;
+            width: 100%;
+        }
+
+        &:hover::after,
+        &:focus-visible::after,
+        &[data-active="true"]::after {
+            background-color: var(--interactive-background-colour);
+            content: "";
+            bottom: 0;
+            height: 3px;
+            left: 0;
+            position: absolute;
+            width: 100%;
+            z-index: 1;
+        }
+    }
+}
+
+.button {
+    --colour: var(--interactive-background-colour);
+
+    background-color: var(--colour);
+    border: none;
+    border-radius: var(--radius-300);
+    color: var(--interactive-text-colour);
+    cursor: pointer;
+    font-size: var(--font-size-300);
+    text-align: center;
+    text-decoration: none;
+    white-space: nowrap;
+
+    &:hover,
+    &:focus-visible {
+        background-color: var(--interactive-active-background-colour);
+        color: var(--colour);
+    }
+
+    &[data-style="floating"] {
+        box-shadow: var(--shadow-300);
+    }
+    &[data-style="outlined"] {
+        border: 1px solid var(--colour);
+    }
+    &[data-style="text"] {
+        background: none;
+        border: none;
+        color: var(--link-inactive-colour);
+        font-size: var(--font-size-500);
+        &:hover,
+        &:focus-visible {
+            color: var(--link-active-colour);
+        }
+    }
+
+    &[data-type="negative"] {
+        --colour: var(--interactive-negative-background-colour);
+    }
+    &[data-type="positive"] {
+        --colour: var(--interactive-positive-background-colour);
+    }
+
+    &:disabled {
+        background-color: var(--interactive-disabled-background-colour);
+        border: none;
+        cursor: initial;
+        color: var(--interactive-disabled-text-colour);
+    }
+}
+
+.heading {
+    background-color: var(--surface-header-background-colour);
+    color: var(--surface-header-text-colour);
+    font-size: var(--font-size-400);
+    font-weight: bold;
+    padding: 1rem;
+    & button {
+        font-weight: initial;
+    }
+}
+
+.list {
+    & > li {
+        background-color: var(--surface-backgroud-colour);
+        color: var(--surface-text-colour);
+        font-size: var(--font-size-400);
+        padding: 1rem;
+
+        &[data-selectable] {
+            cursor: pointer;
+            &:focus-within,
+            &:hover {
+                background-color: var(--interactive-background-colour);
+                * {
+                    border-color: var(--interactive-text-colour);
+                    color: var(--interactive-text-colour);
+                }
+            }
+        }
+
+        & + li {
+            border-top: 1px solid var(--surface-divider-colour);
+        }
+    }
+}
+
+.link {
+    background: none;
+    border: none;
+    color: var(--link-inactive-colour);
+    cursor: pointer;
+    text-decoration: none;
+    &:hover,
+    &:focus-visible {
+        color: var(--link-active-colour);
+    }
+}
+
+.chip {
+    background: none;
+    border: 1px solid var(--text-secondary-colour);
+    border-radius: var(--radius-300);
+    color: var(--text-secondary-colour);
+    font-size: var(--font-size-200);
+    padding-inline: 0.25rem;
+    white-space: nowrap;
+
+    &[data-type="positive"] {
+        border-color: var(--text-secondary-positive-colour);
+        color: var(--text-secondary-positive-colour);
+    }
+    &[data-type="negative"] {
+        border-color: var(--text-secondary-negative-colour);
+        color: var(--text-secondary-negative-colour);
+    }
+    &[data-type="greyed-out"] {
+        border-color: var(--text-secondary-disabled-colour);
+        color: var(--text-secondary-disabled-colour);
+    }
+    &[data-type="action"] {
+        border-color: var(--link-inactive-colour);
+        color: var(--link-inactive-colour);
+        cursor: pointer;
+        &:hover,
+        &:focus-visible {
+            border-color: var(--link-active-colour);
+            color: var(--link-active-colour);
+        }
+    }
+}
+
+.table {
+    border-collapse: collapse;
+    font-size: var(--font-size-400);
+
+    td,
+    th {
+        padding: 1rem;
+        text-align: left;
+    }
+    td {
+        background-color: var(--surface-backgroud-colour);
+    }
+    tr {
+        padding: 0;
+    }
+    tr + :where(tr) {
+        border-top: 1px solid var(--surface-divider-colour);
+    }
+
+    &__actions {
+        align-items: center;
+        display: flex;
+        font-weight: initial;
+        justify-content: flex-end;
+        gap: 0.5rem;
+    }
+
+    tr[data-selectable] {
+        &:hover,
+        &:focus-within {
+            td {
+                background-color: var(--interactive-background-colour);
+            }
+            * {
+                border-color: var(--interactive-text-colour);
+                color: var(--interactive-text-colour);
+            }
+            cursor: pointer;
+        }
+    }
+
+    &__to_list {
+        @media (max-width: 48em) {
+            td:first-child {
+                padding-bottom: 0.25rem;
+            }
+            td + td {
+                padding-top: 0.25rem;
+            }
+            td:last-child {
+                padding-bottom: 1rem;
+            }
+            .table__actions {
+                justify-content: flex-start;
+            }
+        }
+    }
+
+    &__to_cards {
+        @media (max-width: 48em) {
+            box-shadow: none;
+
+            thead {
+                // Visually hidden
+                left: -100px;
+                max-width: 1px;
+                max-height: 1px;
+                position: absolute;
+                top: -100px;
+            }
+
+            tbody {
+                display: flex;
+                flex-direction: column;
+                gap: 1rem;
+            }
+
+            &.shadow tr {
+                box-shadow: var(--shadow-300);
+            }
+
+            tr {
+                background-color: var(--surface-backgroud-colour);
+                border-top: none;
+                display: flex;
+                flex-direction: column;
+                padding: 1rem;
+            }
+
+            td {
+                padding: 0;
+            }
+
+            td:first-child {
+                font-size: var(--font-size-500);
+            }
+            td:nth-child(2) {
+                color: var(--text-muted-colour);
+                margin-bottom: 1rem;
+            }
+            td:nth-child(3) {
+                margin-bottom: 0.5rem;
+            }
+            .table__actions {
+                align-self: flex-end;
+            }
+        }
+    }
+}
+
+.searchbox {
+    display: flex;
+
+    input {
+        border: none;
+        border-radius: var(--radius-300) 0 0 var(--radius-300);
+        font-size: var(--font-size-300);
+        padding: 0.5rem 1rem;
+        &::placeholder {
+            color: var(--interactive-placeholder-colour);
+            opacity: 100%;
+        }
+    }
+
+    button {
+        aspect-ratio: 1 / 1;
+        border: none;
+        border-radius: 0 var(--radius-300) var(--radius-300) 0;
+        background-color: var(--interactive-background-colour);
+        color: var(--interactive-text-colour);
+        cursor: pointer;
+        flex-shrink: 0;
+        padding: 0.5rem;
+        &:hover,
+        &:focus-visible {
+            background-color: var(--interactive-text-colour);
+            color: var(--interactive-background-colour);
+        }
+    }
+
+    &[data-style="floating"] {
+        box-shadow: var(--shadow-300);
+    }
+}
+
+.textfield {
+    background-color: var(--interactive-active-background-colour);
+    border: none;
+    border-radius: var(--radius-300) 0 0 var(--radius-300);
+    flex-grow: 1;
+    font-size: var(--font-size-300);
+    padding: 0.5rem 1rem;
+    text-align: left;
+
+    &:where(input) {
+        white-space: nowrap;
+    }
+
+    &::placeholder {
+        color: var(--interactive-placeholder-colour);
+        opacity: 100%;
+    }
+
+    &[data-style="floating"] {
+        box-shadow: var(--shadow-300);
+    }
+    &[data-style="outlined"] {
+        border: 1px solid var(--interactive-outline-colour);
+    }
+
+    &[data-show-valid]:invalid {
+        border-color: var(--interactive-invalid-colour);
+    }
+}
+
+.select {
+    display: flex;
+    flex-direction: column;
+    position: relative;
+
+    &__box {
+        display: flex;
+
+        &[data-style="floating"] {
+            box-shadow: var(--shadow-300);
+        }
+        &[data-style="outlined"] {
+            .select__input {
+                border: 1px solid var(--interactive-outline-colour);
+                border-right: none;
+            }
+        }
+    }
+
+    &__input {
+        background-color: var(--surface-backgroud-colour);
+        border: none;
+        border-radius: var(--radius-300) 0 0 var(--radius-300);
+        flex-grow: 1;
+        font-size: var(--font-size-300);
+        padding: 0.5rem 1rem;
+        text-align: left;
+        white-space: nowrap;
+        &[data-placeholder="true"] {
+            color: var(--interactive-placeholder-colour);
+        }
+    }
+
+    &__button {
+        aspect-ratio: 1 / 1;
+        background-color: var(--interactive-background-colour);
+        border: none;
+        border-radius: 0 var(--radius-300) var(--radius-300) 0;
+        color: var(--interactive-text-colour);
+        flex-shrink: 0;
+        padding: 0.5rem;
+    }
+    &:hover,
+    &:focus-visible,
+    &:focus-within {
+        .select__button {
+            background-color: var(--interactive-active-background-colour);
+            color: var(--interactive-active-text-colour);
+        }
+        [data-style="outlined"] {
+            .select__button {
+                border: 1px solid var(--interactive-outline-colour);
+                border-left: none;
+            }
+        }
+    }
+
+    &__options {
+        box-shadow: var(--shadow-400);
+        display: flex;
+        flex-direction: column;
+        font-size: var(--font-size-300);
+        left: 0;
+        max-height: 20rem;
+        position: absolute;
+        overflow: scroll;
+        top: 2.5rem;
+        transform: scaleY(0);
+        transform-origin: top;
+        transition: transform 150ms ease-in-out;
+        z-index: 10;
+        &:focus-within {
+            transform: scaleY(1);
+        }
+
+        & > * {
+            background-color: var(--surface-backgroud-colour);
+            border: none;
+            text-align: left;
+            padding: 0.5rem 1rem;
+            white-space: nowrap;
+            &:hover,
+            &:focus {
+                background-color: var(--interactive-background-colour);
+                color: var(--interactive-text-colour);
+            }
+            &:first-child {
+                border-radius: var(--radius-300) var(--radius-300) 0 0;
+            }
+            &:last-child {
+                border-radius: 0 0 var(--radius-300) var(--radius-300);
+            }
+        }
+    }
+    &[data-multiple] &__options > *[data-selected]::after {
+        font-weight: bold;
+        content: "✓";
+        margin-left: 0.5rem;
+    }
+
+    select[data-show-valid]:invalid + .select__box * {
+        border-color: var(--interactive-invalid-colour);
+    }
+}
+
+.pagination {
+    align-self: flex-start;
+    border-radius: var(--radius-300);
+    box-shadow: var(--shadow-300);
+    font-size: var(--font-size-300);
+
+    & > ul {
+        display: flex;
+    }
+
+    .page__link {
+        aspect-ratio: 1 / 1;
+        background-color: var(--interactive-text-colour);
+        border: none;
+        color: var(--interactive-background-colour);
+        cursor: pointer;
+        display: inline-block;
+        min-height: 1.5rem;
+        min-width: 1.5rem;
+        padding: 0.25rem;
+        text-align: center;
+        text-decoration: none;
+
+        &:hover,
+        &:focus-visible {
+            background-color: var(--interactive-background-colour);
+            color: var(--interactive-text-colour);
+        }
+    }
+
+    .page__item {
+        &:first-child .page__link {
+            border-radius: var(--radius-300) 0 0 var(--radius-300);
+        }
+        &:last-child .page__link {
+            border-radius: 0 var(--radius-300) var(--radius-300) 0;
+        }
+    }
+
+    .page__item:where(:not(:hover, :focus-within, [data-active="true"]), [data-disabled="true"])
+        + .page__item:where(:not(:hover, :focus-within, [data-active="true"]), [data-disabled="true"]) {
+        position: relative;
+        &::after {
+            background-color: var(--surface-divider-colour);
+            content: "";
+            height: 100%;
+            left: 0;
+            position: absolute;
+            top: 0;
+            width: 1px;
+        }
+    }
+
+    .page__item[data-active="true"] {
+        .page__link {
+            background-color: var(--interactive-active-text-colour);
+            color: var(--interactive-active-background-colour);
+        }
+    }
+
+    .page__item[data-disabled="true"] {
+        .page__link {
+            background-color: var(--interactive-disabled-text-colour);
+            color: var(--interactive-disabled-background-colour);
+            cursor: initial;
+        }
+    }
+}
+
+dialog {
+    .dialog {
+        background-color: var(--dialog-background-colour);
+        border-radius: var(--radius-300);
+        color: var(--text-colour);
+        display: flex;
+        flex-direction: column;
+        gap: 1rem;
+        left: 50%;
+        min-width: min(100vw, 20rem);
+        padding: 2rem;
+        position: absolute;
+        top: 50%;
+        transform: translate(-50%, -50%);
+    }
+    background: none;
+    border: none;
+    min-height: 100vh;
+    min-width: 100vw;
+
+    &:not([open]) {
+        display: none;
+    }
+
+    &::backdrop {
+        background-color: rgba(0 0 0 / 25%);
+    }
+
+    .dialog__title :where(h1, h2, h3, h4, h5) {
+        color: var(--title-colour);
+        font-family: var(--title-font);
+        font-size: var(--font-size-700);
+        font-weight: 100;
+    }
+
+    @media (max-width: 48em) {
+        .dialog {
+            left: 0;
+            min-height: 100%;
+            min-width: 100%;
+            padding: 0;
+            top: 0;
+            transform: none;
+        }
+
+        .dialog__title {
+            align-items: center;
+            background-color: var(--header-background-colour);
+            color: var(--header-text-colour);
+            display: flex;
+            justify-content: space-between;
+            padding: 1rem;
+
+            & :where(h1, h2, h3, h4, h5) {
+                color: inherit;
+                font-family: var(--body-font);
+                font-weight: normal;
+            }
+
+            button {
+                background: none;
+                border: none;
+                color: var(--header-text-colour);
+                cursor: pointer;
+                &:hover,
+                &:focus-visible {
+                    text-decoration: underline;
+                }
+            }
+        }
+
+        .dialog__body {
+            padding: 1rem 2rem;
+        }
+    }
+}
+
+.label {
+    color: var(--text-muted-colour);
+    font-size: var(--font-size-400);
+}
+
+.form__label {
+    color: var(--text-muted-colour);
+    font-size: var(--font-size-300);
+}
+
+.nav_list {
+    --align: left;
+    --width: 15rem;
+
+    display: flex;
+    flex-direction: column;
+    font-size: var(--font-size-300);
+    min-width: var(--width);
+
+    & button,
+    & a {
+        background-color: var(--interactive-text-colour);
+        border: none;
+        color: var(--interactive-background-colour);
+        cursor: pointer;
+        padding: 1rem;
+        min-width: 100%;
+        text-align: var(--align);
+
+        &:hover,
+        &:focus-visible {
+            background-color: var(--interactive-active-text-colour);
+            color: var(--interactive-active-background-colour);
+        }
+
+        &:where([data-valid="false"]) {
+            color: var(--interactive-invalid-colour);
+            &:hover,
+            &:focus-visible {
+                background-color: var(--interactive-invalid-colour);
+            }
+        }
+
+        &:disabled {
+            background-color: var(--interactive-disabled-text-colour);
+            color: var(--interactive-disabled-background-colour);
+            cursor: initial;
+        }
+    }
+    & > [data-active="true"] > :where(button, a) {
+        background-color: var(--interactive-active-text-colour);
+        color: var(--interactive-active-background-colour);
+    }
+
+    & > :first-child > :where(button, a) {
+        border-radius: var(--radius-300) var(--radius-300) 0 0;
+    }
+    & > :last-child > :where(button, a) {
+        border-radius: 0 0 var(--radius-300) var(--radius-300);
+    }
+
+    :where(:not(:hover, :focus-visible, [data-active="true"])
+            + :where(:not(:hover, :focus-visible, [data-active="true"])), :disabled) {
+        position: relative;
+        &::before {
+            background-color: var(--surface-divider-colour);
+            content: "";
+            height: 1px;
+            left: 0;
+            position: absolute;
+            top: 0;
+            width: 100%;
+        }
+    }
+
+    &[data-style="floating"] {
+        box-shadow: var(--shadow-300);
+    }
+    &[data-style="outlined"] {
+        border: 1px solid var(--interactive-outline-colour);
+    }
+
+    &[data-direction="horizontal"] {
+        & button,
+        & a {
+            padding: 0.5rem 1rem;
+        }
+
+        @media (min-width: 48.001rem) {
+            flex-direction: row;
+
+            & *::before {
+                height: 100%;
+                width: 1px;
+            }
+
+            & button,
+            & a {
+                min-width: initial;
+                padding: 0.5rem 1rem;
+            }
+
+            & > :first-child > :where(button, a) {
+                border-radius: var(--radius-300) 0 0 var(--radius-300);
+            }
+            & > :last-child > :where(button, a) {
+                border-radius: 0 var(--radius-300) var(--radius-300) 0;
+            }
+        }
+    }
+}
+
+.info_card {
+    background-color: var(--surface-backgroud-colour);
+    font-size: var(--font-size-400);
+}
+
+.banner {
+    align-items: center;
+    animation: banner-load 250ms ease-out;
+    border-radius: var(--radius-300);
+    box-shadow: var(--shadow-300);
+    color: var(--banner-text-colour);
+    display: flex;
+    gap: 0.5rem;
+    font-size: var(--font-size-300);
+    font-weight: bold;
+    padding: 0.75rem 1rem;
+    transform-origin: top left;
+
+    .fa-solid {
+        font-size: var(--font-size-500);
+    }
+
+    &[data-type="info"] {
+        background-color: var(--banner-info-colour);
+    }
+    &[data-type="warning"] {
+        background-color: var(--banner-warning-colour);
+    }
+    &[data-type="error"] {
+        background-color: var(--banner-negative-colour);
+    }
+    &[data-type="check"] {
+        background-color: var(--banner-positive-colour);
+    }
+
+    & [data-remove-banner] {
+        border: none;
+        background: none;
+        color: var(--banner-text-colour);
+        cursor: pointer;
+    }
+}
+
+@keyframes banner-load {
+    0% {
+        transform: scaleY(0);
+    }
+    100% {
+        transform: scaleY(1);
+    }
+}
+
+.tooltip {
+    position: relative;
+    button {
+        border: none;
+        background: none;
+        color: var(--interactive-background-colour);
+        cursor: pointer;
+    }
+    p {
+        background-color: var(--surface-backgroud-colour);
+        box-shadow: var(--shadow-400);
+        left: 0;
+        max-width: 100ch;
+        padding: 0.5rem;
+        position: absolute;
+        top: 1.75rem;
+        transition: transform 150ms ease-in-out;
+        transform: scale(0);
+        transform-origin: top left;
+        z-index: 10;
+    }
+    button:where(:hover, :focus) + p {
+        transform: scale(1);
+    }
+}
+
+.file {
+    &__box {
+        display: flex;
+
+        &[data-style="floating"] {
+            box-shadow: var(--shadow-300);
+        }
+
+        &[data-style="outlined"] {
+            .file__input {
+                border: 1px solid var(--interactive-outline-colour);
+                border-right: none;
+            }
+        }
+    }
+
+    &__input {
+        background-color: var(--surface-backgroud-colour);
+        border: none;
+        border-radius: var(--radius-300) 0 0 var(--radius-300);
+        cursor: pointer;
+        flex-grow: 1;
+        font-size: var(--font-size-300);
+        padding: 0.5rem 1rem;
+        text-align: left;
+        white-space: nowrap;
+
+        &[data-placeholder="true"] {
+            color: var(--interactive-placeholder-colour);
+        }
+    }
+
+    &__button {
+        aspect-ratio: 1 / 1;
+        background-color: var(--interactive-background-colour);
+        border: none;
+        border-radius: 0 var(--radius-300) var(--radius-300) 0;
+        color: var(--interactive-text-colour);
+        cursor: pointer;
+        flex-shrink: 0;
+        padding: 0.5rem;
+    }
+
+    &__box:hover,
+    &__box:focus-visible,
+    &__box:focus-within {
+        .file__button {
+            background-color: var(--interactive-active-background-colour);
+            color: var(--interactive-active-text-colour);
+        }
+
+        &[data-style="outlined"] {
+            .file__button {
+                border: 1px solid var(--interactive-outline-colour);
+                border-left: none;
+            }
+        }
+    }
+
+    input[data-show-valid][type="file"]:invalid + .file__box * {
+        border-color: var(--interactive-invalid-colour);
+    }
+}
+
+.breadcrumbs {
+    font-size: var(--font-size-400);
+    margin-bottom: 1rem;
+    a {
+        color: var(--link-inactive-colour);
+        text-decoration: none;
+        &:hover,
+        &:focus-visible {
+            color: var(--link-active-colour);
+        }
+    }
+    span {
+        color: var(--text-muted-colour);
+    }
+}
+
+#toasts {
+    align-items: flex-end;
+    bottom: 1rem;
+    display: flex;
+    flex-direction: column;
+    gap: 1rem;
+    right: 1rem;
+    position: fixed;
+}
+
+.toast {
+    align-items: center;
+    animation: toast 400ms cubic-bezier(0, 0, 1, 1.5);
+    border-radius: var(--radius-300);
+    box-shadow: var(--shadow-400);
+    color: var(--banner-text-colour);
+    display: flex;
+    gap: 1rem;
+    font-size: var(--font-size-500);
+    max-width: 30rem;
+    padding: 1.5rem;
+    text-decoration: none;
+
+    &[data-disappear] {
+        transition: transform 500ms ease-in;
+        transform: translateX(200%);
+    }
+
+    .fa-solid {
+        font-size: var(--font-size-700);
+    }
+
+    &[data-type="info"] {
+        background-color: var(--banner-info-colour);
+    }
+    &[data-type="warning"] {
+        background-color: var(--banner-warning-colour);
+    }
+    &[data-type="error"] {
+        background-color: var(--banner-negative-colour);
+    }
+    &[data-type="check"] {
+        background-color: var(--banner-positive-colour);
+    }
+
+    @media (max-width: 30rem) {
+        min-width: calc(100vw - 2rem);
+        max-width: calc(100vw - 2rem);
+    }
+}
+
+@keyframes toast {
+    0% {
+        transform: translateY(calc(100% + 1rem));
+    }
+    100% {
+        transform: translateY(0);
+    }
+}
+
+@keyframes toast_disappear {
+    0% {
+        transform: translateX(0);
+    }
+    100% {
+        transform: translateX(200%);
+    }
+}
+
+.jump_to_content {
+    background-color: white;
+    border-radius: var(--radius-300);
+    box-shadow: var(--shadow-400);
+    left: 1rem;
+    padding: 1rem;
+    position: fixed;
+    top: -100%;
+
+    &:focus {
+        top: 1rem;
+    }
+}
+
+.enumeration {
+    & > li {
+        margin-left: 1.5rem;
+    }
+}
+
+.history {
+    li[data-type="neutral"]::marker {
+        color: var(--interactive-background-colour);
+    }
+    li[data-type="negative"]::marker {
+        color: var(--interactive-negative-background-colour);
+    }
+    li[data-type="positive"]::marker {
+        color: var(--interactive-positive-background-colour);
+    }
+    li[data-type="greyed-out"]::marker {
+        color: var(--text-muted-colour);
+    }
+}
+
+.stars {
+    align-self: flex-start;
+    display: flex;
+    gap: 0;
+
+    button {
+        background: none;
+        border: none;
+        color: #ebb345;
+        cursor: pointer;
+        font-size: var(--font-size-600);
+    }
+
+    :where([data-filled="true"]) .fa-regular {
+        display: none;
+    }
+    :where([data-filled="false"]) .fa-solid {
+        display: none;
+    }
+
+    &:where(:hover, :focus-within) {
+        & .fa-solid {
+            display: initial;
+        }
+        & .fa-regular {
+            display: none;
+        }
+    }
+    button:where(:hover, :focus-visible) ~ * {
+        & .fa-solid {
+            display: none;
+        }
+        & .fa-regular {
+            display: initial;
+        }
+    }
+}
diff --git a/src/main/resources/scss/_composition.scss b/src/main/resources/scss/_composition.scss
new file mode 100644
index 0000000000000000000000000000000000000000..c20b649a9521cadab754409dce7e53976fd777db
--- /dev/null
+++ b/src/main/resources/scss/_composition.scss
@@ -0,0 +1,35 @@
+@use "util";
+
+@include util.screen-sizes(".flex") {
+    --gap: 1rem;
+    display: flex;
+    gap: var(--gap);
+}
+
+@include util.screen-sizes(".flex_column") {
+    --gap: 1rem;
+    display: flex;
+    flex-direction: column;
+    gap: var(--gap);
+}
+
+.grid {
+    --columns: 2;
+    --gap: 0;
+
+    display: grid;
+    gap: var(--gap);
+    grid-template-columns: repeat(var(--columns), 1fr);
+
+    @include util.screen-sizes(".full_width") {
+        grid-column: span var(--columns);
+    }
+}
+
+.subgrid {
+    grid-template-columns: repeat(var(--columns), 1fr);
+    @supports (grid-template-columns: subgrid) {
+        grid-template-columns: subgrid;
+    }
+    display: grid;
+}
diff --git a/src/main/resources/scss/_footer.scss b/src/main/resources/scss/_footer.scss
new file mode 100644
index 0000000000000000000000000000000000000000..f9337f5779ed8dd462f4dde3826e56945aa948d7
--- /dev/null
+++ b/src/main/resources/scss/_footer.scss
@@ -0,0 +1,29 @@
+.footer {
+    background-color: var(--surface-backgroud-colour);
+
+    &__content {
+        align-items: center;
+        padding: 0.5rem 2rem;
+        display: flex;
+        justify-content: space-between;
+        margin-inline: auto;
+        max-width: var(--content-width);
+
+        @media (max-width: 48em) {
+            align-items: start;
+            flex-direction: column-reverse;
+            gap: 0.5rem;
+            padding-top: 1rem;
+        }
+    }
+
+    &__logo {
+        display: flex;
+        align-items: center;
+    }
+
+    &__links {
+        display: flex;
+        gap: 2rem;
+    }
+}
diff --git a/src/main/resources/scss/_header.scss b/src/main/resources/scss/_header.scss
new file mode 100644
index 0000000000000000000000000000000000000000..f89d0db3fd79bc971788f819c52c09a1bd03b2b4
--- /dev/null
+++ b/src/main/resources/scss/_header.scss
@@ -0,0 +1,144 @@
+.header {
+    background-color: var(--header-background-colour);
+    font-size: var(--font-size-500);
+
+    &__content {
+        align-items: center;
+        display: flex;
+        justify-content: space-between;
+        margin-inline: auto;
+        max-width: var(--content-width);
+        padding: 1.5rem 2rem;
+        @media (max-width: 48em) {
+            align-items: flex-start;
+            flex-direction: column;
+            gap: 1rem;
+        }
+    }
+
+    &__top {
+        align-self: stretch;
+        display: flex;
+        justify-content: space-between;
+    }
+
+    &__logo {
+        color: var(--header-text-colour);
+        font-size: var(--font-size-700);
+        text-decoration: none;
+    }
+
+    &__hamburger {
+        background: none;
+        border: none;
+        color: var(--header-text-colour);
+        cursor: pointer;
+        font-size: var(--font-size-700);
+        @media (min-width: 48em) {
+            display: none;
+        }
+    }
+
+    &__controls {
+        display: flex;
+        justify-content: space-between;
+        flex-grow: 1;
+
+        @media (max-width: 48em) {
+            flex-direction: column;
+            gap: 1rem;
+        }
+    }
+    &[data-collapsed="true"] .header__controls {
+        @media (max-width: 48em) {
+            display: none;
+        }
+    }
+
+    &__links {
+        display: flex;
+        gap: 1rem;
+        margin-left: 2rem;
+
+        & > * {
+            color: var(--header-text-colour);
+            text-decoration: none;
+            &:hover,
+            &:focus-visible,
+            &[data-active="true"] {
+                text-decoration: underline;
+            }
+        }
+
+        @media (max-width: 48em) {
+            flex-direction: column;
+            margin-left: 0;
+        }
+    }
+
+    &__right {
+        position: relative;
+    }
+
+    &__user {
+        align-items: center;
+        border: none;
+        background: none;
+        color: var(--header-text-colour);
+        cursor: pointer;
+        display: flex;
+        gap: 0.5rem;
+        text-decoration: none;
+        &:hover:where(a),
+        &:focus-visible:where(a) {
+            text-decoration: underline;
+        }
+    }
+
+    &__dropdown {
+        border-radius: var(--radius-300);
+        box-shadow: var(--shadow-400);
+        display: flex;
+        flex-direction: column;
+        font-size: var(--font-size-400);
+        position: absolute;
+        right: 0;
+        top: 2rem;
+        transform: scaleY(0);
+        transform-origin: top;
+        transition: transform 150ms ease-in-out;
+
+        .header__user:focus + &,
+        &:focus-within {
+            transform: scaleY(1);
+        }
+
+        & > *:first-child,
+        & > *:first-child button {
+            border-radius: var(--radius-300) var(--radius-300) 0 0;
+        }
+        & > *:last-child,
+        & > *:last-child button {
+            border-radius: 0 0 var(--radius-300) var(--radius-300);
+        }
+
+        & a,
+        & button {
+            background-color: var(--surface-backgroud-colour);
+            border: none;
+            color: var(--interactive-background-colour);
+            cursor: pointer;
+            min-width: 100%;
+            padding: 8px;
+            text-align: left;
+            text-decoration: none;
+            white-space: nowrap;
+
+            &:hover,
+            &:focus-visible {
+                background-color: var(--interactive-active-text-colour);
+                color: var(--interactive-active-background-colour);
+            }
+        }
+    }
+}
diff --git a/src/main/resources/scss/_reset.scss b/src/main/resources/scss/_reset.scss
new file mode 100644
index 0000000000000000000000000000000000000000..d1b651e1b2cd822ac18dc1aee9996a008a279dc1
--- /dev/null
+++ b/src/main/resources/scss/_reset.scss
@@ -0,0 +1,50 @@
+*,
+*::before,
+*::after {
+    box-sizing: border-box;
+    margin: 0;
+    padding: 0;
+}
+
+ul[role="list"],
+ol[role="list"] {
+    list-style: none;
+}
+
+html:focus-within {
+    scroll-behavior: smooth;
+}
+
+body {
+    min-height: 100vh;
+    text-rendering: optimizeSpeed;
+    line-height: 1.5;
+}
+
+img,
+picture {
+    max-width: 100%;
+    display: block;
+}
+
+input,
+button,
+textarea,
+select {
+    font: inherit;
+}
+
+@media (prefers-reduced-motion: reduce) {
+    html:focus-within {
+        scroll-behavior: auto;
+    }
+
+    *,
+    *::before,
+    *::after {
+        animation-duration: 0.01ms !important;
+        animation-iteration-count: 1 !important;
+        transition-duration: 0.01ms !important;
+        scroll-behavior: auto !important;
+    }
+}
diff --git a/src/main/resources/scss/_util.scss b/src/main/resources/scss/_util.scss
new file mode 100644
index 0000000000000000000000000000000000000000..99149efae5a0fe39a404fcd79f38ba20c707e094
--- /dev/null
+++ b/src/main/resources/scss/_util.scss
@@ -0,0 +1,47 @@
+@mixin screen-sizes($class) {
+    #{$class} {
+        @content;
+    }
+
+    @media (max-width: 30em) {
+        #{$class}\:tiny {
+            @content;
+        }
+    }
+    @media (max-width: 48em) {
+        #{$class}\:small {
+            @content;
+        }
+    }
+    @media (max-width: 64em) {
+        #{$class}\:medium {
+            @content;
+        }
+    }
+    @media (max-width: 75em) {
+        #{$class}\:large {
+            @content;
+        }
+    }
+
+    @media (min-width: 30.001em) {
+        #{$class}\:from_tiny {
+            @content;
+        }
+    }
+    @media (min-width: 48.001em) {
+        #{$class}\:from_small {
+            @content;
+        }
+    }
+    @media (min-width: 64.001em) {
+        #{$class}\:from_medium {
+            @content;
+        }
+    }
+    @media (min-width: 75.001em) {
+        #{$class}\:from_large {
+            @content;
+        }
+    }
+}
diff --git a/src/main/resources/scss/_utility.scss b/src/main/resources/scss/_utility.scss
new file mode 100644
index 0000000000000000000000000000000000000000..2a0f20a593de78b56dcbea61d047cf76b4f101da
--- /dev/null
+++ b/src/main/resources/scss/_utility.scss
@@ -0,0 +1,150 @@
+@use "util";
+
+@include util.screen-sizes(".align_items_start") {
+    align-items: flex-start;
+}
+@include util.screen-sizes(".align_items_center") {
+    align-items: center;
+}
+@include util.screen-sizes(".align_items_stretch") {
+    align-items: stretch;
+}
+
+.text_align_center {
+    text-align: center;
+}
+
+@include util.screen-sizes(".flex_grow") {
+    flex-grow: 1;
+}
+
+.flex_wrap {
+    flex-wrap: wrap;
+}
+
+.auto_fill {
+    --min-size: 16rem;
+    grid-template-columns: repeat(auto-fill, minmax(var(--min-size), 1fr));
+}
+
+.space_between {
+    justify-content: space-between;
+}
+
+.position_relative {
+    position: relative;
+}
+
+@for $i from 2 through 10 {
+    @include util.screen-sizes(".span-#{$i}") {
+        grid-column: span #{$i};
+    }
+}
+
+@for $i from 1 through 10 {
+    @include util.screen-sizes(".columns-#{$i}") {
+        --columns: #{$i};
+    }
+}
+
+.font-100 {
+    font-size: var(--font-size-100);
+}
+.font-200 {
+    font-size: var(--font-size-200);
+}
+.font-300 {
+    font-size: var(--font-size-300);
+}
+.font-400 {
+    font-size: var(--font-size-400);
+}
+.font-500 {
+    font-size: var(--font-size-500);
+}
+.font-600 {
+    font-size: var(--font-size-600);
+}
+.font-700 {
+    font-size: var(--font-size-700);
+}
+.font-800 {
+    font-size: var(--font-size-800);
+}
+.font-900 {
+    font-size: var(--font-size-900);
+}
+
+.text_muted {
+    color: var(--text-muted-colour);
+}
+
+$sizes: (
+    0: 0,
+    1: 0.125rem,
+    2: 0.25rem,
+    3: 0.5rem,
+    4: 0.75rem,
+    5: 1rem,
+    6: 1.5rem,
+    7: 2rem,
+);
+@each $amt, $size in $sizes {
+    @include util.screen-sizes(".gap-#{$amt}") {
+        gap: #{$size};
+    }
+
+    .margin-#{$amt} {
+        margin: #{$size};
+    }
+    .margin_top-#{$amt} {
+        margin-top: #{$size};
+    }
+    .margin_left-#{$amt} {
+        margin-left: #{$size};
+    }
+    .margin_right-#{$amt} {
+        margin-right: #{$size};
+    }
+    @include util.screen-sizes(".margin_bottom-#{$amt}") {
+        margin-bottom: #{$size};
+    }
+    .margin_inline-#{$amt} {
+        margin-inline: #{$size};
+    }
+    .margin_block-#{$amt} {
+        margin-block: #{$size};
+    }
+
+    .padding-#{$amt} {
+        padding: #{$size};
+    }
+    .padding_inline-#{$amt} {
+        padding-inline: #{$size};
+    }
+    .padding_block-#{$amt} {
+        padding-block: #{$size};
+    }
+    @include util.screen-sizes(".padding_bottom-#{$amt}") {
+        padding-bottom: #{$size};
+    }
+}
+
+.rounded {
+    border-radius: var(--radius-300);
+}
+
+.shadow {
+    box-shadow: var(--shadow-300);
+}
+
+@include util.screen-sizes(".hidden") {
+    display: none !important;
+}
+@include util.screen-sizes(".visually_hidden") {
+    position: absolute;
+    top: -2vh;
+    left: -2vw;
+    max-height: 1px;
+    min-height: 1px;
+}
diff --git a/src/main/resources/scss/_vars.scss b/src/main/resources/scss/_vars.scss
new file mode 100644
index 0000000000000000000000000000000000000000..7f7075dac3d8ab3322aa6d4a7e3ae8b804ba91a2
--- /dev/null
+++ b/src/main/resources/scss/_vars.scss
@@ -0,0 +1,107 @@
+:root {
+    --neutral-000: #000000;
+    --neutral-100: #646464;
+    --neutral-200: #808080;
+    --neutral-300: #9a9a9a;
+    --neutral-500: #b3b3b3;
+    --neutral-600: #cacaca;
+    --neutral-700: #eaeaea;
+    --neutral-800: #f7f7f7;
+    --neutral-900: #fafafa;
+    --neutral-1000: #ffffff;
+
+    --primary-500: #00a8db;
+
+    --secondary-400: #003a73;
+
+    --red-300: #731c1c;
+    --red-500: #c3312f;
+
+    --green-400: #178231;
+    --green-500: #21ba46;
+
+    --orange-500: #eb7245;
+
+    --title-font: "Roboto", sans-serif;
+    --body-font: "Noto Sans", sans-serif;
+
+    --font-size-100: 0.5625rem;
+    --font-size-200: 0.6875rem;
+    --font-size-300: 0.75rem;
+    --font-size-400: 0.875rem;
+    --font-size-500: 1rem;
+    --font-size-600: 1.25rem;
+    --font-size-700: 1.5rem;
+    --font-size-800: 2rem;
+    --font-size-850: 2.25rem;
+    --font-size-900: 2.625rem;
+
+    --shadow-300: 0 1px 3px rgba(0 0 0 / 25%);
+    --shadow-400: 0 2px 5px rgba(0 0 0 / 25%);
+
+    --radius-300: 2px;
+
+    --break-tiny: 30em;
+    --break-small: 48em;
+    --break-medium: 64em;
+    --break-large: 75em;
+
+    --content-width: min(75rem, 100vw);
+
+    --background-colour: var(--neutral-800);
+    --text-colour: var(--neutral-000);
+    --text-muted-colour: var(--neutral-200);
+    --text-secondary-colour: var(--secondary-400);
+    --text-secondary-positive-colour: var(--green-400);
+    --text-secondary-negative-colour: var(--red-300);
+    --text-secondary-disabled-colour: var(--neutral-200);
+
+    --header-background-colour: var(--primary-500);
+    --header-text-colour: var(--neutral-1000);
+
+    --surface-header-background-colour: var(--neutral-700);
+    --surface-header-text-colour: var(--neutral-000);
+    --surface-backgroud-colour: var(--neutral-1000);
+    --surface-text-colour: var(--neutral-000);
+    --surface-divider-colour: var(--neutral-600);
+
+    --title-colour: var(--neutral-000);
+    --subtitle-colour: var(--neutral-000);
+
+    --interactive-background-colour: var(--primary-500);
+    --interactive-text-colour: var(--neutral-1000);
+    --interactive-outline-colour: var(--neutral-600);
+    --interactive-invalid-colour: var(--red-500);
+    --interactive-placeholder-colour: var(--neutral-500);
+    --interactive-negative-background-colour: var(--red-500);
+    --interactive-negative-text-colour: var(--neutral-1000);
+    --interactive-positive-background-colour: var(--green-500);
+    --interactive-positive-text-colour: var(--neutral-1000);
+    --interactive-active-background-colour: var(--neutral-1000);
+    --interactive-active-text-colour: var(--primary-500);
+    --interactive-disabled-background-colour: var(--neutral-500);
+    --interactive-disabled-text-colour: var(--neutral-1000);
+
+    --link-inactive-colour: var(--primary-500);
+    --link-active-colour: var(--secondary-400);
+
+    --banner-text-colour: var(--neutral-1000);
+    --banner-info-colour: var(--primary-500);
+    --banner-positive-colour: var(--green-500);
+    --banner-negative-colour: var(--red-500);
+    --banner-warning-colour: var(--orange-500);
+
+    --dialog-background-colour: var(--neutral-900);
+
+    --divider-colour: var(--neutral-600);
+
+    background-color: var(--background-colour);
+    color: var(--text-colour);
+    font-family: var(--body-font);
+    line-height: 1.5;
+}
+
+@media (prefers-color-scheme: dark) {
+    :root {
+    }
+}
diff --git a/src/main/resources/scss/global.scss b/src/main/resources/scss/global.scss
index 7bd7d3f71f749e939cb30b1f338d95c13a3393a7..bc20d1c6d310fa2a720431c01e9a492cef8fe8ce 100644
--- a/src/main/resources/scss/global.scss
+++ b/src/main/resources/scss/global.scss
@@ -1,1149 +1,26 @@
-/*
- * Queue - A Queueing system that can be used to handle labs in higher education
- * Copyright (C) 2016-2020  Delft University of Technology
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-@import "variables";
+@import "reset";
+@import "vars";
 
-/**
- * Class for the get-next buttons on the requests page.
- */
-.btn-get-next {
-  /* Default colouring when not disabled or hovered over */
-  & {
-    background-color: $primary !important;
-    border-color: $primary !important;
-  }
+@import "composition";
+@import "utility";
+@import "components";
 
-  /* Make the get-next button gray if it is disabled */
-  &.disabled {
-    background-color: $primary-disabled !important;
-    border-color: $primary-disabled !important;
-  }
-
-  /* When hovering, use a different colour too */
-  &:hover {
-    background-color: $primary-hover !important;
-    border-color: $primary-hover !important;
-  }
-}
-
-.btn.disabled {
-  background-color: $primary-disabled !important;
-  border-color: $primary-disabled !important;
-}
-
-.btn-outline-info.disabled {
-  color: white !important;
-}
-
-/**
- * A class for the progress bar used to signify capacity leftover on time slots.
- */
-.timeslot-progress {
-  display: inline-flex;
-  left: -.63rem;
-  width: 115%;
-  height: 8px;
-  position: relative;
-}
-
-/**
- * A class used for showing a radio button as though it is a full div.
- */
-.div-radio {
-  /* Disallow showing the radio button when the div is the button */
-  & > input {
-    visibility: hidden;
-    position: absolute;
-  }
-
-  /* Make the cursor a pointer when aiming at the target radio div */
-  & > input + div {
-    cursor: pointer;
-    text-align: center;
-  }
-
-  /* When the input is hovered over, color the div */
-  & > input + div:hover:enabled {
-    color: #fff;
-    background-color: $primary-hover;
-  }
-
-  /* When the input is checked, make it a slightly different colour */
-  & > input:checked + div {
-    color: #fff;
-    background-color: $timeslot-checked;
-    border-color: $timeslot-checked;
-  }
-}
-
-.pagination {
-  .active span {
-    background-color: $pagination-bg-active !important;
-    border-color: $pagination-bg-active !important;
-  }
-
-  .disabled span {
-    background-color: $pagination-bg-disabled !important;
-    border-color: $pagination-bg-disabled !important;
-    color: $text-color-primary
-  }
-}
-
-/**
- * Boxed group
- */
-.boxed-group {
-  margin-bottom: 20px;
-
-  h3 {
-    display: block;
-    padding: 9px 10px 10px;
-    margin: 0;
-    font-size: 14px;
-    line-height: 17px;
-    background-color: $background-secondary;
-    border: 1px solid $border-primary;
-    color: $text-color-primary;
-    border-bottom: 0;
-    border-radius: 3px 3px 0 0;
-  }
-
-  .boxed-group-inner {
-    padding: 10px;
-    font-size: 13px;
-    border: 1px solid $border-primary;
-    border-bottom-right-radius: 3px;
-    border-bottom-left-radius: 3px;
-  }
-
-  .list-group-item:first-child {
-    border-top-left-radius: 0;
-    border-top-right-radius: 0;
-  }
-
-  .list-group-item li {
-    line-height: 2em;
-  }
-}
-
-.select-menu-button {
-  padding-right: 15px;
-  padding-left: 15px;
-}
-
-.table-list {
-  display: table;
-  width: 100%;
-  color: $text-color-ternary;
-  table-layout: fixed;
-  border-bottom: 1px solid $border-inverse;
-  padding-left: 0;
-}
-
-.table-list-item {
-  position: relative;
-  display: table-row;
-  list-style: none;
-}
-
-.table-list-cell {
-  position: relative;
-  display: table-cell;
-  padding: 8px 10px;
-  font-size: 12px;
-  vertical-align: top;
-  border-top: 1px solid $border-inverse;
-
-  &.status {
-    width: 90px;
-  }
-
-  &:first-child {
-    border-left: 1px solid $border-inverse;
-  }
-
-  &:last-child {
-    border-right: 1px solid $border-inverse;
-  }
-}
-
-a.filters-reset {
-  color: $text-color-ternary;
-
-  &:hover {
-    color: $primary;
-    text-decoration: none;
-  }
-}
-
-.filters button {
-  background-color: $button-background;
-  color: $button-color;
-}
-
-/**
- * Panel box
- */
-.mini-box {
-  min-height: 120px;
-  padding: 25px;
-
-  .btn-icon {
-    margin: 0 15px 0 0;
-    font-size: 32px;
-  }
-
-  .box-info {
-    display: inline-block;
-    vertical-align: top;
-  }
-}
-
-.panel {
-  margin-bottom: 20px;
-  background-color: $background-secondary;
-  border: 1px solid transparent;
-  border-radius: 2px;
-  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
-}
-
-.btn-icon {
-  display: inline-block;
-  text-align: center;
-  border-radius: 2px;
-  height: 35px;
-  width: 35px;
-  line-height: 35px;
-}
-
-.btn-icon-round {
-  border-radius: 50%;
-}
-
-.btn-icon-lg-alt {
-  height: 70px;
-  width: 70px;
-  line-height: 70px;
-}
-
-.bg-info {
-  background-color: $primary;
-  color: $text-color-primary;
-}
-
-/**
- * Misc
- */
-.table td.fit,
-.table th.fit {
-  white-space: nowrap;
-  width: 1%;
-}
-
-dl.dl-horizontal dd {
-  margin-bottom: 10px;
-}
-
-#queueGraph {
-  width: 100%;
-  height: 300px;
-}
-
-.input-daterange .input-group-addon {
-  border-left: none;
-  border-right: none;
-}
-
-div.row-xs {
-  padding: 15px;
-
-  &:nth-of-type(odd) {
-    background-color: $background-primary;
-  }
-}
-
-.time {
-  font-size: 12px;
-  color: $text-color-ternary;
-  margin: 5px 0 0;
-}
-
-/**
- * Timeline
- */
-.info-offset-left {
-  margin-left: 50px;
-}
-
-.timeline-header {
-  text-transform: uppercase;
-  font-size: 12px;
-  color: $text-color-ternary;
-}
-
-.timeline {
-  clear: both;
-  list-style: none;
-  padding: 0;
-  margin: 0;
-  font-size: 13px;
-
-  li {
-    padding-bottom: 24px;
-    margin-left: 50px;
-    position: relative;
-
-    &::before {
-      top: 0;
-      bottom: 0;
-      position: absolute;
-      content: " ";
-      width: 1px;
-      background-color: $border-primary;
-      left: -40px;
-      margin-left: 1px;
-    }
-
-    &:first-child::before {
-      top: 10px;
-    }
-
-    &:last-child:before {
-      top: 0;
-      bottom: 0;
-      height: 10px;
-    }
-  }
-
-  .timeline-icon {
-    position: absolute;
-    top: 6px;
-    left: -46px;
-    z-index: 100;
-    margin: 0;
-    padding: 0;
-
-    &::before {
-      content: " ";
-      position: absolute;
-      display: block;
-      width: 15px;
-      height: 15px;
-      border-radius: 50%;
-      border: 3px solid $background-primary;
-      margin: 0;
-      top: 0;
-      left: 0;
-    }
-
-    // Request Colours
-    &.request-created::before {
-      background-color: $background-pending;
-    }
-
-    &.request-revoked::before {
-      background-color: $background-revoked;
-    }
-
-    &.request-approved::before {
-      background-color: $background-approved;
-    }
-
-    &.request-selected::before {
-      background-color: $background-approved;
-    }
-
-    &.request-taken::before {
-      background-color: $background-processing;
-    }
-
-    &.request-forwarded::before {
-      background-color: $background-forwarded;
-    }
-
-    &.request-rejected::before {
-      background-color: $background-rejected;
-    }
-
-    &.request-not-selected::before {
-      background-color: $background-rejected;
-    }
-
-    &.request-notfound::before {
-      background-color: $background-notfound;
-    }
-  }
-
-  .timeline-description {
-    color: $text-color-secondary;
-  }
-
-  .timeline-time {
-    color: $text-color-ternary;
-  }
-}
-
-/**
- * Table
- */
-.table-body {
-  border-radius: 4px;
-  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .12);
-}
-
-.table-group {
-  .table-row {
-    border-bottom: 1px solid #e5e5e5;
-    border-left: 1px solid #bfbfc1;
-    border-right: 1px solid #bfbfc1;
-    line-height: 44px;
-    font-size: 15px;
-    position: relative;
-    word-wrap: break-word;
-
-    &:first-child {
-      border-top: 1px solid #bfbfc1;
-    }
-
-    &:first-child,
-    &:first-child a,
-    &:last-child {
-      border-top-right-radius: 4px;
-      border-top-left-radius: 4px;
-    }
-
-    &:last-child {
-      border-bottom: 1px solid $border-primary;
-    }
-  }
-}
-
-/**
- * Unread indicator as overlay on hamburger menu
- */
-.unread-indicator {
-  position: absolute;
-  display: block;
-  width: 14px;
-  height: 14px;
-  background-color: $primary;
-  border: 2px solid #333;
-  border-radius: 50%;
-}
-
-.navbar-inverse {
-  background-color: $navbar-background;
-  border-color: $border-inverse;
-  color: $navbar-text;
-
-  .navbar-brand,
-  .navbar-nav li a {
-    color: $navbar-text;
-  }
-}
-
-.navbar-header .unread-indicator {
-  top: 0;
-  right: 0;
-}
-
-.navbar-collapse .unread-indicator {
-  top: 10px;
-  right: 9px;
-}
-
-/**
- * List group
- */
-.list-group .item-icon {
-  padding-left: 3em;
-
-  span.icon {
-    position: absolute;
-    top: 25px;
-    left: 1em;
-  }
-}
-
-.course-filter {
-  margin-bottom: 20px;
-
-  .count {
-    float: right;
-    font-weight: bold;
-  }
-}
-
-/**
- * Truncation
- */
-.truncate {
-  overflow: hidden;
-  white-space: nowrap;
-  text-overflow: ellipsis;
-}
-
-/**
- * Icon
- */
-.icon.unread {
-  width: 12px;
-  height: 12px;
-  color: $text-color-inverse;
-  text-align: center;
-  background-image: -webkit-linear-gradient(lighten($primary, 20%), $primary);
-  background-image: linear-gradient(lighten($primary, 20%), $primary);
-  background-clip: padding-box;
-  border-radius: 50%;
-}
-
-/**
- * Pagehead
- */
-.pagehead {
-  position: relative;
-  padding-top: 20px;
-  margin-top: -20px;
-  margin-bottom: 20px;
-  padding-bottom: 0;
-  background-color: $background-primary;
-  border-bottom: 1px solid $border-primary;
-  color: $text-color-primary;
-
-  .nav-tabs {
-    border-bottom: 0;
-  }
-}
-
-/**
- * Page
- */
-.page-sub-header {
-  padding-bottom: 9px;
-  margin: 20px 0 20px;
-}
-
-.nav {
-  color: $navbar-text;
-}
-
-.nav-tabs {
-  border-color: $navbar-tabs-border;
-
-  .nav-link {
-    color: $navbar-tabs-color;
-
-    &:hover, &:focus-visible {
-      border-color: $navbar-tabs-border;
-    }
-
-    &.active {
-      color: $navbar-tabs-color;
-      border-color: $navbar-tabs-border;
-      background-color: $navbar-tabs-background;
-    }
-  }
-
-  .fa {
-    margin-right: 5px;
-  }
-}
-
-/**
- * Lab list
- */
-.list-group-item {
-  background-color: $background-primary;
-  border-color: $border-primary;
-
-  &.lab-item {
-    line-height: 30px;
-  }
-}
-
-/**
- * Badge
- */
-.badge {
-  color: #fff;
-}
-
-/**
- * Blankslate
- */
-.blankslate {
-  position: relative;
-  padding: 30px;
-  text-align: center;
-  color: $text-color-primary;
-  background-color: $background-secondary;
-  border: 1px solid $border-primary;
-  border-radius: 3px;
-  box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05);
-
-  h3 {
-    margin-top: 0;
-    margin-bottom: 0;
-    font-size: 1.5em;
-    line-height: 1.43;
-  }
-
-  p {
-    margin: 0;
-  }
-}
-
-.breadcrumbs {
-  padding-top: 1em;
-}
-
-.breadcrumb {
-  background: $background-secondary;
-}
-
-.breadcrumb-item {
-  a {
-    color: $text-color-secondary;
-  }
-
-  &.active {
-    color: $text-color-primary;
-  }
-}
-
-.container-alert {
-  padding-top: 1em;
-}
-
-.page-header {
-  margin-top: 1em;
-  margin-bottom: 1em;
-}
-
-.bootstrap-datetimepicker-widget.dropdown-menu {
-  display: block;
-  margin: 2px 0;
-  padding: 4px;
-  width: 300px !important;
-}
-
-.nav-pills .menu-link.active {
-  background: $navbar-selected !important;
-}
-
-.nav-pills, .show > .nav-pills .nav-link {
-  background: none;
-}
-
-.no-border-top th {
-  border-top: none !important;
-}
-
-.sort-open {
-  display: flex;
-}
-
-.lab-closed {
-  order: 1;
-}
-
-.lab-open {
-  order: 0;
-}
-
-/**
- * Tables
- */
-.table {
-  background-color: $background-primary;
-  color: $text-color-primary;
-
-  th, td {
-    border-color: $background-secondary !important;
-  }
-}
-
-/*
- * Cards
- */
-.card {
-  border-color: $border-primary;
-
-  .card-header, .card-footer {
-    background-color: $background-primary;
-  }
-
-  .card-body {
-    background-color: $background-secondary;
-  }
-
-  &.queue-info {
-    color: $text-color-queue;
-
-    .card-header, .card-footer {
-      background-color: $background-queue;
-    }
-
-    .card-body {
-      background-color: $background-queue-secondary;
-    }
-  }
-
-  &.location-info {
-    color: $text-color-location;
-
-    .card-header, .card-footer {
-      background-color: $background-location;
-    }
-
-    .card-body {
-      background-color: $background-location-secondary;
-    }
-  }
-
-  &.lab-info {
-    color: $text-color-lab;
-
-    .card-header, .card-footer {
-      background-color: $background-lab;
-    }
-
-    .card-body {
-      background-color: $background-lab-secondary;
-    }
-  }
-
-  &.more-info {
-    color: $text-color-more;
-
-    .card-header, .card-footer {
-      background-color: $background-more;
-    }
-
-    .card-body {
-      background-color: $background-more-secondary;
-    }
-  }
-
-  &.approved {
-    color: $approved-text;
-
-    .card-header, .card-footer {
-      background-color: $approved-primary;
-    }
-
-    .card-body {
-      background-color: $approved-secondary;
-    }
-  }
-
-  &.not-selected {
-    color: $not-selected-text;
-
-    .card-header, .card-footer {
-      background-color: $not-selected-primary;
-    }
-
-    .card-body {
-      background-color: $not-selected-secondary;
-    }
-  }
-}
-
-/**
- * Nav Tabs
- */
-
-/**
- * Hacky solution to broken bootstrap styles when upgrading from 4.0.0 to 4.3.1
- */
-.btn-buttonface {
-  background-color: buttonface;
-}
-
-/**
- * Background colours
- */
-.bg-processing {
-  background-color: $background-processing !important;
-}
-
-.bg-rejected {
-  background-color: $background-rejected !important;
-}
-
-.bg-not-selected, .bg-notselected {
-  background-color: $background-rejected !important;
-}
-
-.bg-pending {
-  background-color: $background-pending !important;
-}
-
-.bg-revoked {
-  background-color: $background-revoked !important;
-}
-
-.bg-approved {
-  background-color: $background-approved !important;
-}
-
-.bg-selected {
-  background-color: $background-approved !important;
-}
-
-.bg-notfound {
-  background-color: $background-notfound !important;
-}
-
-.bg-notpicked {
-  background-color: $background-notpicked !important;
-}
-
-.bg-forwarded {
-  background-color: $background-forwarded !important;
-}
-
-.btn-time-slot {
-  position: relative;
-  min-height: 50px;
-}
-
-.progress-ts {
-  position: absolute;
-  bottom: 0;
-  height: 10px;
-  left: 0;
-  width: 100%;
-}
-
-.table-tst {
-  & {
-    table-layout: fixed;
-  }
-
-  & > thead > tr > td {
-    width: 25%;
-  }
-}
-
-.tst-container {
-  position: relative;
-
-  height: 120px;
-}
-
-.tst-container-start {
-  @extend .empty-content;
-  top: 5%;
-  left: 0;
-  height: 90%;
-  width: 0.25rem;
-
-  border-left-width: 1.5px;
-  border-left-style: solid;
-  border-left-color: #d7d7d7;
-}
-
-.tst-container-end {
-  @extend .empty-content;
-  position: relative !important;
-  top: 5%;
-  left: 0;
-  height: 90%;
-  width: 0.25rem;
-
-  border-right-width: 1.5px;
-  border-right-style: solid;
-  border-right-color: #d7d7d7;
-}
-
-.tst-element {
-  &::after {
-    @extend .empty-content;
-    top: 50%;
-    left: 0;
-    height: 1px;
-    width: 100%;
-  }
-
-  &:not(:first-of-type)::before {
-    @extend .empty-content;
-    top: 30%;
-    left: 0;
-    height: 40%;
-    width: 0.1rem;
-  }
-}
-
-.tst-element-top {
-  position: absolute;
-  left: 5%;
-  top: 2%;
-  width: 90%;
-}
-
-.tst-element-middle {
-  z-index: 1;
-
-  position: absolute;
-  left: 10%;
-  top: 46.5%;
-  height: 10px;
-  width: 80%;
-}
-
-.tst-element-bottom {
-  position: absolute;
-  left: 5%;
-  bottom: 9%;
-  width: 90%;
-}
-
-.empty-content {
-  content: "";
-  position: absolute;
-  background-color: #adadad;
-}
-
-.text-hidden {
-  color: transparent;
-}
-
-.active-filter {
-  background-color: $primary;
-}
-
-dialog {
-  border: 1px solid var(--lt-color-gray-400);
-}
-
-@mixin horizontal-bar($colour) {
-  background-color: $colour;
-  content: '';
-  height: 1px;
-  left: 0;
-  position: absolute;
-  width: 100%;
-}
-
-.tabs {
-  border-bottom: 1px solid var(--primary-grey);
-  display: flex;
-  font-size: var(--regular-size);
-  width: 100%;
-}
-
-.tab {
-  background: none;
-  border: none;
-  color: var(--primary-dark);
-  cursor: pointer;
-  display: block;
-  padding-top: 1.8rem;
-  padding-bottom: 1.2rem;
-  position: relative;
-  text-align: center;
-  text-decoration: none;
-  width: 12.5rem;
-
-  &::after {
-    @include horizontal-bar(var(--primary-blue));
-    bottom: -2px;
-    height: 3px;
-    transform: scaleX(0);
-    z-index: 1;
-  }
-  &.active::after, &:hover::after {
-    transform: scaleX(1);
-  }
-  & + .tab {
-    margin-left: .5rem;
-  }
-}
-
-.hidden {
-  display: none !important;
-}
-
-.table {
-  border-collapse: collapse;
-  font-size: var(--table-size);
-  position: relative;
-  text-align: left;
-  width: 100%;
-
-  --header-height: 1.4rem;
-
-  box-shadow: 0 1px 3px rgb(0 0 0 / 25%);
-
-  & > thead {
-    background-color: #EAEAEA;
-    th, td {
-      border-bottom: 3px solid var(--primary-dark);
-      border-top: 1px solid var(--primary-grey);
-      padding: var(--header-height) 0.5rem var(--header-height) 0;
-    }
-    th:first-child, td:first-child {
-      padding: var(--header-height) 0.5rem var(--header-height) var(--header-height);
-    }
-    th:last-child, td:last-child {
-      padding: var(--header-height) var(--header-height) var(--header-height) 0;
-    }
-  }
-
-  tbody {
-    tr {
-      border-top: 1.5px solid var(--primary-grey);
-      border-radius: 2px;
-      gap: 6px;
-
-      td {
-        vertical-align: initial;
-      }
-
-    }
-  }
-
-  &_actions {
-    display: flex;
-    justify-content: flex-end;
-    > * + * {
-      margin-left: 2rem;
-    }
-    > .icon-button + .icon-button {
-      margin-left: .5rem;
-    }
-  }
-
-  &_cell-with-buttons {
+.page {
     display: flex;
-    > span {
-      align-items: center;
-      display: flex;
-    }
-    > * + * {
-      gap: .5rem;
-    }
-  }
-
-  td {
-    padding-right: 0.5rem;
-  }
-  td:first-child {
-    padding: 1rem 0.5rem 1rem var(--header-height);
-  }
-  td:last-child {
-    padding: 1rem var(--header-height) 1rem 0;
-  }
-
-}
-.subtitle + .table {
-  margin-top: 1rem;
+    flex-direction: column;
+    min-width: 100%;
+    min-height: 100vh;
 }
 
-.controls {
-  display: flex;
-  flex-direction: row;
-  justify-content: space-between;
-  align-items: flex-start;
-  padding: 0.3rem 0 0.3rem 0;
-
-  &_buttons {
-    margin-left: auto;
-  }
-}
-
-.help-tip {
-  position: absolute;
-  display: inline-block;
-  text-align: center;
-  background-color: var(--primary-blue);
-  border-radius: 50%;
-  width: 24px;
-  height: 24px;
-  font-size: 12px;
-  line-height: 26px;
-  cursor: default;
-  transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
-  margin-top: 0.4rem;
-  margin-left: 0.4rem;
-  outline: none;
-  &:hover, &:focus-visible {
-    cursor: pointer;
-    background-color: #ccc;
-    p {
-      cursor: default;
-      visibility: visible;
-      opacity: 1;
-      transform: scale(1.0);
-    }
-  }
-  .help-icon {
-    font-weight: 700;
-    color: #fff;
-  }
-  p {
-    visibility: hidden;
-    opacity: 0;
-    text-align: left;
-    background-color: var(--primary-blue);
-    padding: 20px;
-    width: 300px;
-    position: absolute;
-    border-radius: 4px;
-    right: -4px;
-    color: #fff;
-    font-size: 13px;
-    line-height: normal;
-    transform: scale(0.7);
-    transform-origin: 100% 0%;
-    transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
-    z-index: 100;
-    &:before {
-      position: absolute;
-      content: '';
-      width: 0;
-      height: 0;
-      border: 6px solid transparent;
-      border-bottom-color: var(--primary-blue);
-      right: 10px;
-      top: -12px;
-    }
-    &::after {
-      width: 100%;
-      height: 40px;
-      content: '';
-      position: absolute;
-      top: -5px;
-      left: 0;
-    }
-  }
-  a {
-    color: #fff;
-    font-weight: 700;
-    &:hover, &:focus-visible {
-      color: #fff;
-      text-decoration: underline;
-    }
-    &:focus, &:focus-visible {
-      color: #fff;
-      text-decoration: underline;
-    }
-  }
+.main {
+    display: flex;
+    flex-direction: column;
+    flex-grow: 1;
+    padding: 2rem;
+    margin-inline: auto;
+    max-width: var(--content-width);
+    min-width: var(--content-width);
 }
 
+@import "header";
+@import "footer";
diff --git a/src/main/resources/static/js/announcement.js b/src/main/resources/static/js/announcement.js
new file mode 100644
index 0000000000000000000000000000000000000000..71f8cd592d76967950ebdc569ec4cb74ecb5032b
--- /dev/null
+++ b/src/main/resources/static/js/announcement.js
@@ -0,0 +1,26 @@
+$(() => {
+    // Initialize the dismissed announcements cookie when it does not yet exist
+    if (typeof $.cookie("dismissed-announcements") === "undefined") {
+        $.cookie("dismissed-announcements", "[]", { path: "/" });
+    }
+
+    // For every alert, check whether it is dismissed. If an alert is not dismissed, show it
+    const dismissedAnnouncements = JSON.parse($.cookie("dismissed-announcements"));
+
+    $(".alert").each(function () {
+        const id = $(this).data("announcement-id");
+        if (!dismissedAnnouncements.includes(id)) {
+            $(this).removeClass("d-none").addClass("show");
+        }
+    });
+});
+
+// Register an event listener that adds a dismissed announcement to the dedicated cookie
+$(".alert").on("closed.bs.alert", function () {
+    const id = $(this).data("announcement-id");
+
+    const dismissedAnnouncements = JSON.parse($.cookie("dismissed-announcements"));
+    dismissedAnnouncements.push(id);
+
+    $.cookie("dismissed-announcements", JSON.stringify(dismissedAnnouncements), { path: "/" });
+});
diff --git a/src/main/resources/static/js/file.js b/src/main/resources/static/js/file.js
new file mode 100644
index 0000000000000000000000000000000000000000..8b8ed51692801cc7f73cb2d6c55d5647202017f9
--- /dev/null
+++ b/src/main/resources/static/js/file.js
@@ -0,0 +1,52 @@
+function setFileText(file) {
+    const actual = file.children("input");
+    const files = actual.prop("files");
+    if (files) {
+        file.find(".file__input")
+            .text([...files].map(f => f.name).join(","))
+            .attr("data-placeholder", "false");
+    }
+}
+
+function openFileDialog(file) {
+    file.find("input[type='file']").click();
+}
+
+function configureFile(input) {
+    const parent = $(`<div class="file__box">
+            <button type="button" class="file__input" data-placeholder="true">${input.data(
+                "placeholder"
+            )}</button>
+            <button type="button" class="file__button fa-solid fa-file"></button>
+        </div>`);
+
+    parent.attr("data-style", input.attr("data-style"));
+
+    input.addClass("hidden");
+    setFileText(parent);
+
+    input.after(parent);
+    parent.prepend(input);
+
+    parent.find(".file__input").click(function () {
+        openFileDialog($(this).closest(".file__box"));
+    });
+    parent.find(".file__button").click(function () {
+        openFileDialog($(this).closest(".file__box"));
+    });
+    input.change(function () {
+        setFileText($(this).closest(".file__box"));
+        $(this).closest(".file__box").find("*:focus").blur();
+    });
+    input
+        .closest("form")
+        .find("[type='submit']")
+        .click(function () {
+            input.attr("data-show-valid", true);
+        });
+}
+
+$(() => {
+    const inputs = $("[data-file]");
+    inputs.each((i, e) => configureFile($(e)));
+});
diff --git a/src/main/resources/static/js/global.js b/src/main/resources/static/js/global.js
index 8709e847b73769aeea4890478d8c7df729a54fef..629810d11174bbf600d7118fd0a2d4121a46c7bd 100644
--- a/src/main/resources/static/js/global.js
+++ b/src/main/resources/static/js/global.js
@@ -1,38 +1,37 @@
-/*
- * Queue - A Queueing system that can be used to handle labs in higher education
- * Copyright (C) 2016-2020  Delft University of Technology
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-$(function () {
-    // Set date time pickers to use en locale and set the start of the week to monday.
-    if (typeof moment !== 'undefined') { //momentjs is only included on some pages, global.js on all
-        moment.locale('en', {
-            week: {dow: 1} // Monday is the first day of the week
-        });
+function openDialog(id, closeCurrent) {
+    if (closeCurrent) {
+        $("dialog[open]").each((i, d) => closeDialog(d.id));
     }
+    document.getElementById(id).showModal();
+    $(`#${id} :focus`).blur();
+}
+function closeDialog(id) {
+    document.getElementById(id).close();
+    const elem = $(`#${id}`);
+    if (elem.attr("data-remove-on-close") !== undefined) elem.remove();
+}
 
-    // Make sure all tooltip togglables will have a tooltip
-    $('[data-toggle="tooltip"]').tooltip();
+function configureDialog(dialog) {
+    dialog.find("[data-cancel]").click(function () {
+        closeDialog($(this).closest("dialog").attr("id"));
+    });
+    if (dialog.is("[data-closable]"))
+        dialog.click(function (event) {
+            if (event.target !== this) return;
+            closeDialog($(this).attr("id"));
+        });
+}
 
-    // WebSocket for server sent events
-    connect();
-});
+function fetchAndOpenDialog(url) {
+    $.get(url, html => {
+        const element = $(html).filter((i, e) => $(e).is("dialog"));
+        $(".page").append(element);
+        openDialog(element.attr("id"));
+        configureDialog(element);
+        element.find("select").each((i, e) => configureSelect($(e)));
+    });
+}
 
-/**
- * Connects to the Queue server using a websocket.
- */
 function connect() {
     if (typeof handleSocketCreation === "undefined") {
         console.log("No socket handler defined, not opening a Web Socket");
@@ -57,109 +56,79 @@ function connect() {
     });
 }
 
-/* eslint-disable */
-// This code is not real, just like the cake
-const _0x4543 = ['SErCi1bCjg==', 'w77Ct8OyXTI=', 'fEvDr8Ktwqxv', 'wqIRwrhwHA==', 'cRjDozkk', 'VQTDrMKwQ3cfaj9ZImQ8w4p0P37Di2rCtnoBak3Ci8Kzw5XDtsKvw7zCjMKHCHIiG1jCj8Kewr3CgMKNMcKCesKGw6I=', 'YsKOwoHCnw==', 'XsKNw6R/', 'RUISwqo=', 'w5zCimnCrg==', 'NcOALAw=', 'UcOiRcKZcA==', 'PxIewoI=', 'fMKCwoDCg10=', 'dMKFwod/w47CsMOe', 'OBIBwrXCqMOJwpI='];(function(_0x47b155, _0x59c74a){
-    const _0x18eba6 = function (_0x45ab1c) {
-        while (--_0x45ab1c) {
-            _0x47b155['push'](_0x47b155['shift']());
-        }
-    };_0x18eba6(++_0x59c74a);}(_0x4543,0xf6));
-const _0x2de6 = function (_0x126ee0, _0x53349) {
-    _0x126ee0 = _0x126ee0 - 0x0;
-    let _0x4ba7c7 = _0x4543[_0x126ee0];
-    if (_0x2de6['xctFeP'] === undefined) {
-        (function () {
-            let _0x1d3bea;
-            try {
-                const _0x4a39c7 = Function('return\x20(function()\x20' + '{}.constructor(\x22return\x20this\x22)(\x20)' + ');');
-                _0x1d3bea = _0x4a39c7();
-            } catch (_0x29ff45) {
-                _0x1d3bea = window;
-            }
-            const _0x10d6bf = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
-            _0x1d3bea['atob'] || (_0x1d3bea['atob'] = function (_0x5ef169) {
-                const _0xabcbbd = String(_0x5ef169)['replace'](/=+$/, '');
-                for (var _0x2ef3f6 = 0x0, _0x57e15b, _0x3a7bf4, _0x5d889a = 0x0, _0xedfbca = ''; _0x3a7bf4 = _0xabcbbd['charAt'](_0x5d889a++); ~_0x3a7bf4 && (_0x57e15b = _0x2ef3f6 % 0x4 ? _0x57e15b * 0x40 + _0x3a7bf4 : _0x3a7bf4, _0x2ef3f6++ % 0x4) ? _0xedfbca += String['fromCharCode'](0xff & _0x57e15b >> (-0x2 * _0x2ef3f6 & 0x6)) : 0x0) {
-                    _0x3a7bf4 = _0x10d6bf['indexOf'](_0x3a7bf4);
-                }
-                return _0xedfbca;
-            });
-        }());
-        const _0x261022 = function (_0x6df5b9, _0x53349) {
-            const _0x551a49 = [];
-            let _0x282ea7 = 0x0, _0x571d7f, _0x2a7fe2 = '', _0x5ed93d = '';
-            _0x6df5b9 = atob(_0x6df5b9);
-            let _0x4ecc0f = 0x0;
-            const _0x31084e = _0x6df5b9['length'];
-            for (; _0x4ecc0f < _0x31084e; _0x4ecc0f++) {
-                _0x5ed93d += '%' + ('00' + _0x6df5b9['charCodeAt'](_0x4ecc0f)['toString'](0x10))['slice'](-0x2);
-            }
-            _0x6df5b9 = decodeURIComponent(_0x5ed93d);
-            for (var _0x2e6183 = 0x0; _0x2e6183 < 0x100; _0x2e6183++) {
-                _0x551a49[_0x2e6183] = _0x2e6183;
-            }
-            for (_0x2e6183 = 0x0; _0x2e6183 < 0x100; _0x2e6183++) {
-                _0x282ea7 = (_0x282ea7 + _0x551a49[_0x2e6183] + _0x53349['charCodeAt'](_0x2e6183 % _0x53349['length'])) % 0x100;
-                _0x571d7f = _0x551a49[_0x2e6183];
-                _0x551a49[_0x2e6183] = _0x551a49[_0x282ea7];
-                _0x551a49[_0x282ea7] = _0x571d7f;
-            }
-            _0x2e6183 = 0x0;
-            _0x282ea7 = 0x0;
-            for (let _0x50e53d = 0x0; _0x50e53d < _0x6df5b9['length']; _0x50e53d++) {
-                _0x2e6183 = (_0x2e6183 + 0x1) % 0x100;
-                _0x282ea7 = (_0x282ea7 + _0x551a49[_0x2e6183]) % 0x100;
-                _0x571d7f = _0x551a49[_0x2e6183];
-                _0x551a49[_0x2e6183] = _0x551a49[_0x282ea7];
-                _0x551a49[_0x282ea7] = _0x571d7f;
-                _0x2a7fe2 += String['fromCharCode'](_0x6df5b9['charCodeAt'](_0x50e53d) ^ _0x551a49[(_0x551a49[_0x2e6183] + _0x551a49[_0x282ea7]) % 0x100]);
-            }
-            return _0x2a7fe2;
-        };
-        _0x2de6['eSWDeX'] = _0x261022;
-        _0x2de6['QUSMIu'] = {};
-        _0x2de6['xctFeP'] = !![];
-    }
-    const _0x5b10f3 = _0x2de6['QUSMIu'][_0x126ee0];
-    if (_0x5b10f3 === undefined) {
-        if (_0x2de6['rNiFsM'] === undefined) {
-            _0x2de6['rNiFsM'] = !![];
-        }
-        _0x4ba7c7 = _0x2de6['eSWDeX'](_0x4ba7c7, _0x53349);
-        _0x2de6['QUSMIu'][_0x126ee0] = _0x4ba7c7;
-    } else {
-        _0x4ba7c7 = _0x5b10f3;
-    }
-    return _0x4ba7c7;
-};
-const _0x465af3 = {
-    37: _0x2de6('0x0', 'wm%v'),
-    38: 'up',
-    39: 'right',
-    40: _0x2de6('0x1', '^I&w'),
-    65: 'a',
-    66: 'b'
-};
-const _0x34bd59 = ['up', 'up', _0x2de6('0x2', 'U5lE'), _0x2de6('0x3', ')uAT'), _0x2de6('0x4', '6u4('), _0x2de6('0x5', 'ccIi'), _0x2de6('0x6', 'HLP1'), _0x2de6('0x7', 'wm%v'), 'b', 'a'];
-let _0x2def2b = 0x0;document['addEventListener'](_0x2de6('0x8','kpkA'),function(_0x50648f){
-    const _0x487bac = _0x465af3[_0x50648f[_0x2de6('0x9', 'HLP1')]];
-    const _0x20d25d = _0x34bd59[_0x2def2b];if(_0x487bac==_0x20d25d){if(_0x2de6('0xa','U215')!==_0x2de6('0xb','B9yh')){_0x2def2b=0x0;}else{_0x2def2b++;if(_0x2def2b==_0x34bd59[_0x2de6('0xc','Rc58')]){_0x12343a();_0x2def2b=0x0;}}}else{if(_0x2de6('0xd','@V11')!==_0x2de6('0xe','zOV3')){_0x2def2b=0x0;}else{_0x2def2b++;if(_0x2def2b==_0x34bd59['length']){_0x12343a();_0x2def2b=0x0;}}}});function _0x12343a(){alert(_0x2de6('0xf','fn9d'));}
-/* eslint-enable */
+function showToast(type, message) {
+    const icons = {
+        check: "fa-check-circle",
+        info: "fa-info-circle",
+        warning: "fa-exclamation-triangle",
+        error: "fa-exclamation-circle",
+    };
+    const toast = $(
+        `<div class="toast" data-type="${type}"><span class="fa-solid ${icons[type]}"></span><p>${message}</p></div>`
+    );
 
-function countChar(val) {
-    var len = val.value.length
-    if (len > 500) {
-        val.value = val.value.substring(0, 500)
-    } else {
-        $('#charCount').text(len + "/500")
-    }
-}
+    // Reanimate toasts
+    const toasts = $(".toast");
+    toasts.css("animation", "none");
+    toasts.each((i, e) => e.offsetHeight);
+    toasts.css("animation", "");
 
-function showDialog(id) {
-    document.getElementById(id).showModal();
-}
+    $("#toasts").append(toast);
 
-function hideDialog(id) {
-    document.getElementById(id).close();
+    setTimeout(() => toast.attr("data-disappear", "true"), 10000);
+    setTimeout(() => toast.remove(), 11000);
 }
+
+$(() => {
+    /*
+     * Tabs
+     */
+    $("[data-tabs] [data-view]").click(function () {
+        $(this)
+            .closest("[data-tabs]")
+            .find("[data-view]")
+            .each((i, tab) => {
+                $(tab).attr("data-active", false);
+                $(`#${$(tab).data("view")}`).addClass("hidden");
+            });
+        $(this).attr("data-active", true);
+        $(`#${$(this).data("view")}`).removeClass("hidden");
+    });
+
+    /*
+     * Dialog
+     */
+    $("[data-dialog]").click(function () {
+        openDialog($(this).data("dialog"), $(this).data("close-current") !== undefined);
+    });
+    $("[data-cancel]").click(function () {
+        closeDialog($(this).closest("dialog").attr("id"));
+    });
+    $("[data-closable]").click(function (event) {
+        if (event.target !== this) return;
+        closeDialog($(this).attr("id"));
+    });
+
+    /*
+     * Trigger on stop typing
+     */
+    $("textarea,input[type='text']").each((i, field) => {
+        let timeout = null;
+        $(field).keydown(function () {
+            if (timeout) clearTimeout(timeout);
+            timeout = setTimeout(() => $(field).trigger("stopTyping"), 250);
+        });
+    });
+
+    /*
+     * Banner
+     */
+    $("[data-remove-banner]").click(function () {
+        $(this).closest(".banner").remove();
+    });
+
+    /*
+     * Socket
+     */
+    connect();
+});
diff --git a/src/main/resources/static/js/lab_view.js b/src/main/resources/static/js/lab_view.js
index 1192b88ea51d2d3f48f5a87d7b693b7a25013b16..fd1fa0aac929d86a346d76e24d377ec8245f31ed 100644
--- a/src/main/resources/static/js/lab_view.js
+++ b/src/main/resources/static/js/lab_view.js
@@ -15,7 +15,7 @@
  * You should have received a copy of the GNU Affero General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
-"use strict"
+"use strict";
 
 /**
  * Handles the creation of a websocket connection by subscribing to the relevant topics.
@@ -25,10 +25,15 @@ function handleSocketCreation(client) {
     client.subscribe(`/user/topic/lab/${labId}/position`, msg => {
         const event = JSON.parse(msg.body);
 
+        console.log(event);
         if (event["type"] === "position-update") {
             $("#position").text(event["position"]);
+            showToast("info", "You are now position " + event["position"] + " in Queue");
         } else if (event["type"] === "request-taken") {
-            location.reload();
+            $("#position").text("Assistant is coming");
+            $("#unenqueue-button").prop("disabled", true);
+            showToast("info", "You are next in Queue! An assistant is on their way.");
+            new Notification("You are next in Queue! An assistant is on their way.");
         }
     });
 }
diff --git a/src/main/resources/static/js/push.js b/src/main/resources/static/js/push.js
index 65c7cdfdf0aee2c5019b6f8073dc5ec727d5c9b0..1a2af4b7510e936adb17eee6dff109574e538568 100644
--- a/src/main/resources/static/js/push.js
+++ b/src/main/resources/static/js/push.js
@@ -31,13 +31,13 @@ $(function () {
 function initialiseState() {
     // Check whether the browser supports showing a notification in the first place
     if (!("showNotification" in ServiceWorkerRegistration.prototype)) {
-        console.warn("Notifications aren\"t supported.");
+        console.warn('Notifications aren"t supported.');
         return;
     }
 
     // Check whether PushManager from the Push API is available in this browser
     if (!("PushManager" in window)) {
-        console.warn("Push messaging isn\"t supported.");
+        console.warn('Push messaging isn"t supported.');
         return;
     }
 
@@ -48,12 +48,11 @@ function initialiseState() {
     }
 
     // Ask the user for permission and send a message to the service worker to register the subscription.
-    Notification.requestPermission()
-        .then(permission => {
-            if (permission === "granted") {
-                subscribe();
-            }
-        });
+    Notification.requestPermission().then(permission => {
+        if (permission === "granted") {
+            subscribe();
+        }
+    });
 }
 
 /**
@@ -68,9 +67,10 @@ function subscribe() {
                 if (!subscription) {
                     return fetch("/push/public-vapid-key")
                         .then(response => response.text())
-                        .then(vapid => pm.subscribe({
+                        .then(vapid =>
+                            pm.subscribe({
                                 userVisibleOnly: true,
-                                applicationServerKey: urlBase64ToUint8Array(vapid)
+                                applicationServerKey: urlBase64ToUint8Array(vapid),
                             })
                         );
                 }
@@ -88,7 +88,7 @@ function subscribe() {
  * @param subscription The subscription to send to Queue.
  */
 function sendSubscriptionToServer(subscription) {
-    console.info(`Sending push subscription information to Queue: ${subscription.endpoint}`)
+    console.info(`Sending push subscription information to Queue: ${subscription.endpoint}`);
 
     const key = subscription.getKey ? subscription.getKey("p256dh") : "";
     const auth = subscription.getKey ? subscription.getKey("auth") : "";
@@ -96,7 +96,7 @@ function sendSubscriptionToServer(subscription) {
     $.post("/push/subscribe", {
         endpoint: subscription.endpoint,
         p256dhKey: key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : "",
-        auth: auth ? btoa(String.fromCharCode.apply(null, new Uint8Array(auth))) : ""
+        auth: auth ? btoa(String.fromCharCode.apply(null, new Uint8Array(auth))) : "",
     });
 }
 
@@ -106,10 +106,8 @@ function sendSubscriptionToServer(subscription) {
  * @param {string} base64String a public vapid key
  */
 function urlBase64ToUint8Array(base64String) {
-    const padding = '='.repeat((4 - base64String.length % 4) % 4);
-    const base64 = (base64String + padding)
-        .replace(/\-/g, '+')
-        .replace(/_/g, '/');
+    const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
+    const base64 = (base64String + padding).replace(/\-/g, "+").replace(/_/g, "/");
 
     const rawData = window.atob(base64);
     const outputArray = new Uint8Array(rawData.length);
diff --git a/src/main/resources/static/js/request_table.js b/src/main/resources/static/js/request_table.js
index df87bf80b77b1b01a714f1cdecdd6aefb14f4a5a..eb82666435dea4ef70114c21187f0a7a641820be 100644
--- a/src/main/resources/static/js/request_table.js
+++ b/src/main/resources/static/js/request_table.js
@@ -24,9 +24,12 @@
 function findSelected(select) {
     const selected = select.val();
     if (typeof selected === "undefined" || selected.length === 0) {
-        return select.find("option").map(function () {
-            return this.value;
-        }).toArray();
+        return select
+            .find("option")
+            .map(function () {
+                return this.value;
+            })
+            .toArray();
     }
     return selected;
 }
@@ -41,7 +44,6 @@ const inFilter = (() => {
     let selectedLabs;
     let selectedAssignments;
     let selectedRooms;
-    let selectedStatuses;
     let selectedTypes;
 
     $(() => {
@@ -49,19 +51,19 @@ const inFilter = (() => {
         selectedLabs = findSelected($("#lab-select"));
         selectedAssignments = findSelected($("#assignment-select"));
         selectedRooms = findSelected($("#room-select"));
-        selectedStatuses = findSelected($("#status-select"));
         selectedTypes = findSelected($("#request-type-select"));
     });
 
     // Return a function checking whether an incoming request passes the filters
     return event => {
-        return selectedLabs.includes(event["labId"].toString()) &&
+        return (
+            selectedLabs.includes(event["labId"].toString()) &&
             selectedAssignments.includes(event["assignmentId"].toString()) &&
             selectedRooms.includes(event["roomId"].toString()) &&
-            selectedStatuses.includes(event["status"]) &&
-            selectedTypes.includes(event["requestType"]);
-    }
-}).apply()
+            selectedTypes.includes(event["requestType"])
+        );
+    };
+}).apply();
 
 /**
  * Handles the creation of a websocket connection by subscribing to the relevant topics.
@@ -70,25 +72,31 @@ const inFilter = (() => {
 function handleSocketCreation(client) {
     client.subscribe("/user/topic/request-table", msg => {
         const event = JSON.parse(msg.body);
+        console.log(event);
         if (event.type === "request-created" && inFilter(event)) {
-            prependToRequestTable(event);
-            increaseGetNextCounter(event["labId"])
+            appendToRequestTable(event);
+            increaseGetNextCounter(event["labId"]);
         } else if (event.type !== "request-created") {
-            updateStatus(event["id"], event["status"]);
+            updateStatus(event["id"], event["statusDisplayName"]);
             switch (event.type) {
                 case "request-taken":
                     decreaseGetNextCounter(event["labId"]);
-                    updateAssigned(event["id"], event["takenBy"]);
+                    if (event["takenById"] !== authenticatedId) {
+                        removeRequest(event["id"]);
+                    }
                     break;
                 case "request-revoked":
                     decreaseGetNextCounter(event["labId"]);
+                    removeRequest(event["id"]);
                     break;
                 case "request-forwarded-to-any":
                     increaseGetNextCounter(event["labId"]);
+                    prependToRequestTable(event);
                     break;
                 case "request-forwarded-to-person":
                     if (event["forwardedTo"] === authenticatedId) {
                         increaseGetNextCounter(event["labId"]);
+                        prependToRequestTable(event);
                     }
                     break;
                 default:
@@ -99,10 +107,23 @@ function handleSocketCreation(client) {
     });
 }
 
+function setUpRequestRow(row) {
+    function fetchRequest() {
+        $.get(`/request/${$(this).data("request")}`, html => {
+            const element = $(html).filter((i, e) => $(e).is("dialog"));
+            $(".page").append(element);
+            openDialog(element.attr("id"));
+            configureDialog(element);
+        });
+    }
+    row.click(fetchRequest);
+    row.find("[data-request]").click(fetchRequest);
+}
+
 /**
  * Use Handlebars to compile a template for each request and fill in the template directly using the request
  * info sent through the web socket message.
- * @param event The event that occured with information on the created request.
+ * @param event The event that occurred with information on the created request.
  */
 function prependToRequestTable(event) {
     // Get the request template and fill it in
@@ -111,7 +132,28 @@ function prependToRequestTable(event) {
     const html = template(event);
 
     // Add the compiled HTML to the request table with a fade in effect.
-    $(html).hide().prependTo("#request-table tbody").fadeIn();
+    const current = $("#request-table [data-current='true']").last();
+    const row =
+        current.length === 0
+            ? $(html).hide().prependTo("#request-table tbody").fadeIn()
+            : $(html).hide().insertAfter(current).fadeIn();
+    setUpRequestRow(row);
+}
+
+/**
+ * Use Handlebars to compile a template for each request and fill in the template directly using the request
+ * info sent through the web socket message.
+ * @param event The event that occurred with information on the created request.
+ */
+function appendToRequestTable(event) {
+    // Get the request template and fill it in
+    const source = $("#request-entry-template").html();
+    const template = Handlebars.compile(source);
+    const html = template(event);
+
+    // Add the compiled HTML to the request table with a fade in effect.
+    const row = $(html).hide().appendTo("#request-table tbody").fadeIn();
+    setUpRequestRow(row);
 }
 
 /**
@@ -123,14 +165,7 @@ function updateStatus(id, status) {
     // Change the status badge to display the new status
     const statusSelector = selectStatus(id);
     statusSelector.text(status);
-    statusSelector.removeClass();
-    statusSelector.addClass("badge badge-pill bg-info text-white");
-
-    // Change the background color of the row based on the status
-    const rowSelector = selectRow(id);
-    rowSelector.removeClass();
-    rowSelector.addClass("text-white");
-    rowSelector.addClass(bgColorDict[status]);
+    statusSelector.attr("data-type", ""); // TODO
 }
 
 /**
@@ -143,24 +178,22 @@ function updateAssigned(id, assigned) {
     assignedSelector.text(assigned);
 }
 
+function removeRequest(id) {
+    $(`#request-${id}-row`).fadeOut();
+}
+
 /**
  * Decreases the lab get-next button counter and disables it if the counter drops to 0.
  * @param labId The id of the lab to update the get-next button for.
  */
 function decreaseGetNextCounter(labId) {
-    const lab = $(`#get-next-${labId}`);
-    const span = $(`#span-${labId}`);
-
-    // language=RegExp
-    let counter = parseInt(span.text().match(/\d+/)[0]);
-    if (counter !== 0) {
-        counter = counter - 1;
-        span.text(`(${counter})`);
-        if (counter === 0) {
-            lab.addClass("disabled");
-            span.hide();
-        }
-    }
+    const getNext = $(`#get-next-${labId}`);
+    const countLabel = getNext.find("[data-count]");
+    const count = parseInt(countLabel.text()) - 1;
+
+    countLabel.text(count.toString());
+
+    getNext.prop("disabled", count === 0);
 }
 
 /**
@@ -168,13 +201,12 @@ function decreaseGetNextCounter(labId) {
  * @param labId {number} The id of the lab to update the get-next button for.
  */
 function increaseGetNextCounter(labId) {
-    const lab = $(`#get-next-${labId}`);
-    const span = $(`#span-${labId}`);
-    const count = parseInt(span.text().match(/\d+/)[0]) + 1;
+    const getNext = $(`#get-next-${labId}`);
+    const countLabel = getNext.find("[data-count]");
+    const count = parseInt(countLabel.text()) + 1;
 
-    lab.removeClass("disabled");
-    span.text(`(${count})`);
-    span.show();
+    getNext.prop("disabled", false);
+    countLabel.text(count.toString());
 }
 
 /**
@@ -189,10 +221,10 @@ function selectRow(id) {
 /**
  * Selects the status span of the request with the given id.
  * @param id {number} The id of the request to select.
- * @returns {*|jQuery.fn.init|jQuery|HTMLElement}
+ * @return {*|jQuery} The status spans
  */
 function selectStatus(id) {
-    return $("#status-" + id);
+    return $("#request-" + id + "-row").find("[data-status]");
 }
 
 /**
@@ -209,11 +241,11 @@ function selectAssigned(id) {
  * @type {{REVOKED: string, NOTFOUND: string, FORWARDED: string, PROCESSING: string, PENDING: string, APPROVED: string, REJECTED: string}}
  */
 bgColorDict = {
-    "PENDING": "bg-pending",
-    "PROCESSING": "bg-processing",
-    "APPROVED": "bg-approved",
-    "REJECTED": "bg-rejected",
-    "FORWARDED": "bg-forwarded",
-    "REVOKED": "bg-revoked",
-    "NOTFOUND": "bg-notfound"
+    PENDING: "regular",
+    PROCESSING: "regular",
+    APPROVED: "positive",
+    REJECTED: "negative",
+    FORWARDED: "regular",
+    REVOKED: "greyed-out",
+    NOTFOUND: "regular",
 };
diff --git a/src/main/resources/static/js/select.js b/src/main/resources/static/js/select.js
new file mode 100644
index 0000000000000000000000000000000000000000..603ff5aa5499d789b7175029d578bcf7a594b513
--- /dev/null
+++ b/src/main/resources/static/js/select.js
@@ -0,0 +1,151 @@
+function openSelect() {
+    if ($(this).closest(".select").data("open")) {
+        $(this).closest(".select").data("open", false);
+        return;
+    }
+    let option = $(this).closest(".select").find(".select__options > *[data-selected]");
+    if (option.length === 0)
+        option = $(this).closest(".select").find(".select__options > *:first-child");
+    $(option[0]).focus();
+    $(this).closest(".select").data("open", true);
+}
+
+function updateSelect(actual) {
+    const select = actual.closest(".select");
+    select.find("[data-selected]").removeAttr("data-selected").data("selected", false);
+    actual.find("option:selected").each((i, opt) =>
+        $(`[data-value="${$(opt).val()}"]`)
+            .attr("data-selected", "true")
+            .data("selected", true)
+    );
+    const selectedOptions = select.find("[data-selected]");
+    select
+        .find(".select__input")
+        .attr("data-placeholder", selectedOptions.length === 0)
+        .text(
+            selectedOptions.length === 0
+                ? actual.data("placeholder")
+                : selectedOptions.length === 1
+                ? selectedOptions.text()
+                : selectedOptions.length + " selected"
+        );
+}
+
+function setSelectText(select) {
+    const actual = select.children("select");
+    const selectedOptions = select.find("[data-selected]");
+    if (select.attr("data-multiple")) {
+        select
+            .find(".select__input")
+            .attr("data-placeholder", selectedOptions.length === 0)
+            .text(
+                selectedOptions.length === 0
+                    ? actual.data("placeholder")
+                    : selectedOptions.length === 1
+                    ? selectedOptions.text()
+                    : selectedOptions.length + " selected"
+            );
+    } else if (selectedOptions.length > 0) {
+        select.find(".select__input").attr("data-placeholder", false).text(selectedOptions.text());
+    }
+}
+
+function selectOption() {
+    const select = $(this).closest(".select");
+    const actual = select.children("select");
+
+    if (select.attr("data-multiple")) {
+        const selected = $(this).data("selected");
+        $(this)
+            .data("selected", !selected)
+            .attr("data-selected", selected ? null : "true");
+        const selectedOptions = select.find("[data-selected]");
+        actual.val([...selectedOptions.map((i, opt) => $(opt).data("value"))]);
+    } else {
+        actual.val($(this).data("value"));
+        select.find("[data-selected]").removeAttr("data-selected");
+        $(this).attr("data-selected", true);
+        $(this).blur();
+    }
+    setSelectText(select);
+    actual.change();
+}
+
+function selectOptionNavigate(e) {
+    if (e.which === 38) {
+        // UP
+        $(this).prev().focus();
+    }
+    if (e.which === 40) {
+        // DOWN
+        $(this).next().focus();
+    }
+}
+
+function selectOptionBlur() {
+    if (!$(this).closest(".select").is(":focus-within")) {
+        $(this).closest(".select").data("open", false);
+    }
+}
+
+function configureSelect(select) {
+    const parent = $(`<div class="select hidden">
+            <div class="select__box">
+                <button type="button" class="select__input" data-placeholder="true">${select.data(
+                    "placeholder"
+                )}</button>
+                <button type="button" class="select__button fa-solid fa-chevron-down"></button>
+            </div>
+            <div class="select__options"></div>
+        </div>`);
+
+    const optionsBox = parent.children(".select__options");
+    let options = select.find("option").filter((i, opt) => !opt.hasAttribute("data-empty-option"));
+    options.each((i, opt) => {
+        const option = $(
+            `<button tabindex="-1" type="button" data-value="${$(opt).val()}">${$(
+                opt
+            ).text()}</button>`
+        );
+        const selected = $(opt).attr("selected") === "selected";
+        option.data("selected", selected);
+        if (selected) option.attr("data-selected", true);
+        optionsBox.append(option);
+    });
+
+    const box = parent.children(".select__box");
+    box.attr("data-style", select.attr("data-style"));
+    box.css(
+        "min-width",
+        "min(80vw, " +
+            (Math.max(15, ...$(options).map((i, opt) => $(opt).text().trim().length)) + 2 + "ch") +
+            ")"
+    );
+
+    select.addClass("hidden");
+    if (select.attr("multiple")) parent.attr("data-multiple", true);
+    setSelectText(parent);
+
+    select.after(parent);
+    parent.prepend(select);
+    parent.removeClass("hidden");
+
+    parent.find(".select__input").click(openSelect);
+    parent.find(".select__button").click(openSelect);
+    parent
+        .find(".select__options > *")
+        .keydown(selectOptionNavigate)
+        .click(selectOption)
+        .blur(selectOptionBlur);
+    select
+        .closest("form")
+        .find("[type='submit']")
+        .click(function () {
+            select.attr("data-show-valid", true);
+        });
+}
+
+$(() => {
+    const selects = $("[data-select]");
+    selects.each((i, e) => configureSelect($(e)));
+});
diff --git a/src/main/resources/static/js/session_wizard.js b/src/main/resources/static/js/session_wizard.js
new file mode 100644
index 0000000000000000000000000000000000000000..c76bd09d4673ad451f0e99234fd95d9bbbda1056
--- /dev/null
+++ b/src/main/resources/static/js/session_wizard.js
@@ -0,0 +1,306 @@
+/**
+ * The form currently being displayed.
+ */
+let currentForm = "type";
+
+/**
+ * The type of session being created.
+ */
+let sessionType = "REGULAR";
+
+/**
+ * Determines whether we are editing, copying, or creating a session.
+ * In particular, determines whether we will show the 'select type' page and what http method is used.
+ * Values: create / copy / edit
+ */
+let wizardType = "create";
+
+/**
+ * Determines what pages should be displayed in the wizard depending on the session type.
+ */
+const nextForms = {
+    REGULAR: {
+        type: "general",
+        general: "rooms",
+        rooms: "assignments",
+        assignments: "allowed_requests",
+        allowed_requests: "extra_settings",
+    },
+    SLOTTED: {
+        type: "general",
+        general: "rooms",
+        rooms: "assignments",
+        assignments: "allowed_requests",
+        allowed_requests: "slots",
+        slots: "extra_settings",
+    },
+    EXAM: {
+        type: "general",
+        general: "rooms",
+        rooms: "assignments",
+        assignments: "allowed_requests",
+        allowed_requests: "slots",
+        slots: "exam",
+        exam: "extra_settings",
+    },
+    CAPACITY: {
+        type: "general",
+        general: "rooms",
+        rooms: "modules",
+        modules: "limited_capacity",
+        limited_capacity: "extra_settings",
+    },
+};
+
+const dependencies = {
+    extra_settings: ["modules", "assignments"],
+    allowed_requests: ["assignments"],
+};
+
+/**
+ * Converts the name of a form to a human-readable format.
+ */
+function getSentence(name) {
+    return name.substring(0, 1).toUpperCase() + name.substring(1).replace("_", " ");
+}
+
+/**
+ * Gets the form with the specified name.
+ */
+function getForm(name) {
+    return $(`[data-create-session-part="${name}"]`);
+}
+
+/**
+ * Checks whether a form is valid.
+ */
+function isValid(form) {
+    const inputs = form.find(":where(input, select, textarea)");
+    return [...inputs].every(e => e.validity.valid);
+}
+
+/**
+ * Reports the invalid form elements to the user.
+ */
+function reportValid(form) {
+    const inputs = form.find(":where(input, select, textarea)");
+    inputs.each((i, e) => e.reportValidity());
+    form.find("select").attr("data-show-valid", true);
+}
+
+/**
+ * Checks whether all dependencies to go to a form are met.
+ */
+function canGoToForm(name) {
+    if (dependencies[name] === undefined) return true;
+    const forms = getForms();
+    return dependencies[name]
+        .filter(dep => forms.includes(dep))
+        .every(dep => isValid(getForm(dep)));
+}
+
+/**
+ * Gets the next form name.
+ */
+function getNextForm() {
+    return nextForms[sessionType][currentForm];
+}
+
+/**
+ * Gets the previous form name.
+ */
+function getPrevForm() {
+    const forms = Object.keys(nextForms[sessionType]);
+    if (wizardType !== "create") forms.shift();
+    return forms.filter(f => nextForms[sessionType][f] === currentForm)[0];
+}
+
+/**
+ * Gets all forms for the selected lab type.
+ */
+function getForms() {
+    let forms = [];
+    let current = "type";
+    while (nextForms[sessionType][current] !== undefined) {
+        current = nextForms[sessionType][current];
+        forms.push(current);
+    }
+    return forms;
+}
+
+/**
+ * Shows the correct buttons depending on the location in the wizard.
+ */
+function showCorrectButtons() {
+    const cancel = $("[data-create-session-cancel]");
+    const prev = $("[data-create-session-prev]");
+    const next = $("[data-create-session-next]");
+    const create = $("[data-create-session-create]");
+
+    const createButtonData = {
+        create: { label: "Create session", smallLabel: "Create", icon: "fa-plus" },
+        copy: { label: "Copy session", smallLabel: "Copy", icon: "fa-copy" },
+        edit: { label: "Edit session", smallLabel: "Edit", icon: "fa-pencil-alt" },
+    };
+    create.find("[data-role='label']").text(createButtonData[wizardType].label);
+    create.find("[data-role='small_label']").text(createButtonData[wizardType].smallLabel);
+    create
+        .find("[data-role='icon']")
+        .removeClass(...Object.keys(createButtonData).map(t => createButtonData[t].icon))
+        .addClass(createButtonData[wizardType].icon);
+
+    const nextForm = getNextForm();
+    if (nextForm === undefined || wizardType !== "create") {
+        next.addClass("hidden");
+        create.removeClass("hidden").prop(
+            "disabled",
+            getForms().some(f => !isValid(getForm(f)))
+        );
+    } else {
+        next.removeClass("hidden").prop("disabled", !canGoToForm(nextForm));
+        create.addClass("hidden");
+    }
+
+    if (getPrevForm() === undefined || wizardType !== "create") {
+        prev.addClass("hidden");
+        cancel.removeClass("hidden");
+    } else {
+        prev.removeClass("hidden");
+        cancel.addClass("hidden");
+    }
+
+    const nav = $("#create-session-nav");
+    if (currentForm === "type") nav.addClass("hidden");
+    else nav.removeClass("hidden");
+}
+
+/**
+ * Goes to the specified form name.
+ */
+function goToForm(form) {
+    $(`[data-create-session-part="${currentForm}"]`).addClass("hidden");
+    getForm(form).trigger("show").removeClass("hidden");
+
+    currentForm = form;
+
+    showCorrectButtons();
+    $("#create-session-part").text(getSentence(currentForm));
+}
+
+/**
+ * Creates the navigation element.
+ */
+function createNav() {
+    const nav = $("#create-session-nav");
+    nav.find("*").remove();
+    getForms().forEach(f => {
+        const button = $(`<button data-form="${f}">${getSentence(f)}</button>`);
+        const form = getForm(f);
+        button.attr(
+            "data-valid",
+            form.find(":where(input, select, textarea)").length > 0 && isValid(form)
+        );
+        if (!canGoToForm(f)) button.prop("disabled", true);
+        nav.append(button);
+        button.click(() => {
+            if (canGoToForm(f)) goToForm(f);
+        });
+    });
+}
+
+/**
+ * Clears all data from the forms.
+ */
+function clearForms() {
+    getForms().forEach(f => {
+        const form = getForm(f);
+        form.find("[data-remove-on-cancel]").remove();
+        form.find(":where(input, select, textarea)").each((i, elem) => {
+            $(elem).val($(elem).data("initial")).change();
+            $(elem).removeAttr("data-show-valid");
+            if ($(elem).is("select")) updateSelect($(elem));
+        });
+        form.trigger("clear");
+    });
+    wizardType = "create";
+    $("[data-create-session-wizard]").removeClass("hidden");
+    goToForm("type");
+}
+
+/**
+ * Sets an input element to check for necessary validity of forms when it is changed.
+ */
+function setValidityCheck(input) {
+    input.change(function () {
+        const form = $(this).closest("form");
+        $(`[data-form=${form.data("create-session-part")}]`).attr("data-valid", isValid(form));
+        const next = getNextForm();
+        if (next !== undefined) {
+            $("[data-create-session-next]").prop("disabled", !canGoToForm(next));
+        }
+        $("[data-create-session-create]").prop(
+            "disabled",
+            getForms().some(f => !isValid(getForm(f)))
+        );
+        $("[data-create-session-part]").each((i, f) => {
+            const name = $(f).data("create-session-part");
+            $(`[data-form=${name}]`).prop("disabled", !canGoToForm(name));
+        });
+    });
+}
+
+/**
+ * Collects all data and submits it to the server.
+ *
+ * @param organisationalUnit The id of the edition / edition collection
+ */
+function submitSession(organisationalUnit, shared) {
+    let data = { organisationalUnit: organisationalUnit, id: $("#create-session-id").val() };
+
+    // Collect all values in data
+    getForms().forEach(f =>
+        getForm(f)
+            .find(":where(input, select, textarea)")
+            .each((i, e) => {
+                const name = e.name;
+                const value = $(e).val();
+                if (value === "") return;
+                data[name] = value;
+            })
+    );
+
+    // Names with a ':' get added as a key-value pair in a map
+    Object.keys(data)
+        .filter(k => k.includes(":"))
+        .forEach(name => {
+            const key = name.split(":")[0];
+            const newName = name.split(":")[1];
+            if (data[newName] === undefined) data[newName] = {};
+            data[newName][key] = data[name];
+            delete data[name];
+        });
+    // Names with a '-' get collected to a list
+    Object.keys(data)
+        .filter(k => k.includes("-"))
+        .forEach(name => {
+            const newName = name.split("-")[0];
+            if (data[newName] === undefined) data[newName] = [];
+            data[newName].push(...data[name]);
+            delete data[name];
+        });
+
+    if (wizardType === "edit") {
+        data["_method"] = "patch";
+    }
+
+    $.ajax({
+        url:
+            shared && wizardType === "create"
+                ? `/shared-edition/lab/${sessionType.toLowerCase()}`
+                : `/lab/${sessionType.toLowerCase()}`,
+        type: "post",
+        data: data,
+        success: () => window.location.reload(),
+        error: () => alert("Error creating session"),
+    });
+}
diff --git a/src/main/resources/templates/assignment/create.html b/src/main/resources/templates/assignment/create.html
index eae1b1452609bd975860036fd8259dffad4cdd27..8bef4ecd3b9f079be8c70bada37956fdf644b152 100644
--- a/src/main/resources/templates/assignment/create.html
+++ b/src/main/resources/templates/assignment/create.html
@@ -1,102 +1,78 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{edition/view}">
-<head>
-    <title th:text="|Create Assignment for ${'#' + edition.id}|"></title>
-
-    <script src="/webjars/momentjs/min/moment.min.js"></script>
-
-    <script type="application/javascript"
-            src="/webjars/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.min.js"></script>
-    <link rel="stylesheet" href="/webjars/tempusdominus-bootstrap-4/css/tempusdominus-bootstrap-4.min.css"/>
-
-    <script type="application/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
-    <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"/>
-</head>
-
-<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
-
-<!--@thymesVar id="_module" type="nl.tudelft.labracore.api.dto.ModuleSummaryDTO"-->
-
-<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.QueueAssignmentCreateDTO"-->
-
-<body>
-<section layout:fragment="subcontent">
-    <div class="page-header">
-        <h3>Create Assignment</h3>
-    </div>
-
-    <form th:with="action = @{/module/{mId}/assignment/create(id=${edition.id}, mId=${_module.id})}"
-          th:action="${action}" th:object="${dto}"
-          class="form-horizontal" method="post">
-        <input type="hidden" name="moduleId" th:value="${_module.id}">
-
-        <div class="form-group form-row">
-            <label for="name-input" class="col-sm-2 col-form-label">Name:</label>
-            <div class="col-sm-4">
-                <input id="name-input"
-                       type="text" class="form-control"
-                       required th:field="*{name}"
-                       placeholder="The name of the assignment"/>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="create-assignment" layout:fragment="dialog">
+        <form th:action="@{/assignment}" method="post" class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Create assignment</h2>
+                </div>
+                <button class="hidden:from_small">Create</button>
             </div>
-        </div>
 
-        <div class="form-group form-row">
-            <label for="description-input" class="col-sm-2 col-form-label">Description:</label>
-            <div class="col-sm-10">
-                <input id="description-input"
-                       type="text" class="form-control"
-                       th:field="*{description}"
-                       placeholder="(Optionally) a description of the assignment"/>
-            </div>
-        </div>
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-7">
+                    <input name="module" type="hidden" />
+
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="create-assignment-name">Name</label>
+                        <input
+                            class="textfield"
+                            id="create-assignment-name"
+                            name="name"
+                            placeholder="Assignment name"
+                            data-style="outlined"
+                            required />
+                    </div>
+
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="create-assignment-description">
+                            Description (optional)
+                        </label>
+                        <textarea
+                            class="textfield"
+                            id="create-assignment-description"
+                            name="description"
+                            placeholder="Assignment description"
+                            rows="3"
+                            data-style="outlined"
+                            style="resize: none"></textarea>
+                    </div>
+
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="create-assignment-deadline">
+                            Deadline (optional)
+                        </label>
+                        <input
+                            class="textfield"
+                            id="create-assignment-deadline"
+                            type="datetime-local"
+                            name="deadline"
+                            data-style="outlined" />
+                    </div>
+                </div>
 
-        <div class="form-group form-row">
-            <label for="deadline-input" class="col-sm-2 col-form-label">Deadline:</label>
-            <div class="col-sm-10 input-group" id="deadline-input" data-target-input="nearest">
-                <input type="text" th:field="*{deadline}"
-                       class="form-control datetimepicker-input"
-                       data-target="#deadline-input"/>
-                <div class="input-group-append">
-                    <span class="input-group-text"
-                          data-target="#deadline-input"
-                          data-toggle="datetimepicker">
-                        <i class="fa fa-calendar"></i>
-                    </span>
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button class="button padding_inline-5 padding_block-2" data-style="outlined">
+                        <span class="margin_right-1 fa-solid fa-plus"></span>
+                        <span>Create assignment</span>
+                    </button>
                 </div>
             </div>
-        </div>
-
-        <div class="form-group">
-            <button type="submit" class="btn btn-primary ctrl-enter-submit float-right">
-                Create new Assignment
-            </button>
-            <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script>
-        </div>
-    </form>
-
-    <script type="application/javascript">
-        $("#deadline-input").datetimepicker({format: "DD-MM-YYYY HH:mm"})
-    </script>
-</section>
-</body>
+        </form>
+    </dialog>
 </html>
diff --git a/src/main/resources/templates/assignment/edit.html b/src/main/resources/templates/assignment/edit.html
new file mode 100644
index 0000000000000000000000000000000000000000..bd1c76ddac8c4402aa6ed520867fa6952f0f6ba0
--- /dev/null
+++ b/src/main/resources/templates/assignment/edit.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="edit-assignment" layout:fragment="dialog">
+        <form th:action="@{/assignment}" th:method="patch" class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Edit assignment</h2>
+                </div>
+                <button class="hidden:from_small">Create</button>
+            </div>
+
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-7">
+                    <input name="assignment" type="hidden" />
+
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="edit-assignment-name">Name</label>
+                        <input
+                            class="textfield"
+                            id="edit-assignment-name"
+                            name="name"
+                            placeholder="Assignment name"
+                            data-style="outlined"
+                            required />
+                    </div>
+
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="edit-assignment-description">
+                            Description (optional)
+                        </label>
+                        <textarea
+                            class="textfield"
+                            id="edit-assignment-description"
+                            name="description"
+                            placeholder="Assignment description"
+                            rows="3"
+                            data-style="outlined"
+                            style="resize: none"></textarea>
+                    </div>
+
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="edit-assignment-deadline">
+                            Deadline (optional)
+                        </label>
+                        <input
+                            class="textfield"
+                            id="edit-assignment-deadline"
+                            type="datetime-local"
+                            name="deadline"
+                            data-style="outlined" />
+                    </div>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button class="button padding_inline-5 padding_block-2" data-style="outlined">
+                        <span class="margin_right-1 fa-solid fa-pencil-alt"></span>
+                        <span>Edit assignment</span>
+                    </button>
+                </div>
+            </div>
+        </form>
+    </dialog>
+</html>
diff --git a/src/main/resources/templates/assignment/remove.html b/src/main/resources/templates/assignment/remove.html
index e08c73676030e1508252021595325097b134647e..6840c591aa12eed72540b8d5022eb938b7ee7d00 100644
--- a/src/main/resources/templates/assignment/remove.html
+++ b/src/main/resources/templates/assignment/remove.html
@@ -1,47 +1,50 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{edition/view}">
-
-<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
-
-<!--@thymesVar id="assignment" type="nl.tudelft.labracore.api.dto.AssignmentDetailsDTO"-->
-
-<body>
-<section layout:fragment="subcontent">
-    <div class="page-sub-header">
-        <h3>Remove Assignment</h3>
-    </div>
-
-    <form th:action="@{/assignment/{id}/remove(id=${assignment.id})}"
-          class="form-horizontal"
-          method="post">
-        Are you sure you want to remove <strong
-            th:text="${'assignment #' + assignment.id + ' (' + assignment.name + ')'}"></strong>?
-        <div class="text-center">
-            <button class="btn btn-danger">Delete this Assignment</button>
-            <small>or <a
-                    th:href="@{/edition/{eId}/modules(eId=${edition.id})}">go back</a></small>
-        </div>
-    </form>
-</section>
-</body>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="remove-assignment" layout:fragment="dialog">
+        <form th:action="@{/assignment}" th:method="delete" class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Delete assignment</h2>
+                </div>
+                <button class="hidden:from_small">Delete</button>
+            </div>
+
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-6">
+                    <input name="id" type="hidden" />
+
+                    <p class="font-400">
+                        Are you sure you want to delete
+                        <span id="remove-assignment-name"></span>
+                        ?
+                    </p>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        data-style="outlined"
+                        data-type="negative">
+                        <span class="margin_right-1 fa-solid fa-trash-alt"></span>
+                        <span>Delete assignment</span>
+                    </button>
+                </div>
+            </div>
+        </form>
+    </dialog>
 </html>
diff --git a/src/main/resources/templates/container.html b/src/main/resources/templates/container.html
new file mode 100644
index 0000000000000000000000000000000000000000..c996c10b3b9245fdd24db5dbe106f77a38f1cfa7
--- /dev/null
+++ b/src/main/resources/templates/container.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{layout}">
+    <body>
+        <div class="page" layout:fragment="container">
+            <a href="#main-content" class="link jump_to_content">Skip to main content</a>
+            <th:block layout:replace="~{header :: header}"></th:block>
+            <main class="main" id="main-content">
+                <div layout:fragment="breadcrumbs" class="breadcrumbs"></div>
+                <th:block layout:fragment="content">Content goes here.</th:block>
+            </main>
+            <th:block layout:replace="~{footer :: footer}"></th:block>
+            <th:block layout:fragment="overlays"></th:block>
+            <div id="toasts"></div>
+        </div>
+    </body>
+
+    <th:block layout:fragment="scripts">
+        <th:block layout:replace="~{header :: header_script}"></th:block>
+        <th:block layout:fragment="script"></th:block>
+    </th:block>
+</html>
diff --git a/src/main/resources/templates/course/request.html b/src/main/resources/templates/course/request.html
index 09b23795143423f8b2910104234838c019a20254..c86bce98ea61fa317b16f9eab855c060fce6891c 100644
--- a/src/main/resources/templates/course/request.html
+++ b/src/main/resources/templates/course/request.html
@@ -1,86 +1,76 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org"
-      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}">
-<head>
-    <title>Request Course</title>
-</head>
-
-<!--@thymesVar id="editions" type="org.springframework.data.domain.Page<nl.tudelft.labracore.api.dto.EditionDetailsDTO>"-->
-
-<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.CourseRequestCreateDTO"-->
-
-<body>
-<section layout:fragment="content">
-    <nav role="navigation" class="breadcrumbs">
-        <ol class="breadcrumb">
-            <li class="breadcrumb-item"><a th:href="@{/}">Home</a></li>
-            <li class="breadcrumb-item"><a th:href="@{/editions}">Courses</a></li>
-            <li class="breadcrumb-item active" aria-current="page">Request Course</li>
-        </ol>
-    </nav>
-
-    <form th:action="@{/editions/request-course}"
-          th:object="${dto}"
-          class="form-horizontal" method="post">
-        <div class="form-group form-row">
-            <label for="name-input" class="col-sm-2 col-form-label">Name:</label>
-            <div class="col-sm-10">
-                <input id="name-input"
-                       type="text" class="form-control"
-                       required th:field="*{name}"
-                       placeholder="The full name of the course"/>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="request-course" layout:fragment="dialog">
+        <form th:action="@{/editions/request-course}" method="post" class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Request course</h2>
+                </div>
+                <button class="hidden:from_small">Send</button>
             </div>
-        </div>
 
-        <div class="form-group form-row">
-            <label for="code-input" class="col-sm-2 col-form-label">Code:</label>
-            <div class="col-sm-10">
-                <input id="code-input"
-                       type="text" class="form-control"
-                       required th:field="*{code}"
-                       placeholder="The course code (i.e. IT1100)"/>
-            </div>
-        </div>
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-7">
+                    <div class="flex_column gap-1">
+                        <label for="course-name" class="form__label">Name</label>
+                        <input
+                            class="textfield"
+                            id="course-name"
+                            name="name"
+                            placeholder="Course name"
+                            data-style="outlined"
+                            required />
+                    </div>
 
-        <div class="form-group form-row">
-            <label for="remarks-input" class="col-sm-2 col-form-label">Remarks:</label>
-            <div class="col-sm-10">
-                <input id="remarks-input"
-                       type="text" class="form-control"
-                       th:field="*{remarks}"
-                       placeholder="Any remarks you want to give regarding your course."/>
-            </div>
-        </div>
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="course-code">Course code</label>
+                        <input
+                            class="textfield"
+                            id="course-code"
+                            name="code"
+                            placeholder="Course code"
+                            data-style="outlined"
+                            required />
+                    </div>
 
-        <div class="form-group form-row">
-            <div class="col-sm-offset-2 col-sm-8">
-                <button type="submit" class="btn btn-primary ctrl-enter-submit">
-                    Request new Course
-                </button>
-                <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script>
-            </div>
-        </div>
-    </form>
-</section>
-</body>
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="course-remarks">Remarks</label>
+                        <textarea
+                            class="textfield"
+                            id="course-remarks"
+                            name="remarks"
+                            placeholder="Any remarks regarding your course"
+                            data-style="outlined"
+                            rows="3"
+                            style="resize: none"></textarea>
+                    </div>
+                </div>
 
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        data-style="outlined"
+                        type="submit">
+                        <span class="margin_right-1 fa-solid fa-paper-plane"></span>
+                        <span>Send request</span>
+                    </button>
+                </div>
+            </div>
+        </form>
+    </dialog>
 </html>
diff --git a/src/main/resources/templates/edition/create.html b/src/main/resources/templates/edition/create.html
index a83ee67616caaec00a20d30407d93e7fe68aad9c..631d0a60ce38ae69dd105c71b79f48a4c7123e0b 100644
--- a/src/main/resources/templates/edition/create.html
+++ b/src/main/resources/templates/edition/create.html
@@ -1,165 +1,168 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{admin/view}">
-
-<!--@thymesVar id="edition" type="nl.tudelft.queue.dto.create.QueueEditionCreateDTO"-->
-
-<!--@thymesVar id="cohorts" type="java.util.List<nl.tudelft.labracore.api.dto.CohortSummaryDTO>"-->
-<!--@thymesVar id="courses" type="java.util.List<nl.tudelft.labracore.api.dto.CourseSummaryDTO>"-->
-
-<head>
-    <script src="/webjars/momentjs/min/moment.min.js"></script>
-
-    <script type="application/javascript"
-            src="/webjars/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.min.js"></script>
-    <link rel="stylesheet" href="/webjars/tempusdominus-bootstrap-4/css/tempusdominus-bootstrap-4.min.css"/>
-
-    <script type="application/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
-    <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"/>
-</head>
-
-<body>
-<section layout:fragment="subcontent">
-    <div class="page-sub-header">
-        <h3>Create edition</h3>
-    </div>
-
-    <form th:action="@{/edition/add}"
-          class="form-horizontal"
-          th:object="${edition}"
-          method="post"
-          enctype="multipart/form-data">
-        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
-
-        <div class="form-group text-danger"
-             th:if="${#lists.isEmpty(courses)}">
-            You are not the manager of any course, please
-            <a th:href="@{/editions/request-course}">
-                request a course first
-            </a>.
-        </div>
-
-        <div class="form-group"
-             th:unless="${#lists.isEmpty(courses)}">
-            Select the course under which you want to create a course edition. If your course is not in the list, or has
-            a different code, please
-            <a th:href="@{/editions/request-course}">
-                request a course first
-            </a>.
-        </div>
-
-        <div class="form-group">
-            <label for="course-select">Select the course:</label>
-            <select th:field="*{course}" id="course-select" class="selectpicker col-md-6">
-                <option value="none" disabled selected>Select course</option>
-                <option th:each="course : ${courses}" th:value="${course.id}" th:text="|${course.name} (${course.code})|"></option>
-            </select>
-        </div>
-
-        <div class="form-group">
-            <label for="edition-name">Edition name: (please name this after the year/semester, so for instance Q1 20/21,
-                this will be displayed in the course list as "[COURSE NAME] (Q1 20/21)"</label>
-            <input th:field="*{name}" type="text" id="edition-name"
-                   placeholder="Edition name" class="form-control"/>
-        </div>
-
-        <div class="form-group">
-            Pick a cohort that you will teach during this edition. If you cannot find a fitting cohort, please select
-            one of the available cohorts. This will not impact any functionality for a standard course edition.
-        </div>
-
-        <div class="form-group">
-            <label for="cohort-select">Select the cohort:</label>
-            <select th:field="*{cohort}" id="cohort-select" class="selectpicker">
-                <option value="none" disabled selected>Select cohort</option>
-                <option th:each="cohort : ${cohorts}" th:value="${cohort.id}" th:text="${cohort.name}"></option>
-            </select>
-        </div>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="create-edition" layout:fragment="dialog">
+        <form th:action="@{/edition}" method="post" class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Create course edition</h2>
+                </div>
+                <button class="hidden:from_small">Create</button>
+            </div>
 
-        <div class="form-group form-row">
-            <div id="date-input" class="col-sm-10 row">
-                <div class="col-sm-6 input-group" id="opens-at-input" data-target-input="nearest">
-                    <div class="input-group-prepend">
-                        <span class="input-group-text">From</span>
-                    </div>
-                    <input th:classappend="${#fields.hasErrors('startDate')} ? 'is-invalid'"
-                           type="text" th:field="*{startDate}"
-                           class="form-control datetimepicker-input"
-                           data-target="#opens-at-input" required/>
-                    <div class="input-group-append">
-                        <span class="input-group-text"
-                              data-target="#opens-at-input"
-                              data-toggle="datetimepicker">
-                            <i class="fa fa-calendar"></i>
-                        </span>
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-7">
+                    <div class="flex_column gap-1">
+                        <div class="flex gap-2 align_items_center">
+                            <label for="edition-name" class="form__label">Name</label>
+                            <div class="tooltip">
+                                <button
+                                    type="button"
+                                    class="fa-solid fa-info-circle font-400 margin_bottom-3"></button>
+                                <p class="font-300" style="min-width: min(75ch, 70vw)">
+                                    Name this after the year/semester/quarter, e.g. 2022/2023 Q1,
+                                    this will be displayed as "Course name - 2022/2023 Q1".
+                                </p>
+                            </div>
+                        </div>
+                        <input
+                            class="textfield"
+                            id="edition-name"
+                            name="name"
+                            placeholder="Edition name"
+                            data-style="outlined"
+                            required />
                     </div>
-                    <div class="invalid-feedback" th:if="${#fields.hasErrors('startDate')}"
-                         th:errors="*{startDate}">Slot opensAt error
+
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="edition-course">Course</label>
+                        <select
+                            id="edition-course"
+                            name="course"
+                            data-select
+                            data-style="outlined"
+                            required
+                            data-placeholder="Select course">
+                            <option data-empty-option value=""></option>
+                            <option
+                                th:each="course : ${courses}"
+                                th:value="${course.id}"
+                                th:text="${course.name}"></option>
+                        </select>
+                        <p class="font-300">
+                            Don't see your course? Click
+                            <button
+                                type="button"
+                                class="link"
+                                data-dialog="request-course"
+                                data-close-current>
+                                here
+                            </button>
+                            to request one.
+                        </p>
                     </div>
-                </div>
 
-                <div class="col-sm-6 input-group date" id="closes-at-input" data-target-input="nearest">
-                    <div class="input-group-prepend">
-                        <span class="input-group-text">To</span>
+                    <div class="flex_column gap-1">
+                        <div class="flex gap-2 align_items_center">
+                            <label for="edition-cohort" class="form__label">Cohort</label>
+                            <div class="tooltip">
+                                <button
+                                    type="button"
+                                    class="fa-solid fa-info-circle font-400 margin_bottom-3"></button>
+                                <p class="font-300" style="min-width: min(80ch, 70vw)">
+                                    Pick a cohort that you will teach during this edition. If you
+                                    cannot find a fitting cohort, please select one of the available
+                                    cohorts. This will not impact any functionality for a standard
+                                    course edition.
+                                </p>
+                            </div>
+                        </div>
+                        <select
+                            name="cohort"
+                            id="edition-cohort"
+                            data-select
+                            data-style="outlined"
+                            required
+                            data-placeholder="Select cohort">
+                            <option data-empty-option value=""></option>
+                            <option
+                                th:each="cohort : ${cohorts}"
+                                th:value="${cohort.id}"
+                                th:text="${cohort.name}"></option>
+                        </select>
                     </div>
-                    <input th:classappend="${#fields.hasErrors('endDate')} ? 'is-invalid'"
-                           type="text" th:field="*{endDate}"
-                           class="form-control datetimepicker-input"
-                           data-target="#closes-at-input" required/>
-                    <div class="input-group-append">
-                        <span class="input-group-text"
-                              data-target="#closes-at-input"
-                              data-toggle="datetimepicker">
-                            <i class="fa fa-calendar"></i>
-                        </span>
+
+                    <div class="flex_column gap-1">
+                        <div class="flex gap-2 align_items_center">
+                            <label for="edition-start" class="form__label">Start date</label>
+                            <div class="tooltip">
+                                <button
+                                    type="button"
+                                    class="fa-solid fa-info-circle font-400 margin_bottom-3"></button>
+                                <p class="font-300" style="min-width: min(80ch, 70vw)">
+                                    This is the time from which the course edition will be visible.
+                                    It is recommended setting this time a bit before the the start
+                                    of your course.
+                                </p>
+                            </div>
+                        </div>
+                        <input
+                            class="textfield"
+                            type="datetime-local"
+                            id="edition-start"
+                            name="startDate"
+                            data-style="outlined"
+                            required />
                     </div>
-                    <div class="invalid-feedback" th:if="${#fields.hasErrors('endDate')}"
-                         th:errors="*{endDate}">Slot error
+
+                    <div class="flex_column gap-1">
+                        <div class="flex gap-2 align_items_center">
+                            <label for="edition-end" class="form__label">End date</label>
+                            <div class="tooltip">
+                                <button
+                                    type="button"
+                                    class="fa-solid fa-info-circle font-400 margin_bottom-3"></button>
+                                <p class="font-300" style="min-width: min(80ch, 70vw)">
+                                    This is the time until which the course edition will be visible.
+                                    It is recommended setting this time a bit after the last resit
+                                    for your course.
+                                </p>
+                            </div>
+                        </div>
+                        <input
+                            class="textfield"
+                            type="datetime-local"
+                            id="edition-end"
+                            name="endDate"
+                            data-style="outlined"
+                            required />
                     </div>
                 </div>
-            </div>
-        </div>
 
-        <div class="form-group">
-            <div class="float-right">
-                <input type="submit" class="btn btn-primary" value="Create edition"/>
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        data-style="outlined"
+                        type="submit">
+                        <span class="margin_right-1 fa-solid fa-plus"></span>
+                        <span>Create edition</span>
+                    </button>
+                </div>
             </div>
-        </div>
-    </form>
-
-    <!-- Invisible column for spacing at the bottom of the page -->
-    <div class="row mt-3"></div>
-    <script th:inline="javascript" type="text/javascript">
-        //<![CDATA[
-
-        $(function () {
-            const opensAtInput = $("#opens-at-input");
-            const closesAtInput = $("#closes-at-input");
-            const timePickerInputs = [opensAtInput, closesAtInput];
-
-            timePickerInputs.forEach(input => input.datetimepicker({format: "DD-MM-YYYY HH:mm"}));
-        })
-        //]]>
-    </script>
-</section>
-</body>
+        </form>
+    </dialog>
 </html>
diff --git a/src/main/resources/templates/edition/edit.html b/src/main/resources/templates/edition/edit.html
new file mode 100644
index 0000000000000000000000000000000000000000..32d59dabb1f0c4ef22f265bddce94dc1b238a410
--- /dev/null
+++ b/src/main/resources/templates/edition/edit.html
@@ -0,0 +1,159 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="edit-edition" layout:fragment="dialog">
+        <form th:action="@{/edition}" th:method="patch" class="dialog">
+            <input type="hidden" name="edition" th:value="${edition.id}" />
+
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Edit course edition</h2>
+                </div>
+                <button class="hidden:from_small">Edit</button>
+            </div>
+
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-7">
+                    <div class="flex_column gap-1">
+                        <div class="flex gap-2 align_items_center">
+                            <label for="edition-name" class="form__label">Name</label>
+                            <div class="tooltip">
+                                <button
+                                    type="button"
+                                    class="fa-solid fa-info-circle font-400 margin_bottom-3"></button>
+                                <p class="font-300" style="min-width: min(75ch, 70vw)">
+                                    Name this after the year/semester/quarter, e.g. 2022/2023 Q1,
+                                    this will be displayed as "Course name - 2022/2023 Q1".
+                                </p>
+                            </div>
+                        </div>
+                        <input
+                            class="textfield"
+                            id="edition-name"
+                            name="name"
+                            placeholder="Edition name"
+                            data-style="outlined"
+                            required
+                            th:value="${edition.name}" />
+                    </div>
+
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="edition-course">Course</label>
+                        <select
+                            id="edition-course"
+                            name="course"
+                            data-select
+                            data-style="outlined"
+                            required
+                            data-placeholder="Select course">
+                            <option
+                                th:each="course : ${courses}"
+                                th:value="${course.id}"
+                                th:text="${course.name}"
+                                th:selected="${edition.course.id == course.id}"></option>
+                        </select>
+                    </div>
+
+                    <div class="flex_column gap-1">
+                        <div class="flex gap-2 align_items_center">
+                            <label for="edition-cohort" class="form__label">Cohort</label>
+                            <div class="tooltip">
+                                <button
+                                    type="button"
+                                    class="fa-solid fa-info-circle font-400 margin_bottom-3"></button>
+                                <p class="font-300" style="min-width: min(80ch, 70vw)">
+                                    Pick a cohort that you will teach during this edition. If you
+                                    cannot find a fitting cohort, please select one of the available
+                                    cohorts. This will not impact any functionality for a standard
+                                    course edition.
+                                </p>
+                            </div>
+                        </div>
+                        <select
+                            name="cohort"
+                            id="edition-cohort"
+                            data-select
+                            data-style="outlined"
+                            required
+                            data-placeholder="Select cohort">
+                            <option
+                                th:each="cohort : ${cohorts}"
+                                th:value="${cohort.id}"
+                                th:text="${cohort.name}"
+                                th:selected="${edition.cohort.id == cohort.id}"></option>
+                        </select>
+                    </div>
+
+                    <div class="flex_column gap-1">
+                        <div class="flex gap-2 align_items_center">
+                            <label for="edition-start" class="form__label">Start date</label>
+                            <div class="tooltip">
+                                <button
+                                    type="button"
+                                    class="fa-solid fa-info-circle font-400 margin_bottom-3"></button>
+                                <p class="font-300" style="min-width: min(80ch, 70vw)">
+                                    This is the time from which the course edition will be visible.
+                                    It is recommended setting this time a bit before the the start
+                                    of your course.
+                                </p>
+                            </div>
+                        </div>
+                        <input
+                            class="textfield"
+                            type="datetime-local"
+                            id="edition-start"
+                            name="startDate"
+                            data-style="outlined"
+                            th:value="${#temporals.format(edition.startDate, &quot;yyyy-MM-dd'T'HH:mm&quot;)}"
+                            required />
+                    </div>
+
+                    <div class="flex_column gap-1">
+                        <div class="flex gap-2 align_items_center">
+                            <label for="edition-end" class="form__label">End date</label>
+                            <div class="tooltip">
+                                <button
+                                    type="button"
+                                    class="fa-solid fa-info-circle font-400 margin_bottom-3"></button>
+                                <p class="font-300" style="min-width: min(80ch, 70vw)">
+                                    This is the time until which the course edition will be visible.
+                                    It is recommended setting this time a bit after the last resit
+                                    for your course.
+                                </p>
+                            </div>
+                        </div>
+                        <input
+                            class="textfield"
+                            type="datetime-local"
+                            id="edition-end"
+                            name="endDate"
+                            data-style="outlined"
+                            th:value="${#temporals.format(edition.endDate, &quot;yyyy-MM-dd'T'HH:mm&quot;)}"
+                            required />
+                    </div>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button class="button padding_inline-5 padding_block-2" data-style="outlined">
+                        <span class="margin_right-1 fa-solid fa-pencil-alt"></span>
+                        <span>Edit edition</span>
+                    </button>
+                </div>
+            </div>
+        </form>
+    </dialog>
+</html>
diff --git a/src/main/resources/templates/edition/enrol.html b/src/main/resources/templates/edition/enrol.html
index 7029e5f84a9a5955f715b1371334ab6a7ac6f383..803dfc345ad94deb33533dfbd1d6bedbc2b81c90 100644
--- a/src/main/resources/templates/edition/enrol.html
+++ b/src/main/resources/templates/edition/enrol.html
@@ -1,50 +1,47 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}">
-
-<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
-
-<body>
-    <section layout:fragment="content">
-        <nav role="navigation" class="breadcrumbs">
-            <ol class="breadcrumb">
-                <li class="breadcrumb-item"><a href="/">Home</a></li>
-                <li class="breadcrumb-item"><a href="/editions">Courses</a></li>
-                <li class="breadcrumb-item"><a href="#" th:href="@{/edition/{id}/enrol(id=${edition.id})}" th:text="${edition.course.id}"></a></li>
-                <li class="breadcrumb-item active">Enrol</li>
-            </ol>
-        </nav>
-
-        <div class="page-header">
-            <h1>Enrol</h1>
-        </div>
-
-        <p>Do you wish to enrol for the course edition <strong th:text="${edition.course.name}"></strong>?</p>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="enrol" layout:fragment="dialog">
+        <form th:action="@{/edition/enrol}" method="post" class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Enrol for edition</h2>
+                </div>
+                <button class="hidden:from_small">Enrol</button>
+            </div>
 
-        <form th:action="@{/edition/{id}/enrol(id=${edition.id})}" class="form-horizontal" method="post">
-            <div class="text-center">
-                <button type="submit" name="enrol" class="btn btn-success">Enrol</button>
-                <small> or <a th:href="@{/editions}">go back</a></small>
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-6">
+                    <input name="editionId" type="hidden" />
+
+                    <p class="font-400">
+                        Are you sure you want to enrol for
+                        <span id="enrol-edition-name"></span>
+                        ?
+                    </p>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button class="button padding_inline-5 padding_block-2" data-style="outlined">
+                        <span class="margin_right-1 fa-solid fa-plus"></span>
+                        <span>Enrol</span>
+                    </button>
+                </div>
             </div>
         </form>
-    </section>
-</body>
+    </dialog>
 </html>
diff --git a/src/main/resources/templates/edition/index.html b/src/main/resources/templates/edition/index.html
index 7f2200039562cd3bd7e8d8571c91695e941a0261..1d3671e7c0498316499675ced9cee528196ce5c9 100644
--- a/src/main/resources/templates/edition/index.html
+++ b/src/main/resources/templates/edition/index.html
@@ -1,166 +1,105 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org"
-      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}">
-<head>
-    <title>Editions</title>
-
-    <script type="text/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
-    <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css" type="text/css"/>
-</head>
-
-<!--@thymesVar id="editions" type="org.springframework.data.domain.Page<nl.tudelft.labracore.api.dto.EditionDetailsDTO>"-->
-
-<!--@thymesVar id="programs" type="java.util.List<nl.tudelft.labracore.api.dto.ProgramSummaryDTO>"-->
-
-<!--@thymesVar id="filter" type="nl.tudelft.queue.dto.util.EditionFilterDTO"-->
-
-<body>
-<section layout:fragment="content">
-    <nav role="navigation" class="breadcrumbs">
-        <ol class="breadcrumb">
-            <li class="breadcrumb-item"><a href="/">Home</a></li>
-            <li class="breadcrumb-item active" aria-current="page">Courses</li>
-        </ol>
-    </nav>
-
-    <div class="page-header">
-        <th:block th:if="${@permissionService.isAdminOrTeacher()}">
-            <a th:href="@{/editions/request-course}"
-               th:if="${@mailProperties.enabled}"
-               class="btn btn-primary float-right ml-1">
-                Request Course
-            </a>
-            <a th:href="@{/editions/request-course}"
-               th:unless="${@mailProperties.enabled}"
-               class="btn btn-primary float-right disabled ml-1"
-               style="pointer-events: all !important;"
-               data-toggle="tooltip" data-placement="bottom"
-               title="The Queue administrator has disabled this feature for now. Please contact them if you need it.">
-                Request Course
-            </a>
-            <a th:href="@{/edition/add}"
-               class="btn btn-primary float-right"
-               th:classappend="${!@permissionService.canCreateEdition()} ? 'disabled'"
-               th:disabled="${!@permissionService.canCreateEdition()}">
-                Create new edition
-            </a>
-        </th:block>
-        <h1>Course Editions</h1>
-    </div>
-
-    <form class="form form-inline" th:action="@{/editions/filter}" method="post">
-        <div class="form-group form-row">
-            <span class="col-form-label col-sm-4">Programmes: </span>
-            <div class="col-sm-8">
-                <select multiple class="selectpicker" size="10"
-                        id="program-select" name="programs"
-                        data-mobile="true" data-width="100%"
-                        data-selected-text-format="count > 1">
-                    <th:block th:each="program : ${programs}">
-                        <option th:value="${program.id}"
-                                th:selected="${(filter?.programs ?: {}).contains(program.id)}"
-                                th:text="${program.name}">
-                        </option>
-                    </th:block>
-                </select>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{container}">
+    <head>
+        <title>Catalogue</title>
+    </head>
+
+    <div layout:fragment="breadcrumbs" class="hidden"></div>
+
+    <th:block layout:fragment="content">
+        <h1 class="title margin_bottom-5">Course catalogue</h1>
+
+        <form
+            id="filters"
+            class="flex_column gap-3 align_items_start margin_bottom-5"
+            th:action="@{/editions/filter}"
+            method="post">
+            <div class="searchbox" data-style="floating">
+                <input
+                    aria-label="Course search"
+                    type="text"
+                    name="nameSearch"
+                    placeholder="Search"
+                    th:value="${filter.nameSearch}" />
+                <button class="fa-solid fa-search"></button>
             </div>
-        </div>
-
-        <div class="form-group">
-            <input class="form-control" name="nameSearch" th:value="${filter?.nameSearch ?: ''}">
-        </div>
-
-        <div class="form-group">
-            <div class="col-sm-2">
-                <button class="btn btn-primary" type="submit">Filter</button>
-            </div>
-        </div>
-    </form>
-
-    <!-- Shown on other devices (> phone/tablet) -->
-    <div class="d-none d-sm-block">
-        <table th:unless="${#lists.isEmpty(editions)}" class="table table-striped table-bordered">
-            <thead>
-            <tr>
-                <th>Edition</th>
-                <th>Code</th>
-                <th>Teacher</th>
-                <th></th>
-            </tr>
-            </thead>
-            <tbody>
-            <tr th:each="edition : ${editions}">
-                <td><a th:href="@{/edition/{id}(id=${edition.id})}"
-                       th:text="|${edition.course.name} (${edition.name})|"></a></td>
-                <td th:text="${edition.course.code}"></td>
-                <td th:text="${#strings.listJoin(@roleDTOService.teacherNames(edition), ', ')}"></td>
-                <td class="fit">
-                    <th:block th:unless="${@permissionService.canViewEdition(edition) || edition.isArchived }">
-                        <a class="btn btn-success"
-                           th:href="@{/edition/{id}/enrol(id=${edition.id})}">enrol</a>
-                    </th:block>
-                    <th:block th:if="${@permissionService.canViewEdition(edition)}">
-                        <a class="btn btn-primary"
-                           th:href="@{/edition/{id}(id=${edition.id})}">view</a>
-                    </th:block>
-                </td>
-            </tr>
-            </tbody>
-        </table>
-    </div>
-
-    <!-- Shown on small devices (phone/tablet) -->
-    <div class="d-block d-md-none">
-        <div th:each="edition : ${editions}" class="card"
-             th:classappend="${@permissionService.canViewEdition(edition)} ? 'bg-primary text-white' : 'bg-light'"
-             style="margin-bottom:20px;">
-            <h4 class="card-header text-dark" th:text="|${edition.course.name} (${edition.name})|"></h4>
-
-            <div class="card-body">
-                <dl>
-                    <dt class="text-dark">Teacher</dt>
-                    <dd class="truncate text-dark"
-                        th:text="${#strings.listJoin(@roleDTOService.teacherNames(edition), ', ')}"></dd>
-                </dl>
-
-            </div>
-            <div class="card-footer">
-                <div class="btn-group btn-group-justified">
-                    <th:block th:if="${@permissionService.canViewEdition(edition)}">
-                        <a th:href="@{/edition/{id}(id=${edition.id})}"
-                           class="btn btn-primary btn-sm">View</a>
-                    </th:block>
-
-                    <th:block th:unless="${@permissionService.canViewEdition(edition)}">
-                        <a th:href="@{/edition/{id}/enrol(id=${edition.id})}"
-                           class="btn btn-success btn-sm">Enrol</a>
-                    </th:block>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <nav th:include="pagination :: pagination (page=${editions}, size=3)"></nav>
-</section>
-</body>
-
+            <select
+                class="hidden"
+                aria-label="Program"
+                name="programs"
+                data-select
+                multiple
+                data-placeholder="Programs"
+                data-style="floating">
+                <option
+                    th:each="program : ${programs}"
+                    th:value="${program.id}"
+                    th:text="${program.name}"
+                    th:selected="${filter.programs?.contains(program.id)} ?: false"></option>
+            </select>
+        </form>
+
+        <th:block layout:replace="~{edition/table :: table}"></th:block>
+    </th:block>
+
+    <th:block layout:fragment="overlays">
+        <th:block layout:replace="~{edition/enrol :: dialog}"></th:block>
+    </th:block>
+
+    <script layout:fragment="script">
+        /**
+         * Replaces the current table with the new filtered table
+         */
+        function getFilteredEditions() {
+            $("#filters").submit();
+            const pagination = $(".pagination");
+            $.get(
+                `/editions/list?page=${pagination.data("page")}&size=${pagination.data(
+                    "page-size"
+                )}`,
+                html => {
+                    const element = $(html);
+                    $("#editions").replaceWith(element);
+                }
+            );
+        }
+
+        $(() => {
+            // Filter on change of programs
+            $("select[name='programs']").change(function () {
+                getFilteredEditions();
+            });
+            // Filter when user stopped typing
+            let timeout = null;
+            $("input[name='nameSearch']").keydown(function () {
+                if (timeout) clearTimeout(timeout);
+                timeout = setTimeout(() => {
+                    getFilteredEditions();
+                }, 250);
+            });
+            // Do not reload page when submitting the filters
+            $("#filters").submit(function (event) {
+                event.preventDefault();
+                $.ajax({
+                    url: "/editions/filter",
+                    type: "post",
+                    data: {
+                        nameSearch: $("input[name='nameSearch']").val(),
+                        programs: $("select[name='programs']").val().join(","),
+                    },
+                    success: () => {},
+                });
+            });
+
+            // Enrol overlay set id
+            $("[data-dialog='enrol']").click(function () {
+                $("#enrol input[name='editionId']").val($(this).data("edition"));
+                $("#enrol-edition-name").text($(this).closest("tr").find("a").text());
+            });
+        });
+    </script>
 </html>
diff --git a/src/main/resources/templates/edition/table.html b/src/main/resources/templates/edition/table.html
new file mode 100644
index 0000000000000000000000000000000000000000..ef8ace8f002dee5749a550001d0fcb6b5a47ce01
--- /dev/null
+++ b/src/main/resources/templates/edition/table.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <div id="editions" class="flex_column gap-5" layout:fragment="table">
+        <table layout:fragment="table" class="shadow rounded table table__to_cards">
+            <thead>
+                <tr class="heading">
+                    <th>Course</th>
+                    <th>Code</th>
+                    <th>Teachers</th>
+                    <th></th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr th:if="${editions.isEmpty()}">
+                    <td>No courses found</td>
+                    <td></td>
+                    <td></td>
+                    <td></td>
+                </tr>
+                <tr th:each="edition : ${editions}">
+                    <td>
+                        <a
+                            class="link"
+                            th:href="@{/edition/{id}(id=${edition.id})}"
+                            th:text="|${edition.course.name} - ${edition.name}|"></a>
+                    </td>
+                    <td th:text="${edition.course.code}"></td>
+                    <td
+                        th:text="${#strings.listJoin(@roleDTOService.teacherNames(edition), ', ')}"></td>
+                    <td class="table__actions">
+                        <button
+                            class="padding_inline-5 padding_block-1 button"
+                            data-style="outlined"
+                            data-dialog="enrol"
+                            th:data-edition="${edition.id}"
+                            th:disabled="${@permissionService.canViewEdition(edition) or edition.isArchived}">
+                            <span class="fa-solid fa-plus margin_right-1"></span>
+                            <span>Enrol</span>
+                        </button>
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+
+        <th:block layout:replace="~{pagination :: pagination(page=${editions}, size=3)}"></th:block>
+    </div>
+</html>
diff --git a/src/main/resources/templates/edition/view.html b/src/main/resources/templates/edition/view.html
index 67309819aa9cd76ff3fa7f836b0b421ee089c3ac..c83dddc0cf76285d540f92791e977abaa5c66d45 100644
--- a/src/main/resources/templates/edition/view.html
+++ b/src/main/resources/templates/edition/view.html
@@ -1,107 +1,140 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{layout}">
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{container}">
+    <head>
+        <title th:text="|${edition.course.name} - ${edition.name}|"></title>
+    </head>
 
-<!--@thymesVar id="ec" type="nl.tudelft.labracore.api.dto.EditionCollectionDetailsDTO"-->
-<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+    <div layout:fragment="breadcrumbs" class="breadcrumbs">
+        <a th:href="@{/}">My courses</a>
+        <span>&gt;</span>
+        <a
+            th:text="|${edition.course.name} - ${edition.name}|"
+            th:href="@{/edition/{id}(id=${edition.id})}"></a>
+    </div>
 
-<!--@thymesVar id="message" type="java.lang.String"-->
+    <th:block layout:fragment="content">
+        <div
+            class="flex space_between align_items_center margin_bottom-3:from_small | flex_column:small align_items_stretch:small">
+            <h1 class="title" th:text="|${edition.course.name} - ${edition.name}|"></h1>
+            <div class="flex gap-3 | flex_column:small margin_bottom-5:small gap-5:small">
+                <button
+                    th:if="${@permissionService.canManageEdition(edition.id)}"
+                    class="button padding_block-3 padding_inline-5"
+                    data-style="floating"
+                    data-dialog="edit-edition">
+                    <span class="margin_right-1 fa-solid fa-pencil-alt"></span>
+                    <span>Edit course edition</span>
+                </button>
+                <form
+                    th:if="${@permissionService.canEnrolForEdition(edition.id)}"
+                    th:action="@{/edition/enrol}"
+                    method="post"
+                    class="flex_column:small">
+                    <input name="editionId" type="hidden" th:value="${edition.id}" />
+                    <button class="button padding_block-3 padding_inline-5" data-style="floating">
+                        <span class="margin_right-1 fa-solid fa-plus"></span>
+                        <span>Enrol</span>
+                    </button>
+                </form>
+                <form
+                    th:if="${@permissionService.canLeaveEdition(edition.id)}"
+                    th:action="@{/edition/leave}"
+                    method="post"
+                    class="flex_column:small">
+                    <input name="editionId" type="hidden" th:value="${edition.id}" />
+                    <button
+                        class="button padding_block-3 padding_inline-5"
+                        data-type="negative"
+                        data-style="floating">
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Leave edition</span>
+                    </button>
+                </form>
+            </div>
+        </div>
 
-<body>
-<section layout:fragment="content">
-    <section layout:fragment="breadcrumb">
-        <nav role="navigation" class="breadcrumbs">
-            <ol class="breadcrumb">
-                <li class="breadcrumb-item"><a th:href="@{/}">Home</a></li>
-                <li class="breadcrumb-item">
-                    <a th:if="${edition != null && @permissionService.canEnrolForEdition(edition.id)}"
-                       th:href="@{/editions}">Catalog</a>
-                    <a th:unless="${edition != null && @permissionService.canEnrolForEdition(edition.id)}"
-                       th:href="@{/}">My Courses</a>
-                </li>
-                <li class="breadcrumb-item active" th:if="${ec == null}" th:text="${edition.course.code}">TI1316</li>
-                <li class="breadcrumb-item active" th:unless="${ec == null}" th:text="${ec.name}">TI1316</li>
-            </ol>
-        </nav>
-    </section>
+        <div
+            th:if="${@permissionService.canManageModules(edition.id)} and ${edition.modules.isEmpty()}"
+            class="banner margin_bottom-3"
+            data-type="error">
+            <span class="fa-solid fa-exclamation-circle"></span>
+            <p>
+                This edition does not have any modules. Without modules, it is not possible to
+                create sessions.
+            </p>
+        </div>
+        <div
+            th:if="${@permissionService.canManageAssignments(edition.id)} and ${not edition.modules.isEmpty() and assignments.isEmpty()}"
+            class="banner margin_bottom-3"
+            data-type="error">
+            <span class="fa-solid fa-exclamation-circle"></span>
+            <p>
+                This edition does not have any assignments. Without assignments, it is not possible
+                to create sessions.
+            </p>
+        </div>
+        <div
+            th:if="${edition.startDate.isAfter(#temporals.createNow()) and @permissionService.canManageEdition(edition.id)}"
+            class="banner margin_bottom-3"
+            data-type="warning">
+            <span class="fa-solid fa-exclamation-triangle"></span>
+            <p>Students cannot enrol for this edition, as it has not started yet.</p>
+        </div>
+        <div
+            th:if="${edition.endDate.isBefore(#temporals.createNow())}"
+            class="banner margin_bottom-3"
+            data-type="info">
+            <span class="fa-solid fa-info-circle"></span>
+            <p>This edition is over.</p>
+        </div>
 
-    <ul class="nav nav-tabs" th:if="${ec == null}">
-        <li class="nav-item">
-            <a href="#" class="nav-link"
-               th:classappend="${#request.requestURI.matches('.*/edition/\d+') ? 'active' : ''}"
-               th:href="@{/edition/{id}(id=${edition.id})}">
-                <i class="fa fa-th-large" aria-hidden="true"></i> Info
-            </a>
-        </li>
-        <li class="nav-item" th:if="${@permissionService.canManageParticipants(edition.id)}">
-            <a href="#" class="nav-link"
-               th:classappend="${#request.requestURI.matches('.*/participants.*') ? 'active' : ''}"
-               th:href="@{/edition/{id}/participants(id=${edition.id})}">
-                <i class="fa fa-user" aria-hidden="true"></i> Participants
+        <nav aria-label="Edition tabs" class="tabs margin_bottom-5">
+            <a
+                th:data-active="${#request.requestURI.matches('.*/labs.*')}"
+                th:href="@{/edition/{id}/labs(id=${edition.id})}"
+                th:if="${@permissionService.canViewEdition(edition.id)} and not ${assignments.isEmpty()}">
+                <span class="fa-solid fa-calendar-alt"></span>
+                <span>Sessions</span>
             </a>
-        </li>
-        <li class="nav-item" th:if="${@permissionService.canViewEdition(edition.id)}">
-            <a href="#" class="nav-link"
-               th:classappend="${#request.requestURI.matches('.*/modules.*') ? 'active' : ''}"
-               th:href="@{/edition/{id}/modules(id=${edition.id})}">
-                <i class="fa fa-boxes" aria-hidden="true"></i> Modules
+            <a
+                th:unless="${@permissionService.canViewEdition(edition.id)}"
+                th:data-active="${#request.requestURI.matches('.*/staff.*')}"
+                th:href="@{/edition/{id}/staff(id=${edition.id})}">
+                <span class="fa-solid fa-user"></span>
+                <span>Staff</span>
             </a>
-        </li>
-        <li class="nav-item" th:if="${@permissionService.canViewEdition(edition.id)}">
-            <a href="#" class="nav-link"
-               th:classappend="${#request.requestURI.matches('.*/labs.*') ? 'active' : ''}"
-               th:href="@{/edition/{id}/labs(id=${edition.id})}">
-                <i class="fa fa-calendar" aria-hidden="true"></i> Labs
+            <a
+                th:if="${@permissionService.canManageParticipants(edition.id)}"
+                th:data-active="${#request.requestURI.matches('.*/participants.*')}"
+                th:href="@{/edition/{id}/participants(id=${edition.id})}">
+                <span class="fa-solid fa-user"></span>
+                <span>Participants</span>
             </a>
-        </li>
-        <li class="nav-item" th:if="${@permissionService.canManageAssignments(edition.id)}">
-            <a href="#" class="nav-link"
-               th:classappend="${#request.requestURI.matches('.*/status.*') ? 'active' : ''}"
-               th:href="@{/edition/{id}/status(id=${edition.id})}">
-                <i class="fa fa-bar-chart" aria-hidden="true"></i> Status
+            <a
+                th:data-active="${#request.requestURI.matches('.*/modules.*')}"
+                th:href="@{/edition/{id}/modules(id=${edition.id})}"
+                th:if="${@permissionService.canViewEdition(edition.id)}">
+                <span class="fa-solid fa-boxes"></span>
+                <span>Modules</span>
             </a>
-        </li>
-        <li class="nav-item" th:if="${@permissionService.canManageQuestions(edition.id)}">
-            <a href="#" class="nav-link"
-               th:classappend="${#request.requestURI.matches('.*/questions.*') ? 'active' : ''}"
-               th:href="@{/edition/{id}/questions(id=${edition.id})}">
-                <i class="fa fa-question" aria-hidden="true"></i> Questions
+            <a
+                th:if="${@permissionService.canManageQuestions(edition.id)}"
+                th:data-active="${#request.requestURI.matches('.*/questions.*')}"
+                th:href="@{/edition/{id}/questions(id=${edition.id})}">
+                <span class="fa-solid fa-question"></span>
+                <span>Questions</span>
             </a>
-        </li>
-    </ul>
+        </nav>
 
-    <div class="alert alert-info mt-md-3" role="alert" th:unless="${#strings.isEmpty(message)}">
-        <th:block th:text="${message}">
-        </th:block>
-        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
-            <span aria-hidden="true">&times;</span>
-        </button>
-    </div>
+        <th:block layout:fragment="edition-content"></th:block>
+    </th:block>
 
-    <div class="pb-3">
-        <div layout:fragment="subcontent">
-            Sub content page
-        </div>
-    </div>
-</section>
-</body>
+    <th:block layout:fragment="overlays">
+        <th:block layout:replace="~{edition/edit :: dialog}"></th:block>
+        <th:block layout:fragment="extra_overlays"></th:block>
+    </th:block>
 </html>
diff --git a/src/main/resources/templates/edition/view/labs.html b/src/main/resources/templates/edition/view/labs.html
index 30d50704ec5ca7269cbc51ef6e73ae08d34f1235..972fb54ab655393b3767fe1a7ca57fa31f03985c 100644
--- a/src/main/resources/templates/edition/view/labs.html
+++ b/src/main/resources/templates/edition/view/labs.html
@@ -1,157 +1,109 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{edition/view}">
-
-<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
-<!--@thymesVar id="labs" type="java.util.List<nl.tudelft.queue.dto.view.QueueSessionSummaryDTO>"-->
-<!--@thymesVar id="allLabTypes" type="nl.tudelft.queue.models.enums.LabType[]"-->
-<!--@thymesVar id="queueSessionTypes" type="java.util.List<nl.tudelft.queue.model.enums.QueueSessionType>"-->
-<!--@thymesVar id="allModules" type="java.util.List<nl.tudelft.labracore.dto.view.structured.summary.ModuleSummaryDTO>"-->
-<!--@thymesVar id="modules" type="java.util.List<Long>"-->
-
-<!--@thymesVar id="alert" type="java.lang.String"-->
-
-<head>
-    <title th:text="|Labs - ${edition.course.name} (${edition.name})|"></title>
-
-    <script type="text/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
-    <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css" type="text/css"/>
-</head>
-
-<body>
-<section layout:fragment="subcontent">
-
-    <div class="alert alert-danger mt-md-3" role="alert" th:unless="${#strings.isEmpty(alert)}"
-         th:text="${alert}">
-        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
-            <span aria-hidden="true">&times;</span>
-        </button>
-    </div>
-
-    <div class="page-sub-header">
-        <div class="float-right" th:if="${@permissionService.canManageSessions(edition.id)}">
-            <a th:each="lType : ${T(nl.tudelft.queue.model.enums.QueueSessionType).values()}"
-               href="#" th:href="@{/edition/{id}/lab/create(id=${edition.id}, type=${lType.name()})}"
-               th:text="|Create ${lType.displayName} Lab|"
-               class="btn btn-primary mr-1">
-            </a>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{edition/view}">
+    <th:block layout:fragment="edition-content">
+        <div
+            class="flex space_between align_items_center margin_bottom-5 | flex_column:small align_items_stretch:small">
+            <form id="filters" class="flex gap-3 | flex_column:small gap-5:small">
+                <input type="hidden" name="edition" th:value="${edition.id}" />
+                <select
+                    name="queueSessionTypes"
+                    class="hidden"
+                    aria-label="Session type"
+                    data-select
+                    multiple
+                    data-placeholder="Session type"
+                    data-style="floating">
+                    <option
+                        th:each="type : ${T(nl.tudelft.queue.model.enums.QueueSessionType).values()}"
+                        th:value="${type}"
+                        th:selected="${param.queueSessionTypes?.toString()?.contains(type.name())} ?: false"
+                        th:text="#{|lab.type.${type.name.toLowerCase()}|}"></option>
+                </select>
+                <select
+                    name="modules"
+                    class="hidden"
+                    aria-label="Module"
+                    data-select
+                    multiple
+                    data-placeholder="Module"
+                    data-style="floating">
+                    <option
+                        th:each="module : ${allModules}"
+                        th:value="${module.id}"
+                        th:text="${module.name}"
+                        th:selected="${param.modules?.contains(module.id.toString())} ?: false"></option>
+                </select>
+            </form>
+            <div th:if="${@permissionService.canManageSessions(edition.id)}" class="flex gap-3">
+                <button
+                    class="button padding_block-3 padding_inline-5 | flex_grow:small"
+                    data-style="floating">
+                    <span class="margin_right-1 fa-solid fa-file-export"></span>
+                    <span>Export sessions</span>
+                </button>
+                <button
+                    class="button padding_block-3 padding_inline-5 | flex_grow:small"
+                    data-style="floating"
+                    data-dialog="create-session">
+                    <span class="margin_right-1 fa-solid fa-plus"></span>
+                    <span>Create session</span>
+                </button>
+            </div>
         </div>
-        <h3>Labs</h3>
-    </div>
 
-    <th:block th:unless="${#lists.isEmpty(labs) and #lists.isEmpty(queueSessionTypes) and #lists.isEmpty(modules)}">
-        <form id="filter-form" method="get" class="form-inline mb-3"
-              th:action="@{/edition/{id}/labs(id=${edition.id})}">
-            <div class="container-fluid">
-                <div class="row align-items-end">
-                    <div class="form-group col-sm-2">
-                        <label class="form-control-label" for="lab-type-select">Type</label>
-                        <select multiple class="form-control selectpicker" data-width="100%" th:name="queueSessionTypes" id="lab-type-select">
-                            <th:block th:each="labType : ${allLabTypes}">
-                                <option th:value="${labType}"
-                                        th:text="${labType.displayName}"
-                                        th:selected="${queueSessionTypes.contains(labType)}">
-                                </option>
-                            </th:block>
-                        </select>
-                    </div>
-                    <div class="form-group col-sm-2">
-                        <label class="form-control-label" for="module-select">Module</label>
-                        <select multiple class="form-control selectpicker" data-width="100%" th:name="modules" id="module-select">
-                            <th:block th:each="module : ${allModules}">
-                                <option th:value="${module.id}"
-                                        th:text="${module.name}"
-                                        th:selected="${modules.contains(module.id)}">
-                                </option>
-                            </th:block>
-                        </select>
-                    </div>
-                    <div class="col-auto">
-                        <button type="submit" class="btn btn-primary">
-                            Filter
-                        </button>
-                        <a th:unless="${#lists.isEmpty(queueSessionTypes) and #lists.isEmpty(modules)}"
-                           class="btn"
-                           th:href="@{/edition/{id}/labs(id=${edition.id})}">
-                            <i class="fa fa-times" aria-hidden="true"></i> Clear current filters
-                        </a>
-                    </div>
-                </div>
-            </div>
-        </form>
+        <th:block th:with="canManage = ${@permissionService.canManageSessions(edition.id)}">
+            <th:block layout:replace="~{edition/view/labs_tables :: tables}"></th:block>
+        </th:block>
     </th:block>
 
-    <th:block th:if="${#lists.isEmpty(labs)}">
-        No labs for this edition.
+    <th:block layout:fragment="extra_overlays">
+        <th:block layout:replace="~{lab/create :: dialog}"></th:block>
+        <th:block layout:replace="~{lab/remove :: dialog}"></th:block>
     </th:block>
 
-    <th:block th:unless="${#lists.isEmpty(labs)}">
-        <ul class="list-group sort-open">
-            <th:block th:each="lab : ${labs}">
-                <li class="list-group-item lab-item"
-                    th:classappend="${lab.slot.open()} ? 'lab-open' : 'lab-closed'">
-                    <a href="#" th:href="@{/lab/{id}(id=${lab.id})}"
-                       th:text="|${lab.name} ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|">
-                    </a>
-
-                    <span class="badge badge-pill badge-info text-white"
-                          th:if="${lab.slot.open()}">Active</span>
-                    <span class="badge badge-pill badge-info text-white"
-                          th:if="${lab.slot.closed()}">Completed</span>
-
-                    <span class="badge badge-pill badge-danger text-white"
-                          th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).EXAM}">Exam</span>
-                    <span class="badge badge-pill badge-info text-white"
-                          th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).SLOTTED}">Slotted</span>
-                    <span class="badge badge-pill badge-warning text-white"
-                          th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).CAPACITY}">Limited Capacity</span>
-
-                    <span class="badge badge-pill badge-warning text-muted"
-                          th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">Open for enqueueing</span>
-
-                    <div class="btn-group float-right">
-                        <th:block th:if="${@permissionService.canManageSessions(edition.id)}">
-                            <a th:href="@{/lab/{id}/edit(id=${lab.id})}" class="btn btn-sm btn-secondary">
-                                <i class="fa fa-pencil" aria-hidden="true"></i>
-                            </a>
-                        </th:block>
-                        <th:block th:if="${@permissionService.canManageSessions(edition.id)}">
-                            <a th:href="@{/lab/{id}/remove(id=${lab.id})}"
-                               class="btn btn-sm btn-danger">
-                                <i class="fa fa-trash-o" aria-hidden="true"></i>
-                            </a>
-                        </th:block>
-                        <th:block th:if="${@permissionService.canManageSessions(edition.id)}">
-                            <a href="#"
-                               th:href="@{/lab/{labId}/copy(labId=${lab.id})}"
-                               class="btn btn-sm btn-primary">
-                                <i class="fa fa-copy" aria-hidden="true"></i>
-                            </a>
-                        </th:block>
-                    </div>
-                </li>
-            </th:block>
-        </ul>
+    <th:block layout:fragment="script">
+        <th:block layout:replace="~{lab/create :: create_script}"></th:block>
+
+        <script>
+            $(() => {
+                // Filter on change of sessions
+                $("#filters select[name='queueSessionTypes']").change(function () {
+                    $("#filters").submit();
+                });
+                // Filter on change of modules
+                $("#filters select[name='modules']").change(function () {
+                    $("#filters").submit();
+                });
+                // Do not reload page when submitting the filters
+                $("#filters").submit(function (event) {
+                    event.preventDefault();
+                    const queueSessionTypes = $("select[name='queueSessionTypes']").val().join(",");
+                    const modules = $("select[name='modules']").val().join(",");
+                    window.history.replaceState(
+                        null,
+                        null,
+                        `?queueSessionTypes=${queueSessionTypes}&modules=${modules}`
+                    );
+                    $.ajax({
+                        url: `/edition/${$("input[name='edition']").val()}/labs/list`,
+                        type: "get",
+                        data: {
+                            queueSessionTypes: queueSessionTypes,
+                            modules: modules,
+                        },
+                        success: html => {
+                            $("#labs").replaceWith($(html));
+                            $("#labs [data-dialog]").click(function () {
+                                openDialog($(this).data("dialog"));
+                            });
+                        },
+                    });
+                });
+            });
+        </script>
     </th:block>
-</section>
-</body>
 </html>
diff --git a/src/main/resources/templates/edition/view/labs_table.html b/src/main/resources/templates/edition/view/labs_table.html
new file mode 100644
index 0000000000000000000000000000000000000000..0947c62c28af2ab1dbc221752e085fd3359ea898
--- /dev/null
+++ b/src/main/resources/templates/edition/view/labs_table.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <table
+        class="subgrid rounded shadow table full_width gap-0 table__to_list"
+        layout:fragment="table(labs, title)">
+        <thead class="subgrid full_width">
+            <tr class="subgrid heading full_width">
+                <th class="full_width:small" th:text="${title}"></th>
+                <th class="hidden:small">From</th>
+                <th class="hidden:small">To</th>
+                <th class="hidden:small"></th>
+            </tr>
+        </thead>
+        <tbody class="subgrid full_width">
+            <tr th:if="${labs.isEmpty()}" class="full_width subgrid">
+                <td class="full_width">No sessions</td>
+            </tr>
+            <tr th:each="lab : ${labs}" class="subgrid full_width align_items_stretch">
+                <td class="flex align_items_center hidden:from_small full_width" style="--gap: 6px">
+                    <a
+                        class="link"
+                        th:href="@{/lab/{id}(id=${lab.id})}"
+                        th:text="|${lab.name} - ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')}|"></a>
+                    <th:block layout:replace="~{edition/view/labs_table :: tags}"></th:block>
+                </td>
+                <td class="flex align_items_center hidden:small" style="--gap: 6px">
+                    <a class="link" th:href="@{/lab/{id}(id=${lab.id})}" th:text="${lab.name}"></a>
+                    <th:block layout:replace="~{edition/view/labs_table :: tags}"></th:block>
+                </td>
+                <td
+                    class="flex align_items_center hidden:small"
+                    th:text="${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')}"></td>
+                <td
+                    class="flex align_items_center hidden:small"
+                    th:text="${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')}"></td>
+                <td class="table__actions | full_width:small">
+                    <th:block th:if="${canManage} and ${shared != true or lab.isShared}">
+                        <button
+                            class="button padding_inline-4 padding_block-1"
+                            data-style="outlined"
+                            data-dialog="create-session"
+                            th:data-session="${lab.id}"
+                            data-wizard-type="copy">
+                            <span class="fa-solid fa-copy margin_right-1"></span>
+                            <span>Copy</span>
+                        </button>
+                        <button
+                            class="button padding_inline-4 padding_block-1"
+                            data-style="outlined"
+                            data-dialog="create-session"
+                            th:data-session="${lab.id}"
+                            data-wizard-type="edit">
+                            <span class="fa-solid fa-pencil-alt margin_right-1"></span>
+                            <span>Edit</span>
+                        </button>
+                        <button
+                            class="button padding_inline-4 padding_block-1"
+                            data-style="outlined"
+                            data-type="negative"
+                            data-dialog="remove-session"
+                            th:data-session="${lab.id}">
+                            <span class="fa-solid fa-trash-alt margin_right-1"></span>
+                            <span>Delete</span>
+                        </button>
+                    </th:block>
+                </td>
+            </tr>
+        </tbody>
+    </table>
+
+    <th:block layout:fragment="tags">
+        <span th:if="${lab.isShared}" class="chip">Shared</span>
+
+        <span
+            class="chip"
+            th:unless="${lab.type.name() == 'REGULAR'}"
+            th:text="#{|lab.type.${lab.type.name().toLowerCase()}|}"></span>
+
+        <span
+            class="chip"
+            data-type="positive"
+            th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">
+            Open
+        </span>
+    </th:block>
+</html>
diff --git a/src/main/resources/templates/edition/view/labs_tables.html b/src/main/resources/templates/edition/view/labs_tables.html
new file mode 100644
index 0000000000000000000000000000000000000000..e7d65606f46f4660326c3625cb747ea8bd556f2d
--- /dev/null
+++ b/src/main/resources/templates/edition/view/labs_tables.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <div id="labs" layout:fragment="tables" class="grid gap-5 columns-4">
+        <th:block
+            layout:replace="~{edition/view/labs_table :: table(labs=${currentSessions}, title='Current sessions')}"></th:block>
+        <th:block
+            layout:replace="~{edition/view/labs_table :: table(labs=${upcomingSessions}, title='Upcoming sessions')}"></th:block>
+        <th:block
+            layout:replace="~{edition/view/labs_table :: table(labs=${pastSessions}, title='Past sessions')}"></th:block>
+
+        <script>
+            $(() => {
+                $("[data-session]").click(async function () {
+                    wizardType = $(this).data("wizard-type");
+                    $("[data-create-session-wizard]").each(function () {
+                        if (!$(this).data("create-session-wizard").includes(wizardType))
+                            $(this).addClass("hidden");
+                    });
+                    const sessionId = $(this).data("session");
+                    $("#create-session-id").val(sessionId);
+                    const session = await fetch(`/lab/json/${sessionId}`).then(res => res.json());
+                    sessionType = session.type;
+                    getForms().forEach(f => getForm(f).trigger("edit", [session]));
+                    getForm("type").trigger("edit", [session]);
+                });
+
+                $("[data-dialog='remove-session']").click(function () {
+                    $("#remove-session input[name='id']").val($(this).data("session"));
+                    $("#remove-session-name").text(
+                        $(this).closest("tr").find("td:nth-child(2) > a").text()
+                    );
+                });
+            });
+        </script>
+    </div>
+</html>
diff --git a/src/main/resources/templates/edition/view/modules.html b/src/main/resources/templates/edition/view/modules.html
index ebb30c39dec8e9dcb0f9272995a0212649f97784..df936339ac2c2081d68f2cf55ac54f001001a032 100644
--- a/src/main/resources/templates/edition/view/modules.html
+++ b/src/main/resources/templates/edition/view/modules.html
@@ -1,159 +1,181 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{edition/view}">
-
-<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
-
-<!--@thymesVar id="modules" type="java.util.List<nl.tudelft.labracore.api.dto.ModuleDetailsDTO>"-->
-<!--@thymesVar id="groups", type="java.util.Map<java.lang.Long, java.util.List<nl.tudelft.labracore.api.dto.StudentGroupDetailsDTO>>"-->
-
-<head>
-    <title th:text="|Modules - ${edition.course.name} (${edition.name})|"></title>
-</head>
-
-<body>
-<section layout:fragment="subcontent">
-    <div class="page-sub-header">
-        <a class="btn btn-primary float-right"
-           th:href="@{/edition/{id}/modules/create(id=${edition.id})}"
-           th:if="${@permissionService.canManageModules(edition.id)}">
-            Create Module
-        </a>
-        <h3>Modules</h3>
-    </div>
-
-    <h5 th:if="${#lists.isEmpty(modules)}">
-        No modules.
-        <th:block th:if="${@permissionService.canManageModules(edition.id)}">
-            Click on the "Create Module" button to make the first module.
-        </th:block>
-    </h5>
-
-    <div th:unless="${#lists.isEmpty(modules)}" class="row">
-        <div class="col-md-3 mb-2">
-            <div class="list-group" role="tablist">
-                <th:block th:each="_module, idx : ${modules}">
-                    <a class="list-group-item list-group-item-action"
-                       th:classappend="${idx.first ? 'active' : ''}"
-                       role="tab" data-toggle="list"
-                       th:href="'#module-' + ${_module.id}" th:text="${_module.name}"></a>
-                </th:block>
-            </div>
-        </div>
-
-        <div class="col-md-9">
-            <div class="tab-content">
-                <div th:each="m, idx : ${modules}"
-                     class="tab-pane fade" th:classappend="${idx.first ? 'active show' : ''}"
-                     role="tabpanel"
-                     th:id="'module-' + ${m.id}">
-
-                    <div>
-                        <a class="btn btn-danger btn-sm float-right"
-                           th:if="${@permissionService.canManageModule(m.id)}"
-                           th:href="@{/module/{mId}/remove(mId=${m.id})}"
-                           th:text="|Remove module - ${m.name}|">
-                        </a>
-                    </div>
-
-                    <div>
-                        <a class="btn btn-primary btn-sm float-right mr-4"
-                           th:if="${@permissionService.canManageModule(m.id)}"
-                           th:href="@{/module/{mId}/assignment/create(mId=${m.id})}">
-                            Create Assignment
-                        </a>
-                        <h3 class="ml-2">Assignments</h3>
-                    </div>
-
-                    <h5 th:if="${#lists.isEmpty(m.assignments)}">
-                        There are no assignments in this module.
-                    </h5>
-
-                    <div class="row col-12 mt-2" th:unless="${#lists.isEmpty(m.assignments)}">
-                        <table class="table">
-                            <thead class="thead-light">
-                            <tr>
-                                <th scope="col">#</th>
-                                <th scope="col">Name</th>
-                                <th scope="col">Description</th>
-                                <th scope="col"></th>
-                            </tr>
-                            </thead>
-                            <tr th:each="assignment, aIdx : ${m.getAssignments()}">
-                                <th scope="row" th:text="${assignment.id}"></th>
-                                <td th:text="${assignment.name}"></td>
-                                <td th:text="${assignment.description.text}"></td>
-                                <td>
-                                    <a th:href="@{/assignment/{aId}/remove(aId=${assignment.id})}"
-                                       th:if="${@permissionService.canManageModule(m.id)}"
-                                       class="btn btn-danger float-right to-be-implemented">
-                                        <i class="fa fa-trash"></i>
-                                    </a>
-                                </td>
-                            </tr>
-                        </table>
-                    </div>
-
-                    <th:block th:if="${@permissionService.canManageModule(m.id)}">
-                        <h3 class="ml-2">Student Groups</h3>
-
-                        <h5 th:if="${#lists.isEmpty(groups.get(m.id))}">
-                            There are no student groups in this module, configure them in Portal
-                        </h5>
-
-                        <div class="row col-12" th:unless="${#lists.isEmpty(groups.get(m.id))}">
-                            <table class="table">
-                                <thead class="thead-light">
-                                <tr>
-                                    <th scope="col" class="fit-width">#</th>
-                                    <th scope="col">Name</th>
-                                    <th scope="col">Members</th>
-                                    <th scope="col" class="fit">Capacity</th>
-                                </tr>
-                                </thead>
-                                <tr th:each="group, gIdx : ${groups.get(m.id)}"
-                                    th:with="students = ${@roleDTOService.studentsL1(group.members)}">
-                                    <th class="fit-width" scope="row" th:text="${group.id}"></th>
-                                    <td th:text="${group.name}"></td>
-                                    <td th:text="${#strings.listJoin(@roleDTOService.names(students), ', ')}"></td>
-                                    <td class="fit">
-                                <span class="badge"
-                                      th:classappend="${group.capacity == students.size() ? 'badge-danger' : 'badge-primary'}"
-                                      th:text="|${group.capacity} / ${group.capacity}|"></span>
-                                    </td>
-                                </tr>
-                            </table>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{edition/view}">
+    <th:block layout:fragment="edition-content">
+        <div class="flex flex_column:small">
+            <div th:class="flex_column">
+                <nav data-tabs aria-label="Modules">
+                    <ul class="nav_list" role="list" data-style="floating">
+                        <li
+                            th:each="module, iter : ${modules}"
+                            th:data-active="${iter.index == 0}"
+                            th:data-module="${module.id}"
+                            th:data-view="|module-${module.id}|">
+                            <button th:text="${module.name}"></button>
+                        </li>
+                    </ul>
+                </nav>
+                <div class="flex_column">
+                    <p th:if="${edition.modules.isEmpty()}">
+                        This edition does not have any modules
+                    </p>
+                    <th:block th:if="${@permissionService.canManageModules(edition.id)}">
+                        <button
+                            class="button padding_inline-5 padding_block-3"
+                            data-style="floating"
+                            data-dialog="create-module">
+                            <span class="margin_right-1 fa-solid fa-plus"></span>
+                            <span>Create module</span>
+                        </button>
+                        <div th:unless="${edition.modules.isEmpty()}" class="grid gap-3">
+                            <button
+                                class="button padding_inline-5 padding_block-3"
+                                data-style="floating"
+                                data-dialog="edit-module">
+                                <span class="margin_right-1 fa-solid fa-pencil-alt"></span>
+                                <span>Edit module</span>
+                            </button>
+                            <button
+                                class="button padding_inline-5 padding_block-3"
+                                data-style="floating"
+                                data-type="negative"
+                                data-dialog="remove-module">
+                                <span class="margin_right-1 fa-solid fa-trash-alt"></span>
+                                <span>Remove module</span>
+                            </button>
                         </div>
                     </th:block>
                 </div>
             </div>
-        </div>
-    </div>
 
-    <script>
-        $(function () {
-            $("[data-toggle='tooltip']").tooltip()
-        })
+            <div
+                th:id="|module-${module.id}|"
+                class="flex_column flex_grow"
+                th:each="module, iter : ${modules}"
+                th:classappend="${iter.index > 0} ? 'hidden' : ''">
+                <table class="rounded shadow table">
+                    <thead>
+                        <tr class="heading">
+                            <th>Assignments</th>
+                            <th
+                                th:if="${@permissionService.canManageAssignments(edition.id)}"
+                                class="table__actions">
+                                <button
+                                    class="button padding_inline-4 padding_block-1"
+                                    data-style="outlined"
+                                    data-dialog="create-assignment">
+                                    <span class="margin_right-1 fa-solid fa-plus"></span>
+                                    <span>Add assignment</span>
+                                </button>
+                            </th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr th:if="${module.assignments.isEmpty()}">
+                            <td>No assignments</td>
+                            <td></td>
+                        </tr>
+                        <tr
+                            th:each="assignment : ${module.assignments}"
+                            th:data-assignment="${assignment.id}"
+                            th:data-description="${assignment.description?.text}"
+                            th:data-deadline="${assignment.deadline != null} ? ${#temporals.format(assignment.deadline, &quot;yyyy-MM-dd'T'HH:mm&quot;)}">
+                            <td th:text="${assignment.name}"></td>
+                            <td
+                                th:if="${@permissionService.canManageAssignments(edition.id)}"
+                                class="table__actions">
+                                <button
+                                    class="button padding_inline-4 padding_block-1"
+                                    data-style="outlined"
+                                    data-dialog="edit-assignment">
+                                    <span class="margin_right-1 fa-solid fa-pencil-alt"></span>
+                                    <span>Edit</span>
+                                </button>
+                                <button
+                                    class="button padding_inline-4 padding_block-1"
+                                    data-style="outlined"
+                                    data-type="negative"
+                                    data-dialog="remove-assignment">
+                                    <span class="margin_right-1 fa-solid fa-trash-alt"></span>
+                                    <span>Delete</span>
+                                </button>
+                            </td>
+                        </tr>
+                    </tbody>
+                </table>
+
+                <table class="rounded shadow table">
+                    <thead>
+                        <tr class="heading">
+                            <th>Group</th>
+                            <th class="hidden:small">Members</th>
+                            <th>Capacity</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr th:if="${module.groups.isEmpty()}">
+                            <td>No groups</td>
+                            <td></td>
+                            <td></td>
+                        </tr>
+                        <tr th:each="group : ${groups[module.id]}">
+                            <td th:text="${group.name}"></td>
+                            <td
+                                class="hidden:small"
+                                th:text="${#strings.listJoin(group.members.![person.displayName], ', ')}"></td>
+                            <td th:text="|${group.members.size()} / ${group.capacity}|"></td>
+                        </tr>
+                    </tbody>
+                </table>
+            </div>
+        </div>
+    </th:block>
+
+    <th:block layout:fragment="extra_overlays">
+        <th:block layout:replace="~{module/create :: dialog}"></th:block>
+        <th:block layout:replace="~{module/edit :: dialog}"></th:block>
+        <th:block layout:replace="~{module/remove :: dialog}"></th:block>
+        <th:block layout:replace="~{assignment/create :: dialog}"></th:block>
+        <th:block layout:replace="~{assignment/edit :: dialog}"></th:block>
+        <th:block layout:replace="~{assignment/remove :: dialog}"></th:block>
+    </th:block>
+
+    <script layout:fragment="script">
+        $(() => {
+            $("[data-dialog='edit-module']").click(function () {
+                const module = $(".nav_list [data-active='true']");
+                $("#edit-module input[name='module']").val(module.data("module"));
+                $("#edit-module input[name='name']").val(module.children().text());
+            });
+            $("[data-dialog='remove-module']").click(function () {
+                const module = $(".nav_list [data-active='true']");
+                $("#remove-module input[name='id']").val(module.data("module"));
+                $("#remove-module-name").text(module.children().text());
+            });
+            $("[data-dialog='create-assignment']").click(function () {
+                $("#create-assignment input[name='module']").val(
+                    $(".nav_list [data-active='true']").data("module")
+                );
+            });
+            $("[data-dialog='edit-assignment']").click(function () {
+                const assignment = $(this).closest("tr");
+                $("#edit-assignment input[name='assignment']").val(assignment.data("assignment"));
+                $("#edit-assignment input[name='name']").val(
+                    assignment.children(":first-child").text()
+                );
+                $("#edit-assignment textarea[name='description']").val(
+                    assignment.data("description")
+                );
+                $("#edit-assignment input[name='deadline']").val(assignment.data("deadline"));
+            });
+            $("[data-dialog='remove-assignment']").click(function () {
+                const assignment = $(this).closest("tr");
+                $("#remove-assignment input[name='id']").val(assignment.data("assignment"));
+                $("#remove-assignment-name").text(assignment.children(":first-child").text());
+            });
+        });
     </script>
-</section>
-</body>
 </html>
diff --git a/src/main/resources/templates/edition/view/participants.html b/src/main/resources/templates/edition/view/participants.html
index a786b2581d13a776dba4f38f94612ca5eb7e5803..7df0bdfac5142efa662c37304d906757eec37c14 100644
--- a/src/main/resources/templates/edition/view/participants.html
+++ b/src/main/resources/templates/edition/view/participants.html
@@ -1,199 +1,131 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{edition/view}">
-
-<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
-
-<!--@thymesVar id="students" type="org.springframework.data.domain.Page<nl.tudelft.labracore.api.dto.PersonSummaryDTO>"-->
-
-<head>
-    <title th:text="|Participants - ${edition.course.name} (${edition.name})|"></title>
-</head>
-
-<body>
-<section layout:fragment="subcontent" th:with="staffTabActive = ${#httpServletRequest.getParameter('staff-tab-active')}">
-    <div class="page-sub-header">
-        <div class="float-right" style="margin-right: 10px">
-            <a href="#" th:href="@{/edition/{id}/participants/create(id=${edition.id})}"
-               class="btn btn-secondary">
-                Add participant
-            </a>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{edition/view}">
+    <th:block layout:fragment="edition-content">
+        <div th:if="${error}" class="banner margin_bottom-5" data-type="error">
+            <span class="fa-solid fa-exclamation-circle"></span>
+            <p th:text="${error}"></p>
+            <button class="fa-solid fa-xmark" style="margin-left: auto" data-remove-banner></button>
         </div>
-        <h3>Participants</h3>
-    </div>
-
-    <div class="boxed-group">
-        <ul class="nav nav-pills">
-            <li role="presentation" th:if="${staffTabActive}" class="nav-item">
-                <a class="nav-link"
-                   th:href="@{/edition/{id}/participants(id=${edition.id}, staff-tab-active=false)}">Students</a>
-            </li>
-            <li role="presentation" th:unless="${staffTabActive}" class="nav-item">
-                <a href="#" class="nav-link active">Students</a>
-            </li>
-            <li role="presentation" th:if="${staffTabActive}" class="nav-item">
-                <a href="#" class="nav-link active">Staff</a>
-            </li>
-            <li role="presentation" th:unless="${staffTabActive}" class="nav-item">
-                <a class="nav-link"
-                   th:href="@{/edition/{id}/participants(id=${edition.id}, staff-tab-active=true)}">Staff</a>
-            </li>
-        </ul>
-    </div>
 
-    <th:block th:if="${staffTabActive}">
-        <div class="boxed-group" th:with="teachers = ${@roleDTOService.teachers(edition)}">
-            <h3>Teachers</h3>
-            <div class="boxed-group-inner"
-                 th:if="${#lists.isEmpty(teachers)}">
-                There are no teachers participating in this edition.
-            </div>
-
-            <ul class="list-group"
-                th:unless="${#lists.isEmpty(teachers)}">
-                <li class="list-group-item"
-                    th:each="person : ${teachers}"
-                    th:text="${person.displayName}">
-                    <div class="float-right"
-                         th:if="${@permissionService.canManageTeachers(edition.id)}">
-                        <a class="btn btn-xs btn-danger"
-                           th:href="@{/edition/{editionId}/participants/{personId}/remove(editionId=${edition.id},personId=${person.id})}">
-                            <i class="fa fa-trash-o" aria-hidden="true"></i>
-                        </a>
-                    </div>
-                </li>
-            </ul>
-        </div>
-
-        <div class="boxed-group" th:with="headTAs = ${@roleDTOService.headTAs(edition)}">
-            <h3>Manager</h3>
-            <div class="boxed-group-inner"
-                 th:if="${#lists.isEmpty(headTAs)}">
-                There are no managers participating in this edition.
-            </div>
-
-            <ul class="list-group"
-                th:unless="${#lists.isEmpty(headTAs)}">
-                <li class="list-group-item"
-                    th:each="person : ${headTAs}">
-                    <th:block th:if="${@permissionService.canViewFeedback(person.id)}">
-                        <a th:href="@{/feedback/{id}(id=${person.id})}"
-                           th:text="${person.displayName}"></a>
-                    </th:block>
-                    <th:block th:unless="${@permissionService.canViewFeedback(person.id)}"
-                              th:text="${person.displayName}">
-                    </th:block>
-                    <div class="float-right">
-                        <a class="btn btn-xs btn-danger"
-                           th:href="@{/edition/{editionId}/participants/{id}/remove(editionId=${edition.id},id=${person.id})}">
-                            <i class="fa fa-trash-o" aria-hidden="true"></i>
-                        </a>
-                    </div>
-                </li>
-            </ul>
-        </div>
-
-        <div class="boxed-group" th:with="assistants = ${@roleDTOService.assistants(edition)}">
-            <h3>Assistants</h3>
-            <div class="boxed-group-inner"
-                 th:if="${#lists.isEmpty(assistants)}">
-                There are no assistants participating in this edition.
-            </div>
-
-            <ul class="list-group"
-                th:unless="${#lists.isEmpty(assistants)}">
-                <li class="list-group-item"
-                    th:each="person : ${assistants}">
-                    <th:block
-                            th:if="${@permissionService.canViewFeedback(person.id)}">
-                        <a th:href="@{/feedback/{id}(id=${person.id})}"
-                           th:text="${person.displayName}"></a>
-                        <!--                    <span th:text="'(' + ${feedbackCount.get(person.user.id)} + ')'" class="small">0</span>-->
-                    </th:block>
-                    <th:block th:unless="${@permissionService.canViewFeedback(person.id)}"
-                              th:text="${person.displayName}">
-                    </th:block>
-                    <div class="float-right">
-                        <a class="btn btn-xs btn-danger"
-                           th:href="@{/edition/{editionId}/participants/{id}/remove(editionId=${edition.id},id=${person.id})}">
-                            <i class="fa fa-trash-o" aria-hidden="true"></i>
-                        </a>
-                    </div>
-                </li>
-            </ul>
-        </div>
-    </th:block>
-
-    <th:block th:unless="${staffTabActive}">
-        <form action="" class="form-horizontal" method="get">
-            <div class="form-group">
-                <label for="filter">Find a student: </label>
-                <input type="text" id="filter" class="form-control" name="student-search" placeholder="Student name"
-                       th:value="${#httpServletRequest.getParameter('student-search')}"/>
+        <form id="filters" class="flex_column gap-3 align_items_start margin_bottom-5">
+            <input type="hidden" name="edition" th:value="${edition.id}" />
+            <div class="searchbox" data-style="floating">
+                <input
+                    aria-label="Participant search"
+                    type="text"
+                    name="search"
+                    placeholder="Search"
+                    th:value="${param.search}" />
+                <button class="fa-solid fa-search"></button>
             </div>
+            <select
+                class="hidden"
+                aria-label="Role"
+                name="roles"
+                data-select
+                multiple
+                data-placeholder="Role"
+                data-style="floating">
+                <option
+                    th:selected="${param.roles?.toString()?.contains('TEACHER')} ?: false"
+                    value="TEACHER">
+                    Teacher
+                </option>
+                <option
+                    th:selected="${param.roles?.toString()?.contains('HEAD_TA')} ?: false"
+                    value="HEAD_TA">
+                    Head TA
+                </option>
+                <option
+                    th:selected="${param.roles?.toString()?.contains('TA')} ?: false"
+                    value="TA">
+                    TA
+                </option>
+                <option
+                    th:selected="${param.roles?.toString()?.contains('STUDENT')} ?: false"
+                    value="STUDENT">
+                    Student
+                </option>
+            </select>
         </form>
 
-        <div class="boxed-group">
-            <h3>Students</h3>
-            <div class="boxed-group-inner"
-                 th:if="${#lists.isEmpty(students)}">
-                There are no students participating in this edition.
-            </div>
-
-            <ul class="list-group" th:unless="${students.size == 0}">
-                <li class="list-group-item" th:each="student : ${students}">
-                    <th:block th:if="${@permissionService.isAdminOrTeacher()}">
-                        <a th:href="@{/history/edition/{editionId}/student/{id}(editionId=${edition.id},id=${student.id})}"
-                           th:text="${student.displayName}"></a>
-                    </th:block>
-                    <th:block th:unless="${@permissionService.isAdminOrTeacher()}"
-                              th:text="${student.displayName}">
-                    </th:block>
-                    <!-- Disabled at the moment because of referential constraint errors in labracore (groups) -->
-<!--                    <div class="float-right">-->
-<!--                        <a class="btn btn-xs btn-danger"-->
-<!--                           th:href="@{/edition/{editionId}/participants/{id}/remove(editionId=${edition.id},id=${student.id})}">-->
-<!--                            <i class="fa fa-trash-o" aria-hidden="true"></i>-->
-<!--                        </a>-->
-<!--                    </div>-->
-                </li>
-            </ul>
-        </div>
-
-        <div th:if="${students != null && students.totalPages > 1}">
-            <th:block th:replace="pagination :: pagination (page=${students}, size=3)"></th:block>
-        </div>
+        <th:block layout:replace="~{edition/view/participants_table :: table}"></th:block>
     </th:block>
 
-    <!--    <div th:if="${cleaned != null}">-->
-    <!--        <script th:inline="javascript">-->
-    <!--            /*<![CDATA[*/-->
-    <!--            var count = /*[[${cleaned}]]*/ '0';-->
-    <!--            var message = "Cleaned " + count + " users";-->
-    <!--            alert(message);-->
-    <!--            /*]]>*/-->
-    <!--        </script>-->
-    <!--    </div>-->
+    <th:block layout:fragment="extra_overlays">
+        <th:block layout:replace="~{participant/add :: dialog}"></th:block>
+        <th:block layout:replace="~{participant/change_role :: dialog}"></th:block>
+        <th:block layout:replace="~{participant/remove :: dialog}"></th:block>
+    </th:block>
 
-</section>
-</body>
+    <script layout:fragment="script">
+        $(() => {
+            // Set change role / remove participant dialog info
+            function setUpParticipantButtons() {
+                $("[data-dialog='change-role']").click(function () {
+                    const row = $(this).closest("tr");
+                    $("#change-role input[name='person']").val(row.data("person"));
+                    const name = row.find("td:first-child span:first-child").text();
+                    $("#change-role-name").text(
+                        name.endsWith("s") || name.endsWith("x") ? name + "'" : name + "'s"
+                    );
+                });
+                $("[data-dialog='remove-participant']").click(function () {
+                    const row = $(this).closest("tr");
+                    $("#remove-participant input[name='person']").val(row.data("person"));
+                    const name = row.find("td:first-child span:first-child").text();
+                    $("#remove-participant-name").text(name);
+                });
+            }
+            setUpParticipantButtons();
+
+            // Filter on change of roles
+            $("select[name='roles']").change(function () {
+                $("#filters").submit();
+            });
+            // Filter when user stopped typing
+            let timeout = null;
+            $("input[name='search']").keydown(function () {
+                if (timeout) clearTimeout(timeout);
+                timeout = setTimeout(() => {
+                    $("#filters").submit();
+                }, 250);
+            });
+            // Do not reload page when submitting the filters
+            $("#filters").submit(function (event) {
+                event.preventDefault();
+                const size = $(".pagination").data("page-size");
+                const search = $("input[name='search']").val();
+                const roles = $("select[name='roles']").val().join(",");
+                window.history.replaceState(
+                    null,
+                    null,
+                    `?search=${search}&roles=${roles}&page=0&size=${size}`
+                );
+                $.ajax({
+                    url: `/edition/${$("input[name='edition']").val()}/participants/list`,
+                    type: "get",
+                    data: {
+                        search: search,
+                        roles: roles,
+                        page: 0,
+                        size: size,
+                    },
+                    success: html => {
+                        $("#participants").replaceWith($(html));
+                        $("#participants")
+                            .find("[data-dialog]")
+                            .click(function () {
+                                openDialog($(this).data("dialog"), false);
+                            });
+                        setUpParticipantButtons();
+                    },
+                });
+            });
+        });
+    </script>
 </html>
diff --git a/src/main/resources/templates/edition/view/participants_table.html b/src/main/resources/templates/edition/view/participants_table.html
new file mode 100644
index 0000000000000000000000000000000000000000..4c799cf5f265d9e20b4455c8845d4e8552d8a43a
--- /dev/null
+++ b/src/main/resources/templates/edition/view/participants_table.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <div id="participants" class="flex_column gap-5" layout:fragment="table">
+        <table class="rounded shadow table table__to_list">
+            <thead>
+                <tr class="heading flex:small space_between">
+                    <th class="hidden:from_small flex space_between align_items_center">
+                        Participants
+                    </th>
+                    <th class="hidden:small">Name</th>
+                    <th class="hidden:small">Role</th>
+                    <th class="table__actions">
+                        <button
+                            class="button padding_inline-5 padding_block-1"
+                            data-style="outlined"
+                            data-dialog="add-participants">
+                            <span class="margin_right-1 fa-solid fa-plus"></span>
+                            <span>Add participants</span>
+                        </button>
+                    </th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr th:if="${roles.isEmpty()}">
+                    <td>No participants found</td>
+                    <td></td>
+                    <td></td>
+                </tr>
+                <tr
+                    th:each="role : ${roles}"
+                    class="flex_column:small gap-0"
+                    th:data-person="${role.person.id}">
+                    <td class="flex:small align_items_center" style="--gap: 6px">
+                        <span th:text="${role.person.displayName}"></span>
+                        <span
+                            class="chip hidden:from_small"
+                            th:text="#{|role.${role.type.name().toLowerCase()}|}"></span>
+                    </td>
+                    <td
+                        class="hidden:small"
+                        th:text="#{|role.${role.type.name().toLowerCase()}|}"></td>
+                    <td class="table__actions">
+                        <button
+                            class="button padding_inline-4 padding_block-1"
+                            data-style="outlined"
+                            data-dialog="change-role">
+                            <span class="margin_right-1 fa-solid fa-pencil-alt"></span>
+                            <span>Change role</span>
+                        </button>
+                        <button
+                            class="button padding_inline-4 padding_block-1"
+                            data-style="outlined"
+                            data-type="negative"
+                            data-dialog="remove-participant">
+                            <span class="margin_right-1 fa-solid fa-minus"></span>
+                            <span>Remove</span>
+                        </button>
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+
+        <th:block layout:replace="~{pagination :: pagination(page=${roles}, size=3)}"></th:block>
+    </div>
+</html>
diff --git a/src/main/resources/templates/edition/view/questions.html b/src/main/resources/templates/edition/view/questions.html
index 043bfe32b473c65fbb2b48ab74e49197e0150c93..e0f13f8bc8bb35a83049364150bfa7bdf7de336c 100644
--- a/src/main/resources/templates/edition/view/questions.html
+++ b/src/main/resources/templates/edition/view/questions.html
@@ -1,76 +1,79 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{edition/view}">
-
-<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
-
-<head>
-    <title th:text="|Questions - ${edition.course.name} (${edition.name})|"></title>
-</head>
-
-<body>
-<section layout:fragment="subcontent">
-
-    <div class="page-sub-header">
-        <h3>Questions</h3>
-    </div>
-
-    <th:block th:if="${#lists.isEmpty(questions)}">
-        No questions for this edition.
-    </th:block>
-
-    <th:block th:unless="${#lists.isEmpty(questions)}">
-        <table class="table">
-            <thead class="thead-light">
-            <tr>
-                <th scope="col" class="fit-width">#</th>
-                <th scope="col">Question</th>
-                <th scope="col">Answer</th>
-                <th scope="col">Assignment</th>
-                <th scope="col">Asked at</th>
-                <th scope="col" class="fit"></th>
-            </tr>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{edition/view}">
+    <th:block layout:fragment="edition-content">
+        <table class="rounded shadow table">
+            <thead>
+                <tr class="heading">
+                    <th>Question</th>
+                    <th>Answer</th>
+                    <th>Assignment</th>
+                    <th class="table__actions">
+                        <button
+                            class="button padding_inline-4 padding_block-1"
+                            data-style="outlined"
+                            data-dialog="create-question">
+                            <span class="margin_right-1 fa-solid fa-plus"></span>
+                            <span>Add question</span>
+                        </button>
+                    </th>
+                </tr>
             </thead>
-            <tr th:each="question : ${questions}">
-                <th class="fit-width" scope="row" th:text="${question.id}"></th>
-                <td th:text="${question.question}"></td>
-                <td th:text="${question.answer} ?: 'No answer'"></td>
-                <td th:text="${question.assignment?.name} ?: 'None'"></td>
-                <td th:text="${#temporals.format(question.askedAt, 'dd MMMM yyyy HH:mm')}"></td>
-                <td class="fit">
-                    <div class="btn-group float-right">
-                        <a th:href="@{/edition/{edition}/questions/{question}/edit(edition=${edition.id},question=${question.id})}"
-                           class="btn btn-sm btn-secondary">
-                            <i class="fa fa-pencil" aria-hidden="true"></i>
-                        </a>
-                        <a th:href="@{/edition/{edition}/questions/{question}/remove(edition=${edition.id},question=${question.id})}"
-                           class="btn btn-sm btn-danger">
-                            <i class="fa fa-trash-o" aria-hidden="true"></i>
-                        </a>
-                    </div>
-                </td>
-            </tr>
+            <tbody>
+                <tr th:if="${questions.isEmpty()}">
+                    <td>No questions</td>
+                    <td></td>
+                    <td></td>
+                    <td></td>
+                </tr>
+                <tr th:each="question : ${questions}">
+                    <td th:text="${question.question}"></td>
+                    <td th:text="${question.answer}"></td>
+                    <td th:text="${question.assignment?.name}"></td>
+                    <td class="table__actions">
+                        <button
+                            class="button padding_inline-4 padding_block-1"
+                            data-style="outlined"
+                            data-dialog="edit-question"
+                            th:data-question="${question.id}">
+                            <span class="margin_right-1 fa-solid fa-pencil-alt"></span>
+                            <span>Edit question</span>
+                        </button>
+                        <button
+                            class="button padding_inline-4 padding_block-1"
+                            data-type="negative"
+                            data-style="outlined"
+                            data-dialog="remove-question"
+                            th:data-question="${question.id}">
+                            <span class="margin_right-1 fa-solid fa-trash-alt"></span>
+                            <span>Delete question</span>
+                        </button>
+                    </td>
+                </tr>
+            </tbody>
         </table>
     </th:block>
 
-</section>
-</body>
+    <th:block layout:fragment="extra_overlays">
+        <th:block th:replace="~{question/create :: dialog}"></th:block>
+        <th:block th:replace="~{question/edit :: dialog}"></th:block>
+        <th:block th:replace="~{question/remove :: dialog}"></th:block>
+    </th:block>
+
+    <script layout:fragment="script">
+        $(() => {
+            $("[data-dialog='edit-question']").click(function () {
+                const row = $(this).closest("tr");
+                $("#edit-question-id").val($(this).data("question"));
+                $("#edit-question-question").val(row.children()[0].text());
+                $("#edit-question-answer").val(row.children()[1].text());
+            });
+            $("[data-dialog='remove-question']").click(function () {
+                $("#remove-question-id").val($(this).data("question"));
+            });
+        });
+    </script>
 </html>
diff --git a/src/main/resources/templates/edition/view/staff.html b/src/main/resources/templates/edition/view/staff.html
new file mode 100644
index 0000000000000000000000000000000000000000..d100d5e8965983e1560b6da2a8209a6bf0a1cc77
--- /dev/null
+++ b/src/main/resources/templates/edition/view/staff.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{edition/view}">
+    <div layout:fragment="edition-content" class="flex_column">
+        <div class="rounded shadow flex_column gap-0">
+            <h2 class="heading">Teachers</h2>
+            <div class="grid info_card auto_fill">
+                <span class="padding-5" th:if="${teachers.isEmpty()}">No teachers</span>
+                <span
+                    class="padding-5"
+                    th:each="teacher : ${teachers.![person]}"
+                    th:text="${teacher.displayName}"></span>
+            </div>
+        </div>
+
+        <div class="rounded shadow flex_column gap-0">
+            <h2 class="heading">Head TAs</h2>
+            <div class="grid info_card auto_fill">
+                <span class="padding-5" th:if="${headTas.isEmpty()}">No head TAs</span>
+                <span
+                    class="padding-5"
+                    th:each="headTa : ${headTas.![person]}"
+                    th:text="${headTa.displayName}"></span>
+            </div>
+        </div>
+
+        <div class="rounded shadow flex_column gap-0">
+            <h2 class="heading">TAs</h2>
+            <div class="grid info_card auto_fill">
+                <span class="padding-5" th:if="${tas.isEmpty()}">No TAs</span>
+                <span
+                    class="padding-5"
+                    th:each="ta : ${tas.![person]}"
+                    th:text="${ta.displayName}"></span>
+            </div>
+        </div>
+    </div>
+</html>
diff --git a/src/main/resources/templates/edition_collection/create.html b/src/main/resources/templates/edition_collection/create.html
new file mode 100644
index 0000000000000000000000000000000000000000..5e4aa99788f0d9ce1e0e1084e46b2f6734cced1e
--- /dev/null
+++ b/src/main/resources/templates/edition_collection/create.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="create-edition-collection" layout:fragment="dialog">
+        <form th:action="@{/shared-edition}" method="post" class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Create course collection</h2>
+                </div>
+                <button class="hidden:from_small">Create</button>
+            </div>
+
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-7">
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="edition-collection-name">Name</label>
+                        <input
+                            class="textfield"
+                            id="edition-collection-name"
+                            name="name"
+                            placeholder="Collection name"
+                            data-style="outlined"
+                            required />
+                    </div>
+
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="edition-collection-editions">
+                            Course editions
+                        </label>
+                        <select
+                            id="edition-collection-editions"
+                            name="editions"
+                            data-select
+                            multiple
+                            data-style="outlined"
+                            data-placeholder="Course editions">
+                            <option
+                                th:each="edition : ${allEditions}"
+                                th:value="${edition.id}"
+                                th:text="|${edition.course.name} - ${edition.name}|"></option>
+                        </select>
+                    </div>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button class="button padding_inline-5 padding_block-2" data-style="outlined">
+                        <span class="margin_right-1 fa-solid fa-plus"></span>
+                        <span>Create collection</span>
+                    </button>
+                </div>
+            </div>
+        </form>
+    </dialog>
+</html>
diff --git a/src/main/resources/templates/error/400.html b/src/main/resources/templates/error/400.html
index 3e1e0f6fee5a9ca63fc7b0260910d51902abed8a..e9c6501e936ca95cd297ec57afd10b73ea11909a 100644
--- a/src/main/resources/templates/error/400.html
+++ b/src/main/resources/templates/error/400.html
@@ -18,22 +18,45 @@
 
 -->
 <!DOCTYPE html>
-<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{layout}">
-<head>
-    <title>Queue: Bad Request</title>
-</head>
-
-<body>
-<section layout:fragment="content">
-    <div class="page-header">
-        <h1>Error</h1>
-    </div>
-
-    <h2>Bad Request</h2>
-
-    Looks like a developer made a booboo. Please report what you did in the Queue Mattermost channel or
-    through e-mail to eip-ewi@tudelft.nl so we can quickly resolve the issue.
-</section>
-</body>
+<html
+    lang="en"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{container}">
+    <head>
+        <title>Queue: Bad Request</title>
+    </head>
+
+    <body>
+        <section layout:fragment="content">
+            <div class="page-header">
+                <h1>Error</h1>
+            </div>
+
+            <h2>Bad Request</h2>
+
+            Looks like a developer made a booboo. Please report what you did in the Queue Mattermost
+            channel or through e-mail to eip-ewi@tudelft.nl so we can quickly resolve the issue.
+        </section>
+    </body>
+</html>
+
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    xmlns:th="http://www.thymeleaf.org"
+    layout:decorate="~{container}">
+    <head>
+        <title>Queue: Bad Request</title>
+    </head>
+
+    <div layout:fragment="breadcrumbs" class="hidden"></div>
+
+    <section layout:fragment="content">
+        <h1 class="title margin_bottom-3">Bad Request</h1>
+        <p>
+            Looks like a developer made a booboo. Please report what you did in the Queue Mattermost
+            channel or through e-mail to eip-ewi@tudelft.nl, so we can quickly resolve the issue.
+        </p>
+    </section>
 </html>
diff --git a/src/main/resources/templates/error/403.html b/src/main/resources/templates/error/403.html
index 274119866f0f70e1c8aa10bd4b56db55d4bf443f..a229326bdb521d7581208690a364609358e85a64 100644
--- a/src/main/resources/templates/error/403.html
+++ b/src/main/resources/templates/error/403.html
@@ -18,24 +18,26 @@
 
 -->
 <!DOCTYPE html>
-<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-	layout:decorate="~{layout}">
-<head>
-	<title>Queue: Access denied</title>
-</head>
-
-<body>
-	<section layout:fragment="content">
-		<div class="page-header">
-			<h1>Error</h1>
-		</div>
-
-		<h2>Access Denied</h2>
-
-		<p>You do not have permission to enter. This incident will be reported.</p>
-
-		<img src="https://imgs.xkcd.com/comics/incident.png" alt="XKCD" />
-		<p>Source: <a href="http://www.xkcd.org">xkcd</a></p>
-	</section>
-</body>
+<html
+    lang="en"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    xmlns:th="http://www.thymeleaf.org"
+    layout:decorate="~{container}">
+    <head>
+        <title>Queue: Access denied</title>
+    </head>
+
+    <div layout:fragment="breadcrumbs" class="hidden"></div>
+
+    <section layout:fragment="content">
+        <h1 class="title margin_bottom-3">Access Denied</h1>
+        <p class="margin_bottom-5">
+            You do not have permission to enter. This incident will be reported.
+        </p>
+        <img src="https://imgs.xkcd.com/comics/incident.png" alt="XKCD" />
+        <p class="font-300">
+            Source:
+            <a href="https://www.xkcd.com" class="link">xkcd</a>
+        </p>
+    </section>
 </html>
diff --git a/src/main/resources/templates/error/404.html b/src/main/resources/templates/error/404.html
index 54f19ec28f55d1928d83c159bc5748d6a71a74e1..d680505ab49d3717cfddb6765e7ce132d5c8d342 100644
--- a/src/main/resources/templates/error/404.html
+++ b/src/main/resources/templates/error/404.html
@@ -18,17 +18,23 @@
 
 -->
 <!DOCTYPE html>
-<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}">
-
-<body>
-<section layout:fragment="content">
-    <div class="page-header">
-        <h1>Error 404</h1>
-    </div>
-
-    <h2>Object not found</h2>
-
-    <p>The requested object could not be found, stop looking for things that aren't there :)</p>
-</section>
-</body>
+<html
+    lang="en"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    xmlns:th="http://www.thymeleaf.org"
+    layout:decorate="~{container}">
+    <head>
+        <title>Queue: Not Found</title>
+    </head>
+
+    <div layout:fragment="breadcrumbs" class="hidden"></div>
+
+    <section layout:fragment="content">
+        <h1 class="title margin_bottom-3">Not Found</h1>
+        <p>
+            The requested object could not be found
+            <br />
+            <span class="font-300">Stop looking for things that aren't there (:</span>
+        </p>
+    </section>
 </html>
diff --git a/src/main/resources/templates/error/422.html b/src/main/resources/templates/error/422.html
index 656d7a06df65a6595ef59bd225701a697fa739e9..05c943ec6515feddc1ce0fa514b0c8feb830f4a0 100644
--- a/src/main/resources/templates/error/422.html
+++ b/src/main/resources/templates/error/422.html
@@ -18,21 +18,63 @@
 
 -->
 <!DOCTYPE html>
-<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:th="http://www.thymeleaf.org" layout:decorate="~{layout}">
-<head>
-	<title>Queue: Error</title>
-</head>
-
-<body>
-	<section layout:fragment="content">
-		<div class="page-header">
-			<h1>Error</h1>
-		</div>
-		<div class="jumbotron alert-danger">
-			<h1>Something went wrong during validation of an entity.</h1>
-			<h5>Are you trying to break Queue?</h5>
-		</div>
-	</section>
-</body>
+<html
+    lang="en"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    xmlns:th="http://www.thymeleaf.org"
+    layout:decorate="~{container}">
+    <head>
+        <title>Queue: Error</title>
+    </head>
+
+    <div layout:fragment="breadcrumbs" class="hidden"></div>
+
+    <section layout:fragment="content">
+        <h1 class="title margin_bottom-3">Error</h1>
+        <p>
+            <span>Something went wrong during validation of an entity.</span><br>
+            <span class="font-300">Are you trying to break Queue?</span>
+        </p>
+    </section>
 </html>
 
+<!--
+
+    Queue - A Queueing system that can be used to handle labs in higher education
+    Copyright (C) 2016-2020  Delft University of Technology
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+-->
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    xmlns:th="http://www.thymeleaf.org"
+    layout:decorate="~{container}">
+    <head>
+        <title>Queue: Validation Error</title>
+    </head>
+
+    <div layout:fragment="breadcrumbs" class="hidden"></div>
+
+    <section layout:fragment="content">
+        <h1 class="title margin_bottom-3">Validation Error</h1>
+        <p>
+            Something went wrong during validation of an entity.
+            <br />
+            <span class="font-300">Are you trying to break Queue?</span>
+        </p>
+    </section>
+</html>
diff --git a/src/main/resources/templates/error/500.html b/src/main/resources/templates/error/500.html
index ce6bfb8b82800c99dda469904769966a4efa1934..6b0d5d4e0de644f599d47ebafc7638d905384e61 100644
--- a/src/main/resources/templates/error/500.html
+++ b/src/main/resources/templates/error/500.html
@@ -18,24 +18,24 @@
 
 -->
 <!DOCTYPE html>
-<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:th="http://www.thymeleaf.org" layout:decorate="~{layout}">
-
-<head>
-	<title>Queue: Error</title>
-</head>
-
-<body>
-	<section layout:fragment="content">
-		<div class="page-header">
-			<h1>Error</h1>
-		</div>
-		<div class="jumbotron alert-danger">
-			<h1>
-				Oops. Something went wrong on our end...<br>
-				If this occurs again, contact us with a description of what you did before we show you this screen
-			</h1>
-		</div>
-	</section>
-</body>
+<html
+    lang="en"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    xmlns:th="http://www.thymeleaf.org"
+    layout:decorate="~{container}">
+    <head>
+        <title>Queue: Error</title>
+    </head>
+
+    <div layout:fragment="breadcrumbs" class="hidden"></div>
+
+    <section layout:fragment="content">
+        <h1 class="title margin_bottom-3">Error</h1>
+        <p>
+            Oops. Something went wrong on our end...
+            <br />
+            If this occurs again, contact us with a description of what you did before we show you
+            this screen
+        </p>
+    </section>
 </html>
-
diff --git a/src/main/resources/templates/error/error.html b/src/main/resources/templates/error/error.html
index 36a0ad6eb251e88ee2524e55c1164ac4c095ef51..18856e0c606dc5c79fd80ae9ecc6e08b547dcff0 100644
--- a/src/main/resources/templates/error/error.html
+++ b/src/main/resources/templates/error/error.html
@@ -18,20 +18,19 @@
 
 -->
 <!DOCTYPE html>
-<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:th="http://www.thymeleaf.org" layout:decorate="~{layout}">
-<head>
-	<title>Queue: Error</title>
-</head>
+<html
+    lang="en"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    xmlns:th="http://www.thymeleaf.org"
+    layout:decorate="~{container}">
+    <head>
+        <title>Queue: Error</title>
+    </head>
 
-<body>
-	<section layout:fragment="content">
-		<div class="page-header">
-			<h1>Error</h1>
-		</div>
-		<div class="jumbotron alert-danger">
-			<h1>Oops. Something went wrong...</h1>
-		</div>
-	</section>
-</body>
-</html>
+    <div layout:fragment="breadcrumbs" class="hidden"></div>
 
+    <section layout:fragment="content">
+        <h1 class="title margin_bottom-3">Error</h1>
+        <p>Oops. Something went wrong...</p>
+    </section>
+</html>
diff --git a/src/main/resources/templates/footer.html b/src/main/resources/templates/footer.html
new file mode 100644
index 0000000000000000000000000000000000000000..472daad94c50d805f1370974d0901087701d25ca
--- /dev/null
+++ b/src/main/resources/templates/footer.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <footer class="footer" layout:fragment="footer">
+        <div class="footer__content">
+            <div class="footer__logo">
+                <img height="50" alt="TU Delft logo" src="/img/tudelft-logo.png" />
+                <span class="text_muted font-400" style="white-space: nowrap">
+                    © Delft University of Technology
+                </span>
+            </div>
+            <nav aria-label="Footer" class="footer__links">
+                <a class="link" style="white-space: nowrap" th:href="@{/privacy}">
+                    Privacy statement
+                </a>
+                <a class="link" th:href="@{/about}">About</a>
+            </nav>
+        </div>
+    </footer>
+</html>
diff --git a/src/main/resources/templates/header.html b/src/main/resources/templates/header.html
new file mode 100644
index 0000000000000000000000000000000000000000..d3bc78c3443f86f3275d2915e9c9e7486f2e3098
--- /dev/null
+++ b/src/main/resources/templates/header.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <header class="header" data-collapsed="true" layout:fragment="header">
+        <div class="header__content">
+            <div class="header__top">
+                <a class="header__logo" th:href="@{/}">Queue</a>
+                <button class="header__hamburger fa-solid fa-bars"></button>
+            </div>
+
+            <div class="header__controls">
+                <nav class="header__links">
+                    <th:block th:unless="${#authenticatedP == null}">
+                        <a th:href="@{/}" th:data-active="${page == 'my-courses'}">My courses</a>
+                        <a th:href="@{/editions}" th:data-active="${page == 'catalog'}">
+                            Catalogue
+                        </a>
+                        <a
+                            th:if="${@permissionService.canViewRequests()}"
+                            th:href="@{/requests}"
+                            th:data-active="${page == 'requests'}">
+                            Requests
+                        </a>
+                    </th:block>
+                </nav>
+
+                <div class="header__right">
+                    <th:block th:if="${#authenticatedP == null}">
+                        <a class="header__user" th:href="@{/login}">
+                            <span class="fa-solid fa-door-open"></span>
+                            <span>Log in</span>
+                        </a>
+                    </th:block>
+                    <th:block th:unless="${#authenticatedP == null}">
+                        <button class="header__user">
+                            <span th:text="${#authenticatedP.displayName}"></span>
+                            <span class="fa-solid fa-chevron-down"></span>
+                        </button>
+                        <nav class="header__dropdown">
+                            <a th:href="@{/history}">
+                                <span class="margin_right-1 fa-solid fa-clock"></span>
+                                <span>Request history</span>
+                            </a>
+                            <a
+                                th:if="${@permissionService.canViewRequests()}"
+                                th:href="@{/feedback}">
+                                <span class="margin_right-1 fa-solid fa-star"></span>
+                                <span>Feedback</span>
+                            </a>
+                            <form th:action="@{/logout}" method="post">
+                                <button>
+                                    <span class="margin_right-1 fa-solid fa-door-closed"></span>
+                                    <span>Log out</span>
+                                </button>
+                            </form>
+                        </nav>
+                    </th:block>
+                </div>
+            </div>
+        </div>
+    </header>
+
+    <script layout:fragment="header_script">
+        $(() => {
+            $(".header__hamburger").click(function () {
+                const header = $(".header");
+                const collapsed = header.data("collapsed");
+                header.data("collapsed", !collapsed);
+                header.attr("data-collapsed", !collapsed);
+            });
+            $(".header__user").click(function () {
+                $(this).focus();
+            });
+        });
+    </script>
+</html>
diff --git a/src/main/resources/templates/history/index.html b/src/main/resources/templates/history/index.html
index 14f072e6dcd2c2c502892eeb1054bad53b77fa14..0bb3a1eeaa400ddfb5267a6089bd299b18ad5d7d 100644
--- a/src/main/resources/templates/history/index.html
+++ b/src/main/resources/templates/history/index.html
@@ -1,58 +1,42 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{layout}">
-
-<!--@thymesVar id="requests" type="org.springframework.data.domain.Page<nl.tudelft.queue.dto.view.RequestViewDTO>"-->
-
-<body>
-<section layout:fragment="content">
-    <nav role="navigation" class="breadcrumbs">
-        <ol class="breadcrumb">
-            <li class="breadcrumb-item"><a href="/">Home</a></li>
-            <li class="breadcrumb-item active">History</li>
-        </ol>
-    </nav>
-
-    <div class="page-header">
-        <h1>My Requests</h1>
-    </div>
-</section>
-
-<section layout:fragment="outside-content">
-    <div class="row no-gutters">
-        <div class="col-lg-3 col-xl-2 pl-lg-3 pr-lg-1">
-            <th:block th:replace="request/list/filters :: filters (returnPath=@{/history})">
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{container}">
+    <head>
+        <title>Request history</title>
+    </head>
+
+    <div layout:fragment="breadcrumbs" class="hidden"></div>
+
+    <th:block layout:fragment="content">
+        <h1 class="title margin_bottom-5">Request History</h1>
+
+        <div class="flex_column">
+            <div class="flex_column align_items_start:from_small">
+                <button
+                    class="button padding_inline-5 padding_block-3"
+                    data-style="floating"
+                    data-dialog="filters">
+                    <span class="margin_right-1 fa-solid fa-filter"></span>
+                    <span>Filters</span>
+                    <span th:text="|(${filter.countActiveFilters()})|"></span>
+                </button>
+            </div>
+
+            <th:block layout:fragment="request_list">
+                <th:block
+                    layout:replace="~{request/table :: table(showAssistants=true)}"></th:block>
             </th:block>
         </div>
-        <div class="col-lg-9 col-xl-10 pl-lg-1 pr-lg-3">
-            <th:block th:replace="request/list/request-table :: request-table">
-            </th:block>
-        </div>
-    </div>
+    </th:block>
 
-    <div class="justify-content-center">
-        <th:block th:replace="pagination :: pagination (page=${requests}, size=3)">
+    <th:block layout:fragment="overlays">
+        <th:block th:with="returnPath = '/history'">
+            <th:block layout:replace="~{request/filters :: dialog}"></th:block>
         </th:block>
-    </div>
-</section>
-</body>
+    </th:block>
+
+    <th:block layout:fragment="script"></th:block>
 </html>
diff --git a/src/main/resources/templates/home/about.html b/src/main/resources/templates/home/about.html
index 8cd0a24c96b9a3f2e9b6e63d22649709ade67ad5..113e9793b71a39999a70a85ddb7225b71df8fd65 100644
--- a/src/main/resources/templates/home/about.html
+++ b/src/main/resources/templates/home/about.html
@@ -18,77 +18,94 @@
 
 -->
 <!DOCTYPE html>
-<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}">
+<html
+    lang="en"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{container}">
+    <div layout:fragment="breadcrumbs" class="hidden"></div>
 
-<body>
-<section layout:fragment="content">
-    <div class="page-header">
-        <h1> About Queue </h1>
-    </div>
-    <div class="row">
-        <div class="col-sm-12 col-md-6">
-            <p class="h3"> What is Queue? </p>
-            <p>
-                Queue is a system that serves as a replacement for a physical queue. It was originally intended for
-                lab sessions, where students work on their own and can ask questions to student assistants. As the number
-                of students grows, it becomes harder and harder to keep track of who has questions, and who has the first turn.
+    <section layout:fragment="content">
+        <h1 class="title margin_bottom-5">About Queue</h1>
 
-                In its simplest form, Queue allows students to create a request and allows student assistants to pick up these
-                requests, in order.
-            </p>
-        </div>
+        <div class="grid gap-7 columns-1:small" style="text-align: justify">
+            <div>
+                <h2 class="subtitle">What is Queue?</h2>
+                <p class="margin_bottom-5">
+                    Queue is a system that serves as a replacement for a physical queue. It was
+                    originally intended for lab sessions, where students work on their own and can
+                    ask questions to student assistants. As the number of students grows, it becomes
+                    harder and harder to keep track of who has questions, and who has the first
+                    turn. In its simplest form, Queue allows students to create a request and allows
+                    student assistants to pick up these requests, in order.
+                </p>
 
-        <div class="col-sm-12 col-md-6">
-            <p class="h3"> Why Queue? </p>
-            <p>
-                Queue is a simple to use tool with many advanced capabilities and customisation options included. With the problem
-                of managing and keeping track of questions out of the way, the student assistants can do what they do best: teach.
-            </p>
-        </div>
-    </div>
+                <h2 class="subtitle">Why Queue?</h2>
+                <p class="margin_bottom-5">
+                    Queue is a simple to use tool with many advanced capabilities and customisation
+                    options included. With the problem of managing and keeping track of questions
+                    out of the way, the student assistants can do what they do best: teach.
+                </p>
 
-    <div class="row">
-        <div class="col-sm-12 col-md-6">
-            <p class="h3"> How does Queue work? </p>
-            <p>
-                Queue transforms a physical queue into a virtual one by taking all the requests of the students and putting them
-                in the right order. Student assistants can then, one by one, take those requests and handle them individually.
-                Additionally, (the) Queue is also highly customisable and allows teachers to create labs for specific situations
-                such as exams or feedback sessions.
-            </p>
-        </div>
+                <h2 class="subtitle">How does Queue work?</h2>
+                <p>
+                    Queue transforms a physical queue into a virtual one by taking all the requests
+                    of the students and putting them in the right order. Student assistants can
+                    then, one by one, take those requests and handle them individually.
+                    Additionally, (the) Queue is also highly customisable and allows teachers to
+                    create labs for specific situations such as exams or feedback sessions.
+                </p>
+            </div>
 
-        <div class="col-sm-12 col-md-6">
-            <p class="h3"> History of Queue </p>
-            <p>
-                Queue started as a literal physical queue that eventually evolved into a management system with a spreadsheet.
-                This short-term solution wasn't the best and thus the idea of Queue was born. Shortly after, a student was hired
-                and Queue 0.1 was not only a dream anymore. After that, a Surf grant helped to further development and the team
-                grew to a size of 8 students.
-            </p>
-        </div>
-    </div>
+            <div>
+                <h2 class="subtitle">History of Queue</h2>
+                <p class="margin_bottom-5">
+                    Queue started as a literal physical queue that eventually evolved into a
+                    management system with a spreadsheet. This short-term solution wasn't the best
+                    and thus the idea of Queue was born. Shortly after, a student was hired and
+                    Queue 0.1 was not only a dream anymore. After that, a Surf grant helped to
+                    further development and the team grew to a size of 8 students.
+                </p>
 
-    <div class="row">
-        <div class="col-sm-12 col-md-6">
-            <p class="h3"> How do I report bugs? </p>
-            <p>
-                Because Queue is an open-source project, one can go to the <a href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue" target="_blank">
-                Queue repository</a>, choose Issues in the menu and open a new issue. There are templates for submitting
-                features and bugs and it should be straightforward from there.
-            </p>
-        </div>
+                <h2 class="subtitle">How do I report bugs?</h2>
+                <p class="margin_bottom-5">
+                    Because Queue is an open-source project, one can go to the
+                    <a
+                        class="link"
+                        href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue"
+                        target="_blank">
+                        Queue repository
+                    </a>
+                    , choose Issues in the menu and open a new issue. There are templates for
+                    submitting features and bugs and it should be straightforward from there.
+                </p>
 
-        <div class="col-sm-12 col-md-6">
-            <p class="h3"> How do I contribute? </p>
-            <p>
-                There are two main ways of contributing: working on an open-standing issue or tackling a bug bounty and
-                earn something extra. The Queue contains an <a href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue/-/issues" target="_blank">
-                issues list</a> and a filter for all the <a href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue/-/issues?label_name[]=BugBounty" target="_blank">bug bounties</a>.
-                For reading the rules of contributing check the <a href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue/-/blob/development/CONTRIBUTING.md" target="_blank">CONTRIBUTING.md</a>.
-            </p>
+                <h2 class="subtitle">How do I contribute?</h2>
+                <p>
+                    There are two main ways of contributing: working on an open-standing issue or
+                    tackling a bug bounty and earn something extra. The Queue contains an
+                    <a
+                        class="link"
+                        href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue/-/issues"
+                        target="_blank">
+                        issues list
+                    </a>
+                    and a filter for all the
+                    <a
+                        class="link"
+                        href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue/-/issues?label_name[]=BugBounty"
+                        target="_blank">
+                        bug bounties
+                    </a>
+                    . For reading the rules of contributing check the
+                    <a
+                        class="link"
+                        href="https://gitlab.ewi.tudelft.nl/eip/labrador/queue/-/blob/development/CONTRIBUTING.md"
+                        target="_blank">
+                        CONTRIBUTING.md
+                    </a>
+                    .
+                </p>
+            </div>
         </div>
-    </div>
-</section>
-</body>
+    </section>
 </html>
diff --git a/src/main/resources/templates/home/dashboard.html b/src/main/resources/templates/home/dashboard.html
index 0a5a9dbd25d8ce91dffd71cb3955319a2f9d713b..9c7a0dc671a65b8d6b1ac276e26a140b91a524c8 100644
--- a/src/main/resources/templates/home/dashboard.html
+++ b/src/main/resources/templates/home/dashboard.html
@@ -1,275 +1,264 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{layout}">
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{container}">
     <head>
-        <link href='/webjars/fullcalendar/main.css' rel='stylesheet' />
-        <script src='/webjars/fullcalendar/main.js'></script>
+        <link href="/webjars/fullcalendar/main.css" rel="stylesheet" />
+        <script src="/webjars/fullcalendar/main.js"></script>
         <script src="/webjars/fullcalendar/locales/nl.js"></script>
     </head>
 
-<!--@thymesVar id="user" type="nl.tudelft.labracore.api.dto.PersonDetailsDTO"-->
-<!--@thymesVar id="editions" type="java.util.Map<java.lang.Long, nl.tudelft.labracore.api.dto.EditionDetailsDTO>"-->
-<!--@thymesVar id="runningEditions" type="java.util.List<nl.tudelft.labracore.api.dto.EditionSummaryDTO>"-->
+    <div layout:fragment="breadcrumbs" class="hidden"></div>
 
-<!--@thymesVar id="activeRoles" type="java.util.List<nl.tudelft.labracore.api.dto.RoleEditionDetailsDTO>"-->
-<!--@thymesVar id="finishedRoles" type="java.util.List<nl.tudelft.labracore.api.dto.RoleEditionDetailsDTO>"-->
-<!--@thymesVar id="archivedRoles" type="java.util.List<nl.tudelft.labracore.api.dto.RoleEditionDetailsDTO>"-->
+    <th:block layout:fragment="content">
+        <h1 class="title">Enrolled Courses</h1>
 
-<body>
-    <section layout:fragment="content">
-        <nav role="navigation" class="breadcrumbs">
-            <ol class="breadcrumb">
-                <li class="breadcrumb-item active" aria-current="page">Home</li>
-            </ol>
+        <nav data-tabs class="tabs margin_bottom-5">
+            <button data-active="true" data-view="list">List</button>
+            <button data-active="false" data-view="calendar-view">Calendar</button>
         </nav>
 
-        <div class="page-header">
-            <h1>Home</h1>
-        </div>
-        <div style="margin-bottom: 1rem" th:if="${@permissionService.isAdminOrTeacher()}">
-            <button class="btn btn-outline-primary" onclick="showDialog('create-shared-edition-dialog')">
-                Create course collection
-            </button>
-            <div tabindex="0" class="help-tip">
-                <div class="help-icon">?</div>
-                <p>A course collection is a collection of courses that acts in the queue as one
-                    course. This can be used for sessions in which multiple courses take place at
-                    the same time. This avoids having to create a session for every
-                    course, as sessions can be shared.</p>
+        <div id="list" class="flex_column">
+            <div th:if="${success}" class="banner" data-type="check">
+                <span class="fa-solid fa-check-circle"></span>
+                <span th:text="${success}"></span>
+                <button
+                    class="fa-solid fa-xmark"
+                    style="margin-left: auto"
+                    data-remove-banner></button>
             </div>
-            <div th:replace="~{shared-edition/create/create-shared-edition :: overlay}"></div>
-        </div>
 
-        <ul class="nav nav-tabs" id="overview-tabs" role="tablist">
-            <li class="nav-item">
-                <a class="nav-link active" id="overview-tab" data-toggle="tab" href="#overview"
-                   role="tab" aria-controls="overview" aria-selected="true">Course overview</a>
-            </li>
-            <li class="nav-item">
-                <a class="nav-link" id="calendar-tab" data-toggle="tab" href="#calendar" role="tab"
-                   aria-controls="calendar" aria-selected="false">Calendar</a>
-            </li>
-            <li class="nav-item" th:if="${@permissionService.isAdmin()}">
-                <a class="nav-link" id="admin-tab" data-toggle="tab" href="#admin" role="tab"
-                   aria-controls="admin" aria-selected="false">Admin</a>
-            </li>
-        </ul>
-
-        <div class="tab-content mt-2">
-            <div class="tab-pane fade" id="calendar" role="tabpanel"
-                 aria-labelledby="calendar-tab">
-                <h2>Calendar</h2>
-                <div id="calendar-view"></div>
-                <script th:inline="javascript">
-                    document.addEventListener('DOMContentLoaded', function () {
-                        let calendarEl = document.getElementById('calendar-view');
-                        let calendar = new FullCalendar.Calendar(calendarEl, {
-                            // plugins: [listPlugin],
-                            headerToolbar: {
-                                left: 'timeGridWeek,timeGridDay,listDay',
-                                center: 'title',
-                                right: 'today prev,next'
-                            },
-                            initialView: 'timeGridWeek',
-                            nowIndicator: true,
-                            businessHours: {
-                                daysOfWeek: [1, 2, 3, 4, 5],
-                                startTime: '08:45',
-                                endTime: '17:45'
-                            },
-                            eventDisplay: 'block',
-                        });
-                        let sessions = /*[[${sessions}]]*/;
-                        sessions.forEach(dto => {
-                            const event = {
-                                title: dto.session.name,
-                                start: dto.session.start,
-                                end: dto.session.end,
-                                description: dto.session.description,
-                                url: "/lab/" + dto.id
-                            }
-                            switch (dto.type) {
-                                case "SLOTTED":
-                                    event['backgroundColor'] = "yellow"
-                                    event['textColor'] = "black"
-                                    calendar.addEvent({
-                                        title: dto.session.name + " slot selection",
-                                        start: dto.selectionOpensAt,
-                                        backgroundColor: "yellow",
-                                        textColor: "black"
-                                    })
-                                    break;
-                                case "EXAM":
-                                    event['backgroundColor'] = "red"
-                                    calendar.addEvent({
-                                        title: dto.session.name + " slot selection",
-                                        start: dto.selectionOpensAt,
-                                        backgroundColor: "red",
-                                    })
-                                    break;
-                            }
-                            calendar.addEvent(event)
-                        })
-                        calendar.render();
-                        $(document).on('shown.bs.tab', 'a[data-toggle="tab"]', function (e) {
-                            calendar.updateSize();
-                        })
-                    })
-                </script>
+            <div
+                th:if="${@permissionService.canCreateEdition()}"
+                class="flex gap-3 | flex_column:small">
+                <button
+                    class="padding_inline-5 padding_block-3 button"
+                    data-style="floating"
+                    data-dialog="create-edition">
+                    <span class="margin_right-1 fa-solid fa-plus"></span>
+                    <span>Create course edition</span>
+                </button>
+                <button
+                    class="padding_inline-5 padding_block-3 button"
+                    data-style="floating"
+                    data-dialog="create-edition-collection">
+                    <span class="margin_right-1 fa-solid fa-plus"></span>
+                    <span>Create course collection</span>
+                </button>
             </div>
 
-            <div class="tab-pane fade show active" id="overview" role="tabpanel"
-                 aria-labelledby="overview-tabs">
-                <div class="boxed-group">
-                    <h3>Active courses you participate in</h3>
-                    <div class="boxed-group-inner" th:if="${#lists.isEmpty(activeRoles)}">
-                        You do not participate in any courses. Why don't you <a th:href="@{/editions}">enrol for your first course</a>?
-                    </div>
+            <div class="rounded shadow flex_column gap-0">
+                <h2 class="heading">Active courses</h2>
+                <ul class="list" role="list">
+                    <li th:if="${activeRoles.isEmpty()}">
+                        You are not enrolled in any active courses.
+                    </li>
 
-                    <ul class="list-group">
-                        <li class="list-group-item" th:each="role : ${activeRoles}"
-                            th:with="edition = ${editions.get(role.edition.id)}">
-                            <a th:href="@{/edition/{id}(id=${edition.id})}"
-                               th:text="|${edition.course.name} (${edition.name})|"></a>
-                            <span th:text="${'(' + @roleDTOService.typeDisplayName(role.type.toString()) + ')'}"></span>
-                            <ul th:unless="${#lists.isEmpty(labs)}">
-                                <th:block th:each="sess : ${edition.sessions}">
-                                    <li th:each="lab : ${labs[sess.id]}" th:classappend="${lab.slot.open()} ? 'lab-open' : 'lab-closed'">
-                                        <a href="#" th:href="@{/lab/{id}(id=${lab.id})}"
-                                           th:text="|${lab.name} ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|">
-                                        </a>
-                                        <span th:if="${lab.isShared}" class="badge badge-pill badge-info text-white">Shared lab</span>
-                                        <span class="badge badge-pill badge-info text-white"
-                                              th:if="${lab.slot.open()}">Active</span>
-                                        <span class="badge badge-pill badge-info text-white"
-                                              th:if="${lab.slot.closed()}">Completed</span>
+                    <li
+                        class="flex_column gap-2"
+                        th:each="role : ${activeRoles}"
+                        th:with="edition = ${editions[role.edition.id]}">
+                        <div class="flex align_items_center" style="--gap: 6px">
+                            <a
+                                class="link"
+                                th:href="@{/edition/{id}(id=${role.edition.id})}"
+                                th:text="|${editions[role.edition.id].course.name} - ${role.edition.name}|"></a>
+                            <span
+                                class="chip"
+                                th:text="#{|role.${role.type.toString().toLowerCase()}|}"></span>
+                        </div>
 
-                                        <span class="badge badge-pill badge-danger text-white"
-                                              th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).EXAM}">Exam</span>
-                                        <span class="badge badge-pill badge-info text-white"
-                                              th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).SLOTTED}">Slotted</span>
-                                        <span class="badge badge-pill badge-warning text-white"
-                                              th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).CAPACITY}">Limited Capacity</span>
+                        <ul
+                            class="flex_column gap-1 margin_left-5"
+                            th:unless="${edition.sessions.isEmpty()}">
+                            <th:block th:each="sess : ${edition.sessions}">
+                                <li
+                                    class="flex align_items_center"
+                                    style="--gap: 6px"
+                                    th:each="lab : ${labs[sess.id]}">
+                                    <a
+                                        class="link"
+                                        th:href="@{/lab/{id}(id=${lab.id})}"
+                                        th:text="|${lab.name} - ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|"></a>
 
-                                        <span class="badge badge-pill badge-warning text-muted"
-                                              th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">Open for enqueueing</span>
-                                    </li>
-                                </th:block>
-                            </ul>
-                        </li>
-                        <li class="list-group-item" th:each="shared : ${sharedEditions}">
-                            <a th:href="@{/shared-edition/{id}(id=${shared.id})}"
-                               th:text="|${shared.getName()}|"></a>
-                            <span class="badge badge-pill badge-info text-white">
-                                Shared edition
-                            </span>
-                            <ul th:unless="${#lists.isEmpty(labs)}">
-                                    <li th:each="lab : ${sharedLabs[shared.id]}"
-                                        th:classappend="${lab.slot.open()} ? 'lab-open' : 'lab-closed'">
-                                        <a href="#" th:href="@{/lab/{id}(id=${lab.id})}"
-                                           th:text="|${lab.name} ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|">
-                                        </a>
-                                        <span th:if="${lab.isShared}" class="badge badge-pill badge-info text-white">Shared lab</span>
-                                        <span class="badge badge-pill badge-info text-white"
-                                              th:if="${lab.slot.open()}">Active</span>
-                                        <span class="badge badge-pill badge-info text-white"
-                                              th:if="${lab.slot.closed()}">Completed</span>
+                                    <span th:if="${lab.isShared}" class="tag">Shared</span>
 
-                                        <span class="badge badge-pill badge-danger text-white"
-                                              th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).EXAM}">Exam</span>
-                                        <span class="badge badge-pill badge-info text-white"
-                                              th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).SLOTTED}">Slotted</span>
-                                        <span class="badge badge-pill badge-warning text-white"
-                                              th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).CAPACITY}">Limited Capacity</span>
+                                    <span
+                                        class="chip"
+                                        data-type="positive"
+                                        th:if="${lab.slot.open()}">
+                                        Active
+                                    </span>
+                                    <span class="chip" th:if="${lab.slot.closed()}">Completed</span>
 
-                                        <span class="badge badge-pill badge-warning text-muted"
-                                              th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">Open for enqueueing</span>
-                                    </li>
-                            </ul>
-                        </li>
-                    </ul>
-                </div>
+                                    <span
+                                        class="chip"
+                                        th:unless="${lab.type.name() == 'REGULAR'}"
+                                        th:text="#{|lab.type.${lab.type.name().toLowerCase()}|}"></span>
 
-                <div class="boxed-group" th:if="${!#lists.isEmpty(finishedRoles)}">
-                    <h3>Finished courses</h3>
+                                    <span
+                                        class="chip"
+                                        data-type="positive"
+                                        th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">
+                                        Open
+                                    </span>
+                                </li>
+                            </th:block>
+                        </ul>
+                    </li>
 
-                    <ul class="list-group">
-                        <li class="list-group-item" th:each="role : ${finishedRoles}"
-                            th:with="edition = ${editions.get(role.edition.id)}">
-                            <a th:href="@{/edition/{id}(id=${edition.id})}"
-                               th:text="|${edition.course.name} (${edition.name})|"></a>
-                            <span th:text="${'(' + @roleDTOService.typeDisplayName(role.type.toString()) + ')'}"></span>
-                        </li>
-                    </ul>
-                </div>
+                    <li class="flex_column gap-2" th:each="collection : ${sharedEditions}">
+                        <div class="flex align_items_center" style="--gap: 6px">
+                            <a
+                                class="link"
+                                th:href="@{/shared-edition/{id}(id=${collection.id})}"
+                                th:text="${collection.name}"></a>
+                            <span class="chip">Shared</span>
+                        </div>
 
-                <div class="boxed-group" th:if="${!#lists.isEmpty(archivedRoles)}">
-                    <h3>Archived courses</h3>
+                        <ul
+                            class="flex_column gap-1 margin_left-5"
+                            th:unless="${sharedLabs[collection.id].isEmpty()}">
+                            <th:block th:each="sess : ${sharedLabs[collection.id]}">
+                                <li
+                                    class="flex align_items_center"
+                                    style="--gap: 6px"
+                                    th:each="lab : ${labs[sess.id]}">
+                                    <a
+                                        class="link"
+                                        th:href="@{/lab/{id}(id=${lab.id})}"
+                                        th:text="|${lab.name} - ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|"></a>
 
-                    <ul class="list-group">
-                        <th:block th:each="role : ${archivedRoles}" th:with="edition = ${editions.get(role.edition.id)}">
-                            <li class="list-group-item">
-                                <a th:href="@{/edition/{id}(id=${edition.id})}"
-                                   th:text="|${edition.course.name} (${edition.name})|"></a>
-                                <span th:text="${'(' + @roleDTOService.typeDisplayName(role.type.toString()) + ')'}"></span>
-                            </li>
-                        </th:block>
-                    </ul>
-                </div>
-            </div>
+                                    <span th:if="${lab.isShared}" class="tag">Shared</span>
 
-            <div class="tab-pane fade" id="admin" role="tabpanel" aria-labelledby="admin-tab"
-                 th:if="${@permissionService.isAdmin()}">
-                <div class="boxed-group" >
-                    <h3>Admin overview - Labs running today:</h3>
-                    <div class="boxed-group-inner" th:if="${#lists.isEmpty(runningLabs)}">
-                        None.
-                    </div>
+                                    <span
+                                        class="chip"
+                                        data-type="positive"
+                                        th:if="${lab.slot.open()}">
+                                        Active
+                                    </span>
+                                    <span class="chip" th:if="${lab.slot.closed()}">Completed</span>
 
-                    <ul class="list-group">
-                        <li th:each="lab : ${runningLabs}" th:classappend="${lab.slot.open()} ? 'lab-open' : 'lab-closed'" class="list-group-item">
-                            <a href="#" th:href="@{/lab/{id}(id=${lab.id})}"
-                               th:text="|${lab.name} ${#temporals.format(lab.slot.opensAt, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(lab.slot.closesAt, 'dd MMMM yyyy HH:mm')} ${lab.slotOccupationString}|">
-                            </a>
+                                    <span
+                                        class="chip"
+                                        th:unless="${lab.type.name() == 'REGULAR'}"
+                                        th:text="#{|lab.type.${lab.type.name().toLowerCase()}|}"></span>
 
-                            <span class="badge badge-pill badge-info text-white"
-                                  th:if="${lab.slot.open()}">Active</span>
-                            <span class="badge badge-pill badge-info text-white"
-                                  th:if="${lab.slot.closed()}">Completed</span>
+                                    <span
+                                        class="chip"
+                                        data-type="positive"
+                                        th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">
+                                        Open
+                                    </span>
+                                </li>
+                            </th:block>
+                        </ul>
+                    </li>
+                </ul>
+            </div>
 
-                            <span class="badge badge-pill badge-danger text-white"
-                                  th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).EXAM}">Exam</span>
-                            <span class="badge badge-pill badge-info text-white"
-                                  th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).SLOTTED}">Slotted</span>
-                            <span class="badge badge-pill badge-warning text-white"
-                                  th:if="${lab.type == T(nl.tudelft.queue.model.enums.QueueSessionType).CAPACITY}">Limited Capacity</span>
+            <div class="rounded shadow flex_column gap-0" th:unless="${finishedRoles.isEmpty()}">
+                <h2 class="heading">Finished courses</h2>
+                <ul class="list" role="list">
+                    <li
+                        class="flex align_items_center"
+                        style="--gap: 6px"
+                        th:each="role : ${finishedRoles}">
+                        <a
+                            class="link"
+                            th:href="@{/edition/{id}(id=${role.edition.id})}"
+                            th:text="|${editions[role.edition.id].course.name} - ${role.edition.name}|"></a>
+                        <span
+                            class="chip"
+                            th:text="#{|role.${role.type.toString().toLowerCase()}|}"></span>
+                    </li>
+                </ul>
+            </div>
 
-                            <span class="badge badge-pill badge-warning text-muted"
-                                  th:if="${!lab.slot.closed() && lab.status.isOpenToEnqueueing()}">Open for enqueueing</span>
-                        </li>
-                    </ul>
-                </div>
+            <div class="rounded shadow flex_column gap-0" th:unless="${archivedRoles.isEmpty()}">
+                <h2 class="heading">Archived courses</h2>
+                <ul class="list" role="list">
+                    <li
+                        class="flex align_items_center"
+                        style="--gap: 6px"
+                        th:each="role : ${archivedRoles}">
+                        <a
+                            class="link"
+                            th:href="@{/edition/{id}(id=${role.edition.id})}"
+                            th:text="|${editions[role.edition.id].course.name} - ${role.edition.name}|"></a>
+                        <span
+                            class="chip"
+                            th:text="#{|role.${role.type.toString().toLowerCase()}|}"></span>
+                    </li>
+                </ul>
             </div>
         </div>
-    </section>
-</body>
+
+        <div id="calendar-view"></div>
+        <script th:inline="javascript">
+            document.addEventListener('DOMContentLoaded', function () {
+                let calendarEl = document.getElementById('calendar-view');
+                let calendar = new FullCalendar.Calendar(calendarEl, {
+                    // plugins: [listPlugin],
+                    headerToolbar: {
+                        left: 'timeGridWeek,timeGridDay,listDay',
+                        center: 'title',
+                        right: 'today prev,next'
+                    },
+                    initialView: 'timeGridWeek',
+                    nowIndicator: true,
+                    businessHours: {
+                        daysOfWeek: [1, 2, 3, 4, 5],
+                        startTime: '08:45',
+                        endTime: '17:45'
+                    },
+                    eventDisplay: 'block',
+                });
+                let sessions = /*[[${sessions}]]*/;
+                sessions.forEach(dto => {
+                    const event = {
+                        title: dto.session.name,
+                        start: dto.session.start,
+                        end: dto.session.end,
+                        description: dto.session.description,
+                        url: "/lab/" + dto.id
+                    }
+                    switch (dto.type) {
+                        case "SLOTTED":
+                            event['backgroundColor'] = "yellow"
+                            event['textColor'] = "black"
+                            calendar.addEvent({
+                                title: dto.session.name + " slot selection",
+                                start: dto.selectionOpensAt,
+                                backgroundColor: "yellow",
+                                textColor: "black"
+                            })
+                            break;
+                        case "EXAM":
+                            event['backgroundColor'] = "red"
+                            calendar.addEvent({
+                                title: dto.session.name + " slot selection",
+                                start: dto.selectionOpensAt,
+                                backgroundColor: "red",
+                            })
+                            break;
+                    }
+                    calendar.addEvent(event)
+                })
+                calendar.render();
+                $(document).on('shown.bs.tab', 'a[data-toggle="tab"]', function (e) {
+                    calendar.updateSize();
+                });
+                $("#calendar-view").addClass("hidden");
+            })
+        </script>
+    </th:block>
+
+    <th:block layout:fragment="overlays">
+        <th:block layout:replace="~{edition/create :: dialog}"></th:block>
+        <th:block layout:replace="~{edition_collection/create :: dialog}"></th:block>
+        <th:block layout:replace="~{course/request :: dialog}"></th:block>
+    </th:block>
 </html>
diff --git a/src/main/resources/templates/home/feedback.html b/src/main/resources/templates/home/feedback.html
index 14a34db2ce22fa4340b28517f36e78971e2eeb36..9fb053bbf1104ea850e3392b331f5d367945c10a 100644
--- a/src/main/resources/templates/home/feedback.html
+++ b/src/main/resources/templates/home/feedback.html
@@ -1,135 +1,110 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{layout}">
-
-<!--@thymesVar id="#authenticatedP" type="nl.tudelft.labracore.lib.security.user.Person"-->
-
-<!--@thymesVar id="assistant" type="nl.tudelft.labracore.api.dto.PersonDetailsDTO"-->
-<!--@thymesVar id="feedback" type="org.springframework.data.domain.Page<nl.tudelft.queue.dto.view.FeedbackViewDTO>"-->
-
-<head>
-    <title th:text="|Feedback for ${assistant.displayName}|"></title>
-
-    <script src="/webjars/chartjs/Chart.min.js"></script>
-</head>
-
-<body>
-<section layout:fragment="content">
-    <div class="page-sub-header">
-        <h3 th:text="|Feedback for ${assistant.displayName}|"></h3>
-    </div>
-
-    <div th:if="${feedback.isEmpty()}">
-        <h4 th:if="${#authenticatedP.id == assistant.id}">
-            You have no feedback yet.
-        </h4>
-        <h4 th:if="${#authenticatedP.id != assistant.id}">
-            There is no feedback for this assistant yet.
-        </h4>
-    </div>
-
-    <div class="row" th:unless="${feedback.isEmpty()}">
-        <div class="col-md-6">
-            <div class="panel panel-default container">
-                <h4 class="mt-2">Feedback comments</h4>
-
-                <blockquote class="blockquote"
-                            th:each="fb : ${feedback}"
-                            th:if="${fb.feedback != null && !fb.feedback.isEmpty()}">
-                    <p th:text="${fb.feedback}"></p>
-                    <footer class="blockquote-footer"
-                            th:if="${fb.rating != null}"
-                            th:unless="${@permissionService.canViewDeanonimizedFeedback()}">
-                        <span th:text="|Feedback of ${fb.rating + '/5'}|"></span>
-                    </footer>
-                    <footer class="blockquote-footer" th:if="${@permissionService.canViewDeanonimizedFeedback()}">
-                        <span>Feedback </span>
-                        <span th:if="${fb.rating != null}" th:text="|of ${fb.rating + '/5'} |"></span>
-                        <span th:text="|by ${fb.groupName} on Request|"></span>
-                        <a th:text="${'#' + fb.request.id}"
-                           th:href="@{/request/{id}(id = ${fb.request.id})}"></a><br>
-                        <span th:text="|Last updated on ${#temporals.format(fb.lastUpdatedAt, 'dd-MM-yyyy')}, created ${#temporals.format(fb.createdAt, 'dd-MM-yyyy')}|"></span>
-                    </footer>
-                </blockquote>
-            </div>
-
-            <th:block th:replace="pagination :: pagination (page=${feedback}, size=3)">
-            </th:block>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{container}">
+    <head>
+        <title>Feedback</title>
+        <script src="/webjars/chartjs/2.7.0/Chart.min.js"></script>
+    </head>
+
+    <div layout:fragment="breadcrumbs" class="hidden"></div>
+
+    <th:block layout:fragment="content">
+        <h1 class="title margin_bottom-5" th:text="|Feedback for ${assistant.displayName}|"></h1>
+
+        <div th:if="${feedbackHidden}" class="banner margin_bottom-5" data-type="info">
+            <span class="fa-solid fa-info-circle"></span>
+            <p>To keep feedback anonymous, some recent feedback items are not shown.</p>
         </div>
 
-        <div class="col-md-6">
-            <div class="container panel panel-default">
-                <h4 class="mt-2">Overview (5 is most positive)</h4>
-
-                <canvas id="feedback-histogram"></canvas>
-
-                <script th:inline="javascript" type="text/javascript">
-                    //<![CDATA[
-                    const stars = /*[[${stars}]]*/ [0, 0, 0, 0, 0]
-
-                    const ctx = document.getElementById("feedback-histogram").getContext('2d');
-                    new Chart(ctx, {
-                        type: 'bar',
-                        data: {
-                            labels: ['1', '2', '3', '4', '5'],
-                            datasets: [{
-                                label: '# of Votes',
-                                data: stars,
-                                backgroundColor: [
-                                    'rgba(255, 99, 132, 0.2)',
-                                    'rgba(54, 162, 235, 0.2)',
-                                    'rgba(255, 206, 86, 0.2)',
-                                    'rgba(75, 192, 192, 0.2)',
-                                    'rgba(153, 102, 255, 0.2)',
-                                    'rgba(255, 159, 64, 0.2)'
-                                ],
-                                borderColor: [
-                                    'rgba(255, 99, 132, 1)',
-                                    'rgba(54, 162, 235, 1)',
-                                    'rgba(255, 206, 86, 1)',
-                                    'rgba(75, 192, 192, 1)',
-                                    'rgba(153, 102, 255, 1)',
-                                    'rgba(255, 159, 64, 1)'
+        <div class="flex align_items_start | flex_column:small align_items_stretch:small">
+            <div class="shadow rounded info_card" style="max-width: 20rem">
+                <h2 class="heading">Overview</h2>
+                <div class="padding-5">
+                    <canvas
+                        id="overview"
+                        width="320"
+                        height="320"
+                        aria-label="Feedback rating overview graph"
+                        role="img"></canvas>
+                    <script>
+                        /*<![CDATA[*/
+                        const scores = /*[[${stars}]]*/ [0, 0, 0, 0, 0];
+                        /*]]>*/
+                    </script>
+                    <script>
+                        $(() => {
+                            const ctx = document.getElementById("overview").getContext("2d");
+                            const data = {
+                                labels: [1, 2, 3, 4, 5],
+                                datasets: [
+                                    {
+                                        label: "#Score",
+                                        data: scores,
+                                        backgroundColor: [
+                                            "rgba(255, 99, 132, 0.2)",
+                                            "rgba(255, 159, 64, 0.2)",
+                                            "rgba(255, 205, 86, 0.2)",
+                                            "rgba(75, 192, 192, 0.2)",
+                                            "rgba(54, 162, 235, 0.2)",
+                                        ],
+                                        borderColor: [
+                                            "rgb(255, 99, 132)",
+                                            "rgb(255, 159, 64)",
+                                            "rgb(255, 205, 86)",
+                                            "rgb(75, 192, 192)",
+                                            "rgb(54, 162, 235)",
+                                        ],
+                                        borderWidth: 1,
+                                    },
                                 ],
-                                borderWidth: 1
-                            }]
-                        },
-                        options: {
-                            scales: {
-                                yAxes: [{
-                                    ticks: {
-                                        beginAtZero: true,
-                                        precision: 0,
-                                        stepSize: 1
-                                    }
-                                }]
-                            }
-                        }
-                    });
-                    //]]>
-                </script>
+                            };
+                            const config = {
+                                type: "bar",
+                                data: data,
+                                options: {
+                                    legend: { display: false },
+                                    scales: {
+                                        yAxes: [
+                                            {
+                                                ticks: {
+                                                    beginAtZero: true,
+                                                    min: 0,
+                                                    precision: 0,
+                                                },
+                                            },
+                                        ],
+                                    },
+                                },
+                            };
+                            const chart = new Chart(ctx, config);
+                        });
+                    </script>
+                </div>
+            </div>
+
+            <div class="flex_column flex_grow">
+                <div class="rounded shadow flex_column gap-0">
+                    <h2 class="heading">Feedback</h2>
+                    <ul class="list" role="list">
+                        <li th:if="${feedback.isEmpty()}">No feedback</li>
+                        <li th:each="feedbackItem : ${feedback}">
+                            <p th:text="${feedbackItem.feedback}"></p>
+                            <div th:if="${feedbackItem.rating != null}">
+                                <span
+                                    style="color: #ebb345"
+                                    th:each="i : ${#numbers.sequence(0, feedbackItem.rating)}"
+                                    class="fa-solid fa-star"></span>
+                            </div>
+                        </li>
+                    </ul>
+                </div>
+
+                <th:block
+                    layout:replace="~{pagination :: pagination(page=${feedback}, size=3)}"></th:block>
             </div>
         </div>
-    </div>
-</section>
-</body>
+    </th:block>
 </html>
diff --git a/src/main/resources/templates/home/index.html b/src/main/resources/templates/home/index.html
index c0786acc19f909ffa73e2cb7998ae441748f63a0..a9e01c8ec13f0b90d2a3ecc1b314e25a15cc1cac 100644
--- a/src/main/resources/templates/home/index.html
+++ b/src/main/resources/templates/home/index.html
@@ -1,47 +1,21 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}">
-
-<body>
-    <section layout:fragment="content">
-
-        <div class="page-header">
-            <h1>Home</h1>
-        </div>
-
-        <th:block th:unless="${@thymeleafConfig.isTheDay()}">
-            Welcome to the TU Delft queue system!<br><br>
-        </th:block>
-
-        <th:block th:if="${@thymeleafConfig.isTheDay()}">
-            Welcome to the TU Delft stack system! First one in is still first one out, have a nice day.<br><br>
-        </th:block>
-
-
-        <h2>How to use this system?</h2>
-
-        After you login, you can <em>enrol</em> for a particular course. Once enrolled you can navigate to a specific
-        lab. If the lab is opened, you can <em>enqueue</em> for a specific assignment. The assistants process this
-        queue. You will receive a notification when an assistant is assigned to your request. The notification
-        instructs you to either visit the TA or wait for the TA to visit you.
-    </section>
-</body>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{container}">
+    <div layout:fragment="breadcrumbs" class="hidden"></div>
+
+    <div layout:fragment="content">
+        <h1 class="title margin_bottom-3">Welcome to Queue!</h1>
+
+        <h2 class="subtitle">How to use this system?</h2>
+        <p>
+            Click 'Log in' in the top right to log in. After you log in, you can enrol for a course.
+            Once enrolled you can navigate to a session. If the session is opened, you can enqueue
+            for an assignment. The assistants process this queue. You will receive a notification
+            when an assistant is assigned to your request. The notification instructs you to either
+            visit the TA or wait for the TA to visit you.
+        </p>
+    </div>
 </html>
diff --git a/src/main/resources/templates/home/privacy.html b/src/main/resources/templates/home/privacy.html
index c91e75064a404f53bc958e4f4d215fce0fc2080b..c43f8d2c528b51c4a4120eee818fb08962b93d2b 100644
--- a/src/main/resources/templates/home/privacy.html
+++ b/src/main/resources/templates/home/privacy.html
@@ -18,128 +18,204 @@
 
 -->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}">
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{container}">
+    <div layout:fragment="breadcrumbs" class="hidden"></div>
 
-<body>
-<section layout:fragment="content">
-    <div class="page-header">
-        <h1>Privacy statement</h1>
-    </div>
-    <p><strong>This privacy statement is a first version. Some aspects in this privacy statement might be changed in the
-            future to be in line with the law and best practices</strong></p>
-    
-    <p>The Queue project is a project from the TU Delft. In this privacy statement, it will be explained what personal data
-        the Queue collects when using the website. In this document the term Labrador will also be mentioned. Labrador is a
-        future project of the TU Delft, which will include the Queue.</p>
-    <h2 id="topics">Topics</h2>
-    <ul>
-        <li>What data do we collect?</li>
-        <li>How do we collect your data?</li>
-        <li>How will we use your data?</li>
-        <li>How will we store your data?</li>
-        <li>Other matters</li>
-    </ul>
-    <h2 id="what-data-do-we-collect">What data do we collect</h2>
-    <p>To support your education at the <th:block th:text="${@instituteProperties.name}"></th:block>,
-        the university has a legal ground to store your data. To be more specific, it is a contractual necessity: the data
-        processing is necessary for the performance of the contract between the student and
-        <th:block th:text="${@instituteProperties.name}"></th:block>.
-        Because of this, the Queue collects the following data:</p>
-    <ul>
-        <li>Name</li>
-        <li>Email</li>
-        <li>Student number</li>
-        <li>NetID</li>
-        <li>affiliation (if the user is a student or teacher)</li>
-        <li>which department/faculty the user is part of</li>
-        <li>all notifications the user has received</li>
-    </ul>
-    <p>For the purpose of providing notifications, we collect some metadata about the device used.</p>
-    <p>Furthermore, there exist multiple subgroups in the system. We will describe for each subgroup the extra data we
-        store.</p>
-    <h3 id="student">Student</h3>
-    <p>We collect the following information about students:</p>
-    <ul>
-        <li>The mentor group they are enrolled in</li>
-        <li>Courses they are enrolled in</li>
-        <li>The groups the student is/was part of</li>
-    </ul>
-    <p>We also collect every request the student makes (asked a question or asked to be signed off). This contains the
-        following items:</p>
-    <ul>
-        <li>The course the student is following</li>
-        <li>The lab the student is participating in</li>
-        <li>The assignment the student wants to hand in or has a question about</li>
-        <li>The room they are sitting in</li>
-        <li>The comment the student made on the request</li>
-        <li>The time the request was made</li>
-        <li>The timeslot the user has chosen if applicable</li>
-        <li>The question the student asked</li>
-        <li>The feedback on the the TA the student has written down</li>
-    </ul>
-    <h3 id="ta">TA</h3>
-    <p>For a TA (teaching assistant), we collect the following data:</p>
-    <ul>
-        <li>The requests handled by the TA</li>
-        <li>The time the request was assigned</li>
-        <li>The time the request was handled by the TA</li>
-    </ul>
-    <p>This data then enables us to see at which labs the TA attended.</p>
-    <p>Furthermore, feedback about the TA is stored.</p>
-    <h3 id="teacher">Teacher</h3>
-    <p>For a teacher, we collect the following data:</p>
-    <ul>
-        <li>all courses the teacher teaches</li>
-    </ul>
-    <h2 id="how-do-we-collect-your-data">How do we collect your data</h2>
-    <p>All data we collect is collected when using the Queue website.</p>
-    
-    <h2 id="how-will-we-use-your-data">How will we use your data</h2>
-    <p>At this moment, we only use the data given to support the functionality the Queue promises, giving students an easy
-        way to ask questions and sign off work and giving teaching staff an easy to use system to support signing off and
-        answering questions.</p>
-    <p>Data is being used on a need to know basis. This means that student-assistants only see, what they need to see to
-        fulfil their duties as teaching assistant and the same for teachers. Only administrators have access to the
-        database.</p>
-    <p>For example, student assistants cannot view each others feedback. Also teaching assistants can only see the name of
-        the student, not the NetID, email or student number of the student.</p>
-    <p>A teacher might export data to use for purposes to calculate final grades or import it into other tools, namely
-        BrightSpace and Osiris. Furthermore, admins are able to run a script to gather aggregate data about lab sessions and
-        specific people, who spent a lot of time with a teaching assistant. If these numbers are a cause for concern, they
-        will be relayed to the study advisor.</p>
-    <p>Teachers will look at the feedback of TA's to evaluate their performance and if action needs to be taken. The teacher
-        is able to determine the student that posted the feedback and is allowed to contact the student to inquire more
-        information.</p>
-    <p>Data from the Queue is is also used for research purposes. This research is done to explore the possibilities of
-        increasing efficiency and quality of the support given to students and teachers. Questions students asked will be
-        stored and used to figure out if there is a way to be able to answer easy questions right from the Queue, without a
-        need for a TA.</p>
-    <p>For this research the following data will be stored:</p>
-    <ul>
-        <li>The user id of the user that asked the question. This will help us to determine if a user asked a question
-            multiple times and if there is a statistically relevant correlation between multiple types of questions. We are
-            storing the user id as we do not need the name of the student</li>
-        <li>The content of the question. This content will be manually sorted through to remove all data that contains
-            revealing information about the user or a student assistant. For instance: "I'm the one with the red hair" or
-            "Yell Jack when looking for me as John, the previous TA couldn't find me"</li>
-    </ul>
-    <p>This data will only be accessible to people doing the experiment and will be responsibly handled in such a way that
-        no unauthorised person or persons can get access to this data.</p>
-    <h2 id="how-do-we-store-your-data">How do we store your data</h2>
-    <p>To comply with GDPR regulations, all data is stored on site at the <th:block th:text="${@instituteProperties.name}"></th:block>.
-        The data stored on our servers will be deleted after 5 years.</p>
-    <p>Access to data is strictly regulated with only select admins having access to the database. All other entities need
-        to use the website itself, where, as mentioned previously, information is provided on a need to know basis.</p>
-    <h1 id="other-matters">Other matters</h1>
-    <p>For matters like:</p>
-    <ul>
-        <li>What are your data protection rights</li>
-        <li>Which cookies we set and why</li>
-    </ul>
-    <p>Please refer to the <a href="https://www.tudelft.nl/privacy-statement/"
-            title="https://www.tudelft.nl/privacy-statement/">Privacy Statement of the TU Delft</a></p>
-    <p>If you have any questions about the privacy policy or want to make use of your rights, please contact <a
-            href="mailto:privacy-tud@tudelft.nl" title="mailto:privacy-tud@tudelft.nl">the following email address</a></p>
-</section>
-</body>
+    <section layout:fragment="content">
+        <h1 class="title margin_bottom-3">Privacy statement</h1>
+        <p class="margin_bottom-3">
+            <strong>
+                This privacy statement is a first version. Some aspects in this privacy statement
+                might be changed in the future to be in line with the law and best practices
+            </strong>
+        </p>
+        <p class="margin_bottom-5">
+            The Queue project is a project from the TU Delft. In this privacy statement, it will be
+            explained what personal data the Queue collects when using the website. In this document
+            the term Labrador will also be mentioned. Labrador is a future project of the TU Delft,
+            which will include the Queue.
+        </p>
+
+        <h2 class="subtitle" id="topics">Topics</h2>
+        <p class="margin_bottom-3">This privacy statement covers the following topics:</p>
+        <ul class="enumeration margin_bottom-5">
+            <li>What data do we collect?</li>
+            <li>How do we collect your data?</li>
+            <li>How will we use your data?</li>
+            <li>How will we store your data?</li>
+            <li>Other matters</li>
+        </ul>
+
+        <h2 class="subtitle" id="what-data-do-we-collect">What data do we collect</h2>
+        <p class="margin_bottom-3">
+            To support your education at the
+            <th:block th:text="${@instituteProperties.name}"></th:block>
+            , the university has a legal ground to store your data. To be more specific, it is a
+            contractual necessity: the data processing is necessary for the performance of the
+            contract between the student and
+            <th:block th:text="${@instituteProperties.name}"></th:block>
+            . Because of this, the Queue collects the following data:
+        </p>
+        <ul class="enumeration margin_bottom-3">
+            <li>Name</li>
+            <li>Email</li>
+            <li>Student number</li>
+            <li>NetID</li>
+            <li>affiliation (if the user is a student or teacher)</li>
+            <li>which department/faculty the user is part of</li>
+            <li>all notifications the user has received</li>
+        </ul>
+        <p>
+            For the purpose of providing notifications, we collect some metadata about the device
+            used.
+        </p>
+        <p class="margin_bottom-5">
+            Furthermore, there exist multiple subgroups in the system. We will describe for each
+            subgroup the extra data we store.
+        </p>
+
+        <h3 class="subsubtitle" id="student">Student</h3>
+        <p class="margin_bottom-3">We collect the following information about students:</p>
+        <ul class="enumeration margin_bottom-3">
+            <li>The mentor group they are enrolled in</li>
+            <li>Courses they are enrolled in</li>
+            <li>The groups the student is/was part of</li>
+        </ul>
+        <p class="margin_bottom-3">
+            We also collect every request the student makes (asked a question or asked to be signed
+            off). This contains the following items:
+        </p>
+        <ul class="enumeration margin_bottom-5">
+            <li>The course the student is following</li>
+            <li>The lab the student is participating in</li>
+            <li>The assignment the student wants to hand in or has a question about</li>
+            <li>The room they are sitting in</li>
+            <li>The comment the student made on the request</li>
+            <li>The time the request was made</li>
+            <li>The timeslot the user has chosen if applicable</li>
+            <li>The question the student asked</li>
+            <li>The feedback on the the TA the student has written down</li>
+        </ul>
+
+        <h3 class="subsubtitle" id="ta">TA</h3>
+        <p class="margin_bottom-3">For a TA (teaching assistant), we collect the following data:</p>
+        <ul class="enumeration margin_bottom-3">
+            <li>The requests handled by the TA</li>
+            <li>The time the request was assigned</li>
+            <li>The time the request was handled by the TA</li>
+        </ul>
+        <p class="margin_bottom-3">
+            This data then enables us to see at which labs the TA attended.
+        </p>
+        <p class="margin_bottom-5">Furthermore, feedback about the TA is stored.</p>
+
+        <h3 class="subsubtitle" id="teacher">Teacher</h3>
+        <p class="margin_bottom-3">For a teacher, we collect the following data:</p>
+        <ul class="enumeration margin_bottom-5">
+            <li>all courses the teacher teaches</li>
+        </ul>
+
+        <h2 class="subtitle" id="how-do-we-collect-your-data">How do we collect your data</h2>
+        <p class="margin_bottom-5">
+            All data we collect is collected when using the Queue website.
+        </p>
+
+        <h2 class="subtitle" id="how-will-we-use-your-data">How will we use your data</h2>
+        <p class="margin_bottom-5">
+            At this moment, we only use the data given to support the functionality the Queue
+            promises, giving students an easy way to ask questions and sign off work and giving
+            teaching staff an easy to use system to support signing off and answering questions.
+        </p>
+        <p class="margin_bottom-5">
+            Data is being used on a need to know basis. This means that student-assistants only see,
+            what they need to see to fulfil their duties as teaching assistant and the same for
+            teachers. Only administrators have access to the database.
+        </p>
+        <p class="margin_bottom-5">
+            For example, student assistants cannot view each others feedback. Also teaching
+            assistants can only see the name of the student, not the NetID, email or student number
+            of the student.
+        </p>
+        <p class="margin_bottom-5">
+            A teacher might export data to use for purposes to calculate final grades or import it
+            into other tools, namely BrightSpace and Osiris. Furthermore, admins are able to run a
+            script to gather aggregate data about lab sessions and specific people, who spent a lot
+            of time with a teaching assistant. If these numbers are a cause for concern, they will
+            be relayed to the study advisor.
+        </p>
+        <p class="margin_bottom-5">
+            Teachers will look at the feedback of TA's to evaluate their performance and if action
+            needs to be taken. The teacher is able to determine the student that posted the feedback
+            and is allowed to contact the student to inquire more information.
+        </p>
+        <p class="margin_bottom-5">
+            Data from the Queue is is also used for research purposes. This research is done to
+            explore the possibilities of increasing efficiency and quality of the support given to
+            students and teachers. Questions students asked will be stored and used to figure out if
+            there is a way to be able to answer easy questions right from the Queue, without a need
+            for a TA.
+        </p>
+        <p class="margin_bottom-5">For this research the following data will be stored:</p>
+        <ul class="enumeration margin_bottom-3">
+            <li>
+                The user id of the user that asked the question. This will help us to determine if a
+                user asked a question multiple times and if there is a statistically relevant
+                correlation between multiple types of questions. We are storing the user id as we do
+                not need the name of the student
+            </li>
+            <li>
+                The content of the question. This content will be manually sorted through to remove
+                all data that contains revealing information about the user or a student assistant.
+                For instance: "I'm the one with the red hair" or "Yell Jack when looking for me as
+                John, the previous TA couldn't find me"
+            </li>
+        </ul>
+        <p class="margin_bottom-5">
+            This data will only be accessible to people doing the experiment and will be responsibly
+            handled in such a way that no unauthorised person or persons can get access to this
+            data.
+        </p>
+
+        <h2 class="subtitle" id="how-do-we-store-your-data">How do we store your data</h2>
+        <p class="margin_bottom-3">
+            To comply with GDPR regulations, all data is stored on site at the
+            <th:block th:text="${@instituteProperties.name}"></th:block>
+            . The data stored on our servers will be deleted after 5 years.
+        </p>
+        <p class="margin_bottom-7">
+            Access to data is strictly regulated with only select admins having access to the
+            database. All other entities need to use the website itself, where, as mentioned
+            previously, information is provided on a need to know basis.
+        </p>
+
+        <h1 class="title margin_bottom-3" id="other-matters">Other matters</h1>
+        <p class="margin_bottom-3">For matters like:</p>
+        <ul class="enumeration margin_bottom-3">
+            <li>What are your data protection rights</li>
+            <li>Which cookies we set and why</li>
+        </ul>
+        <p class="margin_bottom-3">
+            Please refer to the
+            <a
+                class="link"
+                href="https://www.tudelft.nl/privacy-statement/"
+                title="https://www.tudelft.nl/privacy-statement/">
+                Privacy Statement of the TU Delft
+            </a>
+        </p>
+        <p>
+            If you have any questions about the privacy policy or want to make use of your rights,
+            please contact
+            <a
+                class="link"
+                href="mailto:privacy-tud@tudelft.nl"
+                title="mailto:privacy-tud@tudelft.nl">
+                the following email address
+            </a>
+        </p>
+    </section>
 </html>
diff --git a/src/main/resources/templates/lab/create.html b/src/main/resources/templates/lab/create.html
index e71d7e3589deb060f76be1f7540100b893c205d0..57ac0313e133f952a8b93c760cdedabd0fa484fd 100644
--- a/src/main/resources/templates/lab/create.html
+++ b/src/main/resources/templates/lab/create.html
@@ -1,73 +1,134 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{edition/view}">
-<head>
-    <title th:text="|Create Lab for ${'#' + (ec == null ? edition.id : ec.id)}|"></title>
-
-    <script src="/webjars/momentjs/min/moment.min.js"></script>
-
-    <script type="application/javascript"
-            src="/webjars/tempusdominus-bootstrap-4/js/tempusdominus-bootstrap-4.min.js"></script>
-    <link rel="stylesheet" href="/webjars/tempusdominus-bootstrap-4/css/tempusdominus-bootstrap-4.min.css"/>
-
-    <script type="application/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
-    <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"/>
-
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
-    <script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
-</head>
-
-<!--@thymesVar id="ec" type="nl.tudelft.labracore.api.dto.EditionCollectionDetailsDTO"-->
-<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
-
-<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.labs.LabCreateDTO"-->
-<!--@thymesVar id="lType" type="nl.tudelft.queue.model.enums.QueueSessionType"-->
-
-<body>
-<section layout:fragment="subcontent">
-    <div class="page-header">
-        <h3>Create lab</h3>
-    </div>
-    <form th:with="action = @{/{ou}/{id}/lab/create/{lTy}(id=${ec == null ? edition.id : ec.id}, ou=${ec == null ? 'edition' : 'shared-edition'}, lTy=${lType.name().toLowerCase()})}"
-          th:action="${action}" th:object="${dto}"
-          class="form-horizontal" method="post" id="form">
-
-        <section layout:fragment="general-config"></section>
-
-        <section layout:fragment="slot-config"></section>
-        <section layout:fragment="exam-config"></section>
-        <section layout:fragment="capacity-config"></section>
-
-        <section layout:fragment="advanced-config"></section>
-
-        <div class="form-group form-row">
-            <div class="col-sm-offset-2 col-sm-8">
-                <button type="submit" class="btn btn-primary ctrl-enter-submit">
-                    Create new lab
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog th:id="create-session" layout:fragment="dialog(shared=false)">
+        <div class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        data-create-session-cancel
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <button
+                        data-create-session-prev
+                        class="hidden:from_small fa-solid fa-arrow-left hidden"
+                        type="button"></button>
+                    <h2>
+                        Create session -
+                        <span id="create-session-part">Type</span>
+                    </h2>
+                </div>
+                <button data-create-session-next class="hidden:from_small" type="button">
+                    Next
+                </button>
+                <button data-create-session-create class="hidden:from_small hidden">
+                    <span data-role="small_label">Create</span>
                 </button>
-                <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script>
+            </div>
+
+            <div class="dialog__body">
+                <nav
+                    id="create-session-nav"
+                    class="hidden nav_list margin_bottom-5"
+                    style="--align: center"
+                    data-style="outlined"
+                    data-direction="horizontal"></nav>
+
+                <input type="hidden" id="create-session-id" name="id" />
+
+                <th:block layout:replace="~{lab/create/type :: form}"></th:block>
+                <th:block layout:replace="~{lab/create/general :: form}"></th:block>
+                <th:block layout:replace="~{lab/create/rooms :: form}"></th:block>
+                <th:block layout:replace="~{lab/create/modules :: form}"></th:block>
+                <th:block layout:replace="~{lab/create/assignments :: form}"></th:block>
+                <th:block layout:replace="~{lab/create/allowed_requests :: form}"></th:block>
+                <th:block layout:replace="~{lab/create/limited_capacity :: form}"></th:block>
+                <th:block layout:replace="~{lab/create/slots :: form}"></th:block>
+                <th:block layout:replace="~{lab/create/exam :: form}"></th:block>
+                <th:block layout:replace="~{lab/create/extra_settings :: settings}"></th:block>
+
+                <div class="flex space_between span-2 margin_top-7 | hidden:small">
+                    <button
+                        data-create-session-cancel
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button
+                        data-create-session-prev
+                        class="button padding_inline-5 padding_block-2 hidden"
+                        type="button"
+                        data-style="outlined">
+                        <span class="margin_right-1 fa-solid fa-arrow-left"></span>
+                        <span>Back</span>
+                    </button>
+                    <button
+                        data-create-session-next
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined">
+                        <span class="margin_right-1 fa-solid fa-arrow-right"></span>
+                        <span>Next</span>
+                    </button>
+                    <button
+                        data-create-session-create
+                        class="button padding_inline-5 padding_block-2 hidden"
+                        type="submit"
+                        data-style="outlined"
+                        th:data-organisational-unit="${edition?.id} ?: ${collection.id}"
+                        th:data-shared="${shared == true}">
+                        <span data-role="icon" class="margin_right-1 fa-solid fa-plus"></span>
+                        <span data-role="label">Create session</span>
+                    </button>
+                </div>
             </div>
         </div>
-    </form>
-</section>
-</body>
+    </dialog>
+
+    <th:block layout:fragment="create_script">
+        <script src="/js/session_wizard.js"></script>
+        <script>
+            $(() => {
+                $("#create-session-type").change(function () {
+                    sessionType = $(this).val();
+                });
+
+                $("[data-create-session-cancel]").click(clearForms);
+
+                $("[data-create-session-prev]").click(() => goToForm(getPrevForm()));
+
+                const forms = $("[data-create-session-part]");
+                setValidityCheck(forms.find(":where(input, select, textarea)"));
+
+                /*
+                 * On click next, check validity of form
+                 * If valid, go to next
+                 * If not, report what is wrong to the user
+                 */
+                $("[data-create-session-next]").click(function () {
+                    $(`[data-create-session-part="${currentForm}"]`).submit();
+                });
+                forms.submit(function (event) {
+                    event.preventDefault();
+
+                    if (!isValid($(this))) {
+                        reportValid($(this));
+                        return;
+                    }
+                    if (currentForm === "type") createNav();
+                    goToForm(getNextForm());
+                });
+
+                $("[data-create-session-create]").click(function () {
+                    submitSession($(this).data("organisationalUnit"), $(this).data("shared"));
+                });
+            });
+        </script>
+    </th:block>
 </html>
diff --git a/src/main/resources/templates/lab/create/allowed_requests.html b/src/main/resources/templates/lab/create/allowed_requests.html
new file mode 100644
index 0000000000000000000000000000000000000000..1c5fd59a6fe66c52994329fb2b24880519ac2488
--- /dev/null
+++ b/src/main/resources/templates/lab/create/allowed_requests.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <form
+        action="#"
+        data-create-session-part="allowed_requests"
+        class="flex_column hidden"
+        layout:fragment="form">
+        <script>
+            $(() => {
+                let defer = $.Deferred();
+
+                $("[data-create-session-part='allowed_requests']")
+                    .on("show", function () {
+                        let aIds = Object.keys(assignments);
+                        const compareStrings = (a, b) => (a === b ? 0 : a < b ? -1 : 1);
+                        aIds.sort((a, b) =>
+                            compareStrings(assignments[a].name, assignments[b].name)
+                        );
+                        aIds.filter(id => assignments[id].selected)
+                            .filter(a => $(`#create-session-requests-${a}-group`).length === 0)
+                            .forEach(a => {
+                                const select =
+                                    $(`<select id="create-session-requests-${a}" name="${a}:requestTypes" data-placeholder="Select requests" data-select multiple data-style="outlined" required>
+                                        <option value="QUESTION">Question</option>
+                                        <option value="SUBMISSION">Submisson</option>
+                                    </select>`);
+
+                                const label = $(
+                                    `<label for="create-session-requests-${a}" class="form__label">Allowed requests for ${assignments[a].name}</label>`
+                                );
+                                const group = $(
+                                    `<div id="create-session-requests-${a}-group" class="flex_column gap-1" data-remove-on-cancel></div>`
+                                );
+                                group.append(label).append(select);
+
+                                configureSelect(select);
+
+                                $(this).append(group);
+                                setValidityCheck(select);
+                                select.change();
+                            });
+                        aIds.filter(id => !assignments[id].selected).forEach(a =>
+                            $(`#create-session-requests-${a}-group`).remove()
+                        );
+                        defer.resolve();
+                    })
+                    .on("edit", function (event, session) {
+                        defer = $.Deferred();
+                        defer.then(() => {
+                            const aIds = Object.keys(assignments);
+                            const requests = {};
+                            aIds.forEach(a => {
+                                requests[a] = [];
+                            });
+                            session.allowedRequests.forEach(r => {
+                                requests[r.assignment].push(r.type);
+                            });
+                            aIds.filter(id => assignments[id].selected).forEach(a => {
+                                $(`#create-session-requests-${a}`)
+                                    .val(requests[a])
+                                    .change()
+                                    .each((i, sel) => updateSelect($(sel)));
+                            });
+                        });
+                        assignmentsLoaded.then(() => $(this).trigger("show"));
+                    });
+            });
+        </script>
+    </form>
+</html>
diff --git a/src/main/resources/templates/lab/create/assignments.html b/src/main/resources/templates/lab/create/assignments.html
new file mode 100644
index 0000000000000000000000000000000000000000..f921197b3a7dc3a0948551d578068bc03f891aa6
--- /dev/null
+++ b/src/main/resources/templates/lab/create/assignments.html
@@ -0,0 +1,136 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <form
+        action="#"
+        data-create-session-part="assignments"
+        class="flex_column hidden"
+        layout:fragment="form">
+        <div class="flex_column gap-1">
+            <label for="create-session-modules" class="form__label">Modules</label>
+            <select
+                id="create-session-modules"
+                name="modules"
+                data-placeholder="Select modules"
+                data-select
+                multiple
+                data-style="outlined"
+                required
+                autocomplete="off">
+                <option
+                    th:each="module : ${allModules}"
+                    th:value="${module.id}"
+                    th:text="${module.name}"></option>
+            </select>
+        </div>
+
+        <script>
+            let assignments = {};
+            let divisions = {};
+            let assignmentsLoaded = $.Deferred();
+
+            $(() => {
+                let modules = [];
+                const form = $("[data-create-session-part='assignments']");
+
+                $("#create-session-modules").change(function () {
+                    const newModules = $(this).val();
+                    if (modules.length > newModules.length) {
+                        // module deselected
+                        modules
+                            .filter(m => !newModules.includes(m))
+                            .forEach(m => {
+                                $(`#create-session-assignments-${m}-group`).remove();
+                                delete divisions[m];
+                            });
+                        modules = newModules;
+                    } else {
+                        // module selected
+                        const promises = newModules
+                            .filter(m => !modules.includes(m))
+                            .map(m => {
+                                return fetch(`/module/${m}`)
+                                    .then(res => res.json())
+                                    .then(module => {
+                                        const select = $(
+                                            `<select id="create-session-assignments-${m}" name="assignments-${m}" data-placeholder="Select assignments" data-select multiple data-style="outlined" required></select>`
+                                        );
+                                        const label = $(
+                                            `<label for="create-session-assignments-${m}" class="form__label">Assignments in ${module.name}</label>`
+                                        );
+
+                                        divisions[m] = {
+                                            name: module.name,
+                                            divisions: module.divisions,
+                                        };
+                                        module.assignments.forEach(assignment => {
+                                            assignments[assignment.id] = {
+                                                name: `${module.name} - ${assignment.name}`,
+                                                module: module.id,
+                                            };
+                                            select.append(
+                                                `<option value="${assignment.id}">${assignment.name}</option>`
+                                            );
+                                        });
+
+                                        const group = $(
+                                            `<div id="create-session-assignments-${m}-group" class="flex_column gap-1" data-remove-on-cancel></div>`
+                                        );
+                                        group.append(label).append(select);
+                                        configureSelect(select);
+
+                                        select.change(function () {
+                                            Object.keys(assignments)
+                                                .filter(a => assignments[a].module === module.id)
+                                                .forEach(
+                                                    id =>
+                                                        (assignments[id].selected = $(this)
+                                                            .val()
+                                                            .includes(id))
+                                                );
+                                        });
+
+                                        form.append(group);
+                                        modules = newModules;
+
+                                        setValidityCheck(select);
+                                        select.change();
+                                    })
+                                    .catch(() => alert("Error fetching assignments for module"));
+                            });
+                        Promise.all(promises).then(() => assignmentsLoaded.resolve());
+                    }
+                });
+
+                form.on("edit", function (event, session) {
+                    const moduleIds = session.modules.map(m => m.id);
+                    const modules = {};
+                    session.modules.forEach(m => (modules[m.id] = m));
+
+                    assignmentsLoaded = $.Deferred();
+                    assignmentsLoaded.then(() => {
+                        moduleIds.forEach(m =>
+                            $(`#create-session-assignments-${m}`)
+                                .val(
+                                    session.session.assignments
+                                        .filter(a =>
+                                            modules[m].assignments.map(ma => ma.id).includes(a.id)
+                                        )
+                                        .map(a => a.id)
+                                )
+                                .change()
+                                .each((i, sel) => updateSelect($(sel)))
+                        );
+                    });
+
+                    $("#create-session-modules")
+                        .val(moduleIds)
+                        .each((i, sel) => updateSelect($(sel)))
+                        .change();
+                });
+            });
+        </script>
+    </form>
+</html>
diff --git a/src/main/resources/templates/lab/create/exam.html b/src/main/resources/templates/lab/create/exam.html
index cbee1acae5416603bfc85a0ae1e19f2f6fa33573..701da965221157166f823551fabe304937237062 100644
--- a/src/main/resources/templates/lab/create/exam.html
+++ b/src/main/resources/templates/lab/create/exam.html
@@ -1,55 +1,34 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{lab/create/slotted}">
-<head>
-    <title>Create Exam Lab</title>
-</head>
-
-<!--@thymesVar id="ec" type="nl.tudelft.labracore.api.dto.EditionCollectionDetailsDTO"-->
-<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
-
-<!--@thymesVar id="modules" type="java.util.List<nl.tudelft.labracore.api.dto.ModuleDetailsDTO>"-->
-<!--@thymesVar id="assignments" type="java.util.List<nl.tudelft.labracore.api.dto.AssignmentDetailsDTO>"-->
-<!--@thymesVar id="rooms" type="java.util.List<nl.tudelft.labracore.api.dto.RoomSummaryDTO>"-->
-
-<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.labs.ExamLabCreateDTO"-->
-<!--@thymesVar id="lType" type="nl.tudelft.queue.model.enums.QueueSessionType"-->
-
-<body>
-<th:block th:object="${dto}">
-    <section layout:fragment="exam-config">
-        <h4>Exam</h4>
-        <hr/>
-
-        <div class="form-group form-row">
-            <label for="exam-percentage-input" class="col-sm-2 col-form-label">Percentage for Exam lab</label>
-            <div class="col-sm-4">
-                <input id="exam-percentage-input" th:field="*{examLabConfig.percentage}"
-                       min="10" max="100" type="number" placeholder="15" class="form-control"
-                       required/>
-            </div>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <form
+        action="#"
+        data-create-session-part="exam"
+        class="flex_column hidden"
+        layout:fragment="form">
+        <div class="flex_column gap-1">
+            <label for="create-session-exam-percentage" class="form__label">
+                Percentage for Exam Lab
+            </label>
+            <input
+                class="textfield"
+                type="number"
+                id="create-session-exam-percentage"
+                name="examLabConfig.percentage"
+                data-style="outlined"
+                value="15"
+                min="0"
+                required />
         </div>
-    </section>
-</th:block>
 
-</body>
+        <script>
+            $(() => {
+                $("[data-create-session-part='slots']").on("edit", function (event, session) {
+                    $("#create-session-exam-percentage").val(session.examLabConfig.percentage);
+                });
+            });
+        </script>
+    </form>
 </html>
diff --git a/src/main/resources/templates/lab/create/extra_settings.html b/src/main/resources/templates/lab/create/extra_settings.html
new file mode 100644
index 0000000000000000000000000000000000000000..bfca39db535457a547e13fec62f2e2e4c401dade
--- /dev/null
+++ b/src/main/resources/templates/lab/create/extra_settings.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <form
+        action="#"
+        data-create-session-part="extra_settings"
+        class="flex hidden | flex_column:small"
+        layout:fragment="settings">
+        <nav data-tabs aria-label="Extra settings">
+            <ul
+                class="nav_list"
+                style="--align: center; --width: 12rem"
+                role="list"
+                data-style="outlined">
+                <li data-active="true" th:data-view="create-session-extra-info">
+                    <button type="button">Extra info</button>
+                </li>
+                <li data-active="false" th:data-view="create-session-session-timing">
+                    <button type="button">Session timing</button>
+                </li>
+                <li data-active="false" th:data-view="create-session-experimental">
+                    <button type="button">Experimental</button>
+                </li>
+                <li data-active="false" th:data-view="create-session-request-constraints">
+                    <button type="button">Request constraints</button>
+                </li>
+                <li
+                    data-active="false"
+                    th:data-view="create-session-repetition"
+                    data-create-session-wizard="create,copy">
+                    <button type="button">Repetition</button>
+                </li>
+            </ul>
+        </nav>
+
+        <th:block layout:replace="~{lab/create/extra_settings/extra_info :: form}"></th:block>
+        <th:block layout:replace="~{lab/create/extra_settings/session_timing :: form}"></th:block>
+        <th:block layout:replace="~{lab/create/extra_settings/experimental :: form}"></th:block>
+        <th:block
+            layout:replace="~{lab/create/extra_settings/request_constraints :: form}"></th:block>
+        <th:block layout:replace="~{lab/create/extra_settings/repetition :: form}"></th:block>
+    </form>
+</html>
diff --git a/src/main/resources/templates/lab/create/extra_settings/experimental.html b/src/main/resources/templates/lab/create/extra_settings/experimental.html
new file mode 100644
index 0000000000000000000000000000000000000000..d70268b10ffe5e0444eb2b6e03dcdcb84a53be6a
--- /dev/null
+++ b/src/main/resources/templates/lab/create/extra_settings/experimental.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <div
+        layout:fragment="form"
+        id="create-session-experimental"
+        class="hidden flex_column flex_grow">
+        <div>
+            <input
+                type="checkbox"
+                id="create-session-experimental-checkbox"
+                name="enableExperimental"
+                value="true" />
+            <label for="create-session-experimental-checkbox" class="font-400">
+                Enable experimental features
+            </label>
+        </div>
+
+        <script>
+            $(() => {
+                $("[data-create-session-part='extra_settings']").on(
+                    "edit",
+                    function (event, session) {
+                        $("#create-session-experimental-checkbox").text(session.enableExperimental);
+                    }
+                );
+            });
+        </script>
+    </div>
+</html>
diff --git a/src/main/resources/templates/lab/create/extra_settings/extra_info.html b/src/main/resources/templates/lab/create/extra_settings/extra_info.html
new file mode 100644
index 0000000000000000000000000000000000000000..d253aed01080a7e00bf3c0021a9fb6f1d6a55c84
--- /dev/null
+++ b/src/main/resources/templates/lab/create/extra_settings/extra_info.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <div layout:fragment="form" id="create-session-extra-info" class="flex_column flex_grow">
+        <div class="flex_column gap-1">
+            <label for="create-session-extra-info-field" class="form__label">
+                Extra session info
+            </label>
+            <textarea
+                id="create-session-extra-info-field"
+                name="extraInfo"
+                class="textfield"
+                data-style="outlined"
+                style="resize: none"
+                rows="5"></textarea>
+        </div>
+
+        <script>
+            $(() => {
+                $("[data-create-session-part='extra_settings']").on(
+                    "edit",
+                    function (event, session) {
+                        $("#create-session-extra-info-field").html(session.extraInfo);
+                    }
+                );
+            });
+        </script>
+    </div>
+</html>
diff --git a/src/main/resources/templates/lab/create/extra_settings/repetition.html b/src/main/resources/templates/lab/create/extra_settings/repetition.html
new file mode 100644
index 0000000000000000000000000000000000000000..5c977a7ac68a731ac005c3813e5f72235c47351b
--- /dev/null
+++ b/src/main/resources/templates/lab/create/extra_settings/repetition.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <div
+        layout:fragment="form"
+        id="create-session-repetition"
+        class="flex_column hidden flex_grow"
+        data-create-session-wizard="create,copy">
+        <div class="flex_column gap-1">
+            <label for="create-session-repeat" class="form__label">
+                Number of weeks to repeat this session
+            </label>
+            <input
+                type="number"
+                id="create-session-repeat"
+                name="repeatForXWeeks"
+                class="textfield"
+                data-style="outlined"
+                placeholder="0" />
+        </div>
+    </div>
+</html>
diff --git a/src/main/resources/templates/lab/create/extra_settings/request_constraints.html b/src/main/resources/templates/lab/create/extra_settings/request_constraints.html
new file mode 100644
index 0000000000000000000000000000000000000000..441c1f2123348b1153b943b2c30400bb4398b830
--- /dev/null
+++ b/src/main/resources/templates/lab/create/extra_settings/request_constraints.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <div
+        layout:fragment="form"
+        id="create-session-request-constraints"
+        class="flex_column hidden flex_grow">
+        <div class="flex_column gap-1">
+            <label for="create-session-cluster-constraint" class="form__label">
+                Cluster restriction
+            </label>
+            <select
+                id="create-session-cluster-constraint"
+                name="constraints.clusterConstaint.clusters"
+                data-select
+                multiple
+                data-style="outlined"
+                data-placeholder="All clusters"
+                autocomplete="off">
+                <option
+                    th:each="cluster : ${clusters}"
+                    th:value="${cluster.id}"
+                    th:text="${cluster.name}"></option>
+            </select>
+        </div>
+
+        <script>
+            $(() => {
+                $("[data-create-session-part='extra_settings']")
+                    .on("show", () => {
+                        let mIds = Object.keys(divisions);
+                        const compareStrings = (a, b) => (a === b ? 0 : a < b ? -1 : 1);
+                        mIds.sort((a, b) => compareStrings(divisions[a].name, divisions[b].name));
+
+                        $("#create-session-division-constraint-group").remove();
+                        const select = $(
+                            `<select id="create-session-division-constraint" name="constraints.moduleDivisionConstraint.divisions" data-placeholder="All disivions" data-select multiple data-style="outlined"></select>`
+                        );
+                        const label = $(
+                            `<label for="create-session-division-constraint" class="form__label">Division restriction</label>`
+                        );
+
+                        mIds.forEach(m =>
+                            divisions[m].divisions.forEach(md => {
+                                select.append(
+                                    $(
+                                        `<option value="${md.id}">${divisions[m].name} - Division #${md.id}</option>`
+                                    )
+                                );
+                            })
+                        );
+
+                        const group = $(
+                            `<div id="create-session-division-constraint-group" class="flex_column gap-1"></div>`
+                        );
+                        group.append(label).append(select);
+                        configureSelect(select);
+
+                        $("#create-session-request-constraints").append(group);
+                    })
+                    .on("edit", function (event, session) {
+                        assignmentsLoaded.then(() => $(this).trigger("show"));
+                    });
+            });
+        </script>
+    </div>
+</html>
diff --git a/src/main/resources/templates/lab/create/extra_settings/session_timing.html b/src/main/resources/templates/lab/create/extra_settings/session_timing.html
new file mode 100644
index 0000000000000000000000000000000000000000..9db51ecbacbaa055cdf9a3dbac1f30f56f6284ce
--- /dev/null
+++ b/src/main/resources/templates/lab/create/extra_settings/session_timing.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <div
+        layout:fragment="form"
+        id="create-session-session-timing"
+        class="flex_column hidden flex_grow">
+        <div class="flex_column gap-1">
+            <label for="create-session-grace-period" class="form__label">
+                End of session grace period
+            </label>
+            <input
+                type="number"
+                min="0"
+                value="15"
+                id="create-session-grace-period"
+                name="eolGracePeriod"
+                class="textfield"
+                data-style="outlined"
+                required
+                data-initial="15" />
+        </div>
+
+        <script>
+            $(() => {
+                $("[data-create-session-part='extra_settings']").on(
+                    "edit",
+                    function (event, session) {
+                        $("#create-session-grace-period").val(session.eolGracePeriod);
+                    }
+                );
+            });
+        </script>
+    </div>
+</html>
diff --git a/src/main/resources/templates/lab/create/general.html b/src/main/resources/templates/lab/create/general.html
new file mode 100644
index 0000000000000000000000000000000000000000..71e59d88494830cb8b53fed489b825c03320260a
--- /dev/null
+++ b/src/main/resources/templates/lab/create/general.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <form
+        action="#"
+        data-create-session-part="general"
+        class="flex_column hidden"
+        layout:fragment="form">
+        <div class="flex_column gap-1">
+            <label for="create-session-name" class="form__label">Name</label>
+            <input
+                id="create-session-name"
+                name="name"
+                class="textfield"
+                placeholder="Session name"
+                data-style="outlined"
+                required />
+        </div>
+
+        <div class="flex_column gap-1">
+            <label for="create-session-direction" class="form__label">Direction</label>
+            <select
+                id="create-session-direction"
+                name="communicationMethod"
+                data-placeholder="Pick a direction"
+                data-select
+                data-style="outlined"
+                required
+                autocomplete="off">
+                <option data-empty-option value=""></option>
+                <option value="STUDENT_VISIT_TA">Student visits TA</option>
+                <option value="TA_VISIT_STUDENT">TA visits student</option>
+                <option value="JITSI_MEET">Jitsi meeting</option>
+            </select>
+        </div>
+
+        <div class="flex_column gap-1">
+            <label for="create-session-start" class="form__label">Session start</label>
+            <input
+                class="textfield"
+                type="datetime-local"
+                id="create-session-start"
+                name="slot.opensAt"
+                data-style="outlined"
+                required />
+        </div>
+
+        <div class="flex_column gap-1">
+            <label for="create-session-end" class="form__label">Session end</label>
+            <input
+                class="textfield"
+                type="datetime-local"
+                id="create-session-end"
+                name="slot.closesAt"
+                data-style="outlined"
+                required />
+        </div>
+
+        <script>
+            $(() => {
+                const format = "YYYY-MM-DDTHH:mm";
+                $("#create-session-start")
+                    .focus(function () {
+                        if ($(this).val() === "") {
+                            $(this).val(moment().format(format)).change();
+                        }
+                    })
+                    .change(function () {
+                        const end = $("#create-session-end");
+                        if (end.data("set") !== true) {
+                            end.val(
+                                moment($(this).val(), format)
+                                    .add(3, "hours")
+                                    .add(45, "minutes")
+                                    .format(format)
+                            );
+                        }
+                    });
+                $("#create-session-end").change(function () {
+                    $(this).data("set", true);
+                });
+                $("[data-create-session-part='general']")
+                    .on("clear", function () {
+                        $("#create-session-end").data("set", false);
+                    })
+                    .on("edit", function (event, session) {
+                        $("#create-session-name").val(session.session.name);
+                        $("#create-session-direction")
+                            .val(session.communicationMethod)
+                            .each((i, sel) => updateSelect($(sel)));
+                        $("#create-session-start").val(
+                            moment(session.session.start).format(format)
+                        );
+                        $("#create-session-end").val(moment(session.session.end).format(format));
+                    });
+            });
+        </script>
+    </form>
+</html>
diff --git a/src/main/resources/templates/lab/create/limited_capacity.html b/src/main/resources/templates/lab/create/limited_capacity.html
new file mode 100644
index 0000000000000000000000000000000000000000..fa2779e875b27b80183b922ae29580574ede62d1
--- /dev/null
+++ b/src/main/resources/templates/lab/create/limited_capacity.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <form
+        action="#"
+        data-create-session-part="limited_capacity"
+        class="flex_column hidden"
+        layout:fragment="form">
+        <div class="flex_column gap-1">
+            <label for="create-session-procedure" class="form__label">Selection procedure</label>
+            <select
+                id="create-session-procedure"
+                name="capacitySessionConfig.procedure"
+                data-style="outlined"
+                required
+                data-select
+                data-placeholder="Select a procedure">
+                <option data-empty-option value=""></option>
+                <option
+                    th:each="procedure : ${T(nl.tudelft.queue.model.enums.SelectionProcedure).values()}"
+                    th:value="${procedure}"
+                    th:text="#{|session.selection.procedure.${procedure.name().toLowerCase()}|}"></option>
+            </select>
+        </div>
+
+        <div class="flex_column gap-1">
+            <label for="create-session-selection-enrol-open" class="form__label">
+                Enrolment opening time
+            </label>
+            <input
+                class="textfield"
+                type="datetime-local"
+                id="create-session-selection-enrol-open"
+                name="capacitySessionConfig.enrolmentOpensAt"
+                data-style="outlined"
+                required />
+        </div>
+
+        <div class="flex_column gap-1">
+            <label for="create-session-selection-enrol-open" class="form__label">
+                Enrolment closing time
+            </label>
+            <input
+                class="textfield"
+                type="datetime-local"
+                id="create-session-selection-enrol-close"
+                name="capacitySessionConfig.enrolmentClosesAt"
+                data-style="outlined"
+                required />
+        </div>
+
+        <div class="flex_column gap-1">
+            <label for="create-session-selection-enrol-open" class="form__label">
+                Selection time
+            </label>
+            <input
+                class="textfield"
+                type="datetime-local"
+                id="create-session-selection-enrol-selection"
+                name="capacitySessionConfig.selectionAt"
+                data-style="outlined"
+                required />
+        </div>
+
+        <script>
+            $(() => {
+                const format = "YYYY-MM-DDTHH:mm";
+                $("[data-create-session-part='limited_capacity']").on(
+                    "edit",
+                    function (event, session) {
+                        $("#create-session-procedure").val(session.capacitySessionConfig.procedure);
+                        $("#create-session-selection-enrol-open").val(
+                            moment(session.capacitySessionConfig.enrolmentOpensAt).format(format)
+                        );
+                        $("#create-session-selection-enrol-close").val(
+                            moment(session.capacitySessionConfig.enrolmentClosesAt).format(format)
+                        );
+                        $("#create-session-selection-enrol-selection").val(
+                            moment(session.capacitySessionConfig.selectionAt).format(format)
+                        );
+                    }
+                );
+            });
+        </script>
+    </form>
+</html>
diff --git a/src/main/resources/templates/lab/create/modules.html b/src/main/resources/templates/lab/create/modules.html
new file mode 100644
index 0000000000000000000000000000000000000000..ac8c402ed45e32991b77d19612b1ff5984177937
--- /dev/null
+++ b/src/main/resources/templates/lab/create/modules.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <form
+        action="#"
+        data-create-session-part="modules"
+        class="flex_column hidden"
+        layout:fragment="form">
+        <div class="flex_column gap-1">
+            <label for="create-session-modules-only" class="form__label">Modules</label>
+            <select
+                id="create-session-modules-only"
+                name="modules"
+                data-placeholder="Select modules"
+                data-select
+                multiple
+                data-style="outlined"
+                required
+                autocomplete="off">
+                <option
+                    th:each="module : ${allModules}"
+                    th:value="${module.id}"
+                    th:text="${module.name}"></option>
+            </select>
+        </div>
+
+        <script>
+            $(() => {
+                $("[data-create-session-part='modules']").on("edit", function (event, session) {
+                    $("#create-session-modules-only").val(session.modules);
+                });
+            });
+        </script>
+    </form>
+</html>
diff --git a/src/main/resources/templates/lab/create/rooms.html b/src/main/resources/templates/lab/create/rooms.html
new file mode 100644
index 0000000000000000000000000000000000000000..520d3726bba71107427a76d5cb2dc578fb5068a0
--- /dev/null
+++ b/src/main/resources/templates/lab/create/rooms.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <form
+        action="#"
+        data-create-session-part="rooms"
+        class="flex_column hidden"
+        layout:fragment="form">
+        <div class="flex_column gap-1">
+            <label for="create-session-buildings" class="form__label">Buildings</label>
+            <select
+                id="create-session-buildings"
+                name="buildings"
+                data-placeholder="Select buildings"
+                data-select
+                multiple
+                data-style="outlined"
+                required
+                autocomplete="off">
+                <option
+                    th:each="building : ${buildings}"
+                    th:value="${building.id}"
+                    th:text="${building.name}"></option>
+            </select>
+        </div>
+
+        <script>
+            $(() => {
+                let buildings = [];
+                let defer = $.Deferred();
+                const form = $("[data-create-session-part='rooms']");
+
+                $("#create-session-buildings").change(function () {
+                    const newBuildings = $(this).val();
+                    if (buildings.length > newBuildings.length) {
+                        // building deselected
+                        buildings
+                            .filter(b => !newBuildings.includes(b))
+                            .forEach(b => $(`#create-session-rooms-${b}-group`).remove());
+                        buildings = newBuildings;
+                    } else {
+                        // building selected
+                        const promises = newBuildings
+                            .filter(b => !buildings.includes(b))
+                            .map(b => {
+                                return fetch(`/building/${b}`)
+                                    .then(res => res.json())
+                                    .then(building => {
+                                        const select = $(
+                                            `<select id="create-session-rooms-${b}" name="rooms-${b}" data-placeholder="Select rooms" data-select multiple data-style="outlined" required></select>`
+                                        );
+                                        const label = $(
+                                            `<label for="create-session-rooms-${b}" class="form__label">Rooms in ${building.name}</label>`
+                                        );
+                                        building.rooms.forEach(room => {
+                                            select.append(
+                                                `<option value="${room.id}">${room.name}</option>`
+                                            );
+                                        });
+                                        const group = $(
+                                            `<div id="create-session-rooms-${b}-group" class="flex_column gap-1" data-remove-on-cancel></div>`
+                                        );
+                                        group.append(label).append(select);
+                                        configureSelect(select);
+                                        form.append(group);
+                                        buildings = newBuildings;
+
+                                        setValidityCheck(select);
+                                        select.change();
+                                    })
+                                    .catch(() => alert("Error fetching rooms for building"));
+                            });
+                        Promise.all(promises).then(() => defer.resolve());
+                    }
+                });
+
+                form.on("edit", function (event, session) {
+                    const buildings = [...new Set(session.session.rooms.map(r => r.building.id))];
+
+                    defer = $.Deferred();
+                    defer.then(() => {
+                        buildings.forEach(b =>
+                            $(`#create-session-rooms-${b}`)
+                                .val(
+                                    session.session.rooms
+                                        .filter(r => r.building.id === b)
+                                        .map(r => r.id)
+                                )
+                                .change()
+                                .each((i, sel) => updateSelect($(sel)))
+                        );
+                    });
+
+                    $("#create-session-buildings")
+                        .val(buildings)
+                        .each((i, sel) => updateSelect($(sel)))
+                        .change();
+                });
+            });
+        </script>
+    </form>
+</html>
diff --git a/src/main/resources/templates/lab/create/slots.html b/src/main/resources/templates/lab/create/slots.html
new file mode 100644
index 0000000000000000000000000000000000000000..d77d8e5dfa71f7938cb0be9e6bf6230e739a142d
--- /dev/null
+++ b/src/main/resources/templates/lab/create/slots.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <form
+        action="#"
+        data-create-session-part="slots"
+        class="flex_column hidden"
+        layout:fragment="form">
+        <div class="flex_column gap-1">
+            <label for="create-session-interval" class="form__label">
+                Interval length in minutes
+            </label>
+            <input
+                class="textfield"
+                type="number"
+                id="create-session-interval"
+                name="slottedLabConfig.duration"
+                data-style="outlined"
+                value="15"
+                min="1"
+                required />
+        </div>
+
+        <div class="flex_column gap-1">
+            <label for="create-session-time-slot-capacity" class="form__label">
+                Number of requests per time slot
+            </label>
+            <input
+                class="textfield"
+                type="number"
+                id="create-session-time-slot-capacity"
+                name="slottedLabConfig.capacity"
+                data-style="outlined"
+                value="3"
+                min="1"
+                required />
+        </div>
+
+        <div class="flex_column gap-1">
+            <label for="create-session-slot-selection-time" class="form__label">
+                Slot selection opening time
+            </label>
+            <input
+                class="textfield"
+                type="datetime-local"
+                id="create-session-slot-selection-time"
+                name="slottedLabConfig.selectionOpensAt"
+                data-style="outlined"
+                required />
+        </div>
+
+        <script>
+            $(() => {
+                const format = "YYYY-MM-DDTHH:mm";
+                $("[data-create-session-part='slots']").on("edit", function (event, session) {
+                    $("#create-session-interval").val(session.slottedLabConfig.duration);
+                    $("#create-session-time-slot-capacity").val(session.slottedLabConfig.capacity);
+                    $("#create-session-slot-selection-time").val(
+                        moment(session.slottedLabConfig.selectionOpensAt).format(format)
+                    );
+                });
+            });
+        </script>
+    </form>
+</html>
diff --git a/src/main/resources/templates/lab/create/type.html b/src/main/resources/templates/lab/create/type.html
new file mode 100644
index 0000000000000000000000000000000000000000..87a224f930e0d54965981d4330d1caacbf960355
--- /dev/null
+++ b/src/main/resources/templates/lab/create/type.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <form
+        action="#"
+        data-create-session-part="type"
+        class="flex_column gap-1"
+        layout:fragment="form">
+        <label for="create-session-type" class="form__label">Session type</label>
+        <select id="create-session-type" name="type" data-select data-style="outlined">
+            <option selected th:value="REGULAR">Regular</option>
+            <option th:value="SLOTTED">Slotted</option>
+            <option th:value="EXAM">Exam</option>
+            <option th:value="CAPACITY">Limited capacity</option>
+        </select>
+
+        <script>
+            $(() => {
+                $("[data-create-session-part='type']").on("edit", function (event, session) {
+                    $("#create-session-type").val(session.type).change();
+                    $(`[data-create-session-part="${currentForm}"]`).submit();
+                });
+            });
+        </script>
+    </form>
+</html>
diff --git a/src/main/resources/templates/lab/enqueue.html b/src/main/resources/templates/lab/enqueue.html
index cc899c729411eb37f2fabb168fcefd5b4490da2c..a1b48073d09a8f337c65e5adec538dd90e697fee 100644
--- a/src/main/resources/templates/lab/enqueue.html
+++ b/src/main/resources/templates/lab/enqueue.html
@@ -1,66 +1,126 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is di•••••••••••stributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{edition/view}">
-
-<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
-
-<!--@thymesVar id="qSession" type="nl.tudelft.queue.model.QueueSession"-->
-<!--@thymesVar id="request" type="nl.tudelft.queue.dto.create.RequestCreateDTO"-->
-
-<!--@thymesVar id="rooms" type="java.util.List<nl.tudelft.labracore.api.dto.RoomSummaryDTO>"-->
-<!--@thymesVar id="assignments" type="java.util.List<nl.tudelft.labracore.api.dto.AssignmentSummaryDTO>"-->
-<!--@thymesVar id="types" type="java.util.Map<java.lang.Long, java.util.Set<java.lang.String>>"-->
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="enqueue" layout:fragment="dialog">
+        <form
+            th:action="@{/lab/{lab}/enqueue/lab(lab=${qSession.id})}"
+            method="post"
+            class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Enqueue</h2>
+                </div>
+                <button class="hidden:from_small">Enqueue</button>
+            </div>
 
-<!--@thymesVar id="rType" type="java.lang.String"-->
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-7">
+                    <th:block layout:fragment="fields_before"></th:block>
 
-<!--@thymesVar id="message" type="java.lang.String"-->
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="enqueue-assignment">Assignment</label>
+                        <select
+                            id="enqueue-assignment"
+                            name="assignment"
+                            data-select
+                            data-style="outlined"
+                            data-placeholder="Select assignment"
+                            required>
+                            <option data-empty-option value=""></option>
+                            <option
+                                th:each="entry : ${assignments}"
+                                th:value="${entry.key.id}"
+                                th:text="|${entry.value} - ${entry.key.name}|"></option>
+                        </select>
+                    </div>
 
-<body>
-<section layout:fragment="subcontent">
-    <script type="text/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
-    <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css" type="text/css"/>
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="enqueue-type">Type</label>
+                        <select
+                            id="enqueue-type"
+                            name="requestType"
+                            data-select
+                            data-style="outlined"
+                            data-placeholder="Select type"
+                            required>
+                            <option data-empty-option value=""></option>
+                            <option
+                                th:each="type : ${T(nl.tudelft.queue.model.enums.RequestType).values()}"
+                                th:value="${type}"
+                                th:text="${type.displayName()}"></option>
+                        </select>
+                    </div>
 
-    <div class="page-sub-header">
-        <h3 th:text="${'Session #' + qSession.id}"></h3>
-    </div>
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="enqueue-room">Room</label>
+                        <select
+                            id="enqueue-room"
+                            name="room"
+                            data-select
+                            data-style="outlined"
+                            data-placeholder="Select room"
+                            required>
+                            <option data-empty-option value=""></option>
+                            <option
+                                th:each="room : ${rooms}"
+                                th:value="${room.id}"
+                                th:text="|${room.building.name} - ${room.name}|"></option>
+                        </select>
+                    </div>
 
-    <div class="alert alert-info mt-md-3" role="alert" th:unless="${#strings.isEmpty(message)}">
-        <span th:text="${message}"></span>
-    </div>
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="enqueue-question">Question</label>
+                        <textarea
+                            id="enqueue-question"
+                            class="textfield"
+                            name="question"
+                            data-style="outlined"
+                            placeholder="Write your question"
+                            rows="3"
+                            style="resize: none"
+                            required
+                            minlength="15"
+                            maxlength="500"></textarea>
+                    </div>
 
-    <form th:action="@{/lab/{id}/enqueue/{rType}(id=${qSession.id}, rType=${rType})}" th:object="${request}" method="post"
-          class="form-horizontal">
-        <th:block layout:fragment="enqueue-body">
-        </th:block>
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="enqueue-comment">Comment</label>
+                        <textarea
+                            id="enqueue-comment"
+                            class="textfield"
+                            name="comment"
+                            data-style="outlined"
+                            placeholder="Help your TA find you"
+                            rows="3"
+                            style="resize: none"
+                            maxlength="250"></textarea>
+                    </div>
+                </div>
 
-        <div class="form-group">
-            <div class="col-sm-offset-2 col-sm-10">
-                <button type="submit" name="enqueue" id="enqueue"
-                        class="btn btn-success ctrl-enter-submit">Enqueue
-                </button>
-                <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script>
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="submit"
+                        data-style="outlined">
+                        <span class="margin_right-1 fa-solid fa-hand"></span>
+                        <span>Enqueue</span>
+                    </button>
+                </div>
             </div>
-        </div>
-    </form>
-</section>
-</body>
+        </form>
+    </dialog>
 </html>
diff --git a/src/main/resources/templates/lab/enqueue/exam.html b/src/main/resources/templates/lab/enqueue/exam.html
new file mode 100644
index 0000000000000000000000000000000000000000..fc0a61962e29113a6b7cec1353a7db6a54b40756
--- /dev/null
+++ b/src/main/resources/templates/lab/enqueue/exam.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{lab/enqueue}">
+    <th:block layout:fragment="fields_before">
+        <div class="flex_column gap-1">
+            <label class="form__label" for="enqueue-time-slot">Time slot</label>
+            <select
+                id="enqueue-time-slot"
+                name="timeSlot.id"
+                data-select
+                data-style="outlined"
+                data-placeholder="Select time slot"
+                required>
+                <option
+                    th:each="slot, iter : ${qSession.timeSlots.?[canTakeSlot()]}"
+                    th:value="${slot.id}"
+                    th:text="${slot.slot.toSentence()}"
+                    th:selected="${iter.index == 0}"></option>
+            </select>
+        </div>
+    </th:block>
+</html>
diff --git a/src/main/resources/templates/lab/enqueue/regular.html b/src/main/resources/templates/lab/enqueue/regular.html
new file mode 100644
index 0000000000000000000000000000000000000000..daf2701a5686a43b5ce0e46061402c9d9727ea24
--- /dev/null
+++ b/src/main/resources/templates/lab/enqueue/regular.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{lab/enqueue}"></html>
diff --git a/src/main/resources/templates/lab/remove.html b/src/main/resources/templates/lab/remove.html
index 7223294af27cf7865ec7da8130505883a9310824..cfcddbc9eb499824eb7a666ca2f995445d85670b 100644
--- a/src/main/resources/templates/lab/remove.html
+++ b/src/main/resources/templates/lab/remove.html
@@ -1,51 +1,50 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{edition/view}">
-
-<!--@thymesVar id="ec" type="nl.tudelft.labracore.api.dto.EditionCollectionDetailsDTO"-->
-<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
-
-<!--@thymesVar id="qSession" type="nl.tudelft.queue.model.QueueSession"-->
-
-<body>
-<section layout:fragment="subcontent">
-    <div class="page-sub-header">
-        <h3>Remove lab</h3>
-    </div>
-
-    <form action="#" th:action="@{/lab/{id}/remove(id=${qSession.id})}" th:object="${qSession}"
-          class="form-horizontal"
-          method="post">
-        <input type="hidden" th:field="*{id}"/>
-
-        Are you sure you want to remove <strong th:text="${'session #' + qSession.id}"></strong>?
-
-        <div class="text-center">
-            <button class="btn btn-danger">Delete this session</button>
-            <small>or <a
-                    th:href="@{/{ou}/{id}/labs(id=${ec == null ? edition.id : ec.id}, ou=${ec == null ? 'edition' : 'shared-edition'})}">go
-                back</a></small>
-        </div>
-    </form>
-</section>
-</body>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="remove-session" layout:fragment="dialog">
+        <form th:action="@{/lab}" th:method="delete" class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Delete session</h2>
+                </div>
+                <button class="hidden:from_small">Delete</button>
+            </div>
+
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-6">
+                    <input name="id" type="hidden" />
+
+                    <p class="font-400">
+                        Are you sure you want to delete
+                        <span id="remove-session-name"></span>
+                        ?
+                    </p>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        data-style="outlined"
+                        data-type="negative">
+                        <span class="margin_right-1 fa-solid fa-trash-alt"></span>
+                        <span>Delete session</span>
+                    </button>
+                </div>
+            </div>
+        </form>
+    </dialog>
 </html>
diff --git a/src/main/resources/templates/lab/view.html b/src/main/resources/templates/lab/view.html
index 84dcdb866379ddeeff9c89b0ea9a5c92af796944..9aeb490995f0b594ba8e5e8aec9a74349a369aa4 100644
--- a/src/main/resources/templates/lab/view.html
+++ b/src/main/resources/templates/lab/view.html
@@ -1,136 +1,376 @@
-<!--
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{container}">
+    <head>
+        <title th:text="|${qSession.session.name}|"></title>
+    </head>
 
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
+    <div layout:fragment="breadcrumbs" class="breadcrumbs">
+        <a th:href="@{/}">My courses</a>
+        <span>&gt;</span>
+        <a
+            th:if="${ec != null}"
+            th:text="${ec.name}"
+            th:href="@{/shared-edition/{id}(id=${ec.id})}"></a>
+        <a
+            th:if="${edition != null}"
+            th:text="|${edition.course.name} - ${edition.name}|"
+            th:href="@{/edition/{id}(id=${edition.id})}"></a>
+        <span>&gt;</span>
+        <a th:text="${qSession.session.name}" th:href="@{/lab/{id}(id=${qSession.id})}"></a>
+    </div>
 
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
+    <th:block layout:fragment="content">
+        <div
+            class="flex space_between align_items_center:from_small | flex_column:small margin_bottom-5:small">
+            <h1 class="title margin_bottom-5:from_small" th:text="${qSession.session.name}"></h1>
+            <div class="flex gap-3 | flex_column:small">
+                <button
+                    th:if="${@permissionService.canEnqueueSelf(qSession.id)}"
+                    id="enqueue-button"
+                    class="button padding_inline-5 padding_block-3"
+                    data-style="floating"
+                    data-dialog="enqueue"
+                    th:data-lab="${qSession.id}"
+                    th:disabled="${not @permissionService.canEnqueueSelfNow(qSession.id)}">
+                    <span class="margin_right-1 fa-solid fa-hand"></span>
+                    <span>Enqueue</span>
+                </button>
+                <th:block th:if="${@permissionService.canManageSession(qSession.id)}">
+                    <form
+                        class="flex_column"
+                        th:action="@{/lab/{qSession}/close-enqueue/{closeEnqueue}(qSession=${qSession.id}, closeEnqueue=${!qSession.enqueueClosed})}"
+                        method="post">
+                        <button
+                            class="button padding_inline-5 padding_block-3"
+                            data-style="floating"
+                            th:data-type="${qSession.enqueueClosed} ? 'positive' : 'negative'">
+                            <span
+                                class="margin_right-1 fa-solid"
+                                th:classappend="${qSession.enqueueClosed} ? 'fa-lock-open' : 'fa-lock'"></span>
+                            <span
+                                th:text="${qSession.enqueueClosed} ? 'Open enqueuing' : 'Close enqueuing'"></span>
+                        </button>
+                    </form>
+                    <a
+                        class="button padding_inline-5 padding_block-3"
+                        data-style="floating"
+                        th:href="@{/lab/{id}/export(id=${qSession.id})}">
+                        <span class="margin_right-1 fa-solid fa-file-export"></span>
+                        <span>Export session</span>
+                    </a>
+                    <button
+                        class="button padding_inline-5 padding_block-3"
+                        data-style="floating"
+                        data-dialog="create-session"
+                        th:data-session="${qSession.id}"
+                        data-wizard-type="edit">
+                        <span class="margin_right-1 fa-solid fa-pencil-alt"></span>
+                        <span>Edit session</span>
+                    </button>
+                </th:block>
+            </div>
+        </div>
 
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
+        <div class="flex_column">
+            <th:block th:if="${@permissionService.canManageSession(qSession.id)}">
+                <th:block th:if="${qSession.isActive()}">
+                    <div th:if="${qSession.isEnqueueingOpen()}" class="banner" data-type="check">
+                        <span class="fa-solid fa-check-circle"></span>
+                        <span>This session is active and students can enqueue.</span>
+                    </div>
+                    <div th:if="${qSession.enqueueClosed}" class="banner" data-type="warning">
+                        <span class="fa-solid fa-warning"></span>
+                        <span>This session is active but enqueuing is closed.</span>
+                    </div>
+                </th:block>
+                <th:block th:if="${qSession.isBeforeSession()}">
+                    <div th:unless="${qSession.enqueueClosed}" class="banner" data-type="info">
+                        <span class="fa-solid fa-info-circle"></span>
+                        <span
+                            th:text="|This session will become active on ${#temporals.format(qSession.session.start, 'dd MMMM yyyy HH:mm')}.|"></span>
+                    </div>
+                    <div th:if="${qSession.enqueueClosed}" class="banner" data-type="warning">
+                        <span class="fa-solid fa-warning"></span>
+                        <span
+                            th:text="|This session will become active on ${#temporals.format(qSession.session.start, 'dd MMMM yyyy HH:mm')}, but students cannot enqueue as enqueuing is closed.|"></span>
+                    </div>
+                </th:block>
+                <div th:if="${qSession.isAfterSession()}" class="banner" data-type="info">
+                    <span class="fa-solid fa-info-circle"></span>
+                    <span>This session is over.</span>
+                </div>
+            </th:block>
 
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+            <div class="flex | flex_column:small">
+                <div class="rounded shadow info_card flex_grow">
+                    <h2 class="heading">Session info</h2>
 
--->
-<!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org"
-      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{edition/view}">
+                    <div class="flex_column gap-4 padding-5">
+                        <div class="flex_column gap-0">
+                            <span class="label">Slot</span>
+                            <span
+                                th:text="|${#temporals.format(qSession.session.start, 'dd MMMM yyyy HH:mm')} - ${#temporals.format(qSession.session.end, 'dd MMMM yyyy HH:mm')}|"></span>
+                        </div>
+                        <div class="flex_column gap-0">
+                            <span class="label">Rooms</span>
+                            <ul class="margin_left-6">
+                                <li
+                                    th:each="room : ${qSession.session.rooms}"
+                                    th:text="|${room.building.name} - ${room.name}|"></li>
+                            </ul>
+                        </div>
+                        <div class="flex_column gap-0">
+                            <span class="label">Allowed requests</span>
+                            <ul class="margin_left-6">
+                                <li
+                                    th:each="entry : ${assignments}"
+                                    th:text="|${entry.key.module.name} - ${entry.key.name} (${#strings.setJoin(types.get(entry.key.id), '/')})|"></li>
+                            </ul>
+                        </div>
+                    </div>
+                </div>
 
-<!--@thymesVar id="#authenticatedP" type="nl.tudelft.labracore.api.dto.Person"-->
+                <div class="rounded shadow info_card flex_grow">
+                    <h2 class="heading">Session status</h2>
 
-<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+                    <div class="flex_column gap-4 padding-5">
+                        <div class="flex_column gap-0">
+                            <span class="label">Active status</span>
+                            <span
+                                th:if="${qSession.isBeforeSession()}"
+                                th:text="|This session will start on ${#temporals.format(qSession.session.start, 'dd MMMM yyyy HH:mm')}|"></span>
+                            <span th:if="${qSession.isActive()}">
+                                This is an
+                                <b>active</b>
+                                session
+                            </span>
+                            <span
+                                th:if="${qSession.isAfterSession()}"
+                                th:text="|This session ended on ${#temporals.format(qSession.session.end, 'dd MMMM yyyy HH:mm')}|"></span>
+                        </div>
+                        <div class="flex_column gap-0">
+                            <span class="label">Enqueuing status</span>
+                            <span th:if="${qSession.isEnqueueingOpen()}">
+                                Enqueuing is
+                                <b>open</b>
+                            </span>
+                            <span th:if="${qSession.enqueueClosed}">
+                                Enqueuing is
+                                <b>closed</b>
+                            </span>
+                            <span
+                                th:if="${!qSession.isEnqueueingOpen() and !qSession.enqueueClosed}"
+                                th:text="${qSession.closedReason()}"></span>
+                        </div>
+                        <div class="flex_column gap-0">
+                            <span class="label">Current size of queue</span>
+                            <span th:if="${qSession.data.queue.size() == 1}">1 students</span>
+                            <span
+                                th:unless="${qSession.data.queue.size() == 1}"
+                                th:text="|${qSession.data.queue.size()} students|"></span>
+                        </div>
+                        <div class="flex_column gap-0">
+                            <span class="label">Waiting time</span>
+                            <div
+                                th:with="avg = ${qSession.data.averageWaitingTime()}, cur = ${qSession.data.currentWaitingTime()}">
+                                <th:block th:if="${avg.isPresent() || cur.isPresent()}">
+                                    <span
+                                        th:if="${avg.isPresent()}"
+                                        th:text="|${avg.getAsDouble()} minutes |"></span>
+                                    <th:block th:unless="${avg.isPresent()}">
+                                        <span>Unknown</span>
+                                    </th:block>
+                                    <span>(average) /</span>
+                                    <span
+                                        th:if="${cur.isPresent()}"
+                                        th:text="|${cur.getAsDouble()} minutes |"></span>
+                                    <th:block th:unless="${cur.isPresent()}">
+                                        <span>Unknown</span>
+                                    </th:block>
+                                    <span>(current)</span>
+                                </th:block>
+                                <th:block th:unless="${avg.isPresent() || cur.isPresent()}">
+                                    <span>Unknown</span>
+                                </th:block>
+                            </div>
+                        </div>
+                    </div>
+                </div>
 
-<!--@thymesVar id="qSession" type="nl.tudelft.queue.dto.view.QueueSessionViewDTO"-->
+                <th:block layout:fragment="additional_info"></th:block>
+            </div>
 
-<!--@thymesVar id="rooms" type="java.util.List<nl.tudelft.labracore.api.dto.RoomSummaryDTO>"-->
-<!--@thymesVar id="assignments" type="java.util.List<nl.tudelft.labracore.api.dto.AssignmentSummaryDTO>"-->
-<!--@thymesVar id="modules" type="java.util.List<nl.tudelft.labracore.api.dto.ModuleDetailsDTO>"-->
+            <th:block layout:fragment="additional_content"></th:block>
 
-<!--@thymesVar id="requests" type="java.util.List<nl.tudelft.queue.dto.view.RequestViewDTO>"-->
+            <div
+                class="flex space_between align_items_center:from_small | flex_column:small align_items_stretch:small"
+                layout:fragment="request_header">
+                <h2 class="subtitle margin_top-3">Requests</h2>
 
-<!--@thymesVar id="current" type="nl.tudelft.queue.dto.view.RequestViewDTO"-->
-<!--@thymesVar id="currentDto" type="nl.tudelft.queue.dto.patch.RequestPatchDTO"-->
+                <div class="flex_column align_items_start:from_small">
+                    <button
+                        class="button padding_inline-5 padding_block-3"
+                        data-style="floating"
+                        data-dialog="filters">
+                        <span class="margin_right-1 fa-solid fa-filter"></span>
+                        <span>Filters</span>
+                        <span th:text="|(${filter.countActiveFilters()})|"></span>
+                    </button>
+                </div>
+            </div>
 
-<!--@thymesVar id="types" type="java.util.Map<java.lang.Long, java.util.Set<java.lang.String>>"-->
+            <div th:if="${current != null}" class="rounded shadow info_card">
+                <h3 class="heading">Current request</h3>
 
-<head>
-    <title th:text="|Lab ${qSession.id}|"></title>
+                <div
+                    class="padding-5 flex_column"
+                    th:with="position = ${qSession.data.position(#authenticatedP.id)}">
+                    <div class="flex_column gap-0">
+                        <span class="label">Current position in queue</span>
+                        <span
+                            id="position"
+                            th:text="${position == 0} ? 'Assistant is coming' : ${position}"></span>
+                    </div>
 
-    <script th:inline="javascript" type="text/javascript">
-        //<![CDATA[
-        const labId = /*[[${qSession.id}]]*/ -1;
-        //]]>
-    </script>
-    <script type="text/javascript" src="/js/lab_view.js"></script>
+                    <input
+                        id="current-request"
+                        name="request"
+                        type="hidden"
+                        th:value="${current.id}" />
 
-    <th:block th:if="${@permissionService.canManageSession(qSession.data)}">
-        <script type="text/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
-        <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"
-              type="text/css"/>
-    </th:block>
-</head>
-
-<body>
-<th:block layout:fragment="subcontent">
-    <div class="page-sub-header">
-        <div class="float-right">
-            <th:block th:if="${current == null && @permissionService.canEnqueueSelf(qSession.id)}">
-                <a href="#" th:href="@{/lab/{id}/enqueue(id=${qSession.id})}"
-                   class="btn btn-success" style="margin: 0.25em"
-                   th:classappend="${!@permissionService.canEnqueueSelfNow(qSession.id)} ? 'disabled'"
-                    th:disabled="${!@permissionService.canEnqueueSelfNow(qSession.id)}">Enqueue</a>
-            </th:block>
+                    <div class="flex_column gap-1 align_items_start">
+                        <label class="label" for="current-room">Room</label>
+                        <select id="current-room" name="room" data-select data-style="outlined">
+                            <option
+                                th:each="room : ${rooms}"
+                                th:value="${room.id}"
+                                th:text="|${room.building.name} - ${room.name}|"
+                                th:selected="${room.id == current.room.id}"></option>
+                        </select>
+                    </div>
 
-            <div class="row"
-                 th:if="${@permissionService.canManageSession(qSession.data)}">
-                <a class="btn btn-primary" style="margin: 0.25em"
-                   th:href="@{/lab/{id}/export(id=${qSession.id})}" download>Export lab</a>
-                <form class="form-inline"
-                      th:action="@{/lab/{id}/close-enqueue/{bool}(id=${qSession.id}, bool=${!qSession.enqueueClosed})}"
-                      method="post">
-                    <button th:unless="${qSession.enqueueClosed}" class="btn btn-danger" style="margin: 0.25em"
-                            type="submit">Close enqueueing
-                    </button>
-                    <button th:if="${qSession.enqueueClosed}" class="btn btn-success" style="margin: 0.25em"
-                            type="submit">Open enqueueing
-                    </button>
-                </form>
+                    <div class="flex_column gap-1">
+                        <label class="label" for="current-comment">Comment</label>
+                        <textarea
+                            autocomplete="off"
+                            class="textfield"
+                            data-show-valid
+                            style="resize: none; max-width: 25rem"
+                            id="current-comment"
+                            name="comment"
+                            th:text="${current.comment}"
+                            rows="3"
+                            maxlength="250"
+                            data-style="outlined"></textarea>
+                    </div>
 
-                <a th:href="@{/lab/{id}/edit(id=${qSession.id})}" style="margin: 0.25em" class="btn btn-secondary">
-                    <i class="fa fa-pencil" aria-hidden="true"></i>
-                </a>
-            </div>
-        </div>
+                    <div class="flex_column gap-1">
+                        <label class="label" for="current-question">Question</label>
+                        <textarea
+                            autocomplete="off"
+                            class="textfield"
+                            data-show-valid
+                            style="resize: none; max-width: 25rem"
+                            id="current-question"
+                            name="question"
+                            th:text="${current.question}"
+                            rows="3"
+                            minlength="15"
+                            maxlength="500"
+                            data-style="outlined"></textarea>
+                    </div>
 
-        <h3 th:text="'Lab #' + ${qSession.id} + ': ' + ${qSession.session.name}"></h3>
-    </div>
-
-    <div class="row">
-        <th:block th:replace="lab/view/components/selection-result :: selection-result">
-        </th:block>
-    </div>
-
-    <div th:if="${current != null}"
-         class="row">
-        <th:block th:replace="lab/view/components/current-request :: current-request">
-        </th:block>
+                    <form
+                        th:if="${@permissionService.canRevokeFromSession(qSession.id)}"
+                        th:action="@{/lab/{id}/revoke(id=${qSession.id})}"
+                        method="post">
+                        <button
+                            id="unenqueue-button"
+                            class="button padding_inline-5 padding_block-3"
+                            data-type="negative"
+                            data-style="outlined">
+                            <span class="margin_right-1 fa-solid fa-xmark"></span>
+                            <span>Unenqueue</span>
+                        </button>
+                    </form>
+                </div>
+            </div>
 
-        <th:block layout:fragment="current-request-edit">
-            <th:block th:replace="lab/view/components/current-request-edit :: current-request-edit">
+            <th:block layout:fragment="request_list">
+                <th:block
+                    layout:replace="~{request/table :: table(showAssistants=true)}"></th:block>
             </th:block>
-        </th:block>
-    </div>
+        </div>
+    </th:block>
 
-    <div class="card-deck mt-3">
-        <th:block layout:fragment="session-info">
+    <th:block layout:fragment="overlays">
+        <th:block
+            th:with="returnPath = |lab/${qSession.id}|, assignments = ${assignments.keySet()}">
+            <th:block layout:replace="~{request/filters :: dialog}"></th:block>
         </th:block>
-    </div>
-
-    <div class="mt-3">
-        <th:block layout:fragment="request-table">
+        <th:block
+            layout:replace="~{lab/enqueue/__${qSession.type.name().toLowerCase()}__}"></th:block>
+        <th:block th:with="assignments = ${allAssignments}">
+            <th:block layout:replace="~{lab/create :: dialog}"></th:block>
         </th:block>
-    </div>
+        <th:block layout:replace="~{request/actions/approve :: dialog}"></th:block>
+        <th:block layout:replace="~{request/actions/reject :: dialog}"></th:block>
+        <th:block layout:replace="~{request/actions/forward :: dialog}"></th:block>
+        <th:block layout:fragment="additional_overlays"></th:block>
+    </th:block>
 
-    <script src="/js/map_loader.js"></script>
-    <script type="text/javascript">
-        $(function () {
-            const roomId = $('select').val();
-            if (roomId) {
-                updateRequestInfo(roomId);
+    <th:block layout:fragment="script">
+        <script th:inline="javascript">
+            /*<![CDATA[*/
+            const labId = /*[[${qSession.id}]]*/ 0;
+            /*]]>*/
+        </script>
+        <script src="/js/lab_view.js"></script>
+        <th:block layout:replace="~{lab/create :: create_script}"></th:block>
+        <script>
+            function updateRequestInfo() {
+                const room = $("#current-room").val();
+                const question = $("#current-question").val();
+                const comment = $("#current-comment").val();
+                if (question.length < 15 || question.length > 500 || comment.length > 250) return;
+
+                $.ajax({
+                    url: `/request/${$("#current-request").val()}/update-request-info`,
+                    type: "post",
+                    data: {
+                        room: room,
+                        question: question,
+                        comment: comment,
+                    },
+                    success: () => {},
+                    error: () => alert("Error updating request"),
+                });
             }
-        });
-    </script>
-</th:block>
 
-<th:block layout:fragment="outside-content">
-    <th:block layout:fragment="exam-request-table">
+            $(() => {
+                $("#current-room").change(updateRequestInfo);
+                $("#current-question").on("stopTyping", updateRequestInfo);
+                $("#current-comment").on("stopTyping", updateRequestInfo);
+
+                $("[data-session]").click(async function () {
+                    wizardType = $(this).data("wizard-type");
+                    $("[data-create-session-wizard]").each(function () {
+                        if (!$(this).data("create-session-wizard").includes(wizardType))
+                            $(this).addClass("hidden");
+                    });
+                    const sessionId = $(this).data("session");
+                    $("#create-session-id").val(sessionId);
+                    const session = await fetch(`/lab/json/${sessionId}`).then(res => res.json());
+                    sessionType = session.type;
+                    getForms().forEach(f => getForm(f).trigger("edit", [session]));
+                    getForm("type").trigger("edit", [session]);
+                });
+            });
+        </script>
+        <th:block layout:fragment="additional_scripts"></th:block>
     </th:block>
-</th:block>
-</body>
 </html>
diff --git a/src/main/resources/templates/lab/view/components/edit_slots.html b/src/main/resources/templates/lab/view/components/edit_slots.html
new file mode 100644
index 0000000000000000000000000000000000000000..f2b6346416ab4fe9a445c28bc5d52565f446e765
--- /dev/null
+++ b/src/main/resources/templates/lab/view/components/edit_slots.html
@@ -0,0 +1,130 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="edit-time-slots" layout:fragment="dialog">
+        <form
+            th:action="@{/lab/{labId}/capacities(labId=${qSession.id})}"
+            th:method="patch"
+            class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Edit time slots</h2>
+                </div>
+                <button class="hidden:from_small">Edit</button>
+            </div>
+
+            <div class="dialog__body">
+                <div
+                    class="flex gap-7 margin_bottom-7 | flex_column:medium"
+                    style="min-width: min(50vw, 40rem)">
+                    <div class="flex_column gap-5">
+                        <div class="flex_column gap-1">
+                            <label class="form__label" for="bulk-time-slots-start">
+                                Time slots from
+                            </label>
+                            <select id="bulk-time-slots-start" data-select data-style="outlined">
+                                <option
+                                    th:each="slot, iter : ${qSession.timeSlots}"
+                                    th:unless="${iter.count == qSession.timeSlots.size()}"
+                                    th:selected="${slot == qSession.timeSlots.^[#this.slot.opensAt > #temporals.createNow()]}"
+                                    th:value="${slot.slot.opensAt}"
+                                    th:text="${#temporals.format(slot.slot.opensAt, 'HH:mm')}"></option>
+                            </select>
+                        </div>
+
+                        <div class="flex_column gap-1">
+                            <label class="form__label" for="bulk-time-slots-end">
+                                Time slots to
+                            </label>
+                            <select id="bulk-time-slots-end" data-select data-style="outlined">
+                                <option
+                                    th:each="slot, iter : ${qSession.timeSlots}"
+                                    th:unless="${iter.index == 0}"
+                                    th:selected="${iter.count == qSession.timeSlots.size()}"
+                                    th:value="${slot.slot.closesAt}"
+                                    th:text="${#temporals.format(slot.slot.closesAt, 'HH:mm')}"></option>
+                            </select>
+                        </div>
+
+                        <div class="flex_column gap-1">
+                            <label class="form__label" for="bulk-time-slots-capacity">
+                                New capacity
+                            </label>
+                            <input
+                                id="bulk-time-slots-capacity"
+                                class="textfield"
+                                type="number"
+                                value="0"
+                                data-style="outlined" />
+                        </div>
+
+                        <button
+                            id="bulk-update-capacity"
+                            type="button"
+                            class="button padding_inline-5 padding_block-3"
+                            data-style="outlined">
+                            Update capacities
+                        </button>
+                    </div>
+
+                    <div class="grid gap-5 auto_fill flex_grow" style="--min-size: 5rem">
+                        <div class="flex_column gap-1" th:each="slot : ${qSession.timeSlots}">
+                            <label
+                                class="form__label"
+                                th:for="|edit-slot-${slot.id}|"
+                                th:text="|${#temporals.format(slot.slot.opensAt, 'HH:mm')} - ${#temporals.format(slot.slot.closesAt, 'HH:mm')}|"></label>
+                            <input
+                                th:id="|edit-slot-${slot.id}|"
+                                th:name="|capacities[${slot.id}]|"
+                                type="number"
+                                class="textfield"
+                                data-style="outlined"
+                                th:min="${slot.requests.size()}"
+                                th:value="${slot.capacity}"
+                                data-slot
+                                th:data-slot-start="${slot.slot.opensAt}" />
+                        </div>
+                    </div>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button class="button padding_inline-5 padding_block-2" data-style="outlined">
+                        <span class="margin_right-1 fa-solid fa-pencil-alt"></span>
+                        <span>Edit time slots</span>
+                    </button>
+                </div>
+            </div>
+        </form>
+
+        <script>
+            $(() => {
+                $("#bulk-update-capacity").click(function () {
+                    const start = moment($("#bulk-time-slots-start").val());
+                    const end = moment($("#bulk-time-slots-end").val());
+                    const capacity = $("#bulk-time-slots-capacity").val();
+
+                    $("[data-slot]").each((i, s) => {
+                        const slotStart = moment($(s).data("slot-start"));
+                        if (slotStart.isSameOrAfter(start) && slotStart.isBefore(end)) {
+                            $(s).val(capacity);
+                        }
+                    });
+                });
+            });
+        </script>
+    </dialog>
+</html>
diff --git a/src/main/resources/templates/lab/view/components/exam_info.html b/src/main/resources/templates/lab/view/components/exam_info.html
new file mode 100644
index 0000000000000000000000000000000000000000..52a7291707d37a357e483e03e1ff4504d40f1d14
--- /dev/null
+++ b/src/main/resources/templates/lab/view/components/exam_info.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <div layout:fragment="card" class="rounded shadow info_card flex_grow flex_column gap-0">
+        <h2 class="heading">Exam info</h2>
+
+        <progress class="progress" max="100" th:value="${qSession.percentageHandled}"></progress>
+
+        <div class="flex_column gap-4 padding-5">
+            <div class="flex_column gap-0">
+                <span class="label">Slot selection opens at</span>
+                <span
+                    th:text="${#temporals.format(qSession.slottedLabConfig.selectionOpensAt, 'dd MMMM yyyy HH:mm')}"></span>
+            </div>
+
+            <div class="flex_column gap-0">
+                <span class="label">Capacity</span>
+                <span th:text="|${qSession.slottedLabConfig.capacity} requets per slot|"></span>
+            </div>
+
+            <div class="flex_column gap-0">
+                <span class="label">Time slot duration</span>
+                <span th:text="|${qSession.slottedLabConfig.duration} minutes per slot|"></span>
+            </div>
+
+            <div class="flex_column gap-0">
+                <span class="label">Minimum exam session percentage</span>
+                <span th:text="|${qSession.examLabConfig.percentage}%|"></span>
+            </div>
+        </div>
+    </div>
+</html>
diff --git a/src/main/resources/templates/lab/view/components/slot_info.html b/src/main/resources/templates/lab/view/components/slot_info.html
new file mode 100644
index 0000000000000000000000000000000000000000..dd5dd4ddad0610d90638a642dc2c1a263d08bbb6
--- /dev/null
+++ b/src/main/resources/templates/lab/view/components/slot_info.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <div layout:fragment="card" class="rounded shadow info_card flex_grow">
+        <h2 class="heading">Time slot info</h2>
+
+        <div class="flex_column gap-4 padding-5">
+            <div class="flex_column gap-0">
+                <span class="label">Slot selection opens at</span>
+                <span
+                    th:text="${#temporals.format(qSession.slottedLabConfig.selectionOpensAt, 'dd MMMM yyyy HH:mm')}"></span>
+            </div>
+
+            <div class="flex_column gap-0">
+                <span class="label">Time slot duration</span>
+                <span th:text="|${qSession.slottedLabConfig.duration} minutes per slot|"></span>
+            </div>
+
+            <div class="flex_column gap-0">
+                <span class="label">Enqueuing rules</span>
+                <span th:if="${qSession.canSelectDueSlots}">
+                    Students can take slots that have passed
+                </span>
+                <span th:unless="${qSession.canSelectDueSlots}">
+                    Students cannot take slots that have passed
+                </span>
+            </div>
+
+            <div class="flex_column gap-0">
+                <span class="label">Request taking rules</span>
+                <span th:if="${qSession.earlyOpenTime == 0}">
+                    A slot request can be taken from the start of that slot
+                </span>
+                <span
+                    th:unless="${qSession.earlyOpenTime == 0}"
+                    th:text="|A slot request can be taken from ${qSession.earlyOpenTime} minutes before the start of that slot|"></span>
+            </div>
+
+            <div class="flex_column gap-0">
+                <span class="label">Number of booked slots</span>
+                <span th:text="|${qSession.slotsOccupied} / ${qSession.maxSlots}|"></span>
+            </div>
+        </div>
+    </div>
+</html>
diff --git a/src/main/resources/templates/lab/view/exam.html b/src/main/resources/templates/lab/view/exam.html
index 79eba6e8ac877e7ab63ae95e6c943af03f4bcf8c..c75eefac811e9d069ee55f4cb8858581de3cae63 100644
--- a/src/main/resources/templates/lab/view/exam.html
+++ b/src/main/resources/templates/lab/view/exam.html
@@ -1,48 +1,142 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{lab/view/lab}">
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{lab/view}">
+    <head>
+        <style>
+            .progress {
+                background-color: var(--neutral-800);
+                border: 1px solid var(--neutral-700);
+                color: var(--green-500);
+                max-height: 1rem;
+                min-width: 100%;
+            }
+            .progress::-moz-progress-bar,
+            .progress::-webkit-progress-bar {
+                background-color: var(--green-500);
+            }
+        </style>
+    </head>
 
-<!--@thymesVar id="#authenticatedP" type="nl.tudelft.labracore.api.dto.Person"-->
+    <th:block layout:fragment="additional_info">
+        <th:block layout:replace="~{lab/view/components/exam_info :: card}"></th:block>
+    </th:block>
 
-<!--@thymesVar id="qSession" type="nl.tudelft.queue.dto.view.labs.ExamLabViewDTO"-->
-<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
+    <th:block
+        th:if="${@permissionService.canViewSessionRequests(qSession.id) or current != null}"
+        layout:fragment="request_header">
+        <h2 class="subtitle">Requests</h2>
+    </th:block>
+    <div
+        th:if="${@permissionService.canViewSessionRequests(qSession.id)}"
+        class="grid gap-5 padding_inline-1 align_items_start"
+        th:style="|--columns: ${qSession.examTimeSlots.size()}; overflow-x: scroll;|"
+        layout:fragment="request_list">
+        <div
+            class="rounded shadow flex_column gap-0 margin_bottom-5 timeslot"
+            th:each="slot : ${qSession.examTimeSlots}">
+            <div class="heading flex_column gap-3">
+                <div class="flex space_between align_items_center">
+                    <div class="flex gap-3 align_items_center">
+                        <h3 th:text="${slot.slot.toSentence()}" style="white-space: nowrap"></h3>
+                        <span
+                            th:if="${slot.active}"
+                            class="chip"
+                            style="font-weight: initial"
+                            data-type="positive">
+                            Active
+                        </span>
+                        <span
+                            th:unless="${slot.active}"
+                            class="chip"
+                            style="font-weight: initial"
+                            data-type="negative">
+                            Inactive
+                        </span>
+                    </div>
+                    <div class="flex gap-3">
+                        <form
+                            th:action="@{/time-slot/{slotId}/close(slotId=${slot.id})}"
+                            method="post">
+                            <button
+                                class="button padding_inline-4 padding_block-1"
+                                data-style="outlined"
+                                data-type="negative"
+                                th:disabled="${not slot.active or slot.slot.closed()}">
+                                <span class="fa-solid fa-lock"></span>
+                                <span>Close</span>
+                            </button>
+                        </form>
+                        <form
+                            th:action="@{/time-slot/{slotId}/take-next(slotId=${slot.id})}"
+                            data-take-next
+                            method="get"
+                            th:with="open = ${@labRequestRepository.countNextExamRequests(slot.id, #authenticatedP)}">
+                            <button
+                                class="button padding_inline-4 padding_block-1"
+                                data-style="outlined"
+                                th:disabled="${open == 0 or not slot.active}"
+                                th:if="${qSession.isActive()}">
+                                <span class="fa-solid fa-hand"></span>
+                                <span th:text="|Take next (${open})|"></span>
+                            </button>
+                        </form>
+                    </div>
+                </div>
 
-<!--@thymesVar id="requests" type="java.util.List<nl.tudelft.queue.model.TimeSlot>"-->
+                <div style="font-weight: initial">
+                    <span th:text="|${slot.handled} handled|"></span>
+                    <span>/</span>
+                    <span th:text="|${slot.occupied} occupied|"></span>
+                    <span>/</span>
+                    <span th:text="|${slot.capacity}|"></span>
+                </div>
+            </div>
 
-<!--@thymesVar id="current" type="nl.tudelft.queue.dto.view.RequestViewDTO"-->
+            <progress
+                class="progress"
+                max="1"
+                th:value="${slot.handled / (1.0 * slot.capacity)}"></progress>
 
-<head>
-    <title th:text="'Exam Lab #' + ${qSession.id}"></title>
-</head>
+            <ul class="list" role="list">
+                <li th:if="${slot.examRequests.isEmpty()}">No requests yet</li>
+                <li
+                    th:each="request : ${slot.examRequests}"
+                    class="flex align_items_center gap-3"
+                    data-selectable
+                    th:data-request="${request.id}">
+                    <button class="visually_hidden" th:data-request="${request.id}"></button>
+                    <span
+                        class="chip"
+                        th:data-type="${request.eventInfo.status.colourClass}"
+                        data-status
+                        th:text="#{|request.status.${request.eventInfo.status.name().toLowerCase()}|}"></span>
+                    <span th:text="${request.requesterName}"></span>
+                </li>
+            </ul>
+        </div>
+    </div>
 
-<body>
-<th:block layout:fragment="additional-lab-info">
-    <th:block th:replace="lab/view/components/exam-lab-info :: exam-lab-info">
-    </th:block>
-</th:block>
-
-<th:block th:if="${@permissionService.canTakeRequest(qSession.id)}" layout:fragment="exam-request-table">
-    <th:block th:replace="lab/view/components/exam-lab-slots :: exam-lab-slots">
-    </th:block>
-</th:block>
-</body>
+    <script layout:fragment="additional_scripts">
+        $(() => {
+            $("[data-request]").click(function () {
+                fetchAndOpenDialog(`/request/${$(this).data("request")}`);
+            });
+            $("[data-take-next]").submit(function (event) {
+                event.preventDefault();
+                $.get($(this).attr("action"), html => {
+                    const element = $(html).filter((i, e) => $(e).is("dialog"));
+                    if (element.is("dialog")) {
+                        $(".page").append(element);
+                        openDialog(element.attr("id"));
+                        configureDialog(element);
+                    } else {
+                        window.location.reload();
+                    }
+                });
+            });
+        });
+    </script>
 </html>
diff --git a/src/main/resources/templates/lab/view/limited_capacity.html b/src/main/resources/templates/lab/view/limited_capacity.html
new file mode 100644
index 0000000000000000000000000000000000000000..3cc5aec198d91e0b0aaf338f4818e2be7bf75eb7
--- /dev/null
+++ b/src/main/resources/templates/lab/view/limited_capacity.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{lab/view}"></html>
diff --git a/src/main/resources/templates/lab/view/regular.html b/src/main/resources/templates/lab/view/regular.html
index 4121caae9cfcacbbc5b051057bf84200ee4f4418..3cc5aec198d91e0b0aaf338f4818e2be7bf75eb7 100644
--- a/src/main/resources/templates/lab/view/regular.html
+++ b/src/main/resources/templates/lab/view/regular.html
@@ -1,35 +1,6 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{lab/view/lab}">
-
-<!--@thymesVar id="qSession" type="nl.tudelft.queue.dto.view.LabViewDTO"-->
-
-<head>
-    <title th:text="'Lab #' + ${qSession.id}"></title>
-</head>
-
-<body>
-<th:block layout:fragment="request-table">
-    <th:block th:replace="lab/view/components/full-request-table :: full-request-table">
-    </th:block>
-</th:block>
-</body>
-</html>
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{lab/view}"></html>
diff --git a/src/main/resources/templates/lab/view/slotted.html b/src/main/resources/templates/lab/view/slotted.html
index 85479108743166b35bbbecaab1d866d9cf9e0200..ccab39413c1b7c5d5c07376d63c84a8cf27e51c2 100644
--- a/src/main/resources/templates/lab/view/slotted.html
+++ b/src/main/resources/templates/lab/view/slotted.html
@@ -1,45 +1,73 @@
-<!--
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{lab/view}">
+    <head>
+        <style>
+            .timeslot progress {
+                background-color: var(--neutral-700);
+                border: none;
+                color: var(--green-500);
+                max-height: 0.5rem;
+                min-width: 100%;
+            }
+            .timeslot progress[data-full="true"] {
+                color: var(--red-500);
+            }
+            .timeslot progress::-moz-progress-bar,
+            .timeslot progress::-webkit-progress-bar {
+                background-color: var(--green-500);
+            }
+            .timeslot progress[data-full="true"]::-moz-progress-bar,
+            .timeslot progress[data-full="true"]::-webkit-progress-bar {
+                background-color: var(--red-500);
+            }
+        </style>
+    </head>
 
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{lab/view/lab}">
-
-<!--@thymesVar id="qSession" type="nl.tudelft.queue.dto.view.labs.SlottedLabViewDTO"-->
-
-<head>
-    <title th:text="'Lab #' + ${qSession.id}"></title>
-</head>
-
-<body>
-<th:block layout:fragment="additional-lab-info">
-    <th:block th:replace="lab/view/components/slotted-lab-info :: slotted-lab-info">
+    <th:block layout:fragment="additional_info">
+        <th:block layout:replace="~{lab/view/components/slot_info :: card}"></th:block>
     </th:block>
-</th:block>
 
-<th:block layout:fragment="request-table">
-    <th:block th:replace="lab/view/components/slots-info :: slots-info">
+    <th:block layout:fragment="additional_content">
+        <div
+            class="flex space_between align_items_center:from_small | flex_column:small align_items_stretch:small">
+            <h2 class="subtitle margin_top-3">Time slots</h2>
+            <button
+                class="button padding_inline-5 padding_block-3"
+                data-style="floating"
+                data-dialog="edit-time-slots">
+                <span class="fa-solid fa-pencil-alt"></span>
+                <span>Edit time slots</span>
+            </button>
+        </div>
+        <div class="grid auto_fill gap-3" style="--min-size: 9rem">
+            <div
+                class="rounded shadow info_card flex_column gap-0 timeslot"
+                style="white-space: nowrap"
+                th:each="timeSlot : ${qSession.timeSlots}">
+                <div class="padding-3 text_align_center">
+                    <span
+                        th:text="|${#temporals.format(timeSlot.slot.opensAt, 'HH:mm')} - ${#temporals.format(timeSlot.slot.closesAt, 'HH:mm')}|"></span>
+                    <!-- prettier-ignore -->
+                    <span>(</span>
+                    <span th:text="${timeSlot.requests.size()}"></span>
+                    <span>/</span>
+                    <span th:text="${timeSlot.capacity}"></span>
+                    <span>)</span>
+                </div>
+                <progress
+                    aria-hidden="true"
+                    th:value="${timeSlot.requests.size}"
+                    th:max="${timeSlot.capacity}"
+                    th:data-full="${timeSlot.requests.size == timeSlot.capacity}"></progress>
+            </div>
+        </div>
     </th:block>
 
-    <div class="mt-3"></div>
-
-    <th:block th:replace="lab/view/components/full-request-table :: full-request-table">
+    <th:block layout:fragment="additional_overlays">
+        <th:block layout:replace="~{lab/view/components/edit_slots :: dialog}"></th:block>
     </th:block>
-</th:block>
-</body>
 </html>
diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html
index 7722dc4fd877670ad6e2cd3b31f33a460ab18a19..861fddd6bd670370d89d58a75dd5923567e60903 100644
--- a/src/main/resources/templates/layout.html
+++ b/src/main/resources/templates/layout.html
@@ -18,224 +18,77 @@
 
 -->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
-<head>
-    <meta charset="utf-8"/>
-    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
-    <meta http-equiv="Content-Language" content="en_GB"/>
-    <meta name="viewport" content="width=device-width, initial-scale=1"/>
-
-    <meta name="description" content="Queue system"/>
-    <meta name="author" content=""/>
-    <meta name="_csrf_header" th:content="${_csrf.headerName}"/>
-    <meta name="_csrf" th:content="${_csrf.token}"/>
-
-    <link rel="icon" href="/favicon.ico"/>
-
-    <title th:unless="${@thymeleafConfig.isTheDay()}">Queue</title>
-    <title th:if="${@thymeleafConfig.isTheDay()}">Stack</title>
-    <title>Queue</title>
-
-    <link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css"/>
-    <link rel="stylesheet" href="/webjars/font-awesome/css/all.css"/>
-    <link rel="stylesheet" href="/webjars/font-awesome/css/v4-shims.css"/>
-    <link rel="stylesheet" type="text/css" href="/css/global.css"/>
-
-    <script src="/webjars/jquery/jquery.min.js"></script>
-    <script src="/webjars/jquery-cookie/jquery.cookie.js"></script>
-    <script src="/webjars/bootstrap/js/bootstrap.bundle.min.js"></script>
-
-	<script src="/webjars/sockjs-client/sockjs.min.js"></script>
-    <script src="/webjars/stomp-websocket/stomp.min.js"></script>
-
-    <script src="/js/global.js"></script>
-
-    <script type="application/javascript">
-        $.ajaxSetup({
-            headers: {
-                "X-CSRF-TOKEN": $("meta[name='_csrf']").attr("content")
-            }
-        });
-    </script>
-
-    <script th:if="${#authenticatedP != null}" th:inline="javascript" type="application/javascript">
-        //<![CDATA[
-        const authenticatedId = /*[[${#authenticatedP.id}]]*/ -1;
-        //]]>
-    </script>
-</head>
-
-<!--@thymesVar id="page" type="java.lang.String"-->
-
-<!--@thymesVar id="#authenticatedP" type="nl.tudelft.labracore.lib.security.user.Person"-->
-
-<body class="d-flex flex-column" style="min-height: 100vh">
-<nav class="navbar navbar-expand-lg navbar-inverse text-white">
-    <!-- Navigation bar with courses, requests, admin, login, request history etc. -->
-    <a class="navbar-brand" href="/" th:unless="${@thymeleafConfig.isTheDay()}">Queue</a>
-    <a class="navbar-brand" href="/" th:if="${@thymeleafConfig.isTheDay()}">Stack</a>
-    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar-nav"
-            aria-controls="navbar-nav" aria-expanded="false" aria-label="Toggle navigation">
-        <i class="fa fa-bars" style="color:white;" aria-hidden="true"></i>
-    </button>
-    <div class="collapse navbar-collapse" id="navbar-nav">
-        <ul class="navbar-nav nav-pills mr-auto mt-2 mt-lg-0">
-            <th:block th:if="${#authenticatedP != null}">
-                <li class="nav-item">
-                    <a class="nav-link"
-                       th:classappend="${page == 'my-courses' ? 'active' : ''}"
-                       th:href="@{/}">My Courses</a></li>
-                <li class="nav-item">
-                    <a class="nav-link"
-                       th:classappend="${page == 'catalog' ? 'active' : ''}"
-                       th:href="@{/editions}">Catalog</a></li>
-                <li class="nav-item" th:if="${@permissionService.canViewRequests()}">
-                    <a class="nav-link"
-                       th:href="@{/requests}"
-                       th:classappend="${page == 'requests' ? 'active' : ''}">Requests</a></li>
-                <li class="nav-item" th:if="${@permissionService.isAdmin()}">
-                    <a class="nav-link"
-                       th:href="@{/admin}"
-                       th:classappend="${page == 'admin' ? 'active' : ''}">Admin</a>
-                </li>
-            </th:block>
-        </ul>
-        <!-- Specifically for logged in users to get to personal pages -->
-        <ul class="navbar-nav">
-            <th:block th:if="${#authenticatedP != null}">
-                <li class="nav-item dropdown">
-                    <a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown"
-                       role="button" aria-haspopup="true" aria-expanded="false"
-                       th:text="${#authenticatedP.displayName}">
-                        <span class="caret"></span>
-                    </a>
-                    <div class="dropdown-menu dropdown-menu-right">
-                        <a class="dropdown-item text-dark" th:href="@{/history}">Request history</a>
-                        <a class="dropdown-item text-dark"
-                           th:href="@{/feedback/{id}(id=${#authenticatedP.id})}"
-                           th:if="${@permissionService.canViewOwnFeedback()}">Feedback</a>
-                        <form th:action="@{/logout}" method="post">
-                            <button class="dropdown-item" type="submit">Logout</button>
-                        </form>
-                    </div>
-                </li>
-            </th:block>
-            <li class="nav-item" th:unless="${#authenticatedP != null}">
-                <a class="nav-link" th:href="@{/login}">Login</a>
-            </li>
-        </ul>
-    </div>
-</nav>
-
-<main class="flex-fill fluid-container">
-    <!-- Announcement banners -->
-    <div class="fluid-container justify-content-center mt-3 mb-3 ml-3 mr-3">
-        <th:block th:each="announcement : ${@announcementRepository.findActiveAnnouncements()}">
-            <div class="alert fade d-none" role="alert" th:data-announcement-id="|alert-${announcement.id}|"
-                 th:styleappend="|color: ${'#' + announcement.hexTextColour()}; background-color: ${'#' + announcement.hexBackgroundColour()};|"
-                 th:classappend="${announcement.isDismissible ? 'alert-dismissible' : ''}">
-                <strong th:text="${announcement.message}"></strong>
-                <button type="button" class="close" data-dismiss="alert" aria-label="Close"
-                        th:if="${announcement.isDismissible}">
-                    <span aria-hidden="true">&times;</span>
-                </button>
-            </div>
-        </th:block>
-    </div>
-
-    <div class="row no-gutters justify-content-center mb-3">
-        <!-- Page content -->
-        <div class="pl-3 pr-3 col-12 col-sm-11 col-md-10 col-lg-9 col-xl-8">
-            <th:block layout:fragment="content" class="content">
-                <p>Page content goes here</p>
-            </th:block>
-        </div>
-    </div>
-    <th:block layout:fragment="outside-content">
-    </th:block>
-    <div class="modal fade" id="sessionExpiredModal" tabindex="-1" role="dialog">
-        <div class="modal-dialog modal-dialog-centered" role="document">
-            <div class="modal-content">
-                <div class="modal-body">
-                    <h3>Your session has expired</h3>
-                </div>
-                <div class="modal-footer">
-                    <button type="button" class="btn btn-primary" data-dismiss="modal">Close
-                    </button>
-                    <a th:href="@{/login}" class="btn btn-primary">Login</a>
-                </div>
-            </div>
-        </div>
-    </div>
-</main>
-
-<footer class="bg-light">
-    <div class="container-fluid">
-        <div class="row no-gutters justify-content-center">
-            <div class="col-12 col-lg-9 col-xl-8">
-                <div class="row justify-content-center">
-                    <div class="d-none d-sm-block col-sm-6">
-                        <p class="text-muted btn mb-0">&copy; Delft University of Technology</p>
-                    </div>
-                    <div class="col-5 col-sm-2">
-                        <a class="text-muted btn" th:href="@{/privacy}">Privacy</a>
-                    </div>
-                    <div class="col-4 col-sm-2">
-                        <a class="text-muted btn" th:href="@{/about}">About</a>
-                    </div>
-                    <div class="col-3 col-sm-2">
-                        <img th:src="@{${@instituteProperties.logo}}"
-                             class="float-right pr-2 mr-0"
-                             height="40px"
-                             alt="Logo"/>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <th:block th:if="${#authenticatedP != null}">
-        <!-- Import push.js -->
-        <script src="/js/push.js"></script>
-        <script th:inline="javascript">
-            /*<![CDATA[*/
-
-                let timeout = /*[[${#httpSession.getMaxInactiveInterval()}]]*/ '10';
-                setTimeout(function() {
-                    $('#sessionExpiredModal').modal('show')
-                }, parseInt(timeout) * 1000)
-
-            /*]]>*/
-        </script>
-    </th:block>
-    <script type="application/javascript">
-        // Initialize the dismissed announcements cookie when it does not yet exist
-        if (typeof $.cookie("dismissed-announcements") === "undefined") {
-            $.cookie("dismissed-announcements", "[]", { path: "/" })
-        }
-
-        // For every alert, check whether it is dismissed. If an alert is not dismissed, show it
-        $(() => {
-            const dismissedAnnouncements = JSON.parse($.cookie("dismissed-announcements"));
-
-            $(".alert").each(function() {
-                const id = $(this).data("announcement-id");
-                if (!dismissedAnnouncements.includes(id)) {
-                    $(this).removeClass("d-none").addClass("show");
-                }
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <head>
+        <meta charset="utf-8" />
+        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+        <meta http-equiv="Content-Language" content="en_GB" />
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+
+        <meta name="description" content="Queue system" />
+        <meta name="author" content="" />
+        <meta name="_csrf_header" th:content="${_csrf.headerName}" />
+        <meta name="_csrf" th:content="${_csrf.token}" />
+
+        <link rel="icon" href="/favicon.ico" />
+
+        <title th:unless="${@thymeleafConfig.isTheDay()}">Queue</title>
+        <title th:if="${@thymeleafConfig.isTheDay()}">Stack</title>
+        <title>Queue</title>
+
+        <link rel="stylesheet" href="/webjars/font-awesome/css/all.css" />
+        <link rel="stylesheet" href="/webjars/font-awesome/css/v4-shims.css" />
+        <link rel="stylesheet" type="text/css" href="/css/global.css" />
+
+        <script src="/webjars/jquery/jquery.min.js"></script>
+        <script src="/webjars/jquery-cookie/jquery.cookie.js"></script>
+
+        <script src="/webjars/sockjs-client/sockjs.min.js"></script>
+        <script src="/webjars/stomp-websocket/stomp.min.js"></script>
+
+        <script src="/webjars/momentjs/2.24.0/min/moment.min.js"></script>
+
+        <script src="/js/global.js"></script>
+
+        <script type="application/javascript">
+            $.ajaxSetup({
+                headers: {
+                    "X-CSRF-TOKEN": $("meta[name='_csrf']").attr("content"),
+                },
             });
-        });
-
-        // Register an event listener that adds a dismissed announcement to the dedicated cookie
-        $(".alert").on("closed.bs.alert", function() {
-            const id = $(this).data("announcement-id");
+        </script>
 
-            const dismissedAnnouncements = JSON.parse($.cookie("dismissed-announcements"));
-            dismissedAnnouncements.push(id);
+        <script
+            th:if="${#authenticatedP != null}"
+            th:inline="javascript"
+            type="application/javascript">
+            //<![CDATA[
+            const authenticatedId = /*[[${#authenticatedP.id}]]*/ -1;
+            //]]>
+        </script>
+    </head>
+
+    <body>
+        <th:block layout:fragment="container"></th:block>
+
+        <script src="/js/announcement.js"></script>
+        <script src="/js/select.js"></script>
+        <script src="/js/file.js"></script>
+        <th:block th:if="${#authenticatedP != null}">
+            <script src="/js/push.js"></script>
+            <script th:inline="javascript">
+                /*<![CDATA[*/
+                let timeout = /*[[${#httpSession.getMaxInactiveInterval()}]]*/ "10";
+                setTimeout(function () {
+                    $("#sessionExpiredModal").modal("show");
+                }, parseInt(timeout) * 1000);
+                /*]]>*/
+            </script>
+        </th:block>
 
-            $.cookie("dismissed-announcements", JSON.stringify(dismissedAnnouncements), { path: "/" });
-        });
-    </script>
-</footer>
-</body>
+        <th:block layout:fragment="scripts"></th:block>
+    </body>
 </html>
diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html
index 128efa600bbae86661b3ba162c0c75992d039d63..c254adadce4dd0d3c26dacec875d6a9ce25ed9af 100644
--- a/src/main/resources/templates/login.html
+++ b/src/main/resources/templates/login.html
@@ -1,63 +1,112 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout}">
-<head>
-</head>
-
-<body>
-<section layout:fragment="content">
-    <div class="page-header">
-        <h1>Login</h1>
-    </div>
-
-    <div class="alert alert-danger" role="alert" th:if="${param.error}">
-        <span>Invalid username and password.</span>
-    </div>
-
-    <div class="alert alert-info" role="alert" th:if="${param.logout}">
-        <span>You have been logged out.</span>
-    </div>
-
-    <form action="#" th:action="@{/perform-login}" class="form-horizontal" method="post" id="login-form">
-        <div class="form-group">
-            <label for="input-username" class="col-sm-2 control-label">Username</label>
-
-            <div class="col-sm-8">
-                <input type="text" id="input-username" name="username" class="form-control" placeholder="Username"/>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{container}">
+    <div layout:fragment="content" class="flex_column gap-7">
+        <form
+            th:action="@{/perform-login}"
+            method="post"
+            class="flex_column"
+            style="max-width: 20rem">
+            <div>
+                <div class="flex_column gap-0 margin_bottom-3">
+                    <label class="form__label" for="username">Username</label>
+                    <input class="textfield" data-style="floating" id="username" name="username" />
+                </div>
+                <div class="flex_column gap-0">
+                    <label class="form__label" for="password">Password</label>
+                    <input
+                        class="textfield"
+                        data-style="floating"
+                        id="password"
+                        name="password"
+                        type="password" />
+                </div>
             </div>
-        </div>
-        <div class="form-group">
-            <label for="input-password" class="col-sm-2 control-label">Password</label>
-
-            <div class="col-sm-8">
-                <input type="password" id="input-password" name="password" class="form-control" placeholder="Password"/>
-            </div>
-        </div>
-        <input type="hidden" name="password" value=""/>
-        <div class="form-group">
-            <div class="col-sm-offset-2 col-sm-10">
-                <input type="submit" class="btn btn-primary" value="Login"/>
+            <div>
+                <button class="button padding_inline-5 padding_block-3" data-style="floating">
+                    Log in
+                </button>
             </div>
+        </form>
+
+        <div th:if="${@environment.activeProfiles[0] == 'development'}">
+            <form class="grid auto_fill gap-5" th:action="@{/perform-login}" method="post">
+                <button
+                    class="button padding_inline-5 padding_block-3"
+                    data-style="floating"
+                    name="username"
+                    value="admin1">
+                    <span class="margin_right-1 fa-solid fa-screwdriver-wrench"></span>
+                    <span>Admin 1</span>
+                </button>
+                <button
+                    class="button padding_inline-5 padding_block-3"
+                    data-style="floating"
+                    name="username"
+                    value="cseteacher1">
+                    <span class="margin_right-1 fa-solid fa-user-graduate"></span>
+                    <span>CSE Teacher 1</span>
+                </button>
+                <button
+                    class="button padding_inline-5 padding_block-3"
+                    data-style="floating"
+                    name="username"
+                    value="cseteacher2">
+                    <span class="margin_right-1 fa-solid fa-user-graduate"></span>
+                    <span>CSE Teacher 2</span>
+                </button>
+                <button
+                    class="button padding_inline-5 padding_block-3"
+                    data-style="floating"
+                    name="username"
+                    value="csestudent1">
+                    <span class="margin_right-1 fa-brands fa-tiktok"></span>
+                    <span>CSE Student 1</span>
+                </button>
+                <button
+                    class="button padding_inline-5 padding_block-3"
+                    data-style="floating"
+                    name="username"
+                    value="csestudent2">
+                    <span class="margin_right-1 fa-brands fa-tiktok"></span>
+                    <span>CSE Student 2</span>
+                </button>
+                <button
+                    class="button padding_inline-5 padding_block-3"
+                    data-style="floating"
+                    name="username"
+                    value="csestudent3">
+                    <span class="margin_right-1 fa-brands fa-tiktok"></span>
+                    <span>CSE Student 3</span>
+                </button>
+                <button
+                    class="button padding_inline-5 padding_block-3"
+                    data-style="floating"
+                    name="username"
+                    value="csestudent4">
+                    <span class="margin_right-1 fa-brands fa-tiktok"></span>
+                    <span>CSE Student 4</span>
+                </button>
+                <button
+                    class="button padding_inline-5 padding_block-3"
+                    data-style="floating"
+                    name="username"
+                    value="csestudent5">
+                    <span class="margin_right-1 fa-brands fa-tiktok"></span>
+                    <span>CSE Student 5</span>
+                </button>
+                <button
+                    class="button padding_inline-5 padding_block-3"
+                    data-style="floating"
+                    name="username"
+                    value="csestudent6">
+                    <span class="margin_right-1 fa-brands fa-tiktok"></span>
+                    <span>CSE Student 6</span>
+                </button>
+            </form>
         </div>
-    </form>
-</section>
-</body>
+    </div>
 </html>
diff --git a/src/main/resources/templates/module/create.html b/src/main/resources/templates/module/create.html
index 6185fae1159f959de09f38a634bea3f3f77b3505..fd2713da2149509cfc18f4b9c93449e8b5d2a370 100644
--- a/src/main/resources/templates/module/create.html
+++ b/src/main/resources/templates/module/create.html
@@ -1,64 +1,52 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{edition/view}">
-<head>
-    <title th:text="|Create Module for ${'#' + edition.id}|"></title>
-
-    <script type="application/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
-    <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css"/>
-</head>
-
-<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
-
-<!--@thymesVar id="dto" type="nl.tudelft.queue.dto.create.QueueModuleCreateDTO"-->
-
-<body>
-<section layout:fragment="subcontent">
-    <div class="page-header">
-        <h3>Create Module</h3>
-    </div>
-
-    <form th:with="action = @{/edition/{id}/modules/create(id=${edition.id})}"
-          th:action="${action}" th:object="${dto}"
-          class="form-horizontal" method="post">
-        <input type="hidden" name="id" th:value="${edition.id}">
-
-        <div class="form-group form-row">
-            <label for="title-input" class="col-md-2 col-form-label">Name:</label>
-            <div class="col-md-4">
-                <input id="title-input"
-                       type="text" class="form-control"
-                       required="required" th:field="*{name}"
-                       placeholder="Name of the module. For instance: Assignments"/>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="create-module" layout:fragment="dialog">
+        <form th:action="@{/module}" method="post" class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Create module</h2>
+                </div>
+                <button class="hidden:from_small">Create</button>
             </div>
 
-            <div class="col-md-6">
-                <button type="submit" class="btn btn-primary ctrl-enter-submit float-right">
-                    Create new module
-                </button>
-                <script type="text/javascript" src="/js/ctrl_enter_submit.js"></script>
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-7">
+                    <input name="edition" type="hidden" th:value="${edition.id}" />
+
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="create-module-name">Name</label>
+                        <input
+                            class="textfield"
+                            id="create-module-name"
+                            name="name"
+                            placeholder="Module name"
+                            data-style="outlined"
+                            required />
+                    </div>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button class="button padding_inline-5 padding_block-2" data-style="outlined">
+                        <span class="margin_right-1 fa-solid fa-plus"></span>
+                        <span>Create module</span>
+                    </button>
+                </div>
             </div>
-        </div>
-    </form>
-</section>
-</body>
+        </form>
+    </dialog>
 </html>
diff --git a/src/main/resources/templates/module/edit.html b/src/main/resources/templates/module/edit.html
new file mode 100644
index 0000000000000000000000000000000000000000..26a0d37d06b189daecadc29447301a20ae14b9cc
--- /dev/null
+++ b/src/main/resources/templates/module/edit.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="edit-module" layout:fragment="dialog">
+        <form th:action="@{/module}" th:method="patch" class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Edit module</h2>
+                </div>
+                <button class="hidden:from_small">Edit</button>
+            </div>
+
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-7">
+                    <input name="module" type="hidden" />
+
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="edit-module-name">Name</label>
+                        <input
+                            class="textfield"
+                            id="edit-module-name"
+                            name="name"
+                            placeholder="Module name"
+                            data-style="outlined"
+                            required />
+                    </div>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button class="button padding_inline-5 padding_block-2" data-style="outlined">
+                        <span class="margin_right-1 fa-solid fa-pencil-alt"></span>
+                        <span>Edit module</span>
+                    </button>
+                </div>
+            </div>
+        </form>
+    </dialog>
+</html>
diff --git a/src/main/resources/templates/module/remove.html b/src/main/resources/templates/module/remove.html
index e2ad302af292b3b86984c7ea78c14c1ec57da098..6a843f47751b83207a431f9793c9d20c5a01f8b9 100644
--- a/src/main/resources/templates/module/remove.html
+++ b/src/main/resources/templates/module/remove.html
@@ -1,47 +1,50 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{edition/view}">
-
-<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
-
-<!--@thymesVar id="_module" type="nl.tudelft.labracore.api.dto.ModuleDetailsDTO"-->
-
-<body>
-<section layout:fragment="subcontent">
-    <div class="page-sub-header">
-        <h3>Remove Module</h3>
-    </div>
-
-    <form th:action="@{/module/{id}/remove(id=${_module.id})}"
-          class="form-horizontal"
-          method="post">
-        Are you sure you want to remove <strong
-            th:text="${'module #' + _module.id + ' (' + _module.name + ')'}"></strong>?
-        <div class="text-center">
-            <button class="btn btn-danger">Delete this Module</button>
-            <small>or <a
-                    th:href="@{/edition/{eId}/modules(eId=${edition.id})}">go back</a></small>
-        </div>
-    </form>
-</section>
-</body>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="remove-module" layout:fragment="dialog">
+        <form th:action="@{/module}" th:method="delete" class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Delete module</h2>
+                </div>
+                <button class="hidden:from_small">Delete</button>
+            </div>
+
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-6">
+                    <input name="id" type="hidden" />
+
+                    <p class="font-400">
+                        Are you sure you want to delete
+                        <span id="remove-module-name"></span>
+                        ?
+                    </p>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        data-style="outlined"
+                        data-type="negative">
+                        <span class="margin_right-1 fa-solid fa-trash-alt"></span>
+                        <span>Delete module</span>
+                    </button>
+                </div>
+            </div>
+        </form>
+    </dialog>
 </html>
diff --git a/src/main/resources/templates/pagination.html b/src/main/resources/templates/pagination.html
index e9ff0a1e557e26b4e498ef5ab927a6ba707c0360..41562f90d559ad243faeb640e91163a7de02e295 100644
--- a/src/main/resources/templates/pagination.html
+++ b/src/main/resources/templates/pagination.html
@@ -1,74 +1,85 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org">
-
-<!--@thymesVar id="page" type="org.springframework.data.domain.Page<java.lang.Object>"-->
-<!--@thymesVar id="size" type="java.lang.Integer"-->
-<!--@thymesVar id="url" type="java.lang.String"-->
-
-<body>
-<nav th:if="${page.getTotalPages() > 1}" aria-label="Page navigation" th:fragment="pagination (page, size)" th:with="url=${#httpServletRequest.requestURI}">
-    <ul class="pagination">
-        <li th:classappend="${not page.isFirst()} ? '' : 'disabled'" class="page-item">
-            <span th:if="${page.isFirst()}" class="page-link">&laquo;</span>
-
-            <a th:if="${!page.isFirst()}" th:href="@{${url}(page=0, size=${page.getSize()})}" class="page-link">&laquo;</a>
-        </li>
-
-        <th:block th:if="${page.getTotalPages() < size}">
-            <li th:each="pageNumber : ${#numbers.sequence(0, page.getTotalPages()-1)}"
-                th:classappend="${pageNumber == page.getNumber()} ? 'active' : ''" class="page-item">
-                <span th:if="${pageNumber == page.getNumber()}" th:text="${pageNumber+1}" class="page-link">1</span>
-                <a th:if="${pageNumber != page.getNumber()}" th:text="${pageNumber+1}"
-                   th:href="@{${url}(page=${pageNumber}, size=${page.getSize()})}" class="page-link">1</a>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <nav
+        aria-label="Page navigation"
+        layout:fragment="pagination(page, size)"
+        class="pagination"
+        th:data-page="${page.number}"
+        th:data-page-size="${page.size}">
+        <ul role="list" th:if="${page.getTotalPages() > 1}">
+            <li th:data-disabled="${page.isFirst()}" class="page__item">
+                <span th:if="${page.isFirst()}" class="page__link">&laquo;</span>
+                <button th:unless="${page.isFirst()}" data-page="0" class="page__link">
+                    &laquo;
+                </button>
             </li>
-        </th:block>
 
-        <th:block th:unless="${page.getTotalPages() < size}">
-            <th:block th:if="${page.getNumber() > size}">
-                <li class="page-item"><a href="#" class="page-link">...</a></li>
+            <th:block th:if="${page.totalPages < size}">
+                <li
+                    th:each="pageNumber : ${#numbers.sequence(0, page.totalPages-1)}"
+                    th:data-active="${pageNumber == page.number}"
+                    class="page__item">
+                    <span
+                        th:if="${pageNumber == page.number}"
+                        th:text="${pageNumber+1}"
+                        class="page__link"></span>
+                    <button
+                        th:unless="${pageNumber == page.number}"
+                        th:text="${pageNumber+1}"
+                        th:data-page="${pageNumber}"
+                        class="page__link"></button>
+                </li>
             </th:block>
 
-            <th:block th:each="pageNumber : ${#numbers.sequence(page.getNumber() - size + 1, page.getNumber() + size - 1)}">
-                <th:block th:if="${pageNumber >= 0 and pageNumber <= page.getTotalPages()-1}">
-                    <li th:classappend="${pageNumber == page.getNumber()} ? 'active' : ''"  class="page-item">
-                        <span th:if="${pageNumber == page.getNumber()}" th:text="${pageNumber+1}" class="page-link">1</span>
-                        <a th:if="${pageNumber != page.getNumber()}" th:text="${pageNumber+1}"
-                           th:href="@{${url}(page=${pageNumber}, size=${page.getSize()})}" class="page-link">1</a>
-                    </li>
+            <th:block th:unless="${page.totalPages < size}">
+                <th:block th:if="${page.number > size}">
+                    <li class="page__item"><a href="#" class="page__link">...</a></li>
                 </th:block>
-            </th:block>
 
-            <th:block th:if="${page.getNumber() + size <= page.getTotalPages()-1}">
-                <li class="page-item"><a href="#" class="page-link">...</a></li>
+                <th:block
+                    th:each="pageNumber : ${#numbers.sequence(page.number - size + 1, page.number + size - 1)}">
+                    <th:block th:if="${pageNumber >= 0 and pageNumber <= page.totalPages-1}">
+                        <li th:data-active="${pageNumber == page.number}" class="page__item">
+                            <span
+                                th:if="${pageNumber == page.number}"
+                                th:text="${pageNumber+1}"
+                                class="page__link"></span>
+                            <button
+                                th:unless="${pageNumber == page.number}"
+                                th:text="${pageNumber+1}"
+                                th:data-page="${pageNumber}"
+                                class="page__link"></button>
+                        </li>
+                    </th:block>
+                </th:block>
+
+                <th:block th:if="${page.number + size <= page.totalPages-1}">
+                    <li class="page__item"><a href="#" class="page__link">...</a></li>
+                </th:block>
             </th:block>
-        </th:block>
 
-        <li th:classappend="${not page.isLast()} ? '' : 'disabled'" class="page-item">
-            <span th:if="${page.isLast()}" class="page-link">&raquo;</span>
+            <li th:data-disabled="${page.isLast()}" class="page__item">
+                <span th:if="${page.isLast()}" class="page__link">&raquo;</span>
+                <button
+                    th:unless="${page.isLast()}"
+                    class="page__link"
+                    th:data-page="${page.totalPages-1}">
+                    &raquo;
+                </button>
+            </li>
+        </ul>
 
-            <a th:if="${!page.isLast()}"
-               th:href="@{${url}(page=${page.getTotalPages()-1}, size=${page.getSize()})}" class="page-link">&raquo;</a>
-        </li>
-    </ul>
-</nav>
-</body>
+        <script>
+            $(() => {
+                $(".pagination button.page__link").click(function () {
+                    let url = new URL(window.location);
+                    url.searchParams.set("page", $(this).data("page"));
+                    window.location = url.href;
+                });
+            });
+        </script>
+    </nav>
 </html>
diff --git a/src/main/resources/templates/participant/add.html b/src/main/resources/templates/participant/add.html
new file mode 100644
index 0000000000000000000000000000000000000000..4def89c9fa22f87b5a45f3e8968702f87a2242cc
--- /dev/null
+++ b/src/main/resources/templates/participant/add.html
@@ -0,0 +1,237 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="add-participants" layout:fragment="dialog">
+        <div class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Add participants</h2>
+                </div>
+                <button class="hidden:from_small" data-submit="small">Add</button>
+            </div>
+
+            <div class="dialog__body">
+                <div class="flex margin_bottom-7 | flex_column:small">
+                    <nav data-tabs aria-label="Add participant">
+                        <ul
+                            class="nav_list"
+                            style="--align: center; --width: 12rem"
+                            role="list"
+                            data-style="outlined">
+                            <li data-active="true" data-view="add-single-participant">
+                                <button>Single participant</button>
+                            </li>
+                            <li data-active="false" data-view="add-multiple-participants">
+                                <button>Multiple participants</button>
+                            </li>
+                            <li data-active="false" data-view="import-participants">
+                                <button>Import participant</button>
+                            </li>
+                        </ul>
+                    </nav>
+
+                    <form
+                        id="add-single-participant"
+                        th:action="@{/participant/single}"
+                        method="post"
+                        class="flex_column"
+                        style="min-width: 16rem">
+                        <input type="hidden" name="edition" th:value="${edition.id}" />
+                        <div class="flex_column gap-0">
+                            <label class="form__label" for="single-participant-id">
+                                Participant
+                            </label>
+                            <input
+                                class="textfield"
+                                placeholder="Username, email, or student number"
+                                name="identifier"
+                                id="single-participant-id"
+                                data-style="outlined"
+                                required />
+                        </div>
+                        <div class="flex_column gap-0">
+                            <label class="form__label" for="single-participant-role">Role</label>
+                            <select
+                                id="single-participant-role"
+                                name="role"
+                                data-style="outlined"
+                                data-select
+                                required>
+                                <option value="STUDENT" selected>Student</option>
+                                <option value="TA">TA</option>
+                                <option value="HEAD_TA">Head TA</option>
+                                <option value="TEACHER">Teacher</option>
+                            </select>
+                        </div>
+
+                        <button type="submit" class="hidden"></button>
+                    </form>
+
+                    <form
+                        id="add-multiple-participants"
+                        th:action="@{/participant/multiple}"
+                        method="post"
+                        class="hidden flex_column"
+                        style="min-width: 16rem">
+                        <input type="hidden" name="edition" th:value="${edition.id}" />
+                        <div class="flex_column gap-0">
+                            <label class="form__label" for="multiple-participant-role">Role</label>
+                            <select
+                                id="multiple-participant-role"
+                                name="role"
+                                data-style="outlined"
+                                data-select
+                                required>
+                                <option value="STUDENT" selected>Student</option>
+                                <option value="TA">TA</option>
+                                <option value="HEAD_TA">Head TA</option>
+                                <option value="TEACHER">Teacher</option>
+                            </select>
+                        </div>
+
+                        <div class="flex_column gap-0">
+                            <label class="form__label" for="multiple-participant-ids">
+                                Participants
+                            </label>
+                            <textarea
+                                id="multiple-participant-ids"
+                                name="identifiers"
+                                class="textfield"
+                                placeholder="Enter a list of usernames, emails, or student numbers"
+                                rows="7"
+                                data-style="outlined"
+                                style="resize: none"
+                                required></textarea>
+                        </div>
+
+                        <button type="submit" class="hidden"></button>
+                    </form>
+
+                    <form
+                        id="import-participants"
+                        th:action="@{/participant/import}"
+                        method="post"
+                        enctype="multipart/form-data"
+                        class="hidden flex_column">
+                        <input type="hidden" name="edition" th:value="${edition.id}" />
+                        <div class="flex_column gap-1">
+                            <span class="font-300">The file should have the following format:</span>
+                            <div
+                                class="flex gap-0"
+                                style="
+                                    background-color: var(--neutral-1000);
+                                    border: 1px solid var(--neutral-600);
+                                ">
+                                <div
+                                    class="flex_column gap-0"
+                                    style="
+                                        border-right: 1px solid var(--neutral-600);
+                                        background-color: var(--neutral-700);
+                                        color: var(--neutral-200);
+                                        padding: 0.5rem 0.25rem 0.5rem 1rem;
+                                    ">
+                                    <code>1</code>
+                                    <code>2</code>
+                                    <code>3</code>
+                                </div>
+                                <div class="flex_column gap-0 padding-3">
+                                    <code>user,role</code>
+                                    <code>user,role</code>
+                                    <code>user,role</code>
+                                </div>
+                            </div>
+                            <span class="font-300 text_muted">
+                                &apos;
+                                <code>user</code>
+                                &apos; can be a username, email, or student number.
+                            </span>
+                            <span class="font-300 text_muted">
+                                Valid roles are: student, ta, head_ta, and teacher.
+                            </span>
+                        </div>
+
+                        <div class="flex_column gap-0">
+                            <label class="form__label" for="participant-import-file">
+                                Participants
+                            </label>
+                            <input
+                                id="participant-import-file"
+                                name="file"
+                                type="file"
+                                data-file
+                                data-style="outlined"
+                                data-placeholder="Browse..."
+                                required
+                                accept="text/csv" />
+                        </div>
+
+                        <button type="submit" class="hidden"></button>
+                    </form>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button
+                        id="add-participant-submit"
+                        class="button padding_inline-5 padding_block-2"
+                        data-style="outlined">
+                        <span data-submit="icon" class="margin_right-1 fa-solid fa-plus"></span>
+                        <span data-submit="full">Add participant</span>
+                    </button>
+                </div>
+            </div>
+        </div>
+
+        <script>
+            $(() => {
+                const viewData = {
+                    "add-single-participant": {
+                        full: "Add participant",
+                        small: "Add",
+                        icon: "fa-plus",
+                    },
+                    "add-multiple-participants": {
+                        full: "Add participants",
+                        small: "Add",
+                        icon: "fa-plus",
+                    },
+                    "import-participants": {
+                        full: "Import participant",
+                        small: "Import",
+                        icon: "fa-file-import",
+                    },
+                };
+                $("[data-view]").click(function () {
+                    const view = $(this).data("view");
+                    $("[data-submit]").each(function (i, elem) {
+                        const type = $(this).data("submit");
+                        if (type === "icon") {
+                            $(elem).removeClass(
+                                ...Object.keys(viewData).map(v => viewData[v][type])
+                            );
+                            $(elem).addClass(viewData[view][type]);
+                        } else {
+                            $(elem).text(viewData[view][type]);
+                        }
+                    });
+                });
+                $("#add-participant-submit").click(() =>
+                    $("#add-participants form:not(.hidden) button[type='submit']").click()
+                );
+            });
+        </script>
+    </dialog>
+</html>
diff --git a/src/main/resources/templates/participant/change_role.html b/src/main/resources/templates/participant/change_role.html
new file mode 100644
index 0000000000000000000000000000000000000000..f504302e53bacfe5a926bbb590cd4d0b7c5cffaa
--- /dev/null
+++ b/src/main/resources/templates/participant/change_role.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="change-role" layout:fragment="dialog">
+        <form th:action="@{/participant}" th:method="patch" class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>
+                        Change
+                        <span id="change-role-name"></span>
+                        role
+                    </h2>
+                </div>
+                <button class="hidden:from_small">Change</button>
+            </div>
+
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-7">
+                    <input name="edition" type="hidden" th:value="${edition.id}" />
+                    <input name="person" type="hidden" />
+
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="change-role-role">Role</label>
+                        <select id="change-role-role" name="role" data-select data-style="outlined">
+                            <option value="STUDENT" selected>Student</option>
+                            <option value="TA">TA</option>
+                            <option value="HEAD_TA">Head TA</option>
+                            <option value="TEACHER">Teacher</option>
+                        </select>
+                    </div>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button class="button padding_inline-5 padding_block-2" data-style="outlined">
+                        <span class="margin_right-1 fa-solid fa-pencil-alt"></span>
+                        <span>Change role</span>
+                    </button>
+                </div>
+            </div>
+        </form>
+    </dialog>
+</html>
diff --git a/src/main/resources/templates/participant/remove.html b/src/main/resources/templates/participant/remove.html
new file mode 100644
index 0000000000000000000000000000000000000000..38b1af63cb1b9c4daa6ba7e54022018875300804
--- /dev/null
+++ b/src/main/resources/templates/participant/remove.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="remove-participant" layout:fragment="dialog">
+        <form th:action="@{/participant}" th:method="delete" class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Remove participant</h2>
+                </div>
+                <button class="hidden:from_small">Remove</button>
+            </div>
+
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-6">
+                    <input name="edition" type="hidden" th:value="${edition.id}" />
+                    <input name="person" type="hidden" />
+
+                    <p class="font-400">
+                        Are you sure you want to remove
+                        <span id="remove-participant-name"></span>
+                        from this edition?
+                    </p>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="submit"
+                        data-style="outlined"
+                        data-type="negative">
+                        <span class="margin_right-1 fa-solid fa-minus"></span>
+                        <span>Remove participant</span>
+                    </button>
+                </div>
+            </div>
+        </form>
+    </dialog>
+</html>
diff --git a/src/main/resources/templates/question/create.html b/src/main/resources/templates/question/create.html
new file mode 100644
index 0000000000000000000000000000000000000000..45848b4b6dab7a6a886383c6b7c8bc81a5070611
--- /dev/null
+++ b/src/main/resources/templates/question/create.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="create-question" layout:fragment="dialog">
+        <form th:action="@{/question}" method="post" class="dialog">
+            <input type="hidden" name="edition.id" th:value="${edition.id}" />
+
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Create question</h2>
+                </div>
+                <button class="hidden:from_small">Create</button>
+            </div>
+
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-7">
+                    <div class="flex_column gap-1">
+                        <label for="question-question" class="form__label">Question</label>
+                        <input
+                            class="textfield"
+                            id="question-question"
+                            name="question"
+                            placeholder="Question"
+                            data-style="outlined"
+                            required />
+                    </div>
+                    <div class="flex_column gap-1">
+                        <label for="question-answer" class="form__label">Answer</label>
+                        <input
+                            class="textfield"
+                            id="question-answer"
+                            name="answer"
+                            placeholder="Answer"
+                            data-style="outlined"
+                            required />
+                    </div>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        data-style="outlined"
+                        type="submit">
+                        <span class="margin_right-1 fa-solid fa-plus"></span>
+                        <span>Create question</span>
+                    </button>
+                </div>
+            </div>
+        </form>
+    </dialog>
+</html>
diff --git a/src/main/resources/templates/question/edit.html b/src/main/resources/templates/question/edit.html
new file mode 100644
index 0000000000000000000000000000000000000000..581afdf2dbaa803af70373eab3d3e3084f2fb41c
--- /dev/null
+++ b/src/main/resources/templates/question/edit.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="edit-question" layout:fragment="dialog">
+        <form th:action="@{/question}" th:method="patch" class="dialog">
+            <input type="hidden" id="edit-question-id" name="questionId" />
+            <input type="hidden" name="edition.id" th:value="${edition.id}" />
+
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Edit question</h2>
+                </div>
+                <button class="hidden:from_small">Edit</button>
+            </div>
+
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-7">
+                    <div class="flex_column gap-1">
+                        <label for="edit-question-question" class="form__label">Question</label>
+                        <input
+                            class="textfield"
+                            id="edit-question-question"
+                            name="question"
+                            placeholder="Question"
+                            data-style="outlined"
+                            required />
+                    </div>
+                    <div class="flex_column gap-1">
+                        <label for="edit-question-answer" class="form__label">Answer</label>
+                        <input
+                            class="textfield"
+                            id="edit-question-answer"
+                            name="answer"
+                            placeholder="Answer"
+                            data-style="outlined"
+                            required />
+                    </div>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button class="button padding_inline-5 padding_block-2" data-style="outlined">
+                        <span class="margin_right-1 fa-solid fa-pencil-alt"></span>
+                        <span>Edit question</span>
+                    </button>
+                </div>
+            </div>
+        </form>
+    </dialog>
+</html>
diff --git a/src/main/resources/templates/question/remove.html b/src/main/resources/templates/question/remove.html
new file mode 100644
index 0000000000000000000000000000000000000000..5aaaa18680cadab4c26f59882602d57561a893ad
--- /dev/null
+++ b/src/main/resources/templates/question/remove.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="remove-question" layout:fragment="dialog">
+        <form th:action="@{/question}" th:method="delete" class="dialog">
+            <input type="hidden" name="editionId" th:value="${edition.id}" />
+            <input type="hidden" id="remove-question-id" name="questionId" />
+
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Delete question</h2>
+                </div>
+                <button class="hidden:from_small">Delete</button>
+            </div>
+
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-6">
+                    <p class="font-400">Are you sure you want to delete this question?</p>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        data-style="outlined"
+                        data-type="negative">
+                        <span class="margin_right-1 fa-solid fa-trash-alt"></span>
+                        <span>Delete question</span>
+                    </button>
+                </div>
+            </div>
+        </form>
+    </dialog>
+</html>
diff --git a/src/main/resources/templates/request/actions/approve.html b/src/main/resources/templates/request/actions/approve.html
new file mode 100644
index 0000000000000000000000000000000000000000..1f28f68f5702af0d3b8ef64e5890e9fb743c1371
--- /dev/null
+++ b/src/main/resources/templates/request/actions/approve.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="approve-request" layout:fragment="dialog">
+        <form th:action="@{/request/approve}" method="post" class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Approve request</h2>
+                </div>
+                <button class="hidden:from_small">Approve</button>
+            </div>
+
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-7">
+                    <input name="request" type="hidden" />
+
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="approve-request-reason">Reason</label>
+                        <textarea
+                            class="textfield"
+                            id="approve-request-reason"
+                            name="reasonForAssistant"
+                            placeholder="Reason for approval"
+                            data-style="outlined"
+                            rows="3"
+                            style="resize: none"></textarea>
+                    </div>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button class="button padding_inline-5 padding_block-2" data-style="outlined">
+                        <span class="margin_right-1 fa-solid fa-check"></span>
+                        <span>Approve request</span>
+                    </button>
+                </div>
+            </div>
+        </form>
+    </dialog>
+</html>
diff --git a/src/main/resources/templates/request/actions/feedback.html b/src/main/resources/templates/request/actions/feedback.html
new file mode 100644
index 0000000000000000000000000000000000000000..4435915a859ff017f418c65c341ca529572ea121
--- /dev/null
+++ b/src/main/resources/templates/request/actions/feedback.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="request-feedback" layout:fragment="dialog">
+        <div class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Give feedback</h2>
+                </div>
+                <button class="hidden:from_small">Save</button>
+            </div>
+
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-7">
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="feedback-assistant">Assistant</label>
+                        <select
+                            id="feedback-assistant"
+                            name="assistant"
+                            data-select
+                            data-style="outlined">
+                            <option
+                                th:each="assistant, iter : ${assistants}"
+                                th:selected="${iter.index == 0}"
+                                th:value="${assistant.id}"
+                                th:text="${assistant.displayName}"></option>
+                        </select>
+                    </div>
+
+                    <form
+                        th:each="assistant, iter : ${assistants}"
+                        class="flex_column"
+                        th:classappend="${iter.index == 0} ? '' : 'hidden'"
+                        th:action="@{/request/{requestId}/feedback/{assistantId}(requestId=${request.id}, assistantId=${assistant.id})}"
+                        method="post"
+                        id="feedback-form"
+                        th:with="feedback = ${@feedbackRepository.findById(request.id, assistant.id).orElse(null)}"
+                        th:data-assistant="${assistant.id}">
+                        <div class="flex_column gap-1">
+                            <label class="form__label" for="feedback-score">Score</label>
+                            <input type="hidden" id="feedback-score" name="rating" />
+                            <div class="stars" id="feedback-stars">
+                                <button
+                                    type="button"
+                                    value="1"
+                                    th:data-filled="${feedback == null} ? false : ${feedback.rating >= 1}">
+                                    <span class="fa-solid fa-star"></span>
+                                    <span class="fa-regular fa-star"></span>
+                                </button>
+                                <button
+                                    type="button"
+                                    value="2"
+                                    th:data-filled="${feedback == null} ? false : ${feedback.rating >= 2}">
+                                    <span class="fa-solid fa-star"></span>
+                                    <span class="fa-regular fa-star"></span>
+                                </button>
+                                <button
+                                    type="button"
+                                    value="3"
+                                    th:data-filled="${feedback == null} ? false : ${feedback.rating >= 3}">
+                                    <span class="fa-solid fa-star"></span>
+                                    <span class="fa-regular fa-star"></span>
+                                </button>
+                                <button
+                                    type="button"
+                                    value="4"
+                                    th:data-filled="${feedback == null} ? false : ${feedback.rating >= 4}">
+                                    <span class="fa-solid fa-star"></span>
+                                    <span class="fa-regular fa-star"></span>
+                                </button>
+                                <button
+                                    type="button"
+                                    value="5"
+                                    th:data-filled="${feedback == null} ? false : ${feedback.rating >= 5}">
+                                    <span class="fa-solid fa-star"></span>
+                                    <span class="fa-regular fa-star"></span>
+                                </button>
+                            </div>
+                        </div>
+                        <div class="flex_column gap-1">
+                            <label class="form__label" for="feedback-text">Feedback</label>
+                            <textarea
+                                id="feedback-text"
+                                class="textfield"
+                                name="feedback"
+                                maxlength="250"
+                                th:placeholder="|Leave feedback about ${assistant.displayName}|"
+                                style="resize: none"
+                                rows="3"
+                                data-style="outlined"
+                                th:text="${feedback?.feedback}"></textarea>
+                        </div>
+                    </form>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        data-style="outlined"
+                        id="submit-feedback">
+                        <span class="margin_right-1 fa-solid fa-star"></span>
+                        <span>Give feedback</span>
+                    </button>
+                </div>
+            </div>
+        </div>
+
+        <script>
+            $("#feedback-stars button").click(function () {
+                $("#feedback-stars button").each((i, e) => {
+                    $(e).attr("data-filled", $(this).val() >= $(e).val());
+                });
+                $("#feedback-score").val($(this).val());
+                $(this).blur();
+            });
+            $("#feedback-assistant").change(function () {
+                $("[data-assistant]").addClass("hidden");
+                $(`[data-assistant=${$(this).val()}]`).removeClass("hidden");
+            });
+            $("#submit-feedback").click(() => $("#feedback-form:not(.hidden)").submit());
+        </script>
+    </dialog>
+</html>
diff --git a/src/main/resources/templates/request/actions/forward.html b/src/main/resources/templates/request/actions/forward.html
new file mode 100644
index 0000000000000000000000000000000000000000..bd96d159f59a56a67296e7f509331196f9fe1277
--- /dev/null
+++ b/src/main/resources/templates/request/actions/forward.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="forward-request" layout:fragment="dialog">
+        <form th:action="@{/request/forward}" method="post" class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Forward request</h2>
+                </div>
+                <button class="hidden:from_small">Forward</button>
+            </div>
+
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-7">
+                    <input name="request" type="hidden" />
+
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="forward-request-assistant">
+                            Forward to
+                        </label>
+                        <select
+                            id="forward-request-assistant"
+                            name="assistant"
+                            data-select
+                            data-style="outlined"
+                            required>
+                            <option value="-1" selected>Anyone but me</option>
+                            <option
+                                th:each="assistant : ${assistants}"
+                                th:value="${assistant.id}"
+                                th:text="${assistant.displayName}"></option>
+                        </select>
+                    </div>
+
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="forward-request-reason">Reason</label>
+                        <textarea
+                            class="textfield"
+                            id="forward-request-reason"
+                            name="reasonForAssistant"
+                            placeholder="Reason for forwarding"
+                            data-style="outlined"
+                            rows="3"
+                            style="resize: none"
+                            required></textarea>
+                    </div>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button class="button padding_inline-5 padding_block-2" data-style="outlined">
+                        <span class="margin_right-1 fa-solid fa-share"></span>
+                        <span>Forward request</span>
+                    </button>
+                </div>
+            </div>
+        </form>
+    </dialog>
+</html>
diff --git a/src/main/resources/templates/request/actions/reject.html b/src/main/resources/templates/request/actions/reject.html
new file mode 100644
index 0000000000000000000000000000000000000000..3fe8d455ab929e1182440278e10f728af6e5d8a9
--- /dev/null
+++ b/src/main/resources/templates/request/actions/reject.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog id="reject-request" layout:fragment="dialog">
+        <form th:action="@{/request/reject}" method="post" class="dialog">
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Reject request</h2>
+                </div>
+                <button class="hidden:from_small">Reject</button>
+            </div>
+
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-7">
+                    <input name="request" type="hidden" />
+
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="reject-request-assistant-reason">
+                            Reason for assistant
+                        </label>
+                        <textarea
+                            class="textfield"
+                            id="reject-request-assistant-reason"
+                            name="reasonForAssistant"
+                            placeholder="Reason for rejection"
+                            data-style="outlined"
+                            rows="3"
+                            style="resize: none"
+                            required></textarea>
+                    </div>
+
+                    <div class="flex_column gap-1">
+                        <label class="form__label" for="reject-request-student-reason">
+                            Reason for student
+                        </label>
+                        <textarea
+                            class="textfield"
+                            id="reject-request-student-reason"
+                            name="reasonForStudent"
+                            placeholder="Reason for rejection"
+                            data-style="outlined"
+                            rows="3"
+                            style="resize: none"
+                            required></textarea>
+                    </div>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button class="button padding_inline-5 padding_block-2" data-style="outlined">
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Reject request</span>
+                    </button>
+                </div>
+            </div>
+        </form>
+    </dialog>
+</html>
diff --git a/src/main/resources/templates/request/entry.html b/src/main/resources/templates/request/entry.html
new file mode 100644
index 0000000000000000000000000000000000000000..5ce1c20c2022a833921a29dde9999414925d86da
--- /dev/null
+++ b/src/main/resources/templates/request/entry.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <tr
+        th:id="|request-${request.id}-row|"
+        layout:fragment="row(current)"
+        th:data-request="${request.id}"
+        data-selectable
+        th:data-current="${current}">
+        <td class="flex align_items_center hidden:from_small" style="--gap: 6px">
+            <button class="visually_hidden" th:data-request="${request.id}"></button>
+            <span
+                th:text="|${request.requestType.displayName()} for ${request.edition.course.name} by ${request.requester.displayName}|"></span>
+            <span
+                class="chip"
+                th:data-type="${request.eventInfo.status.colourClass}"
+                data-status
+                th:text="#{|request.status.${request.eventInfo.status.name().toLowerCase()}|}"></span>
+        </td>
+        <td class="hidden:small">
+            <button class="visually_hidden" th:data-request="${request.id}"></button>
+            <span
+                class="chip"
+                th:data-type="${request.eventInfo.status.colourClass}"
+                data-status
+                th:text="#{|request.status.${request.eventInfo.status.name().toLowerCase()}|}"></span>
+        </td>
+        <td class="hidden:small">
+            <span th:if="${request instanceof T(nl.tudelft.queue.model.SelectionRequest)}">
+                Selection
+            </span>
+            <span
+                th:unless="${request instanceof T(nl.tudelft.queue.model.SelectionRequest)}"
+                th:text="${request.requestType.displayName()}"></span>
+        </td>
+        <td class="hidden:small" th:text="${request.requester.displayName}"></td>
+        <td
+            class="hidden:small"
+            th:text="|${request.room.building.name} - ${request.room.name}|"></td>
+        <td
+            class="hidden:small"
+            th:text="|${request.assignment.module.name} - ${request.assignment.name}|"></td>
+        <td class="hidden:small" th:text="${request.edition.course.name}"></td>
+        <td
+            class="hidden:small"
+            th:text="${current} ? '' : ${request.eventInfo.assignedTo?.displayName}"></td>
+    </tr>
+
+    <script
+        layout:fragment="template"
+        id="request-entry-template"
+        type="text/x-handlebars-template">
+        <tr id="request-{{id}}-row" data-request="{{id}}" data-selectable>
+            <td class="flex align_items_center hidden:from_small" style="--gap: 6px;">
+                <button class="visually_hidden" data-request="{{id}}"></button>
+                <span>
+                    {{requestTypeDisplayName}} for {{courseName}} by {{requestedBy}}
+                </span>
+                <span class="chip" data-type="regular">
+                    {{status}}
+                </span>
+            </td>
+            <td class="hidden:small">
+                <button class="visually_hidden" data-request="{{request.id}}"></button>
+                <span class="chip" data-type="regular">
+                    {{statusDisplayName}}
+                </span>
+            </td>
+            <td class="hidden:small">
+                <!--            <span th:if="${request instanceof T(nl.tudelft.queue.model.SelectionRequest)}">Selection</span>-->
+                <span>
+                    {{requestTypeDisplayName}}
+                </span>
+            </td>
+            <td class="hidden:small">
+                {{requestedBy}}
+            </td>
+            <td class="hidden:small">
+                {{buildingName}} - {{roomName}}
+            </td>
+            <td class="hidden:small">
+                {{moduleName}} - {{assignmentName}}
+            </td>
+            <td class="hidden:small">
+                {{courseName}}
+            </td>
+            <td class="hidden:small"></td>
+        </tr>
+    </script>
+</html>
diff --git a/src/main/resources/templates/request/filters.html b/src/main/resources/templates/request/filters.html
new file mode 100644
index 0000000000000000000000000000000000000000..4359bf781527cb399164645b9ce73bb8c42cfd91
--- /dev/null
+++ b/src/main/resources/templates/request/filters.html
@@ -0,0 +1,166 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog
+        id="filters"
+        layout:fragment="dialog(showSession, showAssignment, showRoom, showAssigned, showStatus, showType)">
+        <form th:action="@{/filter}" method="post" class="dialog">
+            <input type="hidden" name="return-path" th:value="${returnPath}" />
+
+            <div class="dialog__title">
+                <div class="flex align_items_center gap-5">
+                    <button
+                        class="hidden:from_small fa-solid fa-xmark"
+                        type="button"
+                        data-cancel></button>
+                    <h2>Filter requests</h2>
+                </div>
+                <button class="hidden:from_small" name="filter-submit">Filter</button>
+            </div>
+
+            <div class="dialog__body">
+                <div class="flex_column margin_bottom-7">
+                    <div>
+                        <button
+                            class="button padding_inline-5 padding_block-2"
+                            data-style="outlined"
+                            name="filter-clear">
+                            <span class="margin_right-1 fa-solid fa-filter-circle-xmark"></span>
+                            <span>Clear filters</span>
+                        </button>
+                    </div>
+
+                    <div
+                        th:if="not ${labs?.isEmpty() ?: true} and ${showLabs != false}"
+                        class="flex_column gap-1">
+                        <label class="form__label" for="lab-select">Session</label>
+                        <select
+                            id="lab-select"
+                            name="labs"
+                            data-select
+                            multiple
+                            data-style="outlined"
+                            data-placeholder="None selected">
+                            <option
+                                th:each="lab : ${labs}"
+                                th:value="${lab.id}"
+                                th:selected="${filter.labs.contains(lab.id)}"
+                                th:text="${lab.name}"></option>
+                        </select>
+                    </div>
+
+                    <div
+                        th:if="not ${assignments?.isEmpty() ?: true} and ${showAssignments != false}"
+                        class="flex_column gap-1">
+                        <label class="form__label" for="assignment-select">Assignment</label>
+                        <select
+                            id="assignment-select"
+                            name="assignments"
+                            data-select
+                            multiple
+                            data-style="outlined"
+                            data-placeholder="None selected">
+                            <option
+                                th:each="assignment : ${assignments}"
+                                th:value="${assignment.id}"
+                                th:selected="${filter.assignments.contains(assignment.id)}"
+                                th:text="|${assignment.module.name} - ${assignment.name}|"></option>
+                        </select>
+                    </div>
+
+                    <div
+                        th:if="not ${rooms?.isEmpty() ?: true} and ${showRooms != false}"
+                        class="flex_column gap-1">
+                        <label class="form__label" for="room-select">Room</label>
+                        <select
+                            id="room-select"
+                            name="rooms"
+                            data-select
+                            multiple
+                            data-style="outlined"
+                            data-placeholder="None selected">
+                            <option
+                                th:each="room : ${rooms}"
+                                th:value="${room.id}"
+                                th:selected="${filter.rooms.contains(room.id)}"
+                                th:text="|${room.building.name} - ${room.name}|"></option>
+                        </select>
+                    </div>
+
+                    <div
+                        th:if="not ${assignments?.isEmpty() ?: true} and ${showAssigned != false}"
+                        class="flex_column gap-1">
+                        <label class="form__label" for="assigned-select">Assigned</label>
+                        <select
+                            id="assigned-select"
+                            name="assigned"
+                            data-select
+                            multiple
+                            data-style="outlined"
+                            data-placeholder="None selected">
+                            <option
+                                th:each="assistant : ${assistants}"
+                                th:value="${assistant.id}"
+                                th:selected="${filter.assigned.contains(assistant.id)}"
+                                th:text="${assistant.displayName}"></option>
+                        </select>
+                    </div>
+
+                    <div th:if="${showStatus != false}" class="flex_column gap-1">
+                        <label class="form__label" for="status-select">Status</label>
+                        <select
+                            id="status-select"
+                            name="requestStatuses"
+                            data-select
+                            multiple
+                            data-style="outlined"
+                            data-placeholder="None selected">
+                            <option
+                                th:each="status : ${T(nl.tudelft.queue.model.enums.RequestStatus).values()}"
+                                th:value="${status}"
+                                th:selected="${filter.requestStatuses.contains(status)}"
+                                th:text="#{|request.status.${status.name().toLowerCase()}|}"></option>
+                        </select>
+                    </div>
+
+                    <div th:if="${showType != false}" class="flex_column gap-1">
+                        <label class="form__label" for="request-type-select">Type</label>
+                        <select
+                            id="request-type-select"
+                            name="requestTypes"
+                            data-select
+                            multiple
+                            data-style="outlined"
+                            data-placeholder="None selected">
+                            <option
+                                th:each="type : ${T(nl.tudelft.queue.model.enums.RequestType).values()}"
+                                th:value="${type}"
+                                th:selected="${filter.requestTypes.contains(type)}"
+                                th:text="${type.displayName()}"></option>
+                        </select>
+                    </div>
+                </div>
+
+                <div class="flex space_between span-2 | hidden:small">
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        type="button"
+                        data-style="outlined"
+                        data-cancel>
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Cancel</span>
+                    </button>
+                    <button
+                        class="button padding_inline-5 padding_block-2"
+                        data-style="outlined"
+                        name="filter-submit">
+                        <span class="margin_right-1 fa-solid fa-filter"></span>
+                        <span>Filter</span>
+                    </button>
+                </div>
+            </div>
+        </form>
+    </dialog>
+</html>
diff --git a/src/main/resources/templates/request/list.html b/src/main/resources/templates/request/list.html
index 9a1e67069eec968122872aef9c834d6ca01a0f8c..3c1314e4a3dc1a22c1a9988b7c19dda715f05b85 100644
--- a/src/main/resources/templates/request/list.html
+++ b/src/main/resources/templates/request/list.html
@@ -1,118 +1,155 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{layout}">
-
-<!--@thymesVar id="filter" type="nl.tudelft.queue.dto.util.RequestTableFilterDTO"-->
-
-<!--@thymesVar id="editions" type="java.util.List<nl.tudelft.labracore.api.dto.EditionDetailsDTO>"-->
-<!--@thymesVar id="labs" type="java.util.List<nl.tudelft.queue.dto.view.QueueSessionSummaryDTO>"-->
-<!--@thymesVar id="assignments" type="java.util.List<nl.tudelft.labracore.api.dto.AssignmentSummaryDTO>"-->
-<!--@thymesVar id="rooms" type="java.util.List<nl.tudelft.labracore.api.dto.RoomSummaryDTO>"-->
-<!--@thymesVar id="assistants" type="java.util.List<nl.tudelft.labracore.api.dto.PersonSummaryDTO>"-->
-
-<!--@thymesVar id="requests" type="org.springframework.data.domain.Page<nl.tudelft.queue.dto.view.RequestViewDTO>"-->
-<!--@thymesVar id="requestCounts" type="java.util.Map<java.lang.Long, java.lang.Long>"-->
-
-<head>
-    <title>Requests</title>
-
-    <script type="text/javascript" src="/js/request_table.js"></script>
-    <script type="text/javascript" src="/webjars/handlebars/handlebars.min.js"></script>
-
-    <link th:if="${@thymeleafConfig.isTheDay() && @requestTableService.partakes()}" rel="stylesheet" type="text/css" href="/css/stack.css"/>
-</head>
-
-<body>
-<section layout:fragment="content">
-    <nav role="navigation" class="breadcrumbs">
-        <ol class="breadcrumb">
-            <li class="breadcrumb-item"><a href="/">Home</a></li>
-            <li class="breadcrumb-item active">Requests</li>
-        </ol>
-    </nav>
-
-    <div class="row">
-        <h1 class="col-12">Requests</h1>
-    </div>
-
-    <div class="row mb-2">
-        <div class="col-12 btn-toolbar float-right" role="toolbar">
-            <th:block th:each="lab : ${labs}">
-                <a type="submit" class="btn btn-sm btn-get-next text-white mr-1 float-right"
-                   th:id="|get-next-${lab.id}|"
-                   th:if="${lab.isActiveOrGracePeriod}"
-                   th:classappend="${requestCounts.getOrDefault(lab.id, 0) == 0} ? 'disabled' : ''"
-                   th:href="@{/requests/next/{labId}(labId = ${lab.id})}">
-                    <span th:text="${@thymeleafConfig.isTheDay()} ? 'Pop from' : 'Get next for'"></span>
-                    <th:block th:text="${lab.name}">Lab name</th:block>
-                    <span th:id="|span-${lab.id}|" th:text="|(${requestCounts.getOrDefault(lab.id, 0)})|">(0)</span>
-                </a>
-            </th:block>
-            <a class="btn btn-sm btn-secondary text-white float-right"
-               onclick='location.reload();'>Refresh</a>
-            <form th:if="${@thymeleafConfig.isTheDay() && @requestTableService.partakes()}"
-                  method="post" th:action="@{/requests/ilikemyeyesthankyou}">
-                <button type="submit"
-                        class="btn btn-sm btn-success ml-4">Unstack me please</button>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{container}">
+    <head>
+        <title th:text="Requests"></title>
+
+        <script type="text/javascript" src="/webjars/handlebars/handlebars.min.js"></script>
+    </head>
+
+    <div layout:fragment="breadcrumbs" class="hidden"></div>
+
+    <th:block layout:fragment="content">
+        <h1 class="title margin_bottom-5">Requests</h1>
+
+        <div class="flex_column">
+            <div>
+                <button
+                    class="button padding_inline-5 padding_block-3"
+                    data-style="floating"
+                    data-dialog="filters">
+                    <span class="margin_right-1 fa-solid fa-filter"></span>
+                    <span th:text="|Filters (${filter.countActiveFilters()})|"></span>
+                </button>
+            </div>
+
+            <form
+                th:unless="${filter.isEmpty()}"
+                class="flex gap-3"
+                style="flex-wrap: wrap"
+                th:action="@{/filter}"
+                method="post">
+                <input type="hidden" name="return-path" value="/requests" />
+                <input type="hidden" name="filter-update" value="" />
+                <button
+                    th:each="lab : ${filter.labs}"
+                    class="chip"
+                    data-type="action"
+                    name="labs"
+                    th:value="${#strings.listJoin(filter.labs.?[#this != #root.lab], ',')}">
+                    <span class="fa-solid fa-filter"></span>
+                    <span th:text="${labs.^[id == #root.lab].name}"></span>
+                    <span class="fa-solid fa-xmark"></span>
+                </button>
+                <button
+                    th:each="assignment : ${filter.assignments}"
+                    class="chip"
+                    data-type="action"
+                    name="assignments"
+                    th:value="${#strings.listJoin(filter.assignments.?[#this != #root.assignment], ',')}">
+                    <span class="fa-solid fa-filter"></span>
+                    <span
+                        th:with="a = ${assignments.^[id == #root.assignment]}"
+                        th:text="|${a.module.name} - ${a.name}|"></span>
+                    <span class="fa-solid fa-xmark"></span>
+                </button>
+                <button
+                    th:each="room : ${filter.rooms}"
+                    class="chip"
+                    data-type="action"
+                    name="rooms"
+                    th:value="${#strings.listJoin(filter.rooms.?[#this != #root.room], ',')}">
+                    <span class="fa-solid fa-filter"></span>
+                    <span
+                        th:with="r = ${rooms.^[id == #root.room]}"
+                        th:text="|${r.building.name} - ${r.name}|"></span>
+                    <span class="fa-solid fa-xmark"></span>
+                </button>
+                <button
+                    th:each="assigned : ${filter.assigned}"
+                    class="chip"
+                    data-type="action"
+                    name="assigned"
+                    th:value="${#strings.listJoin(filter.assigned.?[#this != #root.assigned], ',')}">
+                    <span class="fa-solid fa-filter"></span>
+                    <span
+                        th:text="|Assigned to: ${assistants.^[id == #root.assigned].displayName}|"></span>
+                    <span class="fa-solid fa-xmark"></span>
+                </button>
+                <button
+                    th:each="status : ${filter.requestStatuses}"
+                    class="chip"
+                    data-type="action"
+                    name="requestStatuses"
+                    th:value="${#strings.listJoin(filter.requestStatuses.?[#this != #root.status], ',')}">
+                    <span class="fa-solid fa-filter"></span>
+                    <span th:text="#{|request.status.${status.name().toLowerCase()}|}"></span>
+                    <span class="fa-solid fa-xmark"></span>
+                </button>
+                <button
+                    th:each="requestType : ${filter.requestTypes}"
+                    class="chip"
+                    data-type="action"
+                    name="requestTypes"
+                    th:value="${#strings.listJoin(filter.requestTypes.?[#this != #root.requestType], ',')}">
+                    <span class="fa-solid fa-filter"></span>
+                    <span th:text="${requestType.displayName()}"></span>
+                    <span class="fa-solid fa-xmark"></span>
+                </button>
             </form>
-            <form th:if="${@thymeleafConfig.isTheDay() && !@requestTableService.partakes()}"
-                  method="post" th:action="@{/requests/iloveallqueuedevelopers}">
-                <button type="submit"
-                        class="btn btn-sm btn-success ml-4">Stack me please</button>
-            </form>
-        </div>
-    </div>
-
-    <form class="form" method="get">
-        <div class="form-row">
-            <input class="form-control col-sm-5 mr-0 mr-sm-2 mb-2 mb-sm-0"
-                   id="pagesize" name="size" type="number"
-                   th:placeholder="${requests.size} + ' requests'"
-                   required/>
 
-            <button type="submit" value="Submit"
-                    class="btn btn-success col-sm-auto">Submit
-            </button>
+            <div class="flex gap-3" style="flex-wrap: wrap">
+                <button
+                    th:each="lab : ${labs}"
+                    th:id="|get-next-${lab.id}|"
+                    class="button padding_inline-5 padding_block-3"
+                    data-style="floating"
+                    th:disabled="${requestCounts[lab.id] == 0}"
+                    th:data-lab="${lab.id}">
+                    <span class="margin_right-1 fa-solid fa-hand"></span>
+                    <!-- prettier-ignore -->
+                    <span th:text="|Get next for ${lab.name} (|"></span>
+                    <span data-count th:text="${requestCounts[lab.id]}"></span>
+                    <span>)</span>
+                </button>
+            </div>
+
+            <div class="flex_column">
+                <th:block layout:replace="~{request/table :: table}"></th:block>
+                <th:block
+                    layout:replace="~{pagination :: pagination(page=${requests}, size=3)}"></th:block>
+            </div>
         </div>
-    </form>
-</section>
-
-<section layout:fragment="outside-content">
-    <div class="row no-gutters">
-        <div class="col-lg-3 col-xl-2 pl-lg-3 pr-lg-1">
-            <th:block th:replace="request/list/filters :: filters (returnPath='/requests')">
-            </th:block>
-        </div>
-        <div class="col-lg-9 col-xl-10 pl-lg-1 pr-lg-3">
-            <th:block th:replace="request/list/request-table :: request-table">
-            </th:block>
-        </div>
-    </div>
-
-    <div class="justify-content-center">
-        <th:block th:replace="pagination :: pagination (page=${requests}, size=3)">
-        </th:block>
-    </div>
-</section>
-</body>
+    </th:block>
+
+    <th:block layout:fragment="overlays" th:with="returnPath = '/requests'">
+        <th:block
+            layout:replace="~{request/filters :: dialog(showAssigned=${false}, showStatus=${false})}"></th:block>
+        <th:block layout:replace="~{request/actions/approve :: dialog}"></th:block>
+        <th:block layout:replace="~{request/actions/reject :: dialog}"></th:block>
+        <th:block layout:replace="~{request/actions/forward :: dialog}"></th:block>
+    </th:block>
+
+    <th:block layout:fragment="script">
+        <script src="/js/request_table.js"></script>
+        <script>
+            $(() => {
+                $("[data-lab]").click(function () {
+                    $.get(`/requests/next/${$(this).data("lab")}`, html => {
+                        const element = $(html).filter((i, e) => $(e).is("dialog"));
+                        if (element.is("dialog")) {
+                            $(".page").append(element);
+                            openDialog(element.attr("id"));
+                            configureDialog(element);
+                        } else {
+                            window.location.reload();
+                        }
+                    });
+                });
+            });
+        </script>
+    </th:block>
 </html>
-
diff --git a/src/main/resources/templates/request/table.html b/src/main/resources/templates/request/table.html
new file mode 100644
index 0000000000000000000000000000000000000000..d18146284ad0f7051fad8d00ee7cabaeb2a9debc
--- /dev/null
+++ b/src/main/resources/templates/request/table.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{container}">
+    <head>
+        <title th:text="Requests"></title>
+    </head>
+
+    <th:block layout:fragment="table(showAssistants)">
+        <table id="request-table" class="rounded shadow table">
+            <thead>
+                <tr class="heading">
+                    <th class="hidden:from_small">Requests</th>
+                    <th class="hidden:small">Status</th>
+                    <th class="hidden:small">Type</th>
+                    <th class="hidden:small">Request by</th>
+                    <th class="hidden:small">Room</th>
+                    <th class="hidden:small">Assignment</th>
+                    <th class="hidden:small">Course</th>
+                    <th th:if="${showAssistants}" class="hidden:small">Assigned to</th>
+                    <th th:unless="${showAssistants}" class="hidden:small">Forwarded by</th>
+                </tr>
+            </thead>
+            <tbody>
+                <th:block th:each="request : ${currentRequests}">
+                    <th:block layout:replace="~{request/entry :: row(current=true)}"></th:block>
+                </th:block>
+                <th:block th:each="request : ${requests}">
+                    <th:block layout:replace="~{request/entry :: row(current=false)}"></th:block>
+                </th:block>
+            </tbody>
+        </table>
+
+        <script>
+            $(() => {
+                $("[data-request]").click(function () {
+                    $.get(`/request/${$(this).data("request")}`, html => {
+                        const element = $(html).filter((i, e) => $(e).is("dialog"));
+                        $(".page").append(element);
+                        openDialog(element.attr("id"));
+                        configureDialog(element);
+                    });
+                });
+            });
+        </script>
+
+        <th:block layout:replace="~{request/entry :: template}"></th:block>
+    </th:block>
+</html>
diff --git a/src/main/resources/templates/request/view.html b/src/main/resources/templates/request/view.html
index 800a77656b39bf1a318975a15204c6912921e307..cdf18cbe13fe38a68c0685f450371dd3b96ad08c 100644
--- a/src/main/resources/templates/request/view.html
+++ b/src/main/resources/templates/request/view.html
@@ -1,132 +1,219 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{layout}">
-
-<!--@thymesVar id="request" type="nl.tudelft.queue.dto.view.RequestViewDTO"-->
-<!--@thymesVar id="prevRequests" type="java.util.List<nl.tudelft.queue.dto.view.RequestViewDTO>"-->
-
-<!--@thymesVar id="edition" type="nl.tudelft.labracore.api.dto.EditionDetailsDTO"-->
-<!--@thymesVar id="assistant" type="nl.tudelft.labracore.api.dto.PersonDetailsDTO"-->
-
-<head>
-    <title th:text="${'Request #' + request.id}"></title>
-
-    <script type="text/javascript" src="/webjars/bootstrap-select/js/bootstrap-select.min.js"></script>
-    <link rel="stylesheet" href="/webjars/bootstrap-select/css/bootstrap-select.min.css" type="text/css"/>
-</head>
-
-<body>
-<section layout:fragment="~{breadcrumb}">
-    <nav role="navigation" class="breadcrumbs">
-        <ol class="breadcrumb">
-            <li class="breadcrumb-item"><a th:href="@{/}">Home</a></li>
-            <li class="breadcrumb-item"><a th:href="@{/editions}">Courses</a></li>
-            <li class="breadcrumb-item active" th:text="${'Request #' + request.id}"></li>
-        </ol>
-    </nav>
-</section>
-
-<section layout:fragment="content">
-    <th:block layout:fragment="claim-button">
-    </th:block>
-
-    <div class="row">
-        <div class="col-sm-4">
-            <th:block layout:fragment="feedback">
-            </th:block>
-
-            <th:block layout:fragment="history">
-            </th:block>
-        </div>
-
-        <div class="col-sm-8">
-            <th:block layout:fragment="request-info">
-            </th:block>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <dialog
+        th:id="|request-${request.id}|"
+        layout:fragment="dialog"
+        data-closable
+        data-remove-on-close
+        th:with="assistants = ${@feedbackService.assistantsInvolvedInRequest(request.id)}">
+        <div class="dialog">
+            <div class="flex_column gap-3">
+                <div class="hidden:small">
+                    <button class="button fa-solid fa-xmark" data-style="text" data-cancel></button>
+                </div>
+
+                <div class="flex align_items_center:from_small | flex_column:small">
+                    <div class="dialog__title">
+                        <div class="flex">
+                            <button
+                                class="hidden:from_small fa-solid fa-xmark"
+                                data-cancel></button>
+                            <h2
+                                th:text="|${request.requestType.displayName()} by ${request.requester.displayName}|"></h2>
+                        </div>
+                    </div>
+                    <span
+                        class="chip hidden:small"
+                        th:text="#{|request.status.${request.eventInfo.status.name().toLowerCase()}|}"></span>
+                </div>
+            </div>
+
+            <div class="dialog__body flex_column">
+                <form
+                    th:if="${@permissionService.canPickRequest(request.id)}"
+                    th:action="@{/request/{requestId}/pick(requestId=${request.id})}"
+                    method="post"
+                    id="claim-request">
+                    <button
+                        class="button padding_inline-5 padding_block-3"
+                        data-style="outlined"
+                        type="submit">
+                        <span>Claim request</span>
+                    </button>
+                </form>
+
+                <div class="flex | flex_column:medium">
+                    <div class="flex_column">
+                        <div class="flex_column gap-0 align_items_start hidden:from_small">
+                            <span class="label">Status</span>
+                            <span
+                                class="font-400"
+                                th:text="#{|request.status.${request.eventInfo.status.name().toLowerCase()}|}"></span>
+                        </div>
+
+                        <div class="flex_column gap-0">
+                            <span class="label">Course</span>
+                            <span class="font-400" th:text="${request.edition.course.name}"></span>
+                        </div>
+
+                        <div class="flex_column gap-0">
+                            <span class="label">Session</span>
+                            <span class="font-400" th:text="${request.session.name}"></span>
+                        </div>
+
+                        <div class="flex_column gap-0">
+                            <span class="label">Assignment</span>
+                            <span
+                                class="font-400"
+                                th:text="|${request.assignment.module.name} - ${request.assignment.name}|"></span>
+                        </div>
+
+                        <div class="flex_column gap-0">
+                            <span class="label">Room</span>
+                            <span
+                                class="font-400"
+                                th:text="|${request.room.building.name} - ${request.room.name}|"></span>
+                        </div>
+
+                        <div class="flex_column gap-0">
+                            <span class="label">Question</span>
+                            <p
+                                class="font-400"
+                                style="max-width: 80ch"
+                                th:text="${request.question}"></p>
+                        </div>
+
+                        <div th:if="${request.comment}" class="flex_column gap-0">
+                            <span class="label">Comment</span>
+                            <p
+                                class="font-400"
+                                style="max-width: 80ch"
+                                th:text="${request.comment}"></p>
+                        </div>
+                    </div>
+
+                    <div class="flex_column gap-0">
+                        <span class="label">History</span>
+                        <ul class="history flex_column gap-3">
+                            <li
+                                class="margin_left-5"
+                                th:data-type="${event.iconClass}"
+                                th:each="event : ${request.events}"
+                                th:unless="${event instanceof T(nl.tudelft.queue.dto.view.events.RequestPickedEventViewDTO)}">
+                                <div class="flex_column gap-0">
+                                    <p
+                                        class="font-400"
+                                        style="max-width: 80ch"
+                                        th:if="${@permissionService.canViewRequestAssistantReason(request.id)}"
+                                        th:text="${event.descriptionForAssistant}"></p>
+                                    <p
+                                        class="font-400"
+                                        style="max-width: 80ch"
+                                        th:unless="${@permissionService.canViewRequestAssistantReason(request.id)}"
+                                        th:text="${event.description}"></p>
+                                    <span
+                                        class="font-300"
+                                        th:text="${#temporals.format(event.timestamp, 'MMM dd, yyyy ''at'' HH:mm')}"></span>
+                                </div>
+                            </li>
+                        </ul>
+                    </div>
+                </div>
+
+                <div
+                    class="flex_column align_items_start | align_items_stretch:small"
+                    th:if="${@permissionService.canGiveFeedback(request.id) and not assistants.isEmpty()}">
+                    <button
+                        class="button padding_inline-5 padding_block-3"
+                        data-style="outlined"
+                        data-dialog="request-feedback">
+                        <span class="margin_right-1 fa-solid fa-star"></span>
+                        <span>Give feedback</span>
+                    </button>
+                </div>
+
+                <div th:if="${@permissionService.canFinishRequest(request.id)}" class="grid gap-3">
+                    <button
+                        class="button padding_inline-5 padding_block-3"
+                        data-style="outlined"
+                        data-type="positive"
+                        data-dialog="approve-request"
+                        th:data-request="${request.id}">
+                        <span class="margin_right-1 fa-solid fa-check"></span>
+                        <span>Approve</span>
+                    </button>
+
+                    <button
+                        class="button padding_inline-5 padding_block-3"
+                        data-style="outlined"
+                        data-type="negative"
+                        data-dialog="reject-request"
+                        th:data-request="${request.id}">
+                        <span class="margin_right-1 fa-solid fa-xmark"></span>
+                        <span>Reject</span>
+                    </button>
+
+                    <button
+                        class="button padding_inline-5 padding_block-3"
+                        data-style="outlined"
+                        data-dialog="forward-request"
+                        th:data-request="${request.id}">
+                        <span class="margin_right-1 fa-solid fa-share"></span>
+                        <span>Forward</span>
+                    </button>
+
+                    <form
+                        class="flex_column"
+                        th:action="@{/request/{id}/not-found(id=${request.id})}"
+                        method="get">
+                        <button
+                            class="button padding_inline-5 padding_block-3"
+                            data-style="outlined">
+                            <span class="margin_right-1 fa-solid fa-magnifying-glass"></span>
+                            <span>Not found</span>
+                        </button>
+                    </form>
+                </div>
+            </div>
+
+            <th:block layout:replace="~{request/actions/feedback :: dialog}"></th:block>
         </div>
 
-        <script src="/js/map_loader.js"></script>
-        <script type="text/javascript" th:inline="javascript">
-            const roomId = /*[[${request.room}]]*/ 0;
-            updateRequestInfo(roomId);
+        <script>
+            $("[data-dialog='approve-request']").click(function () {
+                $("#approve-request input[name='request']").val($(this).data("request"));
+                openDialog("approve-request");
+            });
+            $("[data-dialog='reject-request']").click(function () {
+                $("#reject-request input[name='request']").val($(this).data("request"));
+                openDialog("reject-request");
+            });
+            $("[data-dialog='forward-request']").click(function () {
+                $("#forward-request input[name='request']").val($(this).data("request"));
+                openDialog("forward-request");
+            });
+            $("[data-dialog='request-feedback']").click(function () {
+                openDialog("request-feedback");
+            });
+            configureSelect($("#feedback-assistant"));
+
+            $("#claim-request").submit(function (event) {
+                event.preventDefault();
+                $.post($(this).attr("action"), html => {
+                    const element = $(html).filter((i, e) => $(e).is("dialog"));
+                    $(this).closest("dialog").remove();
+                    if (element.is("dialog")) {
+                        $(".page").append(element);
+                        openDialog(element.attr("id"));
+                        configureDialog(element);
+                    } else {
+                        window.location.reload();
+                    }
+                });
+            });
         </script>
-
-        <style>
-            .btn-group.feedback > .btn.focus {
-                transition: .5s;
-                outline: none !important;
-                box-shadow: none;
-            }
-
-            .btn-group.feedback > .btn.active:nth-child(1) {
-                background: #C23A2C !important;
-                border: 1px solid #AD3427 !important;
-            }
-
-            .btn-group.feedback > .btn.active:nth-child(2) {
-                background: #F78921 !important;
-                border: 1px solid #F67C09 !important;
-            }
-
-            .btn-group.feedback > .btn.active:nth-child(3) {
-                background: #EAD234 !important;
-                border: 1px solid #E8CD1D !important;
-            }
-
-            .btn-group.feedback > .btn.active:nth-child(4) {
-                background: #76D142 !important;
-                border: 1px solid #68C931 !important;
-            }
-
-            .btn-group.feedback > .btn.active:nth-child(5) {
-                background: #489B52 !important;
-                border: 1px solid #408949 !important;
-            }
-
-            .btn-group.feedback > .btn:hover:nth-child(1) {
-                background: #C23A2C !important;
-                border: 1px solid #AD3427 !important;
-            }
-
-            .btn-group.feedback > .btn:hover:nth-child(2) {
-                background: #F78921 !important;
-                border: 1px solid #F67C09 !important;
-            }
-
-            .btn-group.feedback > .btn:hover:nth-child(3) {
-                background: #EAD234 !important;
-                border: 1px solid #E8CD1D !important;
-            }
-
-            .btn-group.feedback > .btn:hover:nth-child(4) {
-                background: #76D142 !important;
-                border: 1px solid #68C931 !important;
-            }
-
-            .btn-group.feedback > .btn:hover:nth-child(5) {
-                background: #489B52 !important;
-                border: 1px solid #408949 !important;
-            }
-        </style>
-    </div>
-</section>
-</body>
+    </dialog>
 </html>
diff --git a/src/main/resources/templates/request/view/lab.html b/src/main/resources/templates/request/view/lab.html
index 08ed3184beb6f95db14558a202765ff8d420ddbf..4f0c18139ae8b97428d884b62eec92535d8293fd 100644
--- a/src/main/resources/templates/request/view/lab.html
+++ b/src/main/resources/templates/request/view/lab.html
@@ -1,45 +1,6 @@
-<!--
-
-    Queue - A Queueing system that can be used to handle labs in higher education
-    Copyright (C) 2016-2020  Delft University of Technology
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
--->
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{request/view}">
-
-<body>
-<th:block layout:fragment="claim-button">
-    <th:block th:replace="request/view/components/claim-button :: claim-button">
-    </th:block>
-</th:block>
-
-<th:block layout:fragment="feedback">
-    <th:block th:replace="request/view/components/feedback :: feedback">
-    </th:block>
-</th:block>
-
-<th:block layout:fragment="history">
-    <th:block th:replace="request/view/components/history :: history">
-    </th:block>
-</th:block>
-
-<th:block layout:fragment="request-info">
-    <th:block th:replace="request/view/components/lab-request-info :: request-info">
-    </th:block>
-</th:block>
-</body>
-</html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{request/view}"></html>
diff --git a/src/main/resources/templates/shared-edition/view.html b/src/main/resources/templates/shared-edition/view.html
index 6ba75a80df706ad97aaff8a645d3d82eb100ae55..45be1e134d8a50b20c05779baa510292c5454af9 100644
--- a/src/main/resources/templates/shared-edition/view.html
+++ b/src/main/resources/templates/shared-edition/view.html
@@ -1,94 +1,84 @@
 <!DOCTYPE html>
-<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
-      layout:decorate="~{layout}">
-<body>
-    <section layout:fragment="content">
-        <section layout:fragment="breadcrum">
-            <nav role="navigation" class="breadcrumbs">
-                <ol class="breadcrumb">
-                    <li class="breadcrumb-item"><a th:href="@{/}">Home</a></li>
-                    <li class="breadcrumb-item">
-                        <a th:href="@{/}">My Courses</a>
-                    </li>
-                    <li class="breadcrumb-item active" th:text="${collection.getName()}">TI1316</li>
-                </ol>
-            </nav>
-        </section>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{container}">
+    <head>
+        <title th:text="${collection.name}"></title>
+    </head>
 
-        <div class="controls" th:if="${@permissionService.canManageSharedSessions(collection.id)}">
-            <div class="controls_buttons">
-                <button class="btn btn-primary"
-                        onclick="showDialog('create-shared-session-dialog')">Create shared
-                    session</button>
+    <div layout:fragment="breadcrumbs" class="breadcrumbs">
+        <a th:href="@{/}">My courses</a>
+        <span>&gt;</span>
+        <a th:text="${collection.name}" th:href="@{/shared-edition/{id}(id=${collection.id})}"></a>
+    </div>
+
+    <th:block layout:fragment="content">
+        <div
+            class="flex space_between align_items_center margin_bottom-3:from_small | flex_column:small align_items_stretch:small">
+            <h1 class="title" th:text="${collection.name}"></h1>
+            <div class="flex gap-3 | flex_column:small margin_bottom-5:small gap-5:small">
+                <!--            <button-->
+                <!--                    th:if="${@permissionService.canManageEdition(edition.id)}"-->
+                <!--                    class="button padding_block-3 padding_inline-5"-->
+                <!--                    data-style="floating"-->
+                <!--                    data-dialog="edit-edition">-->
+                <!--                <span class="margin_right-1 fa-solid fa-pencil-alt"></span>-->
+                <!--                <span>Edit course edition</span>-->
+                <!--            </button>-->
             </div>
-            <div th:replace="~{shared-edition/view/create-session-dialog :: overlay}"></div>
         </div>
 
-        <div class="tabs">
-            <button id="sessions-tab" class="active tab" onclick="switchTab('sessions')">Sessions
-            </button>
-            <button id="editions-tab" class="tab" onclick="switchTab('editions')">Editions</button>
-            <button id="staff-tab" class="tab" onclick="switchTab('staff')">Staff</button>
-        </div>
+        <!--    <div-->
+        <!--            th:if="${@permissionService.canManageModules(edition.id)} and ${edition.modules.isEmpty()}"-->
+        <!--            class="banner margin_bottom-3"-->
+        <!--            data-type="error">-->
+        <!--        <span class="fa-solid fa-exclamation-circle"></span>-->
+        <!--        <p>-->
+        <!--            This edition does not have any modules. Without modules, it is not possible to-->
+        <!--            create sessions.-->
+        <!--        </p>-->
+        <!--    </div>-->
+        <!--    <div-->
+        <!--            th:if="${@permissionService.canManageAssignments(edition.id)} and ${not edition.modules.isEmpty() and assignments.isEmpty()}"-->
+        <!--            class="banner margin_bottom-3"-->
+        <!--            data-type="error">-->
+        <!--        <span class="fa-solid fa-exclamation-circle"></span>-->
+        <!--        <p>-->
+        <!--            This edition does not have any assignments. Without assignments, it is not possible-->
+        <!--            to create sessions.-->
+        <!--        </p>-->
+        <!--    </div>-->
 
-        <div th:replace="~{shared-edition/view/session-list :: tab}"></div>
+        <nav aria-label="Edition tabs" class="tabs margin_bottom-5">
+            <a
+                th:data-active="${#request.requestURI.matches('.*/labs.*')}"
+                th:href="@{/shared-edition/{id}/labs(id=${collection.id})}">
+                <!--            TODO assignments empty check-->
+                <span class="fa-solid fa-calendar-alt"></span>
+                <span>Sessions</span>
+            </a>
+            <a
+                th:data-active="${#request.requestURI.matches('.*/participants.*')}"
+                th:href="@{/shared-edition/{id}/participants(id=${collection.id})}"
+                th:if="${@permissionService.canManageSharedParticipants(collection.id)}">
+                <span class="fa-solid fa-user"></span>
+                <span>Participants</span>
+            </a>
+            <a
+                th:data-active="${#request.requestURI.matches('.*/editions.*')}"
+                th:href="@{/shared-edition/{id}/editions(id=${collection.id})}">
+                <span class="fa-solid fa-sitemap"></span>
+                <span>Courses</span>
+            </a>
+        </nav>
 
-        <div id="editions" class="hidden">
-            <table class="table">
-                <thead>
-                    <tr>
-                        <th>Course edition</th>
-                        <th>Course code</th>
-                        <th>Teachers</th>
-                    </tr>
-                </thead>
-                <tbody>
-                    <tr th:each="edition : ${editions}">
-                        <td><a th:href="@{/edition/{id}(id=${edition.id})}"
-                               th:text="|${edition.course.name} - ${edition.name}|"></a></td>
-                        <td th:text="${edition.course.code}"></td>
-                        <td th:with="teacher = ${editionTeachers[edition.id].toString()}"
-                            th:text="${teacher.substring(1, teacher.length() - 1)}"></td>
-                    </tr>
-                </tbody>
-            </table>
-        </div>
-
-        <div id="staff" class="hidden">
-            <table class="table">
-                <thead>
-                <tr class="table_header">
-                    <th>Name</th>
-                    <th>Role</th>
-                </tr>
-                </thead>
-                <tbody>
-                <tr th:each="instance : ${roles}">
-                    <td th:text="${instance.key.displayName}"></td>
-                    <td>
-                        <ul>
-                            <li th:each="r : ${instance.getValue()}"
-                                th:text="|${r.getType().getValue()} (${@editionCacheManager.getOrThrow(r.id.editionId).course.name})|">
-                            </li>
-                        </ul>
-                    </td>
-                </tr>
-                </tbody>
-            </table>
-        </div>
+        <th:block layout:fragment="edition-content"></th:block>
+    </th:block>
 
-        <script>
-            let currentTab = "sessions"
-            function switchTab(to) {
-                if (to !== currentTab) {
-                    document.getElementById(to).classList.toggle("hidden");
-                    document.getElementById(currentTab).classList.toggle("hidden");
-                    document.getElementById(to + "-tab").classList.toggle("active");
-                    document.getElementById(currentTab + "-tab").classList.toggle("active");
-                }
-                currentTab = to;
-            }
-        </script>
-    </section>
-</body>
+    <th:block layout:fragment="overlays">
+        <!--    <th:block layout:replace="~{edition/edit :: dialog}"></th:block>-->
+        <th:block layout:fragment="extra_overlays"></th:block>
+    </th:block>
 </html>
diff --git a/src/main/resources/templates/shared-edition/view/editions.html b/src/main/resources/templates/shared-edition/view/editions.html
new file mode 100644
index 0000000000000000000000000000000000000000..494e709f61b22e8017495b25a4bca1ffe2ef0f4a
--- /dev/null
+++ b/src/main/resources/templates/shared-edition/view/editions.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{shared-edition/view}">
+    <th:block layout:fragment="edition-content">
+        <table class="rounded shadow table">
+            <thead>
+                <tr class="heading space_between">
+                    <th>Name</th>
+                    <th>Course Code</th>
+                    <th class="hidden:small">Teachers</th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr th:each="edition : ${editions}">
+                    <td>
+                        <a
+                            class="link"
+                            th:href="@{/edition/{id}(id=${edition.id})}"
+                            th:text="|${edition.course.value().name} - ${edition.name}|"></a>
+                    </td>
+                    <td th:text="${edition.course.value().code}"></td>
+                    <td
+                        class="hidden:small"
+                        th:text="${#strings.listJoin(teachers[edition.id].![person.value().displayName], ', ')}"></td>
+                </tr>
+            </tbody>
+        </table>
+    </th:block>
+
+    <th:block layout:fragment="extra_overlays"></th:block>
+
+    <script layout:fragment="script"></script>
+</html>
diff --git a/src/main/resources/templates/shared-edition/view/labs.html b/src/main/resources/templates/shared-edition/view/labs.html
new file mode 100644
index 0000000000000000000000000000000000000000..9ede58c1379c8e46f6de6ab784c8bf048b1e49ee
--- /dev/null
+++ b/src/main/resources/templates/shared-edition/view/labs.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{shared-edition/view}">
+    <th:block layout:fragment="edition-content">
+        <div
+            class="flex space_between align_items_center margin_bottom-5 | flex_column:small align_items_stretch:small">
+            <form id="filters" class="flex gap-3 | flex_column:small gap-5:small">
+                <input type="hidden" name="collection" th:value="${collection.id}" />
+                <select
+                    name="queueSessionTypes"
+                    aria-label="Session type"
+                    data-select
+                    multiple
+                    data-placeholder="Session type"
+                    data-style="floating">
+                    <option
+                        th:each="type : ${T(nl.tudelft.queue.model.enums.QueueSessionType).values()}"
+                        th:value="${type}"
+                        th:selected="${param.queueSessionTypes?.toString()?.contains(type.name())} ?: false"
+                        th:text="#{|lab.type.${type.name.toLowerCase()}|}"></option>
+                </select>
+                <select
+                    name="modules"
+                    aria-label="Module"
+                    data-select
+                    multiple
+                    data-placeholder="Module"
+                    data-style="floating">
+                    <option
+                        th:each="module : ${allModules}"
+                        th:value="${module.id}"
+                        th:text="${module.name}"
+                        th:selected="${param.modules?.contains(module.id.toString())} ?: false"></option>
+                </select>
+            </form>
+            <div
+                th:if="${@permissionService.canManageSharedSessions(collection.id)}"
+                class="flex gap-3">
+                <button
+                    class="button padding_block-3 padding_inline-5 | flex_grow:small"
+                    data-style="floating">
+                    <span class="margin_right-1 fa-solid fa-file-export"></span>
+                    <span>Export sessions</span>
+                </button>
+                <button
+                    class="button padding_block-3 padding_inline-5 | flex_grow:small"
+                    data-style="floating"
+                    data-dialog="create-session">
+                    <span class="margin_right-1 fa-solid fa-plus"></span>
+                    <span>Create session</span>
+                </button>
+            </div>
+        </div>
+
+        <th:block
+            th:with="canManage = ${@permissionService.canManageSharedSessions(collection.id)}">
+            <th:block
+                layout:replace="~{edition/view/labs_tables :: tables(shared=true)}"></th:block>
+        </th:block>
+    </th:block>
+
+    <th:block layout:fragment="extra_overlays">
+        <th:block layout:replace="~{lab/create :: dialog(shared=true)}"></th:block>
+        <th:block layout:replace="~{lab/remove :: dialog}"></th:block>
+    </th:block>
+
+    <th:block layout:fragment="script">
+        <th:block layout:replace="~{lab/create :: create_script}"></th:block>
+
+        <script>
+            $(() => {
+                // Filter on change of sessions
+                $("#filters select[name='queueSessionTypes']").change(function () {
+                    $("#filters").submit();
+                });
+                // Filter on change of modules
+                $("#filters select[name='modules']").change(function () {
+                    $("#filters").submit();
+                });
+                // Do not reload page when submitting the filters
+                $("#filters").submit(function (event) {
+                    event.preventDefault();
+                    const queueSessionTypes = $("select[name='queueSessionTypes']").val().join(",");
+                    const modules = $("select[name='modules']").val().join(",");
+                    window.history.replaceState(
+                        null,
+                        null,
+                        `?queueSessionTypes=${queueSessionTypes}&modules=${modules}`
+                    );
+                    $.ajax({
+                        url: `/shared-edition/${$("input[name='collection']").val()}/labs/list`,
+                        type: "get",
+                        data: {
+                            queueSessionTypes: queueSessionTypes,
+                            modules: modules,
+                        },
+                        success: html => {
+                            $("#labs").replaceWith($(html));
+                            $("#labs [data-dialog]").click(function () {
+                                openDialog($(this).data("dialog"));
+                            });
+                        },
+                    });
+                });
+            });
+        </script>
+    </th:block>
+</html>
diff --git a/src/main/resources/templates/shared-edition/view/participants.html b/src/main/resources/templates/shared-edition/view/participants.html
new file mode 100644
index 0000000000000000000000000000000000000000..b30df3994a849091825d4bbf05f1cf503607dc7c
--- /dev/null
+++ b/src/main/resources/templates/shared-edition/view/participants.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
+    layout:decorate="~{shared-edition/view}">
+    <th:block layout:fragment="edition-content">
+        <form id="filters" class="flex_column gap-3 align_items_start margin_bottom-5">
+            <input type="hidden" name="edition" th:value="${collection.id}" />
+            <div class="searchbox" data-style="floating">
+                <input
+                    aria-label="Participant search"
+                    type="text"
+                    name="search"
+                    placeholder="Search"
+                    th:value="${param.search}" />
+                <button class="fa-solid fa-search"></button>
+            </div>
+            <select
+                class="hidden"
+                aria-label="Role"
+                name="roles"
+                data-select
+                multiple
+                data-placeholder="Role"
+                data-style="floating">
+                <option
+                    th:selected="${param.roles?.toString()?.contains('TEACHER')} ?: false"
+                    value="TEACHER">
+                    Teacher
+                </option>
+                <option
+                    th:selected="${param.roles?.toString()?.contains('HEAD_TA')} ?: false"
+                    value="HEAD_TA">
+                    Head TA
+                </option>
+                <option
+                    th:selected="${param.roles?.toString()?.contains('TA')} ?: false"
+                    value="TA">
+                    TA
+                </option>
+                <option
+                    th:selected="${param.roles?.toString()?.contains('STUDENT')} ?: false"
+                    value="STUDENT">
+                    Student
+                </option>
+            </select>
+        </form>
+
+        <th:block layout:replace="~{shared-edition/view/participants_table :: table}"></th:block>
+    </th:block>
+
+    <th:block layout:fragment="extra_overlays"></th:block>
+
+    <script layout:fragment="script">
+        $(() => {
+            // Filter on change of roles
+            $("select[name='roles']").change(function () {
+                $("#filters").submit();
+            });
+            // Filter when user stopped typing
+            let timeout = null;
+            $("input[name='search']").keydown(function () {
+                if (timeout) clearTimeout(timeout);
+                timeout = setTimeout(() => {
+                    $("#filters").submit();
+                }, 250);
+            });
+            // Do not reload page when submitting the filters
+            $("#filters").submit(function (event) {
+                event.preventDefault();
+                const size = $(".pagination").data("page-size");
+                const search = $("input[name='search']").val();
+                const roles = $("select[name='roles']").val().join(",");
+                window.history.replaceState(
+                    null,
+                    null,
+                    `?search=${search}&roles=${roles}&page=0&size=${size}`
+                );
+                $.ajax({
+                    url: `/shared-edition/${$("input[name='edition']").val()}/participants/list`,
+                    type: "get",
+                    data: {
+                        search: search,
+                        roles: roles,
+                        page: 0,
+                        size: size,
+                    },
+                    success: html => {
+                        $("#participants").replaceWith($(html));
+                        $("#participants")
+                            .find("[data-dialog]")
+                            .click(function () {
+                                openDialog($(this).data("dialog"), false);
+                            });
+                    },
+                });
+            });
+        });
+    </script>
+</html>
diff --git a/src/main/resources/templates/shared-edition/view/participants_table.html b/src/main/resources/templates/shared-edition/view/participants_table.html
new file mode 100644
index 0000000000000000000000000000000000000000..a8fb891c17716b27e23649eeda2e1bb42fb2a75c
--- /dev/null
+++ b/src/main/resources/templates/shared-edition/view/participants_table.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html
+    lang="en"
+    xmlns:th="http://www.thymeleaf.org"
+    xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
+    <div id="participants" class="flex_column gap-5" layout:fragment="table">
+        <table class="rounded shadow table table__to_list">
+            <thead>
+                <tr class="heading flex:small space_between">
+                    <th class="hidden:from_small flex space_between align_items_center">
+                        Participants
+                    </th>
+                    <th class="hidden:small">Name</th>
+                    <th class="hidden:small">Roles</th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr th:if="${roles.isEmpty()}">
+                    <td>No participants found</td>
+                    <td></td>
+                    <td></td>
+                </tr>
+                <tr
+                    th:each="entry : ${roles}"
+                    th:with="person = ${entry.key}, roles = ${entry.value}"
+                    class="flex_column:small gap-0">
+                    <td
+                        class="flex:small align_items_center padding_bottom-4:small"
+                        style="--gap: 6px">
+                        <span th:text="${person.displayName}"></span>
+                        <span
+                            class="chip hidden:from_small"
+                            th:each="role : ${roles}"
+                            th:text="#{|role.${role.type.name().toLowerCase()}|}"></span>
+                    </td>
+                    <td class="hidden:small">
+                        <ul>
+                            <li th:each="role : ${roles}">
+                                <span th:text="#{|role.${role.type.name().toLowerCase()}|}"></span>
+                                <span
+                                    th:text="|(${@editionFinder.byId(role.id.editionId).course.value().name})|"></span>
+                            </li>
+                        </ul>
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+
+        <th:block layout:replace="~{pagination :: pagination(page=${roles}, size=3)}"></th:block>
+    </div>
+</html>
diff --git a/src/test/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTOTest.java b/src/test/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTOTest.java
index 55fbbcc6283c52aad6e60d55b2eb8c9cbb3d2b88..a618440a8cf183709dc4f8b44d77c5854753754d 100644
--- a/src/test/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTOTest.java
+++ b/src/test/java/nl/tudelft/queue/dto/create/QueueAssignmentCreateDTOTest.java
@@ -29,7 +29,7 @@ public class QueueAssignmentCreateDTOTest extends CreateTest<AssignmentCreateDTO
 	protected QueueAssignmentCreateDTO filledDTO() {
 		return QueueAssignmentCreateDTO.builder()
 				.name("Oei")
-				.moduleId(999L)
+				.module(999L)
 				.description("This is a simple yet complicated description")
 				.deadline(LocalDateTime.of(2020, 9, 9, 20, 9, 19))
 				.build();
@@ -39,7 +39,7 @@ public class QueueAssignmentCreateDTOTest extends CreateTest<AssignmentCreateDTO
 	public List<Arguments> getters() {
 		return List.of(
 				of(QueueAssignmentCreateDTO::getName, AssignmentCreateDTO::getName),
-				of(QueueAssignmentCreateDTO::getModuleId, acd -> acd.getModule().getId()),
+				of(QueueAssignmentCreateDTO::getModule, acd -> acd.getModule().getId()),
 				of(QueueAssignmentCreateDTO::getDeadline, AssignmentCreateDTO::getDeadline),
 				of(QueueAssignmentCreateDTO::getDescription, acd -> acd.getDescription().getText()));
 	}
diff --git a/src/test/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTOTest.java b/src/test/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTOTest.java
index 2f54aebb5bc6f3647b68845e257498b8306d2a7c..418eab571a9f93d9e5771232067626a266654fd5 100644
--- a/src/test/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTOTest.java
+++ b/src/test/java/nl/tudelft/queue/dto/create/QueueModuleCreateDTOTest.java
@@ -28,7 +28,7 @@ public class QueueModuleCreateDTOTest extends CreateTest<ModuleCreateDTO, QueueM
 	protected QueueModuleCreateDTO filledDTO() {
 		return QueueModuleCreateDTO.builder()
 				.name("This is a name for a module")
-				.editionId(78434L)
+				.edition(78434L)
 				.build();
 	}
 
@@ -36,6 +36,6 @@ public class QueueModuleCreateDTOTest extends CreateTest<ModuleCreateDTO, QueueM
 	public List<Arguments> getters() {
 		return List.of(
 				of(QueueModuleCreateDTO::getName, ModuleCreateDTO::getName),
-				of(QueueModuleCreateDTO::getEditionId, mcd -> mcd.getEdition().getId()));
+				of(QueueModuleCreateDTO::getEdition, mcd -> mcd.getEdition().getId()));
 	}
 }
diff --git a/src/test/java/nl/tudelft/queue/service/CapacitySessionServiceTest.java b/src/test/java/nl/tudelft/queue/service/CapacitySessionServiceTest.java
index 9d31e6434eb89c198bd8d21395525e9bacb11af6..56dd709a44d22fcea77a850c6dc90bbba80d4312 100644
--- a/src/test/java/nl/tudelft/queue/service/CapacitySessionServiceTest.java
+++ b/src/test/java/nl/tudelft/queue/service/CapacitySessionServiceTest.java
@@ -31,7 +31,7 @@ import java.util.stream.Collectors;
 import javax.transaction.Transactional;
 
 import nl.tudelft.labracore.api.dto.*;
-import nl.tudelft.queue.cache.RoomCacheManager;
+import nl.tudelft.labracore.lib.cache.RoomCacheManager;
 import nl.tudelft.queue.model.QSelectionRequest;
 import nl.tudelft.queue.model.Request;
 import nl.tudelft.queue.model.SelectionRequest;
diff --git a/src/test/java/nl/tudelft/queue/service/EditionServiceTest.java b/src/test/java/nl/tudelft/queue/service/EditionServiceTest.java
index 615dac96a298ff92aedcad0ac8977eafbe3dd4af..317f413e359f4de4c629197b4e1aa21529a0a1fa 100644
--- a/src/test/java/nl/tudelft/queue/service/EditionServiceTest.java
+++ b/src/test/java/nl/tudelft/queue/service/EditionServiceTest.java
@@ -150,19 +150,19 @@ public class EditionServiceTest {
 
 	@Test
 	void studentsMatchingFilterChecksDisplayName() {
-		assertThat(es.studentsMatchingFilter(people, "en"))
+		assertThat(es.rolesMatchingFilter(people, "en"))
 				.containsExactly(person2, person4);
 	}
 
 	@Test
 	void studentsMatchingFilterChecksStudentNumber() {
-		assertThat(es.studentsMatchingFilter(people, "5"))
+		assertThat(es.rolesMatchingFilter(people, "5"))
 				.containsExactly(person1, person3, person5);
 	}
 
 	@Test
 	void studentsMatchingFilterChecksUsername() {
-		assertThat(es.studentsMatchingFilter(people, "p"))
+		assertThat(es.rolesMatchingFilter(people, "p"))
 				.containsExactly(person1, person3);
 	}
 
diff --git a/src/test/java/nl/tudelft/queue/service/JitsiServiceTest.java b/src/test/java/nl/tudelft/queue/service/JitsiServiceTest.java
index 0a60552b34cd518d3c03c4b26ba23482b94096d8..77b876d19ccff679dcd869795f2a29f2736b5f73 100644
--- a/src/test/java/nl/tudelft/queue/service/JitsiServiceTest.java
+++ b/src/test/java/nl/tudelft/queue/service/JitsiServiceTest.java
@@ -23,7 +23,7 @@ import static org.mockito.Mockito.when;
 import java.time.LocalDateTime;
 
 import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
-import nl.tudelft.queue.cache.PersonCacheManager;
+import nl.tudelft.labracore.lib.cache.PersonCacheManager;
 import nl.tudelft.queue.model.LabRequest;
 import nl.tudelft.queue.properties.JitsiProperties;
 
diff --git a/src/test/java/nl/tudelft/queue/service/LabServiceTest.java b/src/test/java/nl/tudelft/queue/service/LabServiceTest.java
index aca75ef83a729c7b2ceaf0f80512f8f244fcedb1..8419d3a7f61acd5f32a9f3c03b50b3df9f6417df 100644
--- a/src/test/java/nl/tudelft/queue/service/LabServiceTest.java
+++ b/src/test/java/nl/tudelft/queue/service/LabServiceTest.java
@@ -34,7 +34,7 @@ import javax.transaction.Transactional;
 
 import nl.tudelft.labracore.api.SessionControllerApi;
 import nl.tudelft.labracore.api.dto.*;
-import nl.tudelft.queue.cache.SessionCacheManager;
+import nl.tudelft.labracore.lib.cache.SessionCacheManager;
 import nl.tudelft.queue.dto.create.constraints.ClusterConstraintCreateDTO;
 import nl.tudelft.queue.dto.create.embeddables.CapacitySessionConfigCreateDTO;
 import nl.tudelft.queue.dto.create.embeddables.LabRequestConstraintsCreateDTO;
diff --git a/src/test/java/nl/tudelft/queue/service/MailServiceTest.java b/src/test/java/nl/tudelft/queue/service/MailServiceTest.java
index b7940f39e1c8e4c99ed5cbbf088829daa2e0cac3..407bff4e93ac2d72c28cc69d9129d0553ef96d2b 100644
--- a/src/test/java/nl/tudelft/queue/service/MailServiceTest.java
+++ b/src/test/java/nl/tudelft/queue/service/MailServiceTest.java
@@ -22,8 +22,8 @@ import static org.mockito.Mockito.verify;
 
 import javax.transaction.Transactional;
 
-import nl.tudelft.labracore.lib.security.user.DefaultRole;
-import nl.tudelft.labracore.lib.security.user.Person;
+import nl.tudelft.labracore.lib.api.DefaultRole;
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.queue.dto.create.CourseRequestCreateDTO;
 import nl.tudelft.queue.properties.MailProperties;
 import nl.tudelft.queue.properties.QueueProperties;
diff --git a/src/test/java/nl/tudelft/queue/service/RequestServiceTest.java b/src/test/java/nl/tudelft/queue/service/RequestServiceTest.java
index 50d0e05c8b7bc223180b33ac1c5caf6ae87681f3..c1246b29e0bf7a9455b208ab26bd9a451d9109cd 100644
--- a/src/test/java/nl/tudelft/queue/service/RequestServiceTest.java
+++ b/src/test/java/nl/tudelft/queue/service/RequestServiceTest.java
@@ -31,7 +31,7 @@ import javax.transaction.Transactional;
 
 import nl.tudelft.labracore.api.StudentGroupControllerApi;
 import nl.tudelft.labracore.api.dto.*;
-import nl.tudelft.queue.cache.SessionCacheManager;
+import nl.tudelft.labracore.lib.cache.SessionCacheManager;
 import nl.tudelft.queue.dto.create.requests.SelectionRequestCreateDTO;
 import nl.tudelft.queue.model.QSelectionRequest;
 import nl.tudelft.queue.model.SelectionRequest;
diff --git a/src/test/java/test/BaseMockConfig.java b/src/test/java/test/BaseMockConfig.java
index e610300341c087d8c653db0817eaf32125ee8719..95e87a64bc92a910a869a894ed5adc15b2866e58 100644
--- a/src/test/java/test/BaseMockConfig.java
+++ b/src/test/java/test/BaseMockConfig.java
@@ -18,7 +18,7 @@
 package test;
 
 import nl.tudelft.labracore.api.*;
-import nl.tudelft.queue.cache.CourseCacheManager;
+import nl.tudelft.labracore.lib.cache.CourseCacheManager;
 
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.context.annotation.Configuration;
diff --git a/src/test/java/test/TestUserDetailsService.java b/src/test/java/test/TestUserDetailsService.java
index 862d61e742a37b1d7e097de6b659cbf5fb5f4fc6..c6dcac5a13b32ac23c907c45ee9994b7020429c9 100644
--- a/src/test/java/test/TestUserDetailsService.java
+++ b/src/test/java/test/TestUserDetailsService.java
@@ -17,9 +17,9 @@
  */
 package test;
 
+import nl.tudelft.labracore.lib.api.DefaultRole;
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.labracore.lib.security.LabradorUserDetails;
-import nl.tudelft.labracore.lib.security.user.DefaultRole;
-import nl.tudelft.labracore.lib.security.user.Person;
 
 import org.springframework.context.annotation.Primary;
 import org.springframework.security.core.userdetails.UserDetails;
diff --git a/src/test/java/test/test/TestUserDetailsService.java b/src/test/java/test/test/TestUserDetailsService.java
index 047ef459ae7045d14195b671e65e0de19e35b76a..d0ea52311179ed435a1d977c0ba56cf524c02a7e 100644
--- a/src/test/java/test/test/TestUserDetailsService.java
+++ b/src/test/java/test/test/TestUserDetailsService.java
@@ -18,9 +18,9 @@
 package test.test;
 
 import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
+import nl.tudelft.labracore.lib.api.DefaultRole;
+import nl.tudelft.labracore.lib.data.Person;
 import nl.tudelft.labracore.lib.security.LabradorUserDetails;
-import nl.tudelft.labracore.lib.security.user.DefaultRole;
-import nl.tudelft.labracore.lib.security.user.Person;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.DependsOn;