chg: usr: make fancy og tags - and create a special one for battle sharing #4
This commit is contained in:
@@ -22,6 +22,8 @@ RUN install-php-extensions \
|
|||||||
apcu \
|
apcu \
|
||||||
sodium
|
sodium
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends fonts-dejavu-core && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||||
RUN printf '[opcache]\nopcache.enable=1\nopcache.memory_consumption=256\nopcache.max_accelerated_files=20000\nopcache.validate_timestamps=0\n' \
|
RUN printf '[opcache]\nopcache.enable=1\nopcache.memory_consumption=256\nopcache.max_accelerated_files=20000\nopcache.validate_timestamps=0\n' \
|
||||||
> "$PHP_INI_DIR/conf.d/opcache.ini"
|
> "$PHP_INI_DIR/conf.d/opcache.ini"
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export default function BattleDialog({ games }) {
|
|||||||
const endReason = resign
|
const endReason = resign
|
||||||
? `${resign.charAt(0).toUpperCase() + resign.slice(1)} resigned`
|
? `${resign.charAt(0).toUpperCase() + resign.slice(1)} resigned`
|
||||||
: 'Points';
|
: 'Points';
|
||||||
const shareUrl = `${window.location.origin}/battle/${game.id}`;
|
const shareUrl = `${window.location.origin}/battle/${game.uuid}`;
|
||||||
|
|
||||||
const handleShare = () => {
|
const handleShare = () => {
|
||||||
navigator.clipboard.writeText(shareUrl).then(() => {
|
navigator.clipboard.writeText(shareUrl).then(() => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"php": ">=8.5",
|
"php": ">=8.5",
|
||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
|
"ext-gd": "*",
|
||||||
"doctrine/dbal": "^3.7",
|
"doctrine/dbal": "^3.7",
|
||||||
"doctrine/doctrine-bundle": ">=2.11 <2.14",
|
"doctrine/doctrine-bundle": ">=2.11 <2.14",
|
||||||
"doctrine/doctrine-migrations-bundle": "^3.0",
|
"doctrine/doctrine-migrations-bundle": "^3.0",
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ services:
|
|||||||
resource: '../src/Controller'
|
resource: '../src/Controller'
|
||||||
tags: [ 'controller.service_arguments' ]
|
tags: [ 'controller.service_arguments' ]
|
||||||
|
|
||||||
|
App\Service\BattleCardGenerator:
|
||||||
|
arguments:
|
||||||
|
$cacheDir: '%kernel.project_dir%/var/og-cache'
|
||||||
|
|
||||||
Aws\S3\S3Client:
|
Aws\S3\S3Client:
|
||||||
arguments:
|
arguments:
|
||||||
- version: 'latest'
|
- version: 'latest'
|
||||||
|
|||||||
@@ -10,21 +10,31 @@
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\PlayedGame;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
use App\Repository\PlayedGameRepository;
|
use App\Repository\PlayedGameRepository;
|
||||||
|
use App\Service\BattleCardGenerator;
|
||||||
use App\Service\WebAuthnService;
|
use App\Service\WebAuthnService;
|
||||||
|
use DateTime;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use League\Flysystem\FilesystemException;
|
||||||
use League\Flysystem\FilesystemOperator;
|
use League\Flysystem\FilesystemOperator;
|
||||||
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
|
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use RuntimeException;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
use Symfony\Component\Uid\Uuid;
|
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||||
use Symfony\Component\HttpKernel\Attribute\AsController;
|
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Uid\Uuid;
|
||||||
|
use Throwable;
|
||||||
|
use function count;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class ProfileController
|
* Class ProfileController
|
||||||
@@ -41,7 +51,8 @@ class ProfileController extends AbstractController
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly PlayedGameRepository $repo,
|
private readonly PlayedGameRepository $repo,
|
||||||
private readonly WebAuthnService $webAuthnService
|
private readonly WebAuthnService $webAuthnService,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,15 +68,15 @@ class ProfileController extends AbstractController
|
|||||||
$losses = $this->repo->countLossesForUser($user);
|
$losses = $this->repo->countLossesForUser($user);
|
||||||
$draws = $this->repo->countDrawsForUser($user);
|
$draws = $this->repo->countDrawsForUser($user);
|
||||||
|
|
||||||
// Build monthly buckets for the last 6 months
|
/** Build monthly buckets for the last 6 months */
|
||||||
$monthlyData = [];
|
$monthlyData = [];
|
||||||
for ($i = 5; $i >= 0; $i--) {
|
for ($i = 5; $i >= 0; $i--) {
|
||||||
$dt = new \DateTime("first day of -$i months midnight");
|
$dt = new DateTime("first day of -$i months midnight");
|
||||||
$key = $dt->format('Y-m');
|
$key = $dt->format('Y-m');
|
||||||
$monthlyData[$key] = ['label' => $dt->format('M'), 'wins' => 0, 'losses' => 0, 'draws' => 0];
|
$monthlyData[$key] = ['label' => $dt->format('M'), 'wins' => 0, 'losses' => 0, 'draws' => 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
$since = new \DateTime('first day of -5 months midnight');
|
$since = new DateTime('first day of -5 months midnight');
|
||||||
$recentGames = $this->repo->findFinishedForUserSince($user, $since);
|
$recentGames = $this->repo->findFinishedForUserSince($user, $since);
|
||||||
$userId = $user->getId();
|
$userId = $user->getId();
|
||||||
|
|
||||||
@@ -113,7 +124,7 @@ class ProfileController extends AbstractController
|
|||||||
'bestScore' => $this->repo->findBestScoreForUser($user),
|
'bestScore' => $this->repo->findBestScoreForUser($user),
|
||||||
],
|
],
|
||||||
'recent' => ($recent = $this->repo->findRecentFinishedForUser($user)),
|
'recent' => ($recent = $this->repo->findRecentFinishedForUser($user)),
|
||||||
'gamesData' => array_map(static function (\App\Entity\PlayedGame $game) use ($userId): array {
|
'gamesData' => array_map(static function (PlayedGame $game) use ($userId): array {
|
||||||
$isRed = $game->getRed()?->getId() === $userId;
|
$isRed = $game->getRed()?->getId() === $userId;
|
||||||
$resign = $game->getResign();
|
$resign = $game->getResign();
|
||||||
$myColor = $isRed ? 'red' : 'blue';
|
$myColor = $isRed ? 'red' : 'blue';
|
||||||
@@ -131,6 +142,7 @@ class ProfileController extends AbstractController
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $game->getId(),
|
'id' => $game->getId(),
|
||||||
|
'uuid' => $game->getUuid()?->toRfc4122(),
|
||||||
'redName' =>
|
'redName' =>
|
||||||
$game->getRed()?->getUsername() ?? $game->getRedAnon()?->getUserName() ?? 'Guest',
|
$game->getRed()?->getUsername() ?? $game->getRedAnon()?->getUserName() ?? 'Guest',
|
||||||
'blueName' =>
|
'blueName' =>
|
||||||
@@ -160,16 +172,21 @@ class ProfileController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/battle/{id}', name: 'MineSeekerBundle_battle_share', requirements: ['id' => '\d+'], methods: ['GET'])]
|
#[Route(
|
||||||
public function battleShare(int $id): Response
|
'/battle/{uuid}',
|
||||||
|
name: 'MineSeekerBundle_battle_share',
|
||||||
|
requirements: ['uuid' => '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'],
|
||||||
|
methods: ['GET'],
|
||||||
|
)]
|
||||||
|
public function battleShare(Uuid $uuid): Response
|
||||||
{
|
{
|
||||||
$game = $this->repo->find($id);
|
$game = $this->repo->findOneBy(['uuid' => $uuid]);
|
||||||
if (!$game) {
|
if (!$game) {
|
||||||
throw $this->createNotFoundException('Battle not found.');
|
throw $this->createNotFoundException('Battle not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$redName = $game->getRed()?->getUsername() ?? $game->getRedAnon()?->getUserName() ?? 'Guest';
|
$redName = $game->getRed()?->getUsername() ?? ($game->getRedAnon() !== null ? 'Anonymous' : 'Guest');
|
||||||
$blueName = $game->getBlue()?->getUsername() ?? $game->getBlueAnon()?->getUserName() ?? 'Guest';
|
$blueName = $game->getBlue()?->getUsername() ?? ($game->getBlueAnon() !== null ? 'Anonymous' : 'Guest');
|
||||||
$redPts = $game->getRedPoints();
|
$redPts = $game->getRedPoints();
|
||||||
$bluePts = $game->getBluePoints();
|
$bluePts = $game->getBluePoints();
|
||||||
$resign = $game->getResign();
|
$resign = $game->getResign();
|
||||||
@@ -202,6 +219,29 @@ class ProfileController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route(
|
||||||
|
'/og/battle/{uuid}.png',
|
||||||
|
name: 'MineSeekerBundle_og_battle',
|
||||||
|
requirements: ['uuid' => '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'],
|
||||||
|
methods: ['GET'],
|
||||||
|
)]
|
||||||
|
public function battleOgImage(Uuid $uuid, BattleCardGenerator $generator): BinaryFileResponse
|
||||||
|
{
|
||||||
|
$game = $this->repo->findOneBy(['uuid' => $uuid]);
|
||||||
|
if (!$game) {
|
||||||
|
throw $this->createNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $generator->generate($game);
|
||||||
|
$response = new BinaryFileResponse($path);
|
||||||
|
$response->headers->set('Content-Type', 'image/png');
|
||||||
|
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE);
|
||||||
|
$response->setMaxAge(86400 * 30);
|
||||||
|
$response->setPublic();
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
#[Route('/profile/avatar', name: 'MineSeekerBundle_profile_avatar', methods: ['POST'])]
|
#[Route('/profile/avatar', name: 'MineSeekerBundle_profile_avatar', methods: ['POST'])]
|
||||||
public function uploadAvatar(
|
public function uploadAvatar(
|
||||||
Request $request,
|
Request $request,
|
||||||
@@ -232,18 +272,24 @@ class ProfileController extends AbstractController
|
|||||||
$newPath = sprintf('avatar/%s.%s', Uuid::v4()->toRfc4122(), $ext);
|
$newPath = sprintf('avatar/%s.%s', Uuid::v4()->toRfc4122(), $ext);
|
||||||
$oldPath = $user->getAvatarPath();
|
$oldPath = $user->getAvatarPath();
|
||||||
|
|
||||||
// Remove old file and any cached thumbnails
|
/** Remove old file and any cached thumbnails */
|
||||||
if ($oldPath) {
|
if ($oldPath) {
|
||||||
try {
|
try {
|
||||||
$mediaStorage->delete($oldPath);
|
$mediaStorage->delete($oldPath);
|
||||||
} catch (\Throwable) {
|
} catch (Throwable) {
|
||||||
|
$this->logger->error('Unable to delete old avatar: ' . $oldPath);
|
||||||
}
|
}
|
||||||
$cacheManager->remove($oldPath, 'avatar_thumb');
|
$cacheManager->remove($oldPath, 'avatar_thumb');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload original to MinIO media/avatar/
|
/** Upload original to MinIO media/avatar/ */
|
||||||
$stream = fopen($file->getPathname(), 'r');
|
$stream = fopen($file->getPathname(), 'rb');
|
||||||
|
try {
|
||||||
$mediaStorage->writeStream($newPath, $stream);
|
$mediaStorage->writeStream($newPath, $stream);
|
||||||
|
} catch (FilesystemException $e) {
|
||||||
|
$this->logger->error('Unable to write new avatar: ' . $e->getMessage());
|
||||||
|
throw new RuntimeException('Unable to write new avatar: ' . $e->getMessage());
|
||||||
|
}
|
||||||
fclose($stream);
|
fclose($stream);
|
||||||
|
|
||||||
$user->setAvatarPath($newPath);
|
$user->setAvatarPath($newPath);
|
||||||
@@ -274,7 +320,7 @@ class ProfileController extends AbstractController
|
|||||||
return $this->render('Security/profile_security.html.twig', [
|
return $this->render('Security/profile_security.html.twig', [
|
||||||
'credentials' => $credentialsData,
|
'credentials' => $credentialsData,
|
||||||
'isTotpEnabled' => $user->isTotpAuthenticationEnabled(),
|
'isTotpEnabled' => $user->isTotpAuthenticationEnabled(),
|
||||||
'backupCodesCount' => \count($user->getBackupCodes()),
|
'backupCodesCount' => count($user->getBackupCodes()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use Doctrine\ORM\Mapping\JoinColumn;
|
|||||||
use Doctrine\ORM\Mapping\ManyToOne;
|
use Doctrine\ORM\Mapping\ManyToOne;
|
||||||
use Doctrine\ORM\Mapping\OneToMany;
|
use Doctrine\ORM\Mapping\OneToMany;
|
||||||
use Doctrine\ORM\Mapping\OneToOne;
|
use Doctrine\ORM\Mapping\OneToOne;
|
||||||
|
use Symfony\Component\Uid\Uuid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class PlayedGame
|
* Class PlayedGame
|
||||||
@@ -40,6 +41,9 @@ class PlayedGame
|
|||||||
#[Id, GeneratedValue, Column]
|
#[Id, GeneratedValue, Column]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[Column(type: 'uuid', unique: true)]
|
||||||
|
private ?Uuid $uuid = null;
|
||||||
|
|
||||||
#[Column(length: 50)]
|
#[Column(length: 50)]
|
||||||
private ?string $gameAssoc = null;
|
private ?string $gameAssoc = null;
|
||||||
|
|
||||||
@@ -90,6 +94,7 @@ class PlayedGame
|
|||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->steps = new ArrayCollection();
|
$this->steps = new ArrayCollection();
|
||||||
|
$this->uuid = Uuid::v4();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
@@ -97,6 +102,16 @@ class PlayedGame
|
|||||||
return $this->id;
|
return $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getUuid(): ?Uuid
|
||||||
|
{
|
||||||
|
return $this->uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUuid(?Uuid $uuid): void
|
||||||
|
{
|
||||||
|
$this->uuid = $uuid;
|
||||||
|
}
|
||||||
|
|
||||||
public function getGameAssoc(): ?string
|
public function getGameAssoc(): ?string
|
||||||
{
|
{
|
||||||
return $this->gameAssoc;
|
return $this->gameAssoc;
|
||||||
|
|||||||
47
src/Migrations/2026/04/Version20260414000000.php
Normal file
47
src/Migrations/2026/04/Version20260414000000.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?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 Version20260414000000
|
||||||
|
*
|
||||||
|
* @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. 14.
|
||||||
|
*/
|
||||||
|
final class Version20260414000000 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add uuid column to played_game for shareable URLs';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE played_game ADD uuid UUID DEFAULT NULL');
|
||||||
|
$this->addSql('UPDATE played_game SET uuid = gen_random_uuid() WHERE uuid IS NULL');
|
||||||
|
$this->addSql('ALTER TABLE played_game ADD CONSTRAINT played_game_uuid_unique UNIQUE (uuid)');
|
||||||
|
$this->addSql('ALTER TABLE played_game ALTER COLUMN uuid SET NOT NULL');
|
||||||
|
$this->addSql('COMMENT ON COLUMN played_game.uuid IS \'(DC2Type:uuid)\'');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE played_game DROP uuid');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
180
src/Service/BattleCardGenerator.php
Normal file
180
src/Service/BattleCardGenerator.php
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
<?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\Service;
|
||||||
|
|
||||||
|
use App\Entity\PlayedGame;
|
||||||
|
use Symfony\Component\Uid\Uuid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class BattleCardGenerator
|
||||||
|
*
|
||||||
|
* Generates a 1200x630 PNG battle card for Open Graph sharing.
|
||||||
|
*
|
||||||
|
* @package App\Service
|
||||||
|
* @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. 14.
|
||||||
|
*/
|
||||||
|
class BattleCardGenerator
|
||||||
|
{
|
||||||
|
private const W = 1200;
|
||||||
|
private const H = 630;
|
||||||
|
private const FONT = '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf';
|
||||||
|
|
||||||
|
public function __construct(private readonly string $cacheDir) { }
|
||||||
|
|
||||||
|
/** Returns a deterministic UUID v5 for the given battle ID — same battle always maps to the same filename. */
|
||||||
|
public function cachePath(int $battleId): string
|
||||||
|
{
|
||||||
|
$uuid = Uuid::v5(Uuid::fromString(Uuid::NAMESPACE_URL), 'mineseeker-battle-' . $battleId);
|
||||||
|
|
||||||
|
return $this->cacheDir . '/' . $uuid->toRfc4122() . '.png';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generate(PlayedGame $game): string
|
||||||
|
{
|
||||||
|
$path = $this->cachePath((int)$game->getId());
|
||||||
|
|
||||||
|
if (is_file($path)) {
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_dir($this->cacheDir)) {
|
||||||
|
mkdir($this->cacheDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->render($game, $path);
|
||||||
|
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function render(PlayedGame $game, string $dest): void
|
||||||
|
{
|
||||||
|
$im = imagecreatetruecolor(self::W, self::H);
|
||||||
|
|
||||||
|
// Palette
|
||||||
|
$bg = imagecolorallocate($im, 13, 13, 28);
|
||||||
|
$dot = imagecolorallocate($im, 30, 30, 55);
|
||||||
|
$divider = imagecolorallocate($im, 40, 40, 70);
|
||||||
|
$white = imagecolorallocate($im, 230, 230, 240);
|
||||||
|
$muted = imagecolorallocate($im, 90, 90, 115);
|
||||||
|
$red = imagecolorallocate($im, 246, 125, 82);
|
||||||
|
$blue = imagecolorallocate($im, 149, 207, 245);
|
||||||
|
$gold = imagecolorallocate($im, 255, 200, 50);
|
||||||
|
|
||||||
|
// Background
|
||||||
|
imagefill($im, 0, 0, $bg);
|
||||||
|
|
||||||
|
// Dot-grid texture
|
||||||
|
for ($x = 40; $x < self::W; $x += 40) {
|
||||||
|
for ($y = 40; $y < self::H; $y += 40) {
|
||||||
|
imagesetpixel($im, $x, $y, $dot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal accent lines
|
||||||
|
imageline($im, 0, 90, self::W, 90, $divider);
|
||||||
|
imageline($im, 0, self::H - 60, self::W, self::H - 60, $divider);
|
||||||
|
|
||||||
|
// Vertical centre divider
|
||||||
|
imageline($im, self::W / 2, 110, self::W / 2, self::H - 80, $divider);
|
||||||
|
|
||||||
|
// Resolve names
|
||||||
|
$redName = $game->getRed()?->getUsername()
|
||||||
|
?? ($game->getRedAnon() !== null ? 'Anonymous' : 'Guest');
|
||||||
|
$blueName = $game->getBlue()?->getUsername()
|
||||||
|
?? ($game->getBlueAnon() !== null ? 'Anonymous' : 'Guest');
|
||||||
|
$redPts = $game->getRedPoints();
|
||||||
|
$bluePts = $game->getBluePoints();
|
||||||
|
$resign = $game->getResign();
|
||||||
|
|
||||||
|
// Winner
|
||||||
|
$winner = null;
|
||||||
|
if ($resign === 'red') {
|
||||||
|
$winner = 'blue';
|
||||||
|
} elseif ($resign === 'blue') {
|
||||||
|
$winner = 'red';
|
||||||
|
} elseif ($redPts !== null && $bluePts !== null) {
|
||||||
|
if ($redPts > $bluePts) $winner = 'red';
|
||||||
|
elseif ($bluePts > $redPts) $winner = 'blue';
|
||||||
|
else $winner = 'draw';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->centeredText($im, 'MineSeeker', 20, self::W / 2, 58, $muted);
|
||||||
|
|
||||||
|
$this->centeredText($im, 'RED', 16, self::W / 4, 130, $red);
|
||||||
|
$this->centeredText($im, 'BLUE', 16, self::W * 3 / 4, 130, $blue);
|
||||||
|
|
||||||
|
$redColor = $winner === 'red' ? $gold : ($winner === 'draw' ? $white : $red);
|
||||||
|
$blueColor = $winner === 'blue' ? $gold : ($winner === 'draw' ? $white : $blue);
|
||||||
|
|
||||||
|
$this->centeredTextFit($im, $redName, 48, self::W / 4, 265, $redColor, self::W / 2 - 80);
|
||||||
|
$this->centeredTextFit($im, $blueName, 48, self::W * 3 / 4, 265, $blueColor, self::W / 2 - 80);
|
||||||
|
|
||||||
|
$scoreText = $redPts !== null && $bluePts !== null ? $redPts . ' : ' . $bluePts : 'VS';
|
||||||
|
$this->centeredText($im, $scoreText, 72, self::W / 2, 390, $white);
|
||||||
|
|
||||||
|
if ($winner === 'red') {
|
||||||
|
$resultText = $redName . ' wins';
|
||||||
|
$resultColor = $gold;
|
||||||
|
} elseif ($winner === 'blue') {
|
||||||
|
$resultText = $blueName . ' wins';
|
||||||
|
$resultColor = $gold;
|
||||||
|
} elseif ($winner === 'draw') {
|
||||||
|
$resultText = 'Draw';
|
||||||
|
$resultColor = $muted;
|
||||||
|
} else {
|
||||||
|
$resultText = '';
|
||||||
|
$resultColor = $muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($resultText !== '') {
|
||||||
|
$this->centeredText($im, $resultText, 30, self::W / 2, 460, $resultColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($resign) {
|
||||||
|
$this->centeredText($im, ucfirst($resign) . ' resigned', 18, self::W / 2, 498, $muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->centeredText($im, 'mineseeker.hu', 16, self::W / 2, self::H - 20, $muted);
|
||||||
|
|
||||||
|
imagepng($im, $dest);
|
||||||
|
imagedestroy($im);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Render text centered on $cx. */
|
||||||
|
private function centeredText(\GdImage $im, string $text, int $size, int $cx, int $y, int $color): void
|
||||||
|
{
|
||||||
|
$bbox = imagettfbbox($size, 0, self::FONT, $text);
|
||||||
|
$w = $bbox[2] - $bbox[0];
|
||||||
|
imagettftext($im, $size, 0, (int)($cx - $w / 2), $y, $color, self::FONT, $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Render text centered on $cx, shrinking font size to fit $maxWidth. */
|
||||||
|
private function centeredTextFit(
|
||||||
|
\GdImage $im,
|
||||||
|
string $text,
|
||||||
|
int $size,
|
||||||
|
int $cx,
|
||||||
|
int $y,
|
||||||
|
int $color,
|
||||||
|
int $maxWidth
|
||||||
|
): void {
|
||||||
|
$bbox = imagettfbbox($size, 0, self::FONT, $text);
|
||||||
|
$w = $bbox[2] - $bbox[0];
|
||||||
|
if ($w > $maxWidth) {
|
||||||
|
$size = (int)($size * $maxWidth / $w);
|
||||||
|
}
|
||||||
|
$this->centeredText($im, $text, $size, $cx, $y, $color);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,32 +3,33 @@
|
|||||||
{% block title %} - Battle Report{% endblock %}
|
{% block title %} - Battle Report{% endblock %}
|
||||||
|
|
||||||
{% block metas %}
|
{% block metas %}
|
||||||
{% set shareUrl = url('MineSeekerBundle_battle_share', { id: game.id }) %}
|
{%- set shareUrl = url('MineSeekerBundle_battle_share', { uuid: game.uuid }) -%}
|
||||||
|
{%- set _ogImage = url('MineSeekerBundle_og_battle', { uuid: game.uuid }) -%}
|
||||||
<meta property="og:url" content="{{ shareUrl }}"/>
|
<meta property="og:url" content="{{ shareUrl }}"/>
|
||||||
<meta property="og:type" content="website"/>
|
<meta property="og:type" content="article"/>
|
||||||
|
<meta property="og:site_name" content="MineSeeker"/>
|
||||||
|
<meta property="og:locale" content="en_US"/>
|
||||||
<meta property="og:title" content="{{ ogTitle }}"/>
|
<meta property="og:title" content="{{ ogTitle }}"/>
|
||||||
<meta property="og:description" content="{{ ogDesc }}"/>
|
<meta property="og:description" content="{{ ogDesc }}"/>
|
||||||
<meta property="og:image" content="{{ app.request.getSchemeAndHttpHost() }}{{ asset('images/mine-1600x627.png') }}"/>
|
<meta property="og:image" content="{{ _ogImage }}"/>
|
||||||
<meta property="og:image:width" content="1600"/>
|
<meta property="og:image:width" content="1600"/>
|
||||||
<meta property="og:image:height" content="627"/>
|
<meta property="og:image:height" content="627"/>
|
||||||
|
<meta property="og:image:alt" content="{{ ogTitle }}"/>
|
||||||
<meta name="twitter:card" content="summary_large_image"/>
|
<meta name="twitter:card" content="summary_large_image"/>
|
||||||
|
<meta name="twitter:site" content="@MineSeeker"/>
|
||||||
<meta name="twitter:title" content="{{ ogTitle }}"/>
|
<meta name="twitter:title" content="{{ ogTitle }}"/>
|
||||||
<meta name="twitter:description" content="{{ ogDesc }}"/>
|
<meta name="twitter:description" content="{{ ogDesc }}"/>
|
||||||
<meta name="twitter:image" content="{{ app.request.getSchemeAndHttpHost() }}{{ asset('images/mine-1600x627.png') }}"/>
|
<meta name="twitter:image" content="{{ _ogImage }}"/>
|
||||||
|
<meta name="twitter:image:alt" content="{{ ogTitle }}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="bshare-page">
|
<div class="bshare-page">
|
||||||
|
|
||||||
<div class="bshare-card">
|
<div class="bshare-card">
|
||||||
|
|
||||||
<div class="bshare-card__eyebrow">
|
<div class="bshare-card__eyebrow">
|
||||||
<i class="fas fa-crosshairs"></i> Battle Report
|
<i class="fas fa-crosshairs"></i> Battle Report
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# VS Header #}
|
|
||||||
<div class="bshare-vs">
|
<div class="bshare-vs">
|
||||||
|
|
||||||
<div class="bshare-player bshare-player--red">
|
<div class="bshare-player bshare-player--red">
|
||||||
<div class="bshare-avatar bshare-avatar--red">
|
<div class="bshare-avatar bshare-avatar--red">
|
||||||
{{ redName|slice(0,2)|upper }}
|
{{ redName|slice(0,2)|upper }}
|
||||||
@@ -36,7 +37,6 @@
|
|||||||
<span class="bshare-player__name">{{ redName }}</span>
|
<span class="bshare-player__name">{{ redName }}</span>
|
||||||
<span class="bshare-player__side">Red</span>
|
<span class="bshare-player__side">Red</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bshare-vs__center">
|
<div class="bshare-vs__center">
|
||||||
{% if redPts is not null and bluePts is not null %}
|
{% if redPts is not null and bluePts is not null %}
|
||||||
<div class="bshare-score">
|
<div class="bshare-score">
|
||||||
@@ -48,8 +48,6 @@
|
|||||||
<div class="bshare-score bshare-score--na">— : —</div>
|
<div class="bshare-score bshare-score--na">— : —</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="bshare-vs__label">VS</div>
|
<div class="bshare-vs__label">VS</div>
|
||||||
|
|
||||||
{# Result badge #}
|
|
||||||
{% if resign == 'red' %}
|
{% if resign == 'red' %}
|
||||||
<div class="bshare-badge bshare-badge--blue">
|
<div class="bshare-badge bshare-badge--blue">
|
||||||
<i class="fas fa-trophy"></i> Blue wins
|
<i class="fas fa-trophy"></i> Blue wins
|
||||||
@@ -74,7 +72,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bshare-player bshare-player--blue">
|
<div class="bshare-player bshare-player--blue">
|
||||||
<div class="bshare-avatar bshare-avatar--blue">
|
<div class="bshare-avatar bshare-avatar--blue">
|
||||||
{{ blueName|slice(0,2)|upper }}
|
{{ blueName|slice(0,2)|upper }}
|
||||||
@@ -82,10 +79,7 @@
|
|||||||
<span class="bshare-player__name">{{ blueName }}</span>
|
<span class="bshare-player__name">{{ blueName }}</span>
|
||||||
<span class="bshare-player__side">Blue</span>
|
<span class="bshare-player__side">Blue</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Details #}
|
|
||||||
<div class="bshare-details">
|
<div class="bshare-details">
|
||||||
{% if resign %}
|
{% if resign %}
|
||||||
<div class="bshare-detail">
|
<div class="bshare-detail">
|
||||||
@@ -112,7 +106,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bshare-cta">
|
<div class="bshare-cta">
|
||||||
<a href="{{ path('MineSeekerBundle_gamePlay') }}" class="bshare-btn">
|
<a href="{{ path('MineSeekerBundle_gamePlay') }}" class="bshare-btn">
|
||||||
<i class="fas fa-play"></i> Play MineSeeker
|
<i class="fas fa-play"></i> Play MineSeeker
|
||||||
@@ -121,8 +114,6 @@
|
|||||||
<i class="fas fa-house"></i> Homepage
|
<i class="fas fa-house"></i> Homepage
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -3,12 +3,23 @@
|
|||||||
{% block title %} - The Game{% endblock %}
|
{% block title %} - The Game{% endblock %}
|
||||||
|
|
||||||
{% block metas %}
|
{% block metas %}
|
||||||
|
{%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%}
|
||||||
<meta property="og:url" content="{{ url('MineSeekerBundle_homepage') }}"/>
|
<meta property="og:url" content="{{ url('MineSeekerBundle_homepage') }}"/>
|
||||||
<meta property="og:type" content="website"/>
|
<meta property="og:type" content="website"/>
|
||||||
<meta property="og:title" content="MineSeeker"/>
|
<meta property="og:site_name" content="MineSeeker"/>
|
||||||
<meta property="og:description" content="A multiplayer minesweeper game"/>
|
<meta property="og:locale" content="en_US"/>
|
||||||
<meta property="og:image"
|
<meta property="og:title" content="MineSeeker — Multiplayer Minesweeper"/>
|
||||||
content="{{ app.request.getSchemeAndHttpHost() }}{{ asset('images/mine-1600x627.png') }}"/>
|
<meta property="og:description"
|
||||||
|
content="Race a friend on a hidden minefield. Real-time 1v1 minesweeper in your browser — no account needed. Just play."/>
|
||||||
|
<meta property="og:image" content="{{ _ogImage }}"/>
|
||||||
|
<meta property="og:image:width" content="1600"/>
|
||||||
|
<meta property="og:image:height" content="627"/>
|
||||||
|
<meta property="og:image:alt" content="MineSeeker — Multiplayer Minesweeper"/>
|
||||||
|
<meta name="twitter:card" content="summary_large_image"/>
|
||||||
|
<meta name="twitter:title" content="MineSeeker — Multiplayer Minesweeper"/>
|
||||||
|
<meta name="twitter:description"
|
||||||
|
content="Race a friend on a hidden minefield. Real-time 1v1 minesweeper in your browser — no account needed. Just play."/>
|
||||||
|
<meta name="twitter:image" content="{{ _ogImage }}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
|
|||||||
@@ -2,12 +2,26 @@
|
|||||||
|
|
||||||
{% block title %} - Contact{% endblock %}
|
{% block title %} - Contact{% endblock %}
|
||||||
|
|
||||||
|
{% block metas %}
|
||||||
|
{%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%}
|
||||||
|
<meta property="og:url" content="{{ url('MineSeekerBundle_contact') }}"/>
|
||||||
|
<meta property="og:type" content="website"/>
|
||||||
|
<meta property="og:site_name" content="MineSeeker"/>
|
||||||
|
<meta property="og:title" content="Contact · MineSeeker"/>
|
||||||
|
<meta property="og:description" content="Get in touch with the MineSeeker team."/>
|
||||||
|
<meta property="og:image" content="{{ _ogImage }}"/>
|
||||||
|
<meta property="og:image:width" content="1600"/>
|
||||||
|
<meta property="og:image:height" content="627"/>
|
||||||
|
<meta name="twitter:card" content="summary_large_image"/>
|
||||||
|
<meta name="twitter:title" content="Contact · MineSeeker"/>
|
||||||
|
<meta name="twitter:description" content="Get in touch with the MineSeeker team."/>
|
||||||
|
<meta name="twitter:image" content="{{ _ogImage }}"/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="txt">
|
<div class="txt">
|
||||||
<h2>Contact and user support</h2>
|
<h2>Contact and user support</h2>
|
||||||
|
|
||||||
<h3>Under construction</h3>
|
<h3>Under construction</h3>
|
||||||
|
|
||||||
<a href="mailto:langlasz@gmail.com">langlasz@gmail.com</a>
|
<a href="mailto:langlasz@gmail.com">langlasz@gmail.com</a>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -2,6 +2,22 @@
|
|||||||
|
|
||||||
{% block title %} - Privacy Policy{% endblock %}
|
{% block title %} - Privacy Policy{% endblock %}
|
||||||
|
|
||||||
|
{% block metas %}
|
||||||
|
{%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%}
|
||||||
|
<meta property="og:url" content="{{ url('MineSeekerBundle_privacy') }}"/>
|
||||||
|
<meta property="og:type" content="website"/>
|
||||||
|
<meta property="og:site_name" content="MineSeeker"/>
|
||||||
|
<meta property="og:title" content="Privacy Policy · MineSeeker"/>
|
||||||
|
<meta property="og:description" content="Read how MineSeeker collects, uses and protects your personal data."/>
|
||||||
|
<meta property="og:image" content="{{ _ogImage }}"/>
|
||||||
|
<meta property="og:image:width" content="1600"/>
|
||||||
|
<meta property="og:image:height" content="627"/>
|
||||||
|
<meta name="twitter:card" content="summary_large_image"/>
|
||||||
|
<meta name="twitter:title" content="Privacy Policy · MineSeeker"/>
|
||||||
|
<meta name="twitter:description" content="Read how MineSeeker collects, uses and protects your personal data."/>
|
||||||
|
<meta name="twitter:image" content="{{ _ogImage }}"/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="txt">
|
<div class="txt">
|
||||||
<h2>MineSeeker Privacy Policy</h2>
|
<h2>MineSeeker Privacy Policy</h2>
|
||||||
|
|||||||
@@ -2,6 +2,22 @@
|
|||||||
|
|
||||||
{% block title %} - Terms of Service{% endblock %}
|
{% block title %} - Terms of Service{% endblock %}
|
||||||
|
|
||||||
|
{% block metas %}
|
||||||
|
{%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%}
|
||||||
|
<meta property="og:url" content="{{ url('MineSeekerBundle_terms') }}"/>
|
||||||
|
<meta property="og:type" content="website"/>
|
||||||
|
<meta property="og:site_name" content="MineSeeker"/>
|
||||||
|
<meta property="og:title" content="Terms of Use · MineSeeker"/>
|
||||||
|
<meta property="og:description" content="Read the MineSeeker terms of use before playing."/>
|
||||||
|
<meta property="og:image" content="{{ _ogImage }}"/>
|
||||||
|
<meta property="og:image:width" content="1600"/>
|
||||||
|
<meta property="og:image:height" content="627"/>
|
||||||
|
<meta name="twitter:card" content="summary_large_image"/>
|
||||||
|
<meta name="twitter:title" content="Terms of Use · MineSeeker"/>
|
||||||
|
<meta name="twitter:description" content="Read the MineSeeker terms of use before playing."/>
|
||||||
|
<meta name="twitter:image" content="{{ _ogImage }}"/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="txt">
|
<div class="txt">
|
||||||
<h2>MineSeeker Terms of Service</h2>
|
<h2>MineSeeker Terms of Service</h2>
|
||||||
|
|||||||
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
{% block title %} - Two-Factor Authentication{% endblock %}
|
{% block title %} - Two-Factor Authentication{% endblock %}
|
||||||
|
|
||||||
|
{% block metas %}
|
||||||
|
<meta name="robots" content="noindex,nofollow"/>
|
||||||
|
<meta property="og:url" content="{{ app.request.uri }}"/>
|
||||||
|
<meta property="og:type" content="website"/>
|
||||||
|
<meta property="og:site_name" content="MineSeeker"/>
|
||||||
|
<meta property="og:title" content="Two-Factor Authentication · MineSeeker"/>
|
||||||
|
<meta property="og:description" content="Verify your identity to access your MineSeeker account."/>
|
||||||
|
<meta name="twitter:card" content="summary"/>
|
||||||
|
<meta name="twitter:title" content="Two-Factor Authentication · MineSeeker"/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="auth-page">
|
<div class="auth-page">
|
||||||
<div class="auth-card">
|
<div class="auth-card">
|
||||||
|
|||||||
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
{% block title %} - Enable Two-Factor Authentication{% endblock %}
|
{% block title %} - Enable Two-Factor Authentication{% endblock %}
|
||||||
|
|
||||||
|
{% block metas %}
|
||||||
|
<meta name="robots" content="noindex,nofollow"/>
|
||||||
|
<meta property="og:url" content="{{ app.request.uri }}"/>
|
||||||
|
<meta property="og:type" content="website"/>
|
||||||
|
<meta property="og:site_name" content="MineSeeker"/>
|
||||||
|
<meta property="og:title" content="Enable 2FA · MineSeeker"/>
|
||||||
|
<meta property="og:description" content="Set up two-factor authentication to secure your MineSeeker account."/>
|
||||||
|
<meta name="twitter:card" content="summary"/>
|
||||||
|
<meta name="twitter:title" content="Enable 2FA · MineSeeker"/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="auth-page">
|
<div class="auth-page">
|
||||||
<div class="auth-card auth-card--wide">
|
<div class="auth-card auth-card--wide">
|
||||||
|
|||||||
@@ -2,6 +2,19 @@
|
|||||||
|
|
||||||
{% block title %} - Forgot Password{% endblock %}
|
{% block title %} - Forgot Password{% endblock %}
|
||||||
|
|
||||||
|
{% block metas %}
|
||||||
|
{%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%}
|
||||||
|
<meta name="robots" content="noindex,nofollow"/>
|
||||||
|
<meta property="og:url" content="{{ app.request.uri }}"/>
|
||||||
|
<meta property="og:type" content="website"/>
|
||||||
|
<meta property="og:site_name" content="MineSeeker"/>
|
||||||
|
<meta property="og:title" content="Reset Password · MineSeeker"/>
|
||||||
|
<meta property="og:description" content="Reset your MineSeeker account password."/>
|
||||||
|
<meta property="og:image" content="{{ _ogImage }}"/>
|
||||||
|
<meta name="twitter:card" content="summary"/>
|
||||||
|
<meta name="twitter:title" content="Reset Password · MineSeeker"/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="auth-page">
|
<div class="auth-page">
|
||||||
{% for email in app.flashes('reset_sent') %}
|
{% for email in app.flashes('reset_sent') %}
|
||||||
|
|||||||
@@ -2,6 +2,25 @@
|
|||||||
|
|
||||||
{% block title %} - Sign In{% endblock %}
|
{% block title %} - Sign In{% endblock %}
|
||||||
|
|
||||||
|
{% block metas %}
|
||||||
|
{%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%}
|
||||||
|
<meta name="robots" content="noindex,nofollow"/>
|
||||||
|
<meta property="og:url" content="{{ app.request.uri }}"/>
|
||||||
|
<meta property="og:type" content="website"/>
|
||||||
|
<meta property="og:site_name" content="MineSeeker"/>
|
||||||
|
<meta property="og:title" content="Sign In · MineSeeker"/>
|
||||||
|
<meta property="og:description"
|
||||||
|
content="Sign in to MineSeeker and keep track of your wins, stats and battle history."/>
|
||||||
|
<meta property="og:image" content="{{ _ogImage }}"/>
|
||||||
|
<meta property="og:image:width" content="1600"/>
|
||||||
|
<meta property="og:image:height" content="627"/>
|
||||||
|
<meta name="twitter:card" content="summary_large_image"/>
|
||||||
|
<meta name="twitter:title" content="Sign In · MineSeeker"/>
|
||||||
|
<meta name="twitter:description"
|
||||||
|
content="Sign in to MineSeeker and keep track of your wins, stats and battle history."/>
|
||||||
|
<meta name="twitter:image" content="{{ _ogImage }}"/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="auth-page">
|
<div class="auth-page">
|
||||||
{% for message in app.flashes('success') %}
|
{% for message in app.flashes('success') %}
|
||||||
|
|||||||
@@ -2,6 +2,25 @@
|
|||||||
|
|
||||||
{% block title %} - Profile{% endblock %}
|
{% block title %} - Profile{% endblock %}
|
||||||
|
|
||||||
|
{% block metas %}
|
||||||
|
{%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%}
|
||||||
|
<meta name="robots" content="noindex,nofollow"/>
|
||||||
|
<meta property="og:url" content="{{ url('MineSeekerBundle_profile') }}"/>
|
||||||
|
<meta property="og:type" content="profile"/>
|
||||||
|
<meta property="og:site_name" content="MineSeeker"/>
|
||||||
|
<meta property="og:title" content="{{ app.user.username }} · MineSeeker"/>
|
||||||
|
<meta property="og:description"
|
||||||
|
content="View {{ app.user.username }}'s battle stats, win rate and recent games on MineSeeker."/>
|
||||||
|
<meta property="og:image" content="{{ _ogImage }}"/>
|
||||||
|
<meta property="og:image:width" content="1600"/>
|
||||||
|
<meta property="og:image:height" content="627"/>
|
||||||
|
<meta name="twitter:card" content="summary_large_image"/>
|
||||||
|
<meta name="twitter:title" content="{{ app.user.username }} · MineSeeker"/>
|
||||||
|
<meta name="twitter:description"
|
||||||
|
content="View {{ app.user.username }}'s battle stats, win rate and recent games on MineSeeker."/>
|
||||||
|
<meta name="twitter:image" content="{{ _ogImage }}"/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="profile-page">
|
<div class="profile-page">
|
||||||
<div class="profile-header">
|
<div class="profile-header">
|
||||||
|
|||||||
@@ -2,6 +2,25 @@
|
|||||||
|
|
||||||
{% block title %} - Security Settings{% endblock %}
|
{% block title %} - Security Settings{% endblock %}
|
||||||
|
|
||||||
|
{% block metas %}
|
||||||
|
{%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%}
|
||||||
|
<meta name="robots" content="noindex,nofollow"/>
|
||||||
|
<meta property="og:url" content="{{ url('MineSeekerBundle_profile_security') }}"/>
|
||||||
|
<meta property="og:type" content="website"/>
|
||||||
|
<meta property="og:site_name" content="MineSeeker"/>
|
||||||
|
<meta property="og:title" content="Security Settings · MineSeeker"/>
|
||||||
|
<meta property="og:description"
|
||||||
|
content="Manage your MineSeeker account security — passkeys, two-factor authentication and more."/>
|
||||||
|
<meta property="og:image" content="{{ _ogImage }}"/>
|
||||||
|
<meta property="og:image:width" content="1600"/>
|
||||||
|
<meta property="og:image:height" content="627"/>
|
||||||
|
<meta name="twitter:card" content="summary_large_image"/>
|
||||||
|
<meta name="twitter:title" content="Security Settings · MineSeeker"/>
|
||||||
|
<meta name="twitter:description"
|
||||||
|
content="Manage your MineSeeker account security — passkeys, two-factor authentication and more."/>
|
||||||
|
<meta name="twitter:image" content="{{ _ogImage }}"/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="profile-page">
|
<div class="profile-page">
|
||||||
<div class="profile-actions">
|
<div class="profile-actions">
|
||||||
|
|||||||
@@ -2,6 +2,24 @@
|
|||||||
|
|
||||||
{% block title %} - Register{% endblock %}
|
{% block title %} - Register{% endblock %}
|
||||||
|
|
||||||
|
{% block metas %}
|
||||||
|
{%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%}
|
||||||
|
<meta property="og:url" content="{{ app.request.uri }}"/>
|
||||||
|
<meta property="og:type" content="website"/>
|
||||||
|
<meta property="og:site_name" content="MineSeeker"/>
|
||||||
|
<meta property="og:title" content="Create Account · MineSeeker"/>
|
||||||
|
<meta property="og:description"
|
||||||
|
content="Join MineSeeker for free. Track your wins, relive your best battles and prove you're the better minesweeper."/>
|
||||||
|
<meta property="og:image" content="{{ _ogImage }}"/>
|
||||||
|
<meta property="og:image:width" content="1600"/>
|
||||||
|
<meta property="og:image:height" content="627"/>
|
||||||
|
<meta name="twitter:card" content="summary_large_image"/>
|
||||||
|
<meta name="twitter:title" content="Create Account · MineSeeker"/>
|
||||||
|
<meta name="twitter:description"
|
||||||
|
content="Join MineSeeker for free. Track your wins, relive your best battles and prove you're the better minesweeper."/>
|
||||||
|
<meta name="twitter:image" content="{{ _ogImage }}"/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="auth-page">
|
<div class="auth-page">
|
||||||
{% for email in app.flashes('verify_email') %}
|
{% for email in app.flashes('verify_email') %}
|
||||||
|
|||||||
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
{% block title %} - Reset Password{% endblock %}
|
{% block title %} - Reset Password{% endblock %}
|
||||||
|
|
||||||
|
{% block metas %}
|
||||||
|
<meta name="robots" content="noindex,nofollow"/>
|
||||||
|
<meta property="og:url" content="{{ app.request.uri }}"/>
|
||||||
|
<meta property="og:type" content="website"/>
|
||||||
|
<meta property="og:site_name" content="MineSeeker"/>
|
||||||
|
<meta property="og:title" content="Set New Password · MineSeeker"/>
|
||||||
|
<meta property="og:description" content="Set a new password for your MineSeeker account."/>
|
||||||
|
<meta name="twitter:card" content="summary"/>
|
||||||
|
<meta name="twitter:title" content="Set New Password · MineSeeker"/>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="auth-page">
|
<div class="auth-page">
|
||||||
<div class="auth-card">
|
<div class="auth-card">
|
||||||
|
|||||||
Reference in New Issue
Block a user