Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions src/Controller/PlayerPuzzleResultsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace SpeedPuzzling\Web\Controller;

use SpeedPuzzling\Web\Query\GetPlayerProfile;
use SpeedPuzzling\Web\Query\GetPlayerSolvedPuzzles;
use SpeedPuzzling\Web\Query\GetRanking;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

final class PlayerPuzzleResultsController extends AbstractController
{
public function __construct(
readonly private GetPlayerSolvedPuzzles $getPlayerSolvedPuzzles,
readonly private GetRanking $getRanking,
readonly private GetPlayerProfile $getPlayerProfile,
) {
}

#[Route(
path: '/en/player/{playerId}/puzzle/{puzzleId}/results/{category}',
name: 'player_puzzle_results',
requirements: ['category' => 'solo|duo|team'],
)]
public function __invoke(string $playerId, string $puzzleId, string $category, Request $request): Response
{
$player = $this->getPlayerProfile->byId($playerId);
$results = $this->getPlayerSolvedPuzzles->byPlayerIdPuzzleIdAndCategory($playerId, $puzzleId, $category);

if ($results === []) {
throw $this->createNotFoundException();
}

$ranking = null;
if ($category === 'solo') {
$ranking = $this->getRanking->ofPuzzleForPlayer($puzzleId, $playerId);
}

$templateData = [
'player' => $player,
'results' => $results,
'ranking' => $ranking,
'category' => $category,
];

if ($request->headers->get('Turbo-Frame') === 'modal-frame') {
return $this->render('player-puzzle-results/_modal.html.twig', $templateData);
}

return $this->render('player-puzzle-results/detail.html.twig', $templateData);
}
}
233 changes: 233 additions & 0 deletions src/Query/GetPlayerSolvedPuzzles.php
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,239 @@ public function teamByPlayerId(
}, $data);
}

/**
* @return array<SolvedPuzzle>
* @throws PlayerNotFound
*/
public function byPlayerIdPuzzleIdAndCategory(string $playerId, string $puzzleId, string $category): array
{
if (Uuid::isValid($puzzleId) === false) {
throw new PlayerNotFound();
}

return match ($category) {
// soloByPlayerIdAndPuzzleId returns oldest first, this page renders newest first
'solo' => array_reverse($this->soloByPlayerIdAndPuzzleId($playerId, $puzzleId)),
'duo' => $this->duoByPlayerIdAndPuzzleId($playerId, $puzzleId),
'team' => $this->teamByPlayerIdAndPuzzleId($playerId, $puzzleId),
default => [],
};
}

/**
* @return array<SolvedPuzzle>
* @throws PlayerNotFound
*/
private function duoByPlayerIdAndPuzzleId(string $playerId, string $puzzleId): array
{
if (Uuid::isValid($playerId) === false || Uuid::isValid($puzzleId) === false) {
throw new PlayerNotFound();
}

$query = <<<SQL
WITH filtered_pst_ids AS (
SELECT id
FROM puzzle_solving_time
WHERE
(team::jsonb -> 'puzzlers') @> jsonb_build_array(jsonb_build_object('player_id', CAST(:playerId AS UUID)))
AND puzzling_type = 'duo'
AND puzzle_id = :puzzleId
)
SELECT
pst.id as time_id,
puzzle.id AS puzzle_id,
puzzle.name AS puzzle_name,
puzzle.alternative_name AS puzzle_alternative_name,
CASE WHEN puzzle.hide_image_until IS NOT NULL AND puzzle.hide_image_until > :now::timestamp THEN NULL ELSE puzzle.image END AS puzzle_image,
CASE WHEN puzzle.hide_image_until IS NOT NULL AND puzzle.hide_image_until > :now::timestamp THEN NULL ELSE puzzle.image_ratio END AS puzzle_image_ratio,
pst.seconds_to_solve AS time,
pst.player_id AS player_id,
pieces_count,
finished_puzzle_photo,
tracked_at,
finished_at,
puzzle.identification_number AS puzzle_identification_number,
pst.comment,
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,
competition.slug AS competition_slug,
pst.suspicious
FROM filtered_pst_ids fids
INNER JOIN puzzle_solving_time pst ON pst.id = fids.id
INNER JOIN puzzle ON puzzle.id = pst.puzzle_id
INNER JOIN manufacturer ON manufacturer.id = puzzle.manufacturer_id
LEFT JOIN competition ON competition.id = pst.competition_id
ORDER BY COALESCE(pst.finished_at, pst.tracked_at) DESC
SQL;

$data = $this->database
->executeQuery($query, [
'playerId' => $playerId,
'puzzleId' => $puzzleId,
'now' => $this->clock->now()->format('Y-m-d H:i:s'),
])
->fetchAllAssociative();

/** @var array<string> $timeIds */
$timeIds = array_column($data, 'time_id');

$players = $this->getTeamPlayers->byIds($timeIds);

return array_map(static function (array $row) use ($players): SolvedPuzzle {
/**
* @var array{
* time_id: string,
* team_id: null|string,
* player_id: string,
* player_name: null,
* player_code: string,
* player_country: null,
* puzzle_id: string,
* puzzle_name: string,
* puzzle_alternative_name: null|string,
* manufacturer_name: string,
* puzzle_image: null|string,
* puzzle_image_ratio: null|string,
* time: int,
* pieces_count: int,
* comment: null|string,
* finished_puzzle_photo: null|string,
* puzzle_identification_number: null|string,
* tracked_at: string,
* finished_at: null|string,
* first_attempt: bool,
* unboxed: bool,
* competition_id: null|string,
* competition_name: null|string,
* competition_shortcut: null|string,
* competition_slug: null|string,
* suspicious: bool,
* } $row
*/

$row['players'] = $players[$row['time_id']] ?? null;

// Dummy placeholder values
$row['player_name'] = null;
$row['player_code'] = '';
$row['player_country'] = null;

return SolvedPuzzle::fromDatabaseRow($row);
}, $data);
}

/**
* @return array<SolvedPuzzle>
* @throws PlayerNotFound
*/
private function teamByPlayerIdAndPuzzleId(string $playerId, string $puzzleId): array
{
if (Uuid::isValid($playerId) === false || Uuid::isValid($puzzleId) === false) {
throw new PlayerNotFound();
}

$query = <<<SQL
WITH filtered_pst_ids AS (
SELECT id
FROM puzzle_solving_time
WHERE
(team::jsonb -> 'puzzlers') @> jsonb_build_array(jsonb_build_object('player_id', CAST(:playerId AS UUID)))
AND puzzling_type = 'team'
AND puzzle_id = :puzzleId
)
SELECT
pst.id as time_id,
puzzle.id AS puzzle_id,
puzzle.name AS puzzle_name,
puzzle.alternative_name AS puzzle_alternative_name,
CASE WHEN puzzle.hide_image_until IS NOT NULL AND puzzle.hide_image_until > :now::timestamp THEN NULL ELSE puzzle.image END AS puzzle_image,
CASE WHEN puzzle.hide_image_until IS NOT NULL AND puzzle.hide_image_until > :now::timestamp THEN NULL ELSE puzzle.image_ratio END AS puzzle_image_ratio,
pst.seconds_to_solve AS time,
pst.player_id AS player_id,
pieces_count,
finished_puzzle_photo,
tracked_at,
finished_at,
puzzle.identification_number AS puzzle_identification_number,
pst.comment,
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,
competition.slug AS competition_slug,
pst.suspicious
FROM filtered_pst_ids fids
INNER JOIN puzzle_solving_time pst ON pst.id = fids.id
INNER JOIN puzzle ON puzzle.id = pst.puzzle_id
INNER JOIN manufacturer ON manufacturer.id = puzzle.manufacturer_id
LEFT JOIN competition ON competition.id = pst.competition_id
ORDER BY COALESCE(pst.finished_at, pst.tracked_at) DESC
SQL;

$data = $this->database
->executeQuery($query, [
'playerId' => $playerId,
'puzzleId' => $puzzleId,
'now' => $this->clock->now()->format('Y-m-d H:i:s'),
])
->fetchAllAssociative();

/** @var array<string> $timeIds */
$timeIds = array_column($data, 'time_id');

$players = $this->getTeamPlayers->byIds($timeIds);

return array_map(static function (array $row) use ($players): SolvedPuzzle {
/**
* @var array{
* time_id: string,
* team_id: null|string,
* player_id: string,
* player_name: null,
* player_code: string,
* player_country: null,
* puzzle_id: string,
* puzzle_name: string,
* puzzle_alternative_name: null|string,
* manufacturer_name: string,
* puzzle_image: null|string,
* puzzle_image_ratio: null|string,
* time: int,
* pieces_count: int,
* comment: null|string,
* finished_puzzle_photo: null|string,
* puzzle_identification_number: null|string,
* tracked_at: string,
* finished_at: null|string,
* first_attempt: bool,
* unboxed: bool,
* competition_id: null|string,
* competition_name: null|string,
* competition_shortcut: null|string,
* competition_slug: null|string,
* suspicious: bool,
* } $row
*/

$row['players'] = $players[$row['time_id']] ?? null;

// Dummy placeholder values
$row['player_name'] = null;
$row['player_code'] = '';
$row['player_country'] = null;

return SolvedPuzzle::fromDatabaseRow($row);
}, $data);
}

/**
* Count distinct puzzles solved by player (solo or in team)
*
Expand Down
Loading