Add fullscreen toggle and return-to-page logout redirect (#200)

* feat: add fullscreen toggle and return-to-page logout redirect

Add a Stimulus-driven fullscreen button/keypress to the quiz and
elimination screens, and redirect admins back to the page they were on
after logging out (unless it was a backoffice or elimination page).

* docs: expand CLAUDE.md domain context and testing conventions

Document the WIDM quiz/elimination domain model in more depth, and add
guidance to prefer plain TestCase over WebTestCase/KernelTestCase and to
apply the Boy Scout Rule for small nearby cleanups.

* fix: drop f-keypress fullscreen shortcut

Candidate names can start with "f" while typing, so the keypress
conflicted with the enter-name form. Button-only toggle remains.
This commit is contained in:
2026-07-09 23:30:06 +02:00
committed by GitHub
parent 0ee15e3cbb
commit 5e7028c972
8 changed files with 329 additions and 30 deletions
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Tvdt\Security;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
final readonly class LogoutRedirectListener implements EventSubscriberInterface
{
private const array BLOCKED_TARGET_PREFIXES = ['/backoffice', '/elimination'];
public function __construct(private UrlGeneratorInterface $urlGenerator) {}
public function onLogout(LogoutEvent $event): void
{
$target = $event->getRequest()->query->get('target');
if (\is_string($target) && $this->isAllowedTarget($target)) {
$event->setResponse(new RedirectResponse($target));
return;
}
$event->setResponse(new RedirectResponse($this->urlGenerator->generate('tvdt_quiz_select_season')));
}
public static function getSubscribedEvents(): array
{
// Must run before Symfony's DefaultLogoutListener (priority 64), which only
// sets a response if none is set yet.
return [
LogoutEvent::class => ['onLogout', 128],
];
}
private function isAllowedTarget(string $target): bool
{
if (!str_starts_with($target, '/') || str_starts_with($target, '//')) {
return false;
}
return !array_any(self::BLOCKED_TARGET_PREFIXES, static fn (string $prefix): bool => str_starts_with($target, $prefix));
}
}