Refactor translations to XLIFF format, enhance elimination workflows, and update compose configuration

This commit switches translations from YAML to XLIFF format for better standardization, updates the elimination preparation process with UI and functionality improvements, tweaks form structures, adjusts compose.override.yaml for improved asset handling, and optimizes back office usability with refined translation handling.
This commit is contained in:
2025-06-01 21:16:32 +02:00
parent cd5946bda8
commit c6f9b57c60
27 changed files with 645 additions and 114 deletions

View File

@@ -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')
;
}

View File

@@ -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');
}

View File

@@ -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,
]);
}
}

View File

@@ -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
{

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/** @extends AbstractType<null> */
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],
],
)
;
}
}

View File

@@ -22,6 +22,7 @@ class EnterNameType extends AbstractType
'required' => true,
'label' => $this->translator->trans('Enter your name'),
'translation_domain' => false,
'attr' => ['autofocus' => true],
],
)
;

View File

@@ -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

View File

@@ -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,
};
}