From 83ce626bafbddd66f738089eca438c9994526c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Mike=C5=A1?= Date: Sun, 18 Jan 2026 22:13:02 +0100 Subject: [PATCH 1/2] Unboxed - puzzling without image --- .../controllers/puzzle_add_form_controller.js | 6 +++++ config/services.php | 1 - migrations/Version20260118100000.php | 26 +++++++++++++++++++ src/Component/PlayerSolvedPuzzles.php | 14 ++++++++++ src/Component/PuzzleTimes.php | 12 +++++++++ src/Controller/PuzzleAddController.php | 1 + src/Entity/PuzzleSolvingTime.php | 4 +++ .../EditPuzzleSolvingTimeFormData.php | 2 ++ src/FormData/PuzzleAddFormData.php | 2 ++ src/FormData/PuzzleSolvingTimeFormData.php | 2 ++ src/FormType/PuzzleAddFormType.php | 6 +++++ src/FormType/PuzzleSolvingTimeFormType.php | 6 +++++ src/Message/AddPuzzleSolvingTime.php | 2 ++ src/Message/EditPuzzleSolvingTime.php | 2 ++ .../AddPuzzleSolvingTimeHandler.php | 1 + .../AddPuzzleTrackingHandler.php | 1 + .../EditPuzzleSolvingTimeHandler.php | 1 + src/Query/GetExportableSolvingTimes.php | 2 ++ src/Query/GetFastestGroups.php | 2 ++ src/Query/GetFastestPairs.php | 2 ++ src/Query/GetFastestPlayers.php | 2 ++ src/Query/GetLastSolvedPuzzle.php | 6 +++++ src/Query/GetPlayerSolvedPuzzles.php | 8 ++++++ src/Query/GetPuzzleSolvers.php | 8 +++++- src/Query/GetRecentActivity.php | 6 +++++ src/Results/ExportableSolvingTime.php | 4 +++ src/Results/PuzzleSolver.php | 3 +++ src/Results/PuzzleSolversGroup.php | 3 +++ src/Results/RecentActivityItem.php | 3 +++ src/Results/SolvedPuzzle.php | 3 +++ src/Results/SolvedPuzzleDetail.php | 3 +++ src/Services/PuzzlerDataExporter.php | 1 + src/Services/PuzzlesSorter.php | 21 +++++++++++++++ templates/_player_solvings.html.twig | 5 ++++ templates/_solving_time_form.html.twig | 5 ++++ templates/components/PuzzleTimes.html.twig | 23 +++++++++++++++- templates/components/RecentActivity.html.twig | 5 ++++ .../DataFixtures/PuzzleSolvingTimeFixture.php | 1 + tests/Services/PuzzlerDataExporterTest.php | 1 + translations/messages.en.yml | 4 +++ 40 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 migrations/Version20260118100000.php diff --git a/assets/controllers/puzzle_add_form_controller.js b/assets/controllers/puzzle_add_form_controller.js index e6e9e161..c15f8bca 100644 --- a/assets/controllers/puzzle_add_form_controller.js +++ b/assets/controllers/puzzle_add_form_controller.js @@ -10,6 +10,7 @@ export default class extends Controller { 'timeAndDateSection', // Card with time + date (Speed + Relax) 'timeSection', // Time field only (Speed only) 'firstAttemptSection', // First attempt checkbox (Speed only) + 'unboxedSection', // Unboxed checkbox (Speed only) 'groupSection', // Group players (Speed + Relax) 'competitionSection', // Competition (Speed only) 'commonSection', // Comment, photo (Speed + Relax) @@ -69,6 +70,11 @@ export default class extends Controller { this.firstAttemptSectionTarget.classList.toggle('d-none', !isSpeed); } + // Unboxed checkbox (Speed only) + if (this.hasUnboxedSectionTarget) { + this.unboxedSectionTarget.classList.toggle('d-none', !isSpeed); + } + // Group section (Speed + Relax) if (this.hasGroupSectionTarget) { this.groupSectionTarget.classList.toggle('d-none', isCollection); diff --git a/config/services.php b/config/services.php index 87de1550..f654fd7c 100644 --- a/config/services.php +++ b/config/services.php @@ -14,7 +14,6 @@ use SpeedPuzzling\Web\Doctrine\RegexSchemaAssetFilter; use SpeedPuzzling\Web\Services\Doctrine\FixDoctrineMigrationTableSchema; use SpeedPuzzling\Web\Services\SentryTracesSampler; -use SpeedPuzzling\Web\Services\SentryTransactionNameEnhancer; use SpeedPuzzling\Web\Services\StripeWebhookHandler; use Stripe\StripeClient; use Symfony\Component\HttpClient\Psr18Client; diff --git a/migrations/Version20260118100000.php b/migrations/Version20260118100000.php new file mode 100644 index 00000000..0f1b660a --- /dev/null +++ b/migrations/Version20260118100000.php @@ -0,0 +1,26 @@ +addSql('ALTER TABLE puzzle_solving_time ADD unboxed BOOLEAN DEFAULT false NOT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE puzzle_solving_time DROP unboxed'); + } +} diff --git a/src/Component/PlayerSolvedPuzzles.php b/src/Component/PlayerSolvedPuzzles.php index addb9e51..64b5ee06 100644 --- a/src/Component/PlayerSolvedPuzzles.php +++ b/src/Component/PlayerSolvedPuzzles.php @@ -33,6 +33,9 @@ final class PlayerSolvedPuzzles #[LiveProp(writable: true)] public bool $onlyFirstTries = false; + #[LiveProp(writable: true)] + public bool $onlyUnboxed = false; + #[LiveProp(writable: true)] public string $sortBy = 'fastest'; @@ -123,6 +126,7 @@ public function resetFilters(): void $this->searchQuery = null; $this->onlyRelax = false; $this->onlyFirstTries = false; + $this->onlyUnboxed = false; $this->dateFrom = null; $this->dateTo = null; $this->speedValue = null; @@ -142,6 +146,7 @@ public function populate(): void if ($this->category !== 'solo') { $this->onlyFirstTries = false; + $this->onlyUnboxed = false; } $this->ranking = $this->getRanking->allForPlayer($this->playerId); @@ -164,6 +169,11 @@ public function populate(): void $soloSolvedPuzzlesGrouped = $this->puzzlesSorter->filterOutNonFirstTriesGrouped($soloSolvedPuzzlesGrouped); } + // Only apply unboxed filter if user has membership (members exclusive filter) + if ($this->onlyUnboxed === true && $this->hasMembership()) { + $soloSolvedPuzzlesGrouped = $this->puzzlesSorter->filterOutNonUnboxedGrouped($soloSolvedPuzzlesGrouped); + } + // Apply sorting $soloSolvedPuzzlesGrouped = $this->applySortingGrouped($soloSolvedPuzzlesGrouped); $duoSolvedPuzzles = $this->applySorting($duoSolvedPuzzles); @@ -433,6 +443,10 @@ public function getActiveFiltersCount(): int $count++; } + if ($this->onlyUnboxed !== false) { + $count++; + } + if ($this->onlyRelax !== false) { $count++; } diff --git a/src/Component/PuzzleTimes.php b/src/Component/PuzzleTimes.php index d8c648ce..4c00cdd0 100644 --- a/src/Component/PuzzleTimes.php +++ b/src/Component/PuzzleTimes.php @@ -36,6 +36,9 @@ final class PuzzleTimes #[LiveProp(writable: true)] public bool $onlyFirstTries = false; + #[LiveProp(writable: true)] + public bool $onlyUnboxed = false; + #[LiveProp(writable: true)] public bool $onlyFavoritePlayers = false; @@ -88,6 +91,7 @@ public function populate(): void if ($this->category !== 'solo') { $this->onlyFirstTries = false; + $this->onlyUnboxed = false; } $soloPuzzleSolvers = $this->getPuzzleSolvers->soloByPuzzleId($this->puzzleId); @@ -101,6 +105,10 @@ public function populate(): void $soloPuzzleSolversGrouped = $this->puzzlesSorter->groupPlayers($soloPuzzleSolvers); } + if ($this->onlyUnboxed === true) { + $soloPuzzleSolversGrouped = $this->puzzlesSorter->filterOutNonUnboxedGrouped($soloPuzzleSolversGrouped); + } + $duoPuzzleSolvers = $this->getPuzzleSolvers->duoByPuzzleId($this->puzzleId); $duoPuzzleSolvers = $this->puzzlesSorter->sortByFastest($duoPuzzleSolvers); $duoPuzzleSolversGrouped = $this->puzzlesSorter->groupPlayers($duoPuzzleSolvers); @@ -255,6 +263,10 @@ public function getActiveFiltersCount(): int $count++; } + if ($this->onlyUnboxed !== false) { + $count++; + } + if ($this->onlyFavoritePlayers !== false) { $count++; } diff --git a/src/Controller/PuzzleAddController.php b/src/Controller/PuzzleAddController.php index b9ea9bd0..308798bd 100644 --- a/src/Controller/PuzzleAddController.php +++ b/src/Controller/PuzzleAddController.php @@ -270,6 +270,7 @@ private function handleSpeedPuzzling( groupPlayers: $groupPlayers, finishedAt: $data->finishedAt, firstAttempt: $data->firstAttempt, + unboxed: $data->unboxed, ), ); diff --git a/src/Entity/PuzzleSolvingTime.php b/src/Entity/PuzzleSolvingTime.php index 8d1c10b4..1ca222a7 100644 --- a/src/Entity/PuzzleSolvingTime.php +++ b/src/Entity/PuzzleSolvingTime.php @@ -65,6 +65,8 @@ public function __construct( public null|string $finishedPuzzlePhoto, #[Column(options: ['default' => 0])] public bool $firstAttempt, + #[Column(options: ['default' => 0])] + public bool $unboxed, #[ManyToOne] public null|CompetitionRound $competitionRound = null, #[ManyToOne] @@ -91,6 +93,7 @@ public function modify( DateTimeImmutable $finishedAt, null|string $finishedPuzzlePhoto, bool $firstAttempt, + bool $unboxed, null|Competition $competition, ): void { $this->secondsToSolve = $seconds; @@ -99,6 +102,7 @@ public function modify( $this->finishedAt = $finishedAt; $this->finishedPuzzlePhoto = $finishedPuzzlePhoto; $this->firstAttempt = $firstAttempt; + $this->unboxed = $unboxed; $this->competition = $competition; $this->puzzlersCount = $this->calculatePuzzlersCount(); diff --git a/src/FormData/EditPuzzleSolvingTimeFormData.php b/src/FormData/EditPuzzleSolvingTimeFormData.php index 39155452..4168e1a5 100644 --- a/src/FormData/EditPuzzleSolvingTimeFormData.php +++ b/src/FormData/EditPuzzleSolvingTimeFormData.php @@ -83,6 +83,8 @@ public function setTimeFromSeconds(int $totalSeconds): void public bool $firstAttempt = false; + public bool $unboxed = false; + public function __construct() { $this->finishedAt = new DateTimeImmutable(); diff --git a/src/FormData/PuzzleAddFormData.php b/src/FormData/PuzzleAddFormData.php index 898c708d..ea54257b 100644 --- a/src/FormData/PuzzleAddFormData.php +++ b/src/FormData/PuzzleAddFormData.php @@ -76,6 +76,8 @@ public function hasTime(): bool public bool $firstAttempt = false; + public bool $unboxed = false; + // Speed Puzzling & Relax common fields public null|DateTimeImmutable $finishedAt; diff --git a/src/FormData/PuzzleSolvingTimeFormData.php b/src/FormData/PuzzleSolvingTimeFormData.php index 815fac72..b5b2c5e2 100644 --- a/src/FormData/PuzzleSolvingTimeFormData.php +++ b/src/FormData/PuzzleSolvingTimeFormData.php @@ -42,6 +42,8 @@ final class PuzzleSolvingTimeFormData public bool $firstAttempt = false; + public bool $unboxed = false; + public function __construct() { $this->finishedAt = new DateTimeImmutable(); diff --git a/src/FormType/PuzzleAddFormType.php b/src/FormType/PuzzleAddFormType.php index cff14388..2b3454e3 100644 --- a/src/FormType/PuzzleAddFormType.php +++ b/src/FormType/PuzzleAddFormType.php @@ -215,6 +215,12 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'help' => 'forms.first_attempt_help', ]); + $builder->add('unboxed', CheckboxType::class, [ + 'label' => 'forms.unboxed', + 'required' => false, + 'help' => 'forms.unboxed_help', + ]); + // Common fields (Speed & Relax) $builder->add('finishedAt', DateType::class, [ 'label' => 'forms.date_finished', diff --git a/src/FormType/PuzzleSolvingTimeFormType.php b/src/FormType/PuzzleSolvingTimeFormType.php index 776a72a5..7e60bda2 100644 --- a/src/FormType/PuzzleSolvingTimeFormType.php +++ b/src/FormType/PuzzleSolvingTimeFormType.php @@ -103,6 +103,12 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'help' => 'forms.first_attempt_help', ]); + $builder->add('unboxed', CheckboxType::class, [ + 'label' => 'forms.unboxed', + 'required' => false, + 'help' => 'forms.unboxed_help', + ]); + $builder->add('puzzle', TextType::class, [ 'label' => 'forms.puzzle', 'help' => 'forms.puzzle_help', diff --git a/src/Message/AddPuzzleSolvingTime.php b/src/Message/AddPuzzleSolvingTime.php index 49219112..5b42fc12 100644 --- a/src/Message/AddPuzzleSolvingTime.php +++ b/src/Message/AddPuzzleSolvingTime.php @@ -23,6 +23,7 @@ public function __construct( public array $groupPlayers, public null|DateTimeImmutable $finishedAt, public bool $firstAttempt, + public bool $unboxed, ) { } @@ -45,6 +46,7 @@ public static function fromFormData(UuidInterface $timeId, string $userId, array groupPlayers: $groupPlayers, finishedAt: $data->finishedAt, firstAttempt: $data->firstAttempt, + unboxed: $data->unboxed, ); } } diff --git a/src/Message/EditPuzzleSolvingTime.php b/src/Message/EditPuzzleSolvingTime.php index 58b2cfc5..80b0a653 100644 --- a/src/Message/EditPuzzleSolvingTime.php +++ b/src/Message/EditPuzzleSolvingTime.php @@ -22,6 +22,7 @@ public function __construct( public null|DateTimeImmutable $finishedAt, public null|UploadedFile $finishedPuzzlesPhoto, public bool $firstAttempt, + public bool $unboxed, ) { } @@ -40,6 +41,7 @@ public static function fromFormData(string $userId, string $timeId, array $group finishedAt: $formData->finishedAt, finishedPuzzlesPhoto: $formData->finishedPuzzlesPhoto, firstAttempt: $formData->firstAttempt, + unboxed: $formData->unboxed, ); } } diff --git a/src/MessageHandler/AddPuzzleSolvingTimeHandler.php b/src/MessageHandler/AddPuzzleSolvingTimeHandler.php index edec6d83..35d30694 100644 --- a/src/MessageHandler/AddPuzzleSolvingTimeHandler.php +++ b/src/MessageHandler/AddPuzzleSolvingTimeHandler.php @@ -103,6 +103,7 @@ public function __invoke(AddPuzzleSolvingTime $message): void $message->comment, $finishedPuzzlePhotoPath, $message->firstAttempt, + $message->unboxed, competition: $competition, ); diff --git a/src/MessageHandler/AddPuzzleTrackingHandler.php b/src/MessageHandler/AddPuzzleTrackingHandler.php index bdaa2c6f..3e8b2984 100644 --- a/src/MessageHandler/AddPuzzleTrackingHandler.php +++ b/src/MessageHandler/AddPuzzleTrackingHandler.php @@ -76,6 +76,7 @@ public function __invoke(AddPuzzleTracking $message): void comment: $message->comment, finishedPuzzlePhoto: $finishedPuzzlePhotoPath, firstAttempt: false, + unboxed: false, ); $this->entityManager->persist($solvingTime); diff --git a/src/MessageHandler/EditPuzzleSolvingTimeHandler.php b/src/MessageHandler/EditPuzzleSolvingTimeHandler.php index f8914d15..0f93367c 100644 --- a/src/MessageHandler/EditPuzzleSolvingTimeHandler.php +++ b/src/MessageHandler/EditPuzzleSolvingTimeHandler.php @@ -108,6 +108,7 @@ public function __invoke(EditPuzzleSolvingTime $message): void $finishedAt, $finishedPuzzlePhotoPath, $message->firstAttempt, + $message->unboxed, competition: $competition, ); } diff --git a/src/Query/GetExportableSolvingTimes.php b/src/Query/GetExportableSolvingTimes.php index da85b02d..7c46aa6e 100644 --- a/src/Query/GetExportableSolvingTimes.php +++ b/src/Query/GetExportableSolvingTimes.php @@ -38,6 +38,7 @@ public function byPlayerId(string $playerId): array pst.finished_at, pst.tracked_at, pst.first_attempt, + pst.unboxed, pst.finished_puzzle_photo, pst.comment, pst.puzzling_type AS solving_type, @@ -76,6 +77,7 @@ public function byPlayerId(string $playerId): array * finished_at: string, * tracked_at: string, * first_attempt: bool, + * unboxed: bool, * finished_puzzle_photo: null|string, * comment: null|string, * solving_type: string, diff --git a/src/Query/GetFastestGroups.php b/src/Query/GetFastestGroups.php index 03002923..ff5be9b5 100644 --- a/src/Query/GetFastestGroups.php +++ b/src/Query/GetFastestGroups.php @@ -53,6 +53,7 @@ public function perPiecesCount(int $piecesCount, int $howManyPlayers, null|Count pst.id AS time_id, pst.team ->> 'team_id' AS team_id, first_attempt, + pst.unboxed, player.is_private, competition.id AS competition_id, competition.shortcut AS competition_shortcut, @@ -125,6 +126,7 @@ public function perPiecesCount(int $piecesCount, int $howManyPlayers, null|Count * puzzle_identification_number: null|string, * finished_at: string, * first_attempt: bool, + * unboxed: bool, * is_private: bool, * competition_id: null|string, * competition_name: null|string, diff --git a/src/Query/GetFastestPairs.php b/src/Query/GetFastestPairs.php index 0f1c3018..54d226ae 100644 --- a/src/Query/GetFastestPairs.php +++ b/src/Query/GetFastestPairs.php @@ -53,6 +53,7 @@ public function perPiecesCount(int $piecesCount, int $howManyPlayers, null|Count pst.id AS time_id, pst.team ->> 'team_id' AS team_id, first_attempt, + pst.unboxed, player.is_private, competition.id AS competition_id, competition.shortcut AS competition_shortcut, @@ -125,6 +126,7 @@ public function perPiecesCount(int $piecesCount, int $howManyPlayers, null|Count * players: null|string, * finished_at: string, * first_attempt: bool, + * unboxed: bool, * is_private: bool, * competition_id: null|string, * competition_name: null|string, diff --git a/src/Query/GetFastestPlayers.php b/src/Query/GetFastestPlayers.php index 3ca90b96..a08c1b1c 100644 --- a/src/Query/GetFastestPlayers.php +++ b/src/Query/GetFastestPlayers.php @@ -69,6 +69,7 @@ public function perPiecesCount(int $piecesCount, int $limit, null|CountryCode $c puzzle_solving_time.id AS time_id, puzzle.identification_number AS puzzle_identification_number, puzzle_solving_time.first_attempt, + puzzle_solving_time.unboxed, is_private, competition.id AS competition_id, competition.shortcut AS competition_shortcut, @@ -114,6 +115,7 @@ public function perPiecesCount(int $piecesCount, int $limit, null|CountryCode $c * puzzle_identification_number: null|string, * finished_at: string, * first_attempt: bool, + * unboxed: bool, * is_private: bool, * competition_id: null|string, * competition_name: null|string, diff --git a/src/Query/GetLastSolvedPuzzle.php b/src/Query/GetLastSolvedPuzzle.php index 1b99b19f..0a9686a5 100644 --- a/src/Query/GetLastSolvedPuzzle.php +++ b/src/Query/GetLastSolvedPuzzle.php @@ -47,6 +47,7 @@ public function forPlayer(string $playerId, int $limit): array puzzle_solving_time.finished_puzzle_photo AS finished_puzzle_photo, puzzle_solving_time.team ->> 'team_id' AS team_id, first_attempt, + puzzle_solving_time.unboxed, is_private, competition.id AS competition_id, competition.shortcut AS competition_shortcut, @@ -103,6 +104,7 @@ public function forPlayer(string $playerId, int $limit): array * puzzle_identification_number: null|string, * finished_at: string, * first_attempt: bool, + * unboxed: bool, * is_private: bool, * competition_id: null|string, * competition_name: null|string, @@ -142,6 +144,7 @@ public function limit(int $limit): array puzzle_solving_time.finished_puzzle_photo AS finished_puzzle_photo, puzzle_solving_time.team ->> 'team_id' AS team_id, first_attempt, + puzzle_solving_time.unboxed, is_private, competition.id AS competition_id, competition.shortcut AS competition_shortcut, @@ -196,6 +199,7 @@ public function limit(int $limit): array * puzzle_identification_number: null|string, * finished_at: string, * first_attempt: bool, + * unboxed: bool, * is_private: bool, * competition_id: null|string, * competition_name: null|string, @@ -259,6 +263,7 @@ public function ofPlayerFavorites(int $limit, string $playerId): array pst.finished_puzzle_photo AS finished_puzzle_photo, pst.team ->> 'team_id' AS team_id, first_attempt, + pst.unboxed, is_private, competition.id AS competition_id, competition.shortcut AS competition_shortcut, @@ -315,6 +320,7 @@ public function ofPlayerFavorites(int $limit, string $playerId): array * puzzle_identification_number: null|string, * finished_at: string, * first_attempt: bool, + * unboxed: bool, * is_private: bool, * competition_id: null|string, * competition_name: null|string, diff --git a/src/Query/GetPlayerSolvedPuzzles.php b/src/Query/GetPlayerSolvedPuzzles.php index aae82582..43a6353d 100644 --- a/src/Query/GetPlayerSolvedPuzzles.php +++ b/src/Query/GetPlayerSolvedPuzzles.php @@ -78,6 +78,7 @@ public function byTimeId(string $timeId): SolvedPuzzleDetail finished_at, finished_puzzle_photo, first_attempt, + puzzle_solving_time.unboxed, competition.id AS competition_id, CASE WHEN puzzle_solving_time.team IS NOT NULL THEN @@ -121,6 +122,7 @@ public function byTimeId(string $timeId): SolvedPuzzleDetail * finished_at: string, * finished_puzzle_photo: string, * first_attempt: bool, + * unboxed: bool, * competition_id: null|string, * } $row */ @@ -180,6 +182,7 @@ public function soloByPlayerId( manufacturer.name AS manufacturer_name, puzzle_solving_time.finished_puzzle_photo AS finished_puzzle_photo, first_attempt, + puzzle_solving_time.unboxed, solved_counts.solved_times AS solved_times, competition.id AS competition_id, competition.shortcut AS competition_shortcut, @@ -248,6 +251,7 @@ public function soloByPlayerId( * puzzle_identification_number: null|string, * finished_at: string, * first_attempt: bool, + * unboxed: bool, * solved_times: int, * competition_id: null|string, * competition_name: null|string, @@ -321,6 +325,7 @@ public function duoByPlayerId( manufacturer.name AS manufacturer_name, pst.team ->> 'team_id' AS team_id, first_attempt, + pst.unboxed, competition.id AS competition_id, competition.shortcut AS competition_shortcut, competition.name AS competition_name, @@ -369,6 +374,7 @@ public function duoByPlayerId( * tracked_at: string, * finished_at: string, * first_attempt: bool, + * unboxed: bool, * competition_id: null|string, * competition_name: null|string, * competition_shortcut: null|string, @@ -448,6 +454,7 @@ public function teamByPlayerId( manufacturer.name AS manufacturer_name, pst.team ->> 'team_id' AS team_id, first_attempt, + pst.unboxed, competition.id AS competition_id, competition.shortcut AS competition_shortcut, competition.name AS competition_name, @@ -496,6 +503,7 @@ public function teamByPlayerId( * tracked_at: string, * finished_at: string, * first_attempt: bool, + * unboxed: bool, * competition_id: null|string, * competition_name: null|string, * competition_shortcut: null|string, diff --git a/src/Query/GetPuzzleSolvers.php b/src/Query/GetPuzzleSolvers.php index 1c341d5e..4bcdd948 100644 --- a/src/Query/GetPuzzleSolvers.php +++ b/src/Query/GetPuzzleSolvers.php @@ -37,6 +37,7 @@ public function soloByPuzzleId(string $puzzleId): array puzzle_solving_time.seconds_to_solve AS time, finished_at, first_attempt, + unboxed, is_private, competition.id AS competition_id, competition.shortcut AS competition_shortcut, @@ -69,6 +70,7 @@ public function soloByPuzzleId(string $puzzleId): array * time: int, * finished_at: string, * first_attempt: bool, + * unboxed: bool, * is_private: bool, * competition_id: null|string, * competition_shortcut: null|string, @@ -100,6 +102,7 @@ public function duoByPuzzleId(string $puzzleId): array pst.team ->> 'team_id' AS team_id, finished_at, first_attempt, + pst.unboxed, competition.id AS competition_id, competition.shortcut AS competition_shortcut, competition.name AS competition_name, @@ -116,7 +119,7 @@ public function duoByPuzzleId(string $puzzleId): array FROM puzzle_solving_time pst LEFT JOIN competition ON competition.id = pst.competition_id, - LATERAL json_array_elements(pst.team -> 'puzzlers') WITH ORDINALITY AS player_elem(player, ordinality) + LATERAL json_array_elements(pst.team -> 'puzzlers') WITH ORDINALITY AS player_elem(player, ordinality) LEFT JOIN player p ON p.id = (player_elem.player ->> 'player_id')::UUID WHERE pst.puzzle_id = :puzzleId @@ -145,6 +148,7 @@ public function duoByPuzzleId(string $puzzleId): array * players: string, * finished_at: string, * first_attempt: bool, + * unboxed: bool, * competition_id: null|string, * competition_shortcut: null|string, * competition_name: null|string, @@ -175,6 +179,7 @@ public function teamByPuzzleId(string $puzzleId): array pst.team ->> 'team_id' AS team_id, finished_at, first_attempt, + pst.unboxed, competition.id AS competition_id, competition.shortcut AS competition_shortcut, competition.name AS competition_name, @@ -220,6 +225,7 @@ public function teamByPuzzleId(string $puzzleId): array * players: string, * finished_at: string, * first_attempt: bool, + * unboxed: bool, * competition_id: null|string, * competition_shortcut: null|string, * competition_name: null|string, diff --git a/src/Query/GetRecentActivity.php b/src/Query/GetRecentActivity.php index b898ac95..249b30ac 100644 --- a/src/Query/GetRecentActivity.php +++ b/src/Query/GetRecentActivity.php @@ -47,6 +47,7 @@ public function forPlayer(string $playerId, int $limit): array puzzle_solving_time.finished_puzzle_photo AS finished_puzzle_photo, puzzle_solving_time.team ->> 'team_id' AS team_id, first_attempt, + puzzle_solving_time.unboxed, is_private, competition.id AS competition_id, competition.shortcut AS competition_shortcut, @@ -103,6 +104,7 @@ public function forPlayer(string $playerId, int $limit): array * puzzle_identification_number: null|string, * finished_at: string, * first_attempt: bool, + * unboxed: bool, * is_private: bool, * competition_id: null|string, * competition_name: null|string, @@ -142,6 +144,7 @@ public function latest(int $limit): array puzzle_solving_time.finished_puzzle_photo AS finished_puzzle_photo, puzzle_solving_time.team ->> 'team_id' AS team_id, first_attempt, + puzzle_solving_time.unboxed, is_private, competition.id AS competition_id, competition.shortcut AS competition_shortcut, @@ -196,6 +199,7 @@ public function latest(int $limit): array * puzzle_identification_number: null|string, * finished_at: string, * first_attempt: bool, + * unboxed: bool, * is_private: bool, * competition_id: null|string, * competition_name: null|string, @@ -259,6 +263,7 @@ public function ofPlayerFavorites(int $limit, string $playerId): array pst.finished_puzzle_photo AS finished_puzzle_photo, pst.team ->> 'team_id' AS team_id, first_attempt, + pst.unboxed, is_private, competition.id AS competition_id, competition.shortcut AS competition_shortcut, @@ -315,6 +320,7 @@ public function ofPlayerFavorites(int $limit, string $playerId): array * puzzle_identification_number: null|string, * finished_at: string, * first_attempt: bool, + * unboxed: bool, * is_private: bool, * competition_id: null|string, * competition_name: null|string, diff --git a/src/Results/ExportableSolvingTime.php b/src/Results/ExportableSolvingTime.php index 8bfa49f7..50b70483 100644 --- a/src/Results/ExportableSolvingTime.php +++ b/src/Results/ExportableSolvingTime.php @@ -20,6 +20,7 @@ public function __construct( public DateTimeImmutable $trackedAt, public string $type, public bool $firstAttempt, + public bool $unboxed, public int $playersCount, public string $teamMembers, public null|string $finishedPuzzlePhotoUrl, @@ -38,6 +39,7 @@ public function __construct( * finished_at: string, * tracked_at: string, * first_attempt: bool, + * unboxed: bool, * finished_puzzle_photo: null|string, * comment: null|string, * solving_type: string, @@ -63,6 +65,7 @@ public static function fromDatabaseRow(array $row, string $baseUrl): self trackedAt: new DateTimeImmutable($row['tracked_at']), type: $row['solving_type'], firstAttempt: $row['first_attempt'], + unboxed: $row['unboxed'], playersCount: $row['players_count'], teamMembers: $row['team_members'] ?? '', finishedPuzzlePhotoUrl: $photoUrl, @@ -100,6 +103,7 @@ public function toArray(): array 'tracked_at' => $this->trackedAt->format('Y-m-d H:i:s'), 'type' => $this->type, 'first_attempt' => $this->firstAttempt, + 'unboxed' => $this->unboxed, 'players_count' => $this->playersCount, 'team_members' => $this->teamMembers, 'finished_puzzle_photo_url' => $this->finishedPuzzlePhotoUrl, diff --git a/src/Results/PuzzleSolver.php b/src/Results/PuzzleSolver.php index f9990a17..383ff350 100644 --- a/src/Results/PuzzleSolver.php +++ b/src/Results/PuzzleSolver.php @@ -18,6 +18,7 @@ public function __construct( public int $time, public DateTimeImmutable $finishedAt, public bool $firstAttempt, + public bool $unboxed, public bool $isPrivate, public null|string $competitionId, public null|string $competitionShortcut, @@ -36,6 +37,7 @@ public function __construct( * time: int, * finished_at: string, * first_attempt: bool, + * unboxed: bool, * is_private: bool, * competition_id: null|string, * competition_shortcut: null|string, @@ -54,6 +56,7 @@ public static function fromDatabaseRow(array $row): self time: $row['time'], finishedAt: new DateTimeImmutable($row['finished_at']), firstAttempt: $row['first_attempt'], + unboxed: $row['unboxed'], isPrivate: $row['is_private'], competitionId: $row['competition_id'], competitionShortcut: $row['competition_shortcut'], diff --git a/src/Results/PuzzleSolversGroup.php b/src/Results/PuzzleSolversGroup.php index 29c5e1fe..caa6134b 100644 --- a/src/Results/PuzzleSolversGroup.php +++ b/src/Results/PuzzleSolversGroup.php @@ -16,6 +16,7 @@ public function __construct( public array $players, public DateTimeImmutable $finishedAt, public bool $firstAttempt, + public bool $unboxed, public null|string $competitionId, public null|string $competitionShortcut, public null|string $competitionName, @@ -33,6 +34,7 @@ public function __construct( * players: string, * finished_at: string, * first_attempt: bool, + * unboxed: bool, * competition_id: null|string, * competition_shortcut: null|string, * competition_name: null|string, @@ -49,6 +51,7 @@ public static function fromDatabaseRow(array $row): self players: $players, finishedAt: new DateTimeImmutable($row['finished_at']), firstAttempt: $row['first_attempt'], + unboxed: $row['unboxed'], competitionId: $row['competition_id'], competitionShortcut: $row['competition_shortcut'], competitionName: $row['competition_name'], diff --git a/src/Results/RecentActivityItem.php b/src/Results/RecentActivityItem.php index b1596c6c..5404e424 100644 --- a/src/Results/RecentActivityItem.php +++ b/src/Results/RecentActivityItem.php @@ -32,6 +32,7 @@ public function __construct( public null|array $players, public null|string $puzzleIdentificationNumber, public bool $firstAttempt, + public bool $unboxed, public bool $isPrivate, public null|string $competitionId, public null|string $competitionShortcut, @@ -72,6 +73,7 @@ public function isSpeedMode(): bool * team_id?: null|string, * players?: null|string|array, * first_attempt: bool, + * unboxed: bool, * is_private?: bool, * competition_id: null|string, * competition_name: null|string, @@ -112,6 +114,7 @@ public static function fromDatabaseRow(array $row): self players: $players, puzzleIdentificationNumber: $row['puzzle_identification_number'], firstAttempt: $row['first_attempt'], + unboxed: $row['unboxed'], isPrivate: $row['is_private'] ?? false, competitionId: $row['competition_id'], competitionShortcut: $row['competition_shortcut'], diff --git a/src/Results/SolvedPuzzle.php b/src/Results/SolvedPuzzle.php index 80d98dc4..402531f3 100644 --- a/src/Results/SolvedPuzzle.php +++ b/src/Results/SolvedPuzzle.php @@ -33,6 +33,7 @@ public function __construct( public null|string $puzzleIdentificationNumber, public DateTimeImmutable $finishedAt, public bool $firstAttempt, + public bool $unboxed, public bool $isPrivate, public null|string $competitionId, public null|string $competitionShortcut, @@ -65,6 +66,7 @@ public function __construct( * solved_times?: int, * finished_at: string, * first_attempt: bool, + * unboxed: bool, * is_private?: bool, * competition_id: null|string, * competition_name: null|string, @@ -107,6 +109,7 @@ public static function fromDatabaseRow(array $row): self puzzleIdentificationNumber: $row['puzzle_identification_number'], finishedAt: new DateTimeImmutable($row['finished_at']), firstAttempt: $row['first_attempt'], + unboxed: $row['unboxed'], isPrivate: $row['is_private'] ?? false, competitionId: $row['competition_id'], competitionShortcut: $row['competition_shortcut'], diff --git a/src/Results/SolvedPuzzleDetail.php b/src/Results/SolvedPuzzleDetail.php index 618c6a9b..d048af4e 100644 --- a/src/Results/SolvedPuzzleDetail.php +++ b/src/Results/SolvedPuzzleDetail.php @@ -27,6 +27,7 @@ public function __construct( public DateTimeImmutable $finishedAt, public null|string $finishedPuzzlePhoto, public bool $firstAttempt, + public bool $unboxed, public null|string $competitionId, ) { } @@ -49,6 +50,7 @@ public function __construct( * finished_at: string, * finished_puzzle_photo: string, * first_attempt: bool, + * unboxed: bool, * competition_id: null|string, * } $row */ @@ -76,6 +78,7 @@ public static function fromDatabaseRow(array $row): self finishedAt: new DateTimeImmutable($row['finished_at']), finishedPuzzlePhoto: $row['finished_puzzle_photo'], firstAttempt: $row['first_attempt'], + unboxed: $row['unboxed'], competitionId: $row['competition_id'], ); } diff --git a/src/Services/PuzzlerDataExporter.php b/src/Services/PuzzlerDataExporter.php index e51491aa..22ae43a4 100644 --- a/src/Services/PuzzlerDataExporter.php +++ b/src/Services/PuzzlerDataExporter.php @@ -26,6 +26,7 @@ 'tracked_at', 'type', 'first_attempt', + 'unboxed', 'players_count', 'team_members', 'finished_puzzle_photo_url', diff --git a/src/Services/PuzzlesSorter.php b/src/Services/PuzzlesSorter.php index 5261d2a7..9155c16e 100644 --- a/src/Services/PuzzlesSorter.php +++ b/src/Services/PuzzlesSorter.php @@ -386,6 +386,27 @@ public function filterOutNonFirstTriesGrouped(array $groupedSolvers): array ); } + /** + * @template T of PuzzleSolver|PuzzleSolversGroup|SolvedPuzzle + * @param array> $groupedSolvers + * @return array> + */ + public function filterOutNonUnboxedGrouped(array $groupedSolvers): array + { + return array_filter( + array: $groupedSolvers, + callback: function (array $grouped): bool { + foreach ($grouped as $solvedPuzzle) { + if ($solvedPuzzle->unboxed === true) { + return true; + } + } + + return false; + }, + ); + } + /** * @template T of PuzzleSolver|PuzzleSolversGroup * @param array> $groupedSolvers diff --git a/templates/_player_solvings.html.twig b/templates/_player_solvings.html.twig index 5aebf2e6..9593e6c9 100644 --- a/templates/_player_solvings.html.twig +++ b/templates/_player_solvings.html.twig @@ -129,6 +129,11 @@ {{ 'first_attempt'|trans }} {% endif %} + {% if puzzle.unboxed %} +
+ {{ 'unboxed'|trans }} + {% endif %} + {% if puzzle.suspicious %}
diff --git a/templates/_solving_time_form.html.twig b/templates/_solving_time_form.html.twig index 993fe8c0..340d93ae 100644 --- a/templates/_solving_time_form.html.twig +++ b/templates/_solving_time_form.html.twig @@ -192,6 +192,11 @@
{{ form_row(solving_time_form.firstAttempt) }}
+ + {# Unboxed - Speed Puzzling only #} +
+ {{ form_row(solving_time_form.unboxed) }} +
{# Group players section (Speed + Relax only) #} diff --git a/templates/components/PuzzleTimes.html.twig b/templates/components/PuzzleTimes.html.twig index 257ceb7c..f66af9a9 100644 --- a/templates/components/PuzzleTimes.html.twig +++ b/templates/components/PuzzleTimes.html.twig @@ -72,7 +72,7 @@ + {# Unboxed filter #} +
+ + +
+ {# Date range filter #}
diff --git a/translations/messages.cs.yml b/translations/messages.cs.yml index a773ed79..913cb939 100644 --- a/translations/messages.cs.yml +++ b/translations/messages.cs.yml @@ -61,6 +61,7 @@ average_time_short: "Průměr: %time%" fastest_time: "Nejrychlejší:" average_time: "Průměrný:" only_first_tries_filter: "Pouze 1. pokusy" +only_unboxed_filter: "Pouze naslepo" only_favorite_players_filter: "Pouze oblíbení hráči" filtered_out_puzzlers_count_info: "Schováno %count% puzzlerů bez infa o 1. pokusu." my_time: "Můj čas:" @@ -87,6 +88,7 @@ any: "Libovolný" all_pieces: "Vše" solo_solved: "Skládáno SOLO:" first_attempt: "1. pokus" +unboxed: "Naslepo" ranking_out_of: "z" wjpc_participant: "Účastník (%name%)" share: "Sdílet" @@ -299,6 +301,8 @@ forms: send_feedback: "Odeslat" first_attempt: "Tvé první skládání těchto puzzlí v životě" first_attempt_help: "Pokud jste skládali ve skupině, pak pouze pokud tyto puzzle nikdo předtím neskládal, ani sám" + unboxed: "Složeno bez pohledu na krabici" + unboxed_help: "Zaškrtni, pokud jsi puzzle složil/a bez koukání na obrázek na krabici" competition_participant: "Vaše jméno na soutěži" player_code: "Kód hráče" player_code_help: "Unikátní kód hráče - minimálně 3 znaky, maximálně 8 znaků. Může obsahovat pouze písmena a číslice, bez speciálních znaků nebo mezer." diff --git a/translations/messages.de.yml b/translations/messages.de.yml index 08281aec..2378a1cf 100644 --- a/translations/messages.de.yml +++ b/translations/messages.de.yml @@ -61,6 +61,7 @@ average_time_short: "Ø: %time%" fastest_time: "Schnellste Zeit:" average_time: "Durchschnitt:" only_first_tries_filter: "Nur erste Versuche" +only_unboxed_filter: "Nur blind gelegt" only_favorite_players_filter: "Nur Lieblingsspieler" filtered_out_puzzlers_count_info: "Versteckt %count% Puzzler ohne Info zum ersten Versuch." my_time: "Meine Zeit:" @@ -87,6 +88,7 @@ any: "Beliebig" all_pieces: "Alle" solo_solved: "SOLO gelöst:" first_attempt: "1. Versuch" +unboxed: "Blind gelegt" ranking_out_of: "von" wjpc_participant: "WJPC 2024 Teilnehmer (%name%)" share: "Teilen" @@ -312,6 +314,8 @@ forms: send_feedback: "Senden" first_attempt: "Ihr erstes Zusammensetzen dieses Puzzles überhaupt" first_attempt_help: "Beim Zusammensetzen in einer Gruppe nur dann, wenn niemand dieses Puzzle vorher zusammengesetzt hat, auch nicht solo" + unboxed: "Ohne Blick auf die Verpackung gelegt" + unboxed_help: "Aktivieren Sie dies, wenn Sie das Puzzle ohne Blick auf die Verpackung zusammengesetzt haben" competition_participant: "Ihr Name beim Wettbewerb" player_code: "Spielercode" player_code_help: "Ein eindeutiger Spielercode - mindestens 3 Zeichen, bis zu 8 Zeichen. Er darf nur Buchstaben und Zahlen enthalten, keine Sonderzeichen oder Leerzeichen." diff --git a/translations/messages.es.yml b/translations/messages.es.yml index 9f1d444b..17a1996f 100644 --- a/translations/messages.es.yml +++ b/translations/messages.es.yml @@ -64,6 +64,7 @@ average_time_short: "Prom: %time%" fastest_time: "Más rápido:" average_time: "Promedio:" only_first_tries_filter: "Solo primeros intentos" +only_unboxed_filter: "Solo sin caja" only_favorite_players_filter: "Solo jugadores favoritos" filtered_out_puzzlers_count_info: "Ocultos %count% puzzleros sin información del primer intento." my_time: "Mi tiempo:" @@ -90,6 +91,7 @@ any: "Cualquiera" all_pieces: "Todas" solo_solved: "Resuelto SOLO:" first_attempt: "1er intento" +unboxed: "Sin caja" ranking_out_of: "de" wjpc_participant: "Participante WJPC 2024 (%name%)" share: "Compartir" @@ -327,6 +329,8 @@ forms: send_feedback: "Enviar" first_attempt: "Tu primer armado de este rompecabezas" first_attempt_help: "Si armas en grupo, entonces solo si nadie armó este rompecabezas antes, ni siquiera solo" + unboxed: "Armado sin mirar la imagen de la caja" + unboxed_help: "Marca esto si armaste el rompecabezas sin consultar la caja o imagen" competition_participant: "Tu nombre en la competencia" player_code: "Código de Jugador" player_code_help: "Un código de jugador único - al menos 3 caracteres, hasta 8 caracteres. Solo puede contener letras y números, sin caracteres especiales o espacios." diff --git a/translations/messages.fr.yml b/translations/messages.fr.yml index 4ba77aa4..c2f307c5 100644 --- a/translations/messages.fr.yml +++ b/translations/messages.fr.yml @@ -64,6 +64,7 @@ average_time_short: "Moy : %time%" fastest_time: "Le plus rapide :" average_time: "Moyenne :" only_first_tries_filter: "1ères tentatives seulement" +only_unboxed_filter: "Sans boîte uniquement" only_favorite_players_filter: "Joueurs favoris uniquement" filtered_out_puzzlers_count_info: "Masqué %count% puzzleurs sans info de 1ère tentative." my_time: "Mon temps :" @@ -90,6 +91,7 @@ any: "Tous" all_pieces: "Toutes" solo_solved: "Résolu SOLO :" first_attempt: "1ère tentative" +unboxed: "Sans boîte" ranking_out_of: "sur" wjpc_participant: "Participant WJPC 2024 (%name%)" share: "Partager" @@ -327,6 +329,8 @@ forms: send_feedback: "Envoyer" first_attempt: "Votre premier assemblage de ce puzzle" first_attempt_help: "Si assemblage en groupe, seulement si personne n'a assemblé ce puzzle auparavant, même pas en solo" + unboxed: "Assemblé sans regarder l'image de la boîte" + unboxed_help: "Cochez cette case si vous avez assemblé le puzzle sans consulter la boîte ou l'image" competition_participant: "Votre nom sur la compétition" player_code: "Code joueur" player_code_help: "Un code joueur unique - au moins 3 caractères, jusqu'à 8 caractères. Il ne peut contenir que des lettres et des chiffres, pas de caractères spéciaux ou d'espaces." diff --git a/translations/messages.ja.yml b/translations/messages.ja.yml index 46a4cd14..d3623c76 100644 --- a/translations/messages.ja.yml +++ b/translations/messages.ja.yml @@ -64,6 +64,7 @@ average_time_short: "平均: %time%" fastest_time: "最速:" average_time: "平均:" only_first_tries_filter: "初回のみ" +only_unboxed_filter: "箱なしのみ" only_favorite_players_filter: "お気に入りのみ" filtered_out_puzzlers_count_info: "初回情報のない%count%人のパズラーを非表示にしています。" my_time: "マイタイム:" @@ -90,6 +91,7 @@ any: "すべて" all_pieces: "すべて" solo_solved: "ソロ完了:" first_attempt: "初回" +unboxed: "箱なし" ranking_out_of: "の" wjpc_participant: "WJPC 2024参加者 (%name%)" share: "シェア" @@ -326,6 +328,8 @@ forms: send_feedback: "送信" first_attempt: "このパズルの初回組み立て" first_attempt_help: "グループで組み立てる場合、誰も以前にこのパズルを組み立てていない場合のみ(ソロでも含む)" + unboxed: "箱の画像を見ずに組み立て" + unboxed_help: "箱や画像を参照せずにパズルを組み立てた場合はチェックしてください" competition_participant: "競技会での氏名" player_code: "プレイヤーコード" player_code_help: "ユニークなプレイヤーコード - 最低3文字、最大8文字。文字と数字のみ、特殊文字やスペースは使用できません。"