diff --git a/Justfile b/Justfile index 3da51c2..ac5a1fb 100644 --- a/Justfile +++ b/Justfile @@ -23,7 +23,7 @@ fixtures: docker compose exec php bin/console doctrine:fixtures:load --purge-with-truncate --no-interaction translations: - docker compose exec php bin/console translation:extract --domain=messages --force --format=yaml --sort=asc --clean nl + docker compose exec php bin/console translation:extract --force --format=xliff --sort=asc --clean nl fix-cs: docker compose exec php vendor/bin/php-cs-fixer fix diff --git a/assets/backoffice.js b/assets/backoffice.js index ac508f3..530abfb 100644 --- a/assets/backoffice.js +++ b/assets/backoffice.js @@ -1,4 +1,4 @@ import 'bootstrap/dist/css/bootstrap.min.css' import * as bootstrap from 'bootstrap' -import './styles/app.scss'; +import './styles/backoffice.scss'; diff --git a/assets/img/green.png b/assets/img/green.png new file mode 100644 index 0000000..df0f408 Binary files /dev/null and b/assets/img/green.png differ diff --git a/assets/img/red.png b/assets/img/red.png new file mode 100644 index 0000000..4cac2f7 Binary files /dev/null and b/assets/img/red.png differ diff --git a/assets/quiz.js b/assets/quiz.js index 3ed5286..2642d80 100644 --- a/assets/quiz.js +++ b/assets/quiz.js @@ -2,3 +2,23 @@ import 'bootstrap/dist/css/bootstrap.min.css' import * as bootstrap from 'bootstrap' import './styles/quiz.scss' + +document.addEventListener('DOMContentLoaded', function() { + // Check if we're on the elimination candidate screen + const eliminationScreen = document.querySelector('.elimination-screen'); + if (eliminationScreen) { + // Add event listener for any keypress + document.addEventListener('keydown', function(event) { + // Get the current URL + const currentUrl = window.location.href; + // Extract the elimination ID from the URL + const urlParts = currentUrl.split('/'); + // Remove the candidate hash (last part of the URL) + urlParts.pop(); + // Construct the URL to the main elimination page + const redirectUrl = urlParts.join('/'); + // Redirect to the main elimination page + window.location.href = redirectUrl; + }); + } +}); diff --git a/assets/styles/app.scss b/assets/styles/backoffice.scss similarity index 100% rename from assets/styles/app.scss rename to assets/styles/backoffice.scss diff --git a/assets/styles/quiz.scss b/assets/styles/quiz.scss index 61c451c..4fb76ee 100644 --- a/assets/styles/quiz.scss +++ b/assets/styles/quiz.scss @@ -10,3 +10,17 @@ html, body { align-items: center; justify-self: center; } + +.elimination-screen { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100vh; + object-fit: contain; + background-color: white; + z-index: 1000; + display: flex; + justify-content: center; + align-items: center; +} diff --git a/compose.override.yaml b/compose.override.yaml index fe5b7c5..65cb06d 100644 --- a/compose.override.yaml +++ b/compose.override.yaml @@ -9,6 +9,7 @@ services: - ./frankenphp/Caddyfile:/etc/caddy/Caddyfile:ro - ./frankenphp/conf.d/20-app.dev.ini:/usr/local/etc/php/app.conf.d/20-app.dev.ini:ro - ./frankenphp/data:/data + - sass:/app/var/sass environment: MERCURE_EXTRA_DIRECTIVES: demo # See https://xdebug.org/docs/all_settings#mode @@ -34,7 +35,8 @@ services: sass: image: ${IMAGES_PREFIX:-}app-php volumes: - - ./:/app + - ./:/app:ro + - sass:/app/var/sass entrypoint: '' depends_on: - php @@ -42,6 +44,7 @@ services: - bin/console - sass:build - --watch + - -v ###> symfony/mercure-bundle ### ###< symfony/mercure-bundle ### @@ -62,3 +65,6 @@ services: MP_SMTP_AUTH_ACCEPT_ANY: 1 MP_SMTP_AUTH_ALLOW_INSECURE: 1 ###< symfony/mailer ### + +volumes: + sass: diff --git a/src/Command/ClaimSeasonCommand.php b/src/Command/ClaimSeasonCommand.php index 60ec3a7..c659f3c 100644 --- a/src/Command/ClaimSeasonCommand.php +++ b/src/Command/ClaimSeasonCommand.php @@ -31,7 +31,7 @@ class ClaimSeasonCommand extends Command protected function configure(): void { $this - ->addArgument('email', InputArgument::REQUIRED, 'The email of the user to make admin') + ->addArgument('email', InputArgument::REQUIRED, 'The email of the user thats claims the season') ->addArgument('season', InputArgument::REQUIRED, 'The season to claim') ; } diff --git a/src/Controller/PrepareEliminationController.php b/src/Controller/Backoffice/PrepareEliminationController.php similarity index 86% rename from src/Controller/PrepareEliminationController.php rename to src/Controller/Backoffice/PrepareEliminationController.php index 5e4875c..96ee405 100644 --- a/src/Controller/PrepareEliminationController.php +++ b/src/Controller/Backoffice/PrepareEliminationController.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Controller; +namespace App\Controller\Backoffice; use App\Entity\Elimination; use App\Entity\Quiz; @@ -31,6 +31,9 @@ final class PrepareEliminationController extends AbstractController $elimination->updateFromInputBag($request->request); $em->flush(); + if (true === $request->request->getBoolean('start')) { + return $this->redirectToRoute('app_elimination', ['elimination' => $elimination->getId()]); + } $this->addFlash('success', 'Elimination updated'); } diff --git a/src/Controller/EliminationController.php b/src/Controller/EliminationController.php index 77559d0..c5587cf 100644 --- a/src/Controller/EliminationController.php +++ b/src/Controller/EliminationController.php @@ -5,12 +5,14 @@ declare(strict_types=1); namespace App\Controller; use App\Entity\Candidate; -use App\Entity\Season; +use App\Entity\Elimination; use App\Enum\FlashType; +use App\Form\EliminationEnterNameType; use App\Helpers\Base64; use App\Repository\CandidateRepository; use App\Security\Voter\SeasonVoter; use Symfony\Bridge\Doctrine\Attribute\MapEntity; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\Routing\Attribute\Route; @@ -25,30 +27,50 @@ final class EliminationController extends AbstractController { public function __construct(private readonly TranslatorInterface $translator) {} - #[Route('/elimination/{seasonCode}', name: 'app_elimination')] - #[IsGranted(SeasonVoter::ELIMINATION, 'season')] - public function index(#[MapEntity] Season $season): Response + #[Route('/elimination/{elimination}', name: 'app_elimination')] + #[IsGranted(SeasonVoter::ELIMINATION, 'elimination')] + public function index(#[MapEntity] Elimination $elimination, Request $request): Response { - return $this->render('elimination/index.html.twig', [ + $form = $this->createForm(EliminationEnterNameType::class); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $name = $form->get('name')->getData(); + + return $this->redirectToRoute('app_elimination_candidate', ['elimination' => $elimination->getId(), 'candidateHash' => Base64::base64UrlEncode($name)]); + } + + return $this->render('quiz/elimination/index.html.twig', [ + 'form' => $form, 'controller_name' => 'EliminationController', ]); } - #[Route('/elimination/{seasonCode}/{candidateHash}', name: 'app_elimination_cadidate')] - #[IsGranted(SeasonVoter::ELIMINATION, 'season')] - public function candidateScreen(Season $season, string $candidateHash, CandidateRepository $candidateRepository): Response + #[Route('/elimination/{elimination}/{candidateHash}', name: 'app_elimination_candidate')] + #[IsGranted(SeasonVoter::ELIMINATION, 'elimination')] + public function candidateScreen(Elimination $elimination, string $candidateHash, CandidateRepository $candidateRepository): Response { - $candidate = $candidateRepository->getCandidateByHash($season, $candidateHash); + $candidate = $candidateRepository->getCandidateByHash($elimination->getQuiz()->getSeason(), $candidateHash); if (!$candidate instanceof Candidate) { $this->addFlash(FlashType::Warning, t('Cound not find candidate with name %name%', ['%name%' => Base64::base64UrlDecode($candidateHash)])->trans($this->translator) ); - throw new \InvalidArgumentException('Candidate not found'); + + return $this->redirectToRoute('app_elimination', ['elimination' => $elimination->getId()]); } - return $this->render('elimination/candidate.html.twig', [ - 'season' => $season, + $screenColour = $elimination->getScreenColour($candidate->getName()); + + if (null === $screenColour) { + $this->addFlash(FlashType::Warning, $this->translator->trans('Cound not find candidate with name %name% in elimination.', ['%name%' => $candidate->getName()])); + + return $this->redirectToRoute('app_elimination', ['elimination' => $elimination->getId()]); + } + + return $this->render('quiz/elimination/candidate.html.twig', [ 'candidate' => $candidate, + 'colour' => $screenColour, ]); } } diff --git a/src/Entity/Elimination.php b/src/Entity/Elimination.php index 1bd4ec0..beaae93 100644 --- a/src/Entity/Elimination.php +++ b/src/Entity/Elimination.php @@ -76,6 +76,11 @@ class Elimination return $this; } + public function getScreenColour(?string $name): ?string + { + return $this->data[$name] ?? null; + } + #[ORM\PrePersist] public function setCreatedAtValue(): void { diff --git a/src/Form/EliminationEnterNameType.php b/src/Form/EliminationEnterNameType.php new file mode 100644 index 0000000..52cd20a --- /dev/null +++ b/src/Form/EliminationEnterNameType.php @@ -0,0 +1,30 @@ + */ +class EliminationEnterNameType extends AbstractType +{ + public function __construct(private readonly TranslatorInterface $translator) {} + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('name', TextType::class, + [ + 'required' => true, + 'label' => $this->translator->trans('Enter name'), + 'translation_domain' => false, + 'attr' => ['autofocus' => true], + ], + ) + ; + } +} diff --git a/src/Form/EnterNameType.php b/src/Form/EnterNameType.php index e5e9c09..ac73e45 100644 --- a/src/Form/EnterNameType.php +++ b/src/Form/EnterNameType.php @@ -22,6 +22,7 @@ class EnterNameType extends AbstractType 'required' => true, 'label' => $this->translator->trans('Enter your name'), 'translation_domain' => false, + 'attr' => ['autofocus' => true], ], ) ; diff --git a/src/Form/SelectSeasonType.php b/src/Form/SelectSeasonType.php index 53020af..cdf4a7f 100644 --- a/src/Form/SelectSeasonType.php +++ b/src/Form/SelectSeasonType.php @@ -19,10 +19,13 @@ class SelectSeasonType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { $builder - ->add('season_code', TextType::class, - ['required' => true, 'constraints' => new Regex(pattern: "/^[A-Za-z\d]{5}$/"), 'label' => $this->translator->trans('Season Code'), 'translation_domain' => false] - ) - ; + ->add('season_code', TextType::class, [ + 'required' => true, + 'constraints' => new Regex(pattern: "/^[A-Za-z\d]{5}$/"), + 'label' => $this->translator->trans('Season Code'), + 'translation_domain' => false, + 'attr' => ['autofocus' => true], + ]); } public function configureOptions(OptionsResolver $resolver): void diff --git a/src/Security/Voter/SeasonVoter.php b/src/Security/Voter/SeasonVoter.php index 31d8541..58a9cc2 100644 --- a/src/Security/Voter/SeasonVoter.php +++ b/src/Security/Voter/SeasonVoter.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Security\Voter; +use App\Entity\Elimination; use App\Entity\Season; use App\Entity\User; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; @@ -21,10 +22,10 @@ final class SeasonVoter extends Voter protected function supports(string $attribute, mixed $subject): bool { return \in_array($attribute, [self::EDIT, self::DELETE, self::ELIMINATION], true) - && $subject instanceof Season; + && ($subject instanceof Season || $subject instanceof Elimination); } - /** @param Season $subject */ + /** @param Season|Elimination $subject */ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { $user = $token->getUser(); @@ -36,8 +37,10 @@ final class SeasonVoter extends Voter return true; } + $season = $subject instanceof Season ? $subject : $subject->getQuiz()->getSeason(); + return match ($attribute) { - self::EDIT, self::DELETE, self::ELIMINATION => $subject->isOwner($user), + self::EDIT, self::DELETE, self::ELIMINATION => $season->isOwner($user), default => false, }; } diff --git a/templates/backoffice/prepare_elimination/index.html.twig b/templates/backoffice/prepare_elimination/index.html.twig index d4b3ab1..ca79685 100644 --- a/templates/backoffice/prepare_elimination/index.html.twig +++ b/templates/backoffice/prepare_elimination/index.html.twig @@ -24,16 +24,18 @@
{% endfor %}
- + + value="1">{{ 'Save and start elimination'|trans }} {{ 'Back'|trans }}
diff --git a/templates/backoffice/quiz.html.twig b/templates/backoffice/quiz.html.twig index 9bd1f54..51acce3 100644 --- a/templates/backoffice/quiz.html.twig +++ b/templates/backoffice/quiz.html.twig @@ -53,7 +53,7 @@

{{ 'Score'|trans }}