diff --git a/.idea/TijdVoorDeTest.iml b/.idea/TijdVoorDeTest.iml
index 2922a68..325953a 100644
--- a/.idea/TijdVoorDeTest.iml
+++ b/.idea/TijdVoorDeTest.iml
@@ -164,6 +164,8 @@
+
+
diff --git a/.idea/php.xml b/.idea/php.xml
index 84b5fd3..7e4821b 100644
--- a/.idea/php.xml
+++ b/.idea/php.xml
@@ -199,6 +199,8 @@
+
+
diff --git a/assets/backoffice.js b/assets/backoffice.js
index 530abfb..dcb5730 100644
--- a/assets/backoffice.js
+++ b/assets/backoffice.js
@@ -1,3 +1,4 @@
+import './bootstrap.js';
import 'bootstrap/dist/css/bootstrap.min.css'
import * as bootstrap from 'bootstrap'
diff --git a/assets/bootstrap.js b/assets/bootstrap.js
new file mode 100644
index 0000000..d4e50c9
--- /dev/null
+++ b/assets/bootstrap.js
@@ -0,0 +1,5 @@
+import { startStimulusApp } from '@symfony/stimulus-bundle';
+
+const app = startStimulusApp();
+// register any custom, 3rd party controllers here
+// app.register('some_controller_name', SomeImportedController);
diff --git a/assets/controllers.json b/assets/controllers.json
new file mode 100644
index 0000000..29ea244
--- /dev/null
+++ b/assets/controllers.json
@@ -0,0 +1,15 @@
+{
+ "controllers": {
+ "@symfony/ux-turbo": {
+ "turbo-core": {
+ "enabled": true,
+ "fetch": "eager"
+ },
+ "mercure-turbo-stream": {
+ "enabled": false,
+ "fetch": "eager"
+ }
+ }
+ },
+ "entrypoints": []
+}
diff --git a/assets/controllers/csrf_protection_controller.js b/assets/controllers/csrf_protection_controller.js
new file mode 100644
index 0000000..2811f21
--- /dev/null
+++ b/assets/controllers/csrf_protection_controller.js
@@ -0,0 +1,79 @@
+const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
+const tokenCheck = /^[-_\/+a-zA-Z0-9]{24,}$/;
+
+// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
+document.addEventListener('submit', function (event) {
+ generateCsrfToken(event.target);
+}, true);
+
+// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie
+// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked
+document.addEventListener('turbo:submit-start', function (event) {
+ const h = generateCsrfHeaders(event.detail.formSubmission.formElement);
+ Object.keys(h).map(function (k) {
+ event.detail.formSubmission.fetchRequest.headers[k] = h[k];
+ });
+});
+
+// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted
+document.addEventListener('turbo:submit-end', function (event) {
+ removeCsrfToken(event.detail.formSubmission.formElement);
+});
+
+export function generateCsrfToken (formElement) {
+ const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
+
+ if (!csrfField) {
+ return;
+ }
+
+ let csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
+ let csrfToken = csrfField.value;
+
+ if (!csrfCookie && nameCheck.test(csrfToken)) {
+ csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken);
+ csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
+ csrfField.dispatchEvent(new Event('change', { bubbles: true }));
+ }
+
+ if (csrfCookie && tokenCheck.test(csrfToken)) {
+ const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';
+ document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
+ }
+}
+
+export function generateCsrfHeaders (formElement) {
+ const headers = {};
+ const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
+
+ if (!csrfField) {
+ return headers;
+ }
+
+ const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
+
+ if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
+ headers[csrfCookie] = csrfField.value;
+ }
+
+ return headers;
+}
+
+export function removeCsrfToken (formElement) {
+ const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
+
+ if (!csrfField) {
+ return;
+ }
+
+ const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
+
+ if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
+ const cookie = csrfCookie + '_' + csrfField.value + '=0; path=/; samesite=strict; max-age=0';
+
+ document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
+ }
+}
+
+/* stimulusFetch: 'lazy' */
+export default 'csrf-protection-controller';
diff --git a/assets/quiz.js b/assets/quiz.js
index 488df76..f7c2c54 100644
--- a/assets/quiz.js
+++ b/assets/quiz.js
@@ -1,3 +1,4 @@
+import './bootstrap.js';
import 'bootstrap/dist/css/bootstrap.min.css'
import * as bootstrap from 'bootstrap'
diff --git a/composer.json b/composer.json
index f3eb576..8551e0f 100644
--- a/composer.json
+++ b/composer.json
@@ -35,6 +35,7 @@
"symfony/serializer": "7.2.*",
"symfony/twig-bundle": "7.2.*",
"symfony/uid": "7.2.*",
+ "symfony/ux-turbo": "^2.26",
"symfony/yaml": "7.2.*",
"symfonycasts/sass-bundle": "^0.8.2",
"symfonycasts/verify-email-bundle": "^1.17.3",
diff --git a/composer.lock b/composer.lock
index 9176628..1fe063b 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "a42d81785aafd03b84dac3f6d813d5c2",
+ "content-hash": "9a4a40083e24d7e3f49ad01ace830440",
"packages": [
{
"name": "composer/pcre",
@@ -6614,6 +6614,75 @@
],
"time": "2024-09-25T14:20:29+00:00"
},
+ {
+ "name": "symfony/stimulus-bundle",
+ "version": "v2.26.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/stimulus-bundle.git",
+ "reference": "750c770f66b45b40135eb32d753dbd7d84182c92"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/750c770f66b45b40135eb32d753dbd7d84182c92",
+ "reference": "750c770f66b45b40135eb32d753dbd7d84182c92",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/config": "^5.4|^6.0|^7.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/deprecation-contracts": "^2.0|^3.0",
+ "symfony/finder": "^5.4|^6.0|^7.0",
+ "symfony/http-kernel": "^5.4|^6.0|^7.0",
+ "twig/twig": "^2.15.3|^3.8"
+ },
+ "require-dev": {
+ "symfony/asset-mapper": "^6.3|^7.0",
+ "symfony/framework-bundle": "^5.4|^6.0|^7.0",
+ "symfony/phpunit-bridge": "^5.4|^6.0|^7.0",
+ "symfony/twig-bundle": "^5.4|^6.0|^7.0",
+ "zenstruck/browser": "^1.4"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\UX\\StimulusBundle\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Integration with your Symfony app & Stimulus!",
+ "keywords": [
+ "symfony-ux"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/stimulus-bundle/tree/v2.26.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-05-23T09:27:30+00:00"
+ },
{
"name": "symfony/stopwatch",
"version": "v7.2.4",
@@ -7279,6 +7348,105 @@
],
"time": "2024-09-25T14:21:43+00:00"
},
+ {
+ "name": "symfony/ux-turbo",
+ "version": "v2.26.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/ux-turbo.git",
+ "reference": "12d9989fb945f2074b41aad4915fc593ecd4ed00"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/ux-turbo/zipball/12d9989fb945f2074b41aad4915fc593ecd4ed00",
+ "reference": "12d9989fb945f2074b41aad4915fc593ecd4ed00",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/stimulus-bundle": "^2.9.1"
+ },
+ "conflict": {
+ "symfony/flex": "<1.13"
+ },
+ "require-dev": {
+ "dbrekelmans/bdi": "dev-main",
+ "doctrine/doctrine-bundle": "^2.4.3",
+ "doctrine/orm": "^2.8 | 3.0",
+ "php-webdriver/webdriver": "^1.15",
+ "phpstan/phpstan": "^2.1.17",
+ "symfony/asset-mapper": "^6.4|^7.0",
+ "symfony/debug-bundle": "^5.4|^6.0|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/form": "^5.4|^6.0|^7.0",
+ "symfony/framework-bundle": "^6.4|^7.0",
+ "symfony/mercure-bundle": "^0.3.7",
+ "symfony/messenger": "^5.4|^6.0|^7.0",
+ "symfony/panther": "^2.2",
+ "symfony/phpunit-bridge": "^5.4|^6.0|^7.0",
+ "symfony/process": "^5.4|6.3.*|^7.0",
+ "symfony/property-access": "^5.4|^6.0|^7.0",
+ "symfony/security-core": "^5.4|^6.0|^7.0",
+ "symfony/stopwatch": "^5.4|^6.0|^7.0",
+ "symfony/twig-bundle": "^6.4|^7.0",
+ "symfony/ux-twig-component": "^2.21",
+ "symfony/web-profiler-bundle": "^5.4|^6.0|^7.0"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/ux",
+ "name": "symfony/ux"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\UX\\Turbo\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "kevin@dunglas.fr"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Hotwire Turbo integration for Symfony",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "hotwire",
+ "javascript",
+ "mercure",
+ "symfony-ux",
+ "turbo",
+ "turbo-stream"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/ux-turbo/tree/v2.26.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-05-26T20:34:50+00:00"
+ },
{
"name": "symfony/ux-twig-component",
"version": "v2.25.2",
diff --git a/config/bundles.php b/config/bundles.php
index 9fcc9db..c5e0675 100644
--- a/config/bundles.php
+++ b/config/bundles.php
@@ -12,6 +12,8 @@ use Symfony\Bundle\MakerBundle\MakerBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle;
+use Symfony\UX\StimulusBundle\StimulusBundle;
+use Symfony\UX\Turbo\TurboBundle;
use Symfony\UX\TwigComponent\TwigComponentBundle;
use SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle;
use Symfonycasts\SassBundle\SymfonycastsSassBundle;
@@ -32,4 +34,6 @@ return [
SymfonyCastsVerifyEmailBundle::class => ['all' => true],
SentryBundle::class => ['prod' => true],
SymfonycastsSassBundle::class => ['all' => true],
+ StimulusBundle::class => ['all' => true],
+ TurboBundle::class => ['all' => true],
];
diff --git a/importmap.php b/importmap.php
index 1f71f6d..56f5dc6 100644
--- a/importmap.php
+++ b/importmap.php
@@ -32,4 +32,13 @@ return [
'version' => '5.3.6',
'type' => 'css',
],
+ '@hotwired/stimulus' => [
+ 'version' => '3.2.2',
+ ],
+ '@symfony/stimulus-bundle' => [
+ 'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
+ ],
+ '@hotwired/turbo' => [
+ 'version' => '7.3.0',
+ ],
];
diff --git a/src/Controller/QuizController.php b/src/Controller/QuizController.php
index b51aedc..12dd24f 100644
--- a/src/Controller/QuizController.php
+++ b/src/Controller/QuizController.php
@@ -110,6 +110,8 @@ final class QuizController extends AbstractController
->setAnswer($answer)
->setQuiz($answer->getQuestion()->getQuiz());
$givenAnswerRepository->save($givenAnswer);
+
+ return $this->redirectToRoute('app_quiz_quizpage', ['seasonCode' => $season->getSeasonCode(), 'nameHash' => $nameHash]);
}
$question = $questionRepository->findNextQuestionForCandidate($candidate);
diff --git a/symfony.lock b/symfony.lock
index 6cb1ce2..9c3b93e 100644
--- a/symfony.lock
+++ b/symfony.lock
@@ -1,4 +1,13 @@
{
+ "doctrine/deprecations": {
+ "version": "1.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.0",
+ "ref": "87424683adc81d7dc305eefec1fced883084aab9"
+ }
+ },
"doctrine/doctrine-bundle": {
"version": "2.13",
"recipe": {
@@ -230,6 +239,21 @@
"config/routes/security.yaml"
]
},
+ "symfony/stimulus-bundle": {
+ "version": "2.26",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "2.20",
+ "ref": "3acc494b566816514a6873a89023a35440b6386d"
+ },
+ "files": [
+ "assets/bootstrap.js",
+ "assets/controllers.json",
+ "assets/controllers/csrf_protection_controller.js",
+ "assets/controllers/hello_controller.js"
+ ]
+ },
"symfony/translation": {
"version": "7.2",
"recipe": {
@@ -265,6 +289,15 @@
"ref": "0df5844274d871b37fc3816c57a768ffc60a43a5"
}
},
+ "symfony/ux-turbo": {
+ "version": "2.26",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "2.20",
+ "ref": "c85ff94da66841d7ff087c19cbcd97a2df744ef9"
+ }
+ },
"symfony/ux-twig-component": {
"version": "2.22",
"recipe": {