chg: dev: add RecentBattle entity that is a Materialized View to speed up the view - and further refactor on ProfileController #8
This commit is contained in:
@@ -16,6 +16,7 @@ doctrine:
|
|||||||
auto_mapping: true
|
auto_mapping: true
|
||||||
schema_ignore_classes:
|
schema_ignore_classes:
|
||||||
- App\Entity\UserStats
|
- App\Entity\UserStats
|
||||||
|
- App\Entity\RecentBattle
|
||||||
mappings:
|
mappings:
|
||||||
App:
|
App:
|
||||||
is_bundle: false
|
is_bundle: false
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ use App\Dto\ProfileGameDto;
|
|||||||
use App\Dto\ProfileGameDtoFactory;
|
use App\Dto\ProfileGameDtoFactory;
|
||||||
use App\Dto\ProfileStatsDto;
|
use App\Dto\ProfileStatsDto;
|
||||||
use App\Dto\ProfileViewDto;
|
use App\Dto\ProfileViewDto;
|
||||||
use App\Entity\PlayedGame;
|
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Entity\RecentBattle;
|
||||||
use App\Repository\PlayedGameRepository;
|
use App\Repository\PlayedGameRepository;
|
||||||
|
use App\Repository\RecentBattleRepository;
|
||||||
use App\Repository\UserStatsRepository;
|
use App\Repository\UserStatsRepository;
|
||||||
use App\Service\BattleCardGenerator;
|
use App\Service\BattleCardGenerator;
|
||||||
use App\Service\WebAuthnService;
|
use App\Service\WebAuthnService;
|
||||||
@@ -59,6 +60,7 @@ class ProfileController extends AbstractController
|
|||||||
private readonly LoggerInterface $logger,
|
private readonly LoggerInterface $logger,
|
||||||
private readonly PlayedGameRepository $repo,
|
private readonly PlayedGameRepository $repo,
|
||||||
private readonly UserStatsRepository $userStatsRepo,
|
private readonly UserStatsRepository $userStatsRepo,
|
||||||
|
private readonly RecentBattleRepository $recentBattleRepo,
|
||||||
private readonly WebAuthnService $webAuthnService,
|
private readonly WebAuthnService $webAuthnService,
|
||||||
private readonly ProfileGameDtoFactory $profileGameDtoFactory,
|
private readonly ProfileGameDtoFactory $profileGameDtoFactory,
|
||||||
private readonly ProfileChartDataFactory $profileChartDataFactory,
|
private readonly ProfileChartDataFactory $profileChartDataFactory,
|
||||||
@@ -74,10 +76,10 @@ class ProfileController extends AbstractController
|
|||||||
|
|
||||||
$userId = $user->id;
|
$userId = $user->id;
|
||||||
$stats = ProfileStatsDto::fromUserStats($this->userStatsRepo->findByUserId($userId));
|
$stats = ProfileStatsDto::fromUserStats($this->userStatsRepo->findByUserId($userId));
|
||||||
$recent = $this->repo->findRecentFinishedForUser($user, 30);
|
$recent = $this->recentBattleRepo->findRecentForUser($userId, 30);
|
||||||
|
|
||||||
$gamesData = array_map(
|
$gamesData = array_map(
|
||||||
fn(PlayedGame $game): ProfileGameDto => $this->profileGameDtoFactory->create($game, $userId),
|
fn(RecentBattle $battle): ProfileGameDto => $this->profileGameDtoFactory->createFromRecentBattle($battle),
|
||||||
$recent,
|
$recent,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
namespace App\Dto;
|
namespace App\Dto;
|
||||||
|
|
||||||
use App\Entity\PlayedGame;
|
use App\Entity\PlayedGame;
|
||||||
|
use App\Entity\RecentBattle;
|
||||||
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
|
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,4 +92,43 @@ final readonly class ProfileGameDtoFactory
|
|||||||
|
|
||||||
return 'draw';
|
return 'draw';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a ProfileGameDto directly from the recent_battles materialized view row.
|
||||||
|
* Avatar paths are still resolved via LiipImagine (they are stored as storage paths).
|
||||||
|
*/
|
||||||
|
public function createFromRecentBattle(RecentBattle $battle): ProfileGameDto
|
||||||
|
{
|
||||||
|
$myPts = $battle->isRed ? $battle->redPoints : $battle->bluePoints;
|
||||||
|
$oppPts = $battle->isRed ? $battle->bluePoints : $battle->redPoints;
|
||||||
|
|
||||||
|
return new ProfileGameDto(
|
||||||
|
id: $battle->gameId,
|
||||||
|
uuid: $battle->uuid,
|
||||||
|
redName: $battle->redName,
|
||||||
|
blueName: $battle->blueName,
|
||||||
|
redAvatar: $battle->redAvatarPath
|
||||||
|
? $this->cacheManager->generateUrl($battle->redAvatarPath, 'avatar_thumb')
|
||||||
|
: null,
|
||||||
|
blueAvatar: $battle->blueAvatarPath
|
||||||
|
? $this->cacheManager->generateUrl($battle->blueAvatarPath, 'avatar_thumb')
|
||||||
|
: null,
|
||||||
|
redPoints: $battle->redPoints,
|
||||||
|
bluePoints: $battle->bluePoints,
|
||||||
|
redExplodedBomb: $battle->redExplodedBomb,
|
||||||
|
blueExplodedBomb: $battle->blueExplodedBomb,
|
||||||
|
resign: $battle->resign,
|
||||||
|
created: $battle->created?->format('Y-m-d H:i'),
|
||||||
|
date: $battle->updated?->format('Y-m-d H:i'),
|
||||||
|
isRed: $battle->isRed,
|
||||||
|
result: $battle->result,
|
||||||
|
myPoints: $myPts,
|
||||||
|
oppPoints: $oppPts,
|
||||||
|
redBonusPoints: $battle->redBonusPoints,
|
||||||
|
blueBonusPoints: $battle->blueBonusPoints,
|
||||||
|
redBonusStats: $battle->redBonusStats,
|
||||||
|
blueBonusStats: $battle->blueBonusStats,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
100
src/Entity/RecentBattle.php
Normal file
100
src/Entity/RecentBattle.php
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\RecentBattleRepository;
|
||||||
|
use DateTime;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping\Column;
|
||||||
|
use Doctrine\ORM\Mapping\Entity;
|
||||||
|
use Doctrine\ORM\Mapping\Id;
|
||||||
|
use Doctrine\ORM\Mapping\Table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class RecentBattle
|
||||||
|
*
|
||||||
|
* Read-only entity mapped to the recent_battles materialized view.
|
||||||
|
* Each row represents one game from the perspective of a single user.
|
||||||
|
*
|
||||||
|
* @package App\Entity
|
||||||
|
* @author Lang <https://www.splendidbear.org>
|
||||||
|
* @category Class
|
||||||
|
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
||||||
|
* @link www.splendidbear.org
|
||||||
|
* @since 2026. 04. 20.
|
||||||
|
*/
|
||||||
|
#[Entity(repositoryClass: RecentBattleRepository::class, readOnly: true), Table(name: 'recent_battles')]
|
||||||
|
class RecentBattle
|
||||||
|
{
|
||||||
|
/** Composite PK: (user_id, game_id) — mapped via the unique index. */
|
||||||
|
#[Id, Column]
|
||||||
|
public int $userId = 0;
|
||||||
|
|
||||||
|
#[Id, Column]
|
||||||
|
public int $gameId = 0;
|
||||||
|
|
||||||
|
#[Column(name: 'uuid')]
|
||||||
|
public string $uuid = '';
|
||||||
|
|
||||||
|
#[Column]
|
||||||
|
public bool $isRed = false;
|
||||||
|
|
||||||
|
#[Column]
|
||||||
|
public string $redName = '';
|
||||||
|
|
||||||
|
#[Column]
|
||||||
|
public string $blueName = '';
|
||||||
|
|
||||||
|
#[Column(nullable: true)]
|
||||||
|
public ?string $redAvatarPath = null;
|
||||||
|
|
||||||
|
#[Column(nullable: true)]
|
||||||
|
public ?string $blueAvatarPath = null;
|
||||||
|
|
||||||
|
#[Column(nullable: true)]
|
||||||
|
public ?int $redPoints = null;
|
||||||
|
|
||||||
|
#[Column(nullable: true)]
|
||||||
|
public ?int $bluePoints = null;
|
||||||
|
|
||||||
|
#[Column(nullable: true)]
|
||||||
|
public ?bool $redExplodedBomb = null;
|
||||||
|
|
||||||
|
#[Column(nullable: true)]
|
||||||
|
public ?bool $blueExplodedBomb = null;
|
||||||
|
|
||||||
|
#[Column(nullable: true)]
|
||||||
|
public ?string $resign = null;
|
||||||
|
|
||||||
|
#[Column]
|
||||||
|
public float $redBonusPoints = 0.0;
|
||||||
|
|
||||||
|
#[Column]
|
||||||
|
public float $blueBonusPoints = 0.0;
|
||||||
|
|
||||||
|
#[Column(type: Types::JSON)]
|
||||||
|
public array $redBonusStats = [];
|
||||||
|
|
||||||
|
#[Column(type: Types::JSON)]
|
||||||
|
public array $blueBonusStats = [];
|
||||||
|
|
||||||
|
#[Column]
|
||||||
|
public string $result = 'draw';
|
||||||
|
|
||||||
|
#[Column]
|
||||||
|
public bool $oppIsGuest = false;
|
||||||
|
|
||||||
|
#[Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
||||||
|
public ?DateTime $created = null;
|
||||||
|
|
||||||
|
#[Column(type: Types::DATETIME_MUTABLE, nullable: true)]
|
||||||
|
public ?DateTime $updated = null;
|
||||||
|
}
|
||||||
124
src/Migrations/2026/04/Version20260420130000.php
Normal file
124
src/Migrations/2026/04/Version20260420130000.php
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Migrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Version20260420130000
|
||||||
|
*
|
||||||
|
* Creates recent_battles materialized view for ProfileController optimization.
|
||||||
|
*
|
||||||
|
* @package App\Migrations
|
||||||
|
* @author Lang <https://www.splendidbear.org>
|
||||||
|
* @category Class
|
||||||
|
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
||||||
|
* @link www.splendidbear.org
|
||||||
|
* @since 2026. 04. 20.
|
||||||
|
*/
|
||||||
|
final class Version20260420130000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Create recent_battles materialized view for profile recent games list.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('
|
||||||
|
CREATE MATERIALIZED VIEW recent_battles AS
|
||||||
|
SELECT
|
||||||
|
pg.id AS game_id,
|
||||||
|
pg.uuid::text AS uuid,
|
||||||
|
u.id AS user_id,
|
||||||
|
CASE WHEN pg.red_id = u.id THEN true ELSE false END AS is_red,
|
||||||
|
|
||||||
|
COALESCE(ru.username, ra.user_name, \'Guest\') AS red_name,
|
||||||
|
COALESCE(bu.username, ba.user_name, \'Guest\') AS blue_name,
|
||||||
|
|
||||||
|
ru.avatar_path AS red_avatar_path,
|
||||||
|
bu.avatar_path AS blue_avatar_path,
|
||||||
|
|
||||||
|
pg.red_points,
|
||||||
|
pg.blue_points,
|
||||||
|
pg.red_exploded_bomb,
|
||||||
|
pg.blue_exploded_bomb,
|
||||||
|
pg.resign,
|
||||||
|
|
||||||
|
COALESCE(pg.red_bonus_points, 0.0) AS red_bonus_points,
|
||||||
|
COALESCE(pg.blue_bonus_points, 0.0) AS blue_bonus_points,
|
||||||
|
COALESCE(pg.red_bonus_stats, \'[]\'::json) AS red_bonus_stats,
|
||||||
|
COALESCE(pg.blue_bonus_stats, \'[]\'::json) AS blue_bonus_stats,
|
||||||
|
|
||||||
|
CASE
|
||||||
|
WHEN pg.red_id = u.id AND pg.resign = \'red\' THEN \'loss\'
|
||||||
|
WHEN pg.blue_id = u.id AND pg.resign = \'blue\' THEN \'loss\'
|
||||||
|
WHEN pg.red_id = u.id AND pg.resign = \'blue\' THEN \'win\'
|
||||||
|
WHEN pg.blue_id = u.id AND pg.resign = \'red\' THEN \'win\'
|
||||||
|
WHEN pg.red_id = u.id AND pg.red_points IS NOT NULL AND pg.blue_points IS NOT NULL
|
||||||
|
AND pg.red_points > pg.blue_points THEN \'win\'
|
||||||
|
WHEN pg.blue_id = u.id AND pg.blue_points IS NOT NULL AND pg.red_points IS NOT NULL
|
||||||
|
AND pg.blue_points > pg.red_points THEN \'win\'
|
||||||
|
WHEN pg.red_id = u.id AND pg.red_points IS NOT NULL AND pg.blue_points IS NOT NULL
|
||||||
|
AND pg.red_points < pg.blue_points THEN \'loss\'
|
||||||
|
WHEN pg.blue_id = u.id AND pg.blue_points IS NOT NULL AND pg.red_points IS NOT NULL
|
||||||
|
AND pg.blue_points < pg.red_points THEN \'loss\'
|
||||||
|
ELSE \'draw\'
|
||||||
|
END AS result,
|
||||||
|
|
||||||
|
-- Whether the opponent in this game is an anonymous guest (no app_user account)
|
||||||
|
CASE
|
||||||
|
WHEN pg.red_id = u.id AND pg.blue_id IS NULL THEN true
|
||||||
|
WHEN pg.blue_id = u.id AND pg.red_id IS NULL THEN true
|
||||||
|
ELSE false
|
||||||
|
END AS opp_is_guest,
|
||||||
|
|
||||||
|
pg.created,
|
||||||
|
pg.updated
|
||||||
|
FROM app_user u
|
||||||
|
JOIN played_game pg
|
||||||
|
ON pg.red_id = u.id
|
||||||
|
OR pg.blue_id = u.id
|
||||||
|
LEFT JOIN app_user ru ON ru.id = pg.red_id
|
||||||
|
LEFT JOIN app_user bu ON bu.id = pg.blue_id
|
||||||
|
LEFT JOIN gamer ra ON ra.id = pg.red_anon
|
||||||
|
LEFT JOIN gamer ba ON ba.id = pg.blue_anon
|
||||||
|
');
|
||||||
|
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX idx_recent_battles_pk ON recent_battles (user_id, game_id)');
|
||||||
|
$this->addSql('CREATE INDEX idx_recent_battles_user_upd ON recent_battles (user_id, updated DESC)');
|
||||||
|
|
||||||
|
$this->addSql('
|
||||||
|
CREATE OR REPLACE FUNCTION refresh_recent_battles()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
REFRESH MATERIALIZED VIEW CONCURRENTLY recent_battles;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql
|
||||||
|
');
|
||||||
|
|
||||||
|
$this->addSql('
|
||||||
|
CREATE TRIGGER trigger_refresh_recent_battles
|
||||||
|
AFTER INSERT OR UPDATE OR DELETE ON played_game
|
||||||
|
FOR EACH STATEMENT
|
||||||
|
EXECUTE FUNCTION refresh_recent_battles()
|
||||||
|
');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP TRIGGER IF EXISTS trigger_refresh_recent_battles ON played_game');
|
||||||
|
$this->addSql('DROP FUNCTION IF EXISTS refresh_recent_battles()');
|
||||||
|
$this->addSql('DROP MATERIALIZED VIEW IF EXISTS recent_battles');
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/Repository/RecentBattleRepository.php
Normal file
63
src/Repository/RecentBattleRepository.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\RecentBattle;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\DBAL\Exception;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class RecentBattleRepository
|
||||||
|
*
|
||||||
|
* @package App\Repository
|
||||||
|
* @author Lang <https://www.splendidbear.org>
|
||||||
|
* @category Class
|
||||||
|
* @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License
|
||||||
|
* @link www.splendidbear.org
|
||||||
|
* @since 2026. 04. 20.
|
||||||
|
*
|
||||||
|
* @method RecentBattle|null find($id, $lockMode = null, $lockVersion = null)
|
||||||
|
* @method RecentBattle|null findOneBy(array $criteria, array $orderBy = null)
|
||||||
|
* @method RecentBattle[] findAll()
|
||||||
|
* @method RecentBattle[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
|
||||||
|
*/
|
||||||
|
class RecentBattleRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, RecentBattle::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findRecentForUser(int $userId, int $limit = 30): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('rb')
|
||||||
|
->where('rb.userId = :uid')
|
||||||
|
->setParameter('uid', $userId)
|
||||||
|
->orderBy('rb.updated', 'DESC')
|
||||||
|
->setMaxResults($limit)
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function refreshMaterializedView(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this
|
||||||
|
->getEntityManager()
|
||||||
|
->getConnection()
|
||||||
|
->executeStatement('REFRESH MATERIALIZED VIEW CONCURRENTLY recent_battles');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
throw new RuntimeException("Failed to refresh materialized view: {$e->getMessage()}", 0, $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -127,32 +127,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="profile-games" data-batch-size="5">
|
<div class="profile-games" data-batch-size="5">
|
||||||
{% for game in recent %}
|
{% for game in recent %}
|
||||||
{% set is_red = game.red and game.red.id == app.user.id %}
|
{% set is_red = game.isRed %}
|
||||||
{% set my_points = is_red ? game.redPoints : game.bluePoints %}
|
{% set my_points = is_red ? game.redPoints : game.bluePoints %}
|
||||||
{% set opp_points = is_red ? game.bluePoints : game.redPoints %}
|
{% set opp_points = is_red ? game.bluePoints : game.redPoints %}
|
||||||
{% set opp = is_red ? game.blue : game.red %}
|
{% set opp_name = is_red ? game.blueName : game.redName %}
|
||||||
{% set opp_anon = is_red ? game.blueAnon : game.redAnon %}
|
{% set result = game.result %}
|
||||||
|
|
||||||
{% set result = 'draw' %}
|
{% set is_finished = game.resign is not null
|
||||||
{% set is_finished = false %}
|
or (my_points is not null and opp_points is not null
|
||||||
{% set is_anonymous = not opp and opp_anon %}
|
and (my_points > 25 or opp_points > 25)) %}
|
||||||
{% if game.resign == (is_red ? 'red' : 'blue') %}
|
{% set is_anonymous = game.oppIsGuest %}
|
||||||
{% set result = 'loss' %}
|
|
||||||
{% set is_finished = true %}
|
|
||||||
{% elseif game.resign == (is_red ? 'blue' : 'red') %}
|
|
||||||
{% set result = 'win' %}
|
|
||||||
{% set is_finished = true %}
|
|
||||||
{% elseif my_points is not null and opp_points is not null %}
|
|
||||||
{% if my_points > opp_points %}
|
|
||||||
{% set result = 'win' %}
|
|
||||||
{% set is_finished = (my_points > 25 or opp_points > 25) %}
|
|
||||||
{% elseif my_points < opp_points %}
|
|
||||||
{% set result = 'loss' %}
|
|
||||||
{% set is_finished = (my_points > 25 or opp_points > 25) %}
|
|
||||||
{% else %}
|
|
||||||
{% set is_finished = (my_points > 25 or opp_points > 25) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="profile-game profile-game--{{ result }}{% if not is_finished and not is_anonymous %} profile-game--ongoing{% elseif is_anonymous %} profile-game--abandoned{% endif %}{% if loop.index0 >= 5 %} profile-game--hidden{% endif %}" data-game-index="{{ loop.index0 }}">
|
<div class="profile-game profile-game--{{ result }}{% if not is_finished and not is_anonymous %} profile-game--ongoing{% elseif is_anonymous %} profile-game--abandoned{% endif %}{% if loop.index0 >= 5 %} profile-game--hidden{% endif %}" data-game-index="{{ loop.index0 }}">
|
||||||
<span class="profile-game__badge">
|
<span class="profile-game__badge">
|
||||||
@@ -168,15 +152,7 @@
|
|||||||
{{ my_points ?? '—' }} : {{ opp_points ?? '—' }}
|
{{ my_points ?? '—' }} : {{ opp_points ?? '—' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="profile-game__vs">vs</span>
|
<span class="profile-game__vs">vs</span>
|
||||||
<span class="profile-game__opponent">
|
<span class="profile-game__opponent">{{ opp_name }}</span>
|
||||||
{% if opp %}
|
|
||||||
{{ opp.username }}
|
|
||||||
{% elseif opp_anon %}
|
|
||||||
{{ opp_anon.userName }}
|
|
||||||
{% else %}
|
|
||||||
Guest
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
<span class="profile-game__color">
|
<span class="profile-game__color">
|
||||||
<i class="fas fa-circle" style="color: {{ is_red ? '#c0392b' : '#2980b9' }}"></i>
|
<i class="fas fa-circle" style="color: {{ is_red ? '#c0392b' : '#2980b9' }}"></i>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user