From d515f42cfd04fd6dbc55372b82e00c1dd17bb57b Mon Sep 17 00:00:00 2001 From: Lang <7system7@gmail.com> Date: Tue, 14 Apr 2026 18:54:44 +0200 Subject: [PATCH] chg: usr: make fancy og tags - and create a special one for battle sharing #4 --- Dockerfile | 2 + assets/js/components/BattleDialog.jsx | 22 +- composer.json | 181 +++++++-------- config/services.yaml | 4 + src/Controller/ProfileController.php | 80 +++++-- src/Entity/PlayedGame.php | 15 ++ .../2026/04/Version20260414000000.php | 47 ++++ src/Service/BattleCardGenerator.php | 180 +++++++++++++++ templates/Game/battle_share.html.twig | 41 ++-- templates/Game/index.html.twig | 19 +- templates/Official/contact.html.twig | 30 ++- templates/Official/privacy.html.twig | 110 ++++++---- templates/Official/terms.html.twig | 206 ++++++++++-------- templates/Security/2fa.html.twig | 13 +- templates/Security/2fa_setup.html.twig | 11 + templates/Security/forgot_password.html.twig | 53 +++-- templates/Security/login.html.twig | 19 ++ templates/Security/profile.html.twig | 19 ++ templates/Security/profile_security.html.twig | 19 ++ templates/Security/register.html.twig | 18 ++ templates/Security/reset_password.html.twig | 11 + 21 files changed, 782 insertions(+), 318 deletions(-) create mode 100644 src/Migrations/2026/04/Version20260414000000.php create mode 100644 src/Service/BattleCardGenerator.php diff --git a/Dockerfile b/Dockerfile index 3540f0f..27fc57e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,8 @@ RUN install-php-extensions \ apcu \ 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 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" diff --git a/assets/js/components/BattleDialog.jsx b/assets/js/components/BattleDialog.jsx index b57a9ab..6454b24 100644 --- a/assets/js/components/BattleDialog.jsx +++ b/assets/js/components/BattleDialog.jsx @@ -134,8 +134,8 @@ function StatRow({ icon, label, value, valueColor }) { } export default function BattleDialog({ games }) { - const [open, setOpen] = useState(false); - const [game, setGame] = useState(null); + const [open, setOpen] = useState(false); + const [game, setGame] = useState(null); const [copied, setCopied] = useState(false); useEffect(() => { @@ -156,18 +156,18 @@ export default function BattleDialog({ games }) { return ; } - const meta = RESULT_META[game.result] ?? RESULT_META.draw; - const resign = game.resign; + const meta = RESULT_META[game.result] ?? RESULT_META.draw; + const resign = game.resign; const endReason = resign - ? `${resign.charAt(0).toUpperCase() + resign.slice(1)} resigned` - : 'Points'; - const shareUrl = `${window.location.origin}/battle/${game.id}`; + ? `${resign.charAt(0).toUpperCase() + resign.slice(1)} resigned` + : 'Points'; + const shareUrl = `${window.location.origin}/battle/${game.uuid}`; const handleShare = () => { - navigator.clipboard.writeText(shareUrl).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 2200); - }); + navigator.clipboard.writeText(shareUrl).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2200); + }); }; return ( diff --git a/composer.json b/composer.json index 405c130..fc48f07 100644 --- a/composer.json +++ b/composer.json @@ -1,94 +1,95 @@ { - "type": "project", - "license": "proprietary", - "require": { - "php": ">=8.5", - "ext-iconv": "*", - "ext-json": "*", - "doctrine/dbal": "^3.7", - "doctrine/doctrine-bundle": ">=2.11 <2.14", - "doctrine/doctrine-migrations-bundle": "^3.0", - "doctrine/orm": "^2.6", - "endroid/qr-code": "^6.1", - "league/flysystem-aws-s3-v3": "^3.0", - "league/flysystem-bundle": "^3.6", - "liip/imagine-bundle": "^2.13", - "pentatrion/vite-bundle": "^8.2", - "scheb/2fa-backup-code": "^8.5", - "scheb/2fa-bundle": "^8.5", - "scheb/2fa-totp": "^8.5", - "symfony/console": "7.4.*", - "symfony/flex": "^2.10.0", - "symfony/form": "7.4.*", - "symfony/framework-bundle": "7.4.*", - "symfony/http-client": "7.4.*", - "symfony/mailer": "7.4.*", - "symfony/mercure": "^0.6", - "symfony/mercure-bundle": "*", - "symfony/monolog-bundle": "^3.8", - "symfony/security-bundle": "7.4.*", - "symfony/translation": "7.4.*", - "symfony/twig-bundle": "7.4.*", - "symfony/validator": "7.4.*", - "symfony/yaml": "7.4.*", - "web-auth/webauthn-framework": "^5.2" + "type": "project", + "license": "proprietary", + "require": { + "php": ">=8.5", + "ext-iconv": "*", + "ext-json": "*", + "ext-gd": "*", + "doctrine/dbal": "^3.7", + "doctrine/doctrine-bundle": ">=2.11 <2.14", + "doctrine/doctrine-migrations-bundle": "^3.0", + "doctrine/orm": "^2.6", + "endroid/qr-code": "^6.1", + "league/flysystem-aws-s3-v3": "^3.0", + "league/flysystem-bundle": "^3.6", + "liip/imagine-bundle": "^2.13", + "pentatrion/vite-bundle": "^8.2", + "scheb/2fa-backup-code": "^8.5", + "scheb/2fa-bundle": "^8.5", + "scheb/2fa-totp": "^8.5", + "symfony/console": "7.4.*", + "symfony/flex": "^2.10.0", + "symfony/form": "7.4.*", + "symfony/framework-bundle": "7.4.*", + "symfony/http-client": "7.4.*", + "symfony/mailer": "7.4.*", + "symfony/mercure": "^0.6", + "symfony/mercure-bundle": "*", + "symfony/monolog-bundle": "^3.8", + "symfony/security-bundle": "7.4.*", + "symfony/translation": "7.4.*", + "symfony/twig-bundle": "7.4.*", + "symfony/validator": "7.4.*", + "symfony/yaml": "7.4.*", + "web-auth/webauthn-framework": "^5.2" + }, + "require-dev": { + "firebase/php-jwt": "^7.0", + "roave/security-advisories": "dev-master", + "symfony/dotenv": "7.4.*", + "symfony/maker-bundle": "^1.5", + "symfony/stopwatch": "7.4.*", + "symfony/web-profiler-bundle": "7.4.*" + }, + "config": { + "preferred-install": { + "*": "dist" }, - "require-dev": { - "firebase/php-jwt": "^7.0", - "roave/security-advisories": "dev-master", - "symfony/dotenv": "7.4.*", - "symfony/maker-bundle": "^1.5", - "symfony/stopwatch": "7.4.*", - "symfony/web-profiler-bundle": "7.4.*" - }, - "config": { - "preferred-install": { - "*": "dist" - }, - "sort-packages": true, - "allow-plugins": { - "symfony/flex": true - } - }, - "autoload": { - "psr-4": { - "App\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "App\\Tests\\": "tests/" - } - }, - "replace": { - "symfony/polyfill-iconv": "*", - "symfony/polyfill-ctype": "*", - "symfony/polyfill-php73": "*", - "symfony/polyfill-php72": "*", - "symfony/polyfill-php71": "*", - "symfony/polyfill-php70": "*", - "symfony/polyfill-php56": "*" - }, - "scripts": { - "auto-scripts": { - "cache:clear": "symfony-cmd", - "assets:install --symlink --relative %PUBLIC_DIR%": "symfony-cmd" - }, - "post-install-cmd": [ - "@auto-scripts" - ], - "post-update-cmd": [ - "@auto-scripts" - ] - }, - "conflict": { - "symfony/symfony": "*", - "doctrine/persistence": "<1.3" - }, - "extra": { - "symfony": { - "allow-contrib": false, - "require": "7.4.*" - } + "sort-packages": true, + "allow-plugins": { + "symfony/flex": true } + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + }, + "replace": { + "symfony/polyfill-iconv": "*", + "symfony/polyfill-ctype": "*", + "symfony/polyfill-php73": "*", + "symfony/polyfill-php72": "*", + "symfony/polyfill-php71": "*", + "symfony/polyfill-php70": "*", + "symfony/polyfill-php56": "*" + }, + "scripts": { + "auto-scripts": { + "cache:clear": "symfony-cmd", + "assets:install --symlink --relative %PUBLIC_DIR%": "symfony-cmd" + }, + "post-install-cmd": [ + "@auto-scripts" + ], + "post-update-cmd": [ + "@auto-scripts" + ] + }, + "conflict": { + "symfony/symfony": "*", + "doctrine/persistence": "<1.3" + }, + "extra": { + "symfony": { + "allow-contrib": false, + "require": "7.4.*" + } + } } diff --git a/config/services.yaml b/config/services.yaml index 3f6e65d..8560eaa 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -25,6 +25,10 @@ services: resource: '../src/Controller' tags: [ 'controller.service_arguments' ] + App\Service\BattleCardGenerator: + arguments: + $cacheDir: '%kernel.project_dir%/var/og-cache' + Aws\S3\S3Client: arguments: - version: 'latest' diff --git a/src/Controller/ProfileController.php b/src/Controller/ProfileController.php index bfec582..1899398 100644 --- a/src/Controller/ProfileController.php +++ b/src/Controller/ProfileController.php @@ -10,21 +10,31 @@ namespace App\Controller; +use App\Entity\PlayedGame; use App\Entity\User; use App\Repository\PlayedGameRepository; +use App\Service\BattleCardGenerator; use App\Service\WebAuthnService; +use DateTime; use Doctrine\ORM\EntityManagerInterface; +use League\Flysystem\FilesystemException; use League\Flysystem\FilesystemOperator; use Liip\ImagineBundle\Imagine\Cache\CacheManager; +use Psr\Log\LoggerInterface; +use RuntimeException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\File\UploadedFile; -use Symfony\Component\Uid\Uuid; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Uid\Uuid; +use Throwable; +use function count; /** * Class ProfileController @@ -41,7 +51,8 @@ class ProfileController extends AbstractController { public function __construct( 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); $draws = $this->repo->countDrawsForUser($user); - // Build monthly buckets for the last 6 months + /** Build monthly buckets for the last 6 months */ $monthlyData = []; 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'); $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); $userId = $user->getId(); @@ -113,7 +124,7 @@ class ProfileController extends AbstractController 'bestScore' => $this->repo->findBestScoreForUser($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; $resign = $game->getResign(); $myColor = $isRed ? 'red' : 'blue'; @@ -131,6 +142,7 @@ class ProfileController extends AbstractController return [ 'id' => $game->getId(), + 'uuid' => $game->getUuid()?->toRfc4122(), 'redName' => $game->getRed()?->getUsername() ?? $game->getRedAnon()?->getUserName() ?? 'Guest', 'blueName' => @@ -160,16 +172,21 @@ class ProfileController extends AbstractController ]); } - #[Route('/battle/{id}', name: 'MineSeekerBundle_battle_share', requirements: ['id' => '\d+'], methods: ['GET'])] - public function battleShare(int $id): Response + #[Route( + '/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) { throw $this->createNotFoundException('Battle not found.'); } - $redName = $game->getRed()?->getUsername() ?? $game->getRedAnon()?->getUserName() ?? 'Guest'; - $blueName = $game->getBlue()?->getUsername() ?? $game->getBlueAnon()?->getUserName() ?? 'Guest'; + $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(); @@ -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'])] public function uploadAvatar( Request $request, @@ -232,18 +272,24 @@ class ProfileController extends AbstractController $newPath = sprintf('avatar/%s.%s', Uuid::v4()->toRfc4122(), $ext); $oldPath = $user->getAvatarPath(); - // Remove old file and any cached thumbnails + /** Remove old file and any cached thumbnails */ if ($oldPath) { try { $mediaStorage->delete($oldPath); - } catch (\Throwable) { + } catch (Throwable) { + $this->logger->error('Unable to delete old avatar: ' . $oldPath); } $cacheManager->remove($oldPath, 'avatar_thumb'); } - // Upload original to MinIO media/avatar/ - $stream = fopen($file->getPathname(), 'r'); - $mediaStorage->writeStream($newPath, $stream); + /** Upload original to MinIO media/avatar/ */ + $stream = fopen($file->getPathname(), 'rb'); + try { + $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); $user->setAvatarPath($newPath); @@ -274,7 +320,7 @@ class ProfileController extends AbstractController return $this->render('Security/profile_security.html.twig', [ 'credentials' => $credentialsData, 'isTotpEnabled' => $user->isTotpAuthenticationEnabled(), - 'backupCodesCount' => \count($user->getBackupCodes()), + 'backupCodesCount' => count($user->getBackupCodes()), ]); } } diff --git a/src/Entity/PlayedGame.php b/src/Entity/PlayedGame.php index 4c00c13..b8d2701 100644 --- a/src/Entity/PlayedGame.php +++ b/src/Entity/PlayedGame.php @@ -23,6 +23,7 @@ use Doctrine\ORM\Mapping\JoinColumn; use Doctrine\ORM\Mapping\ManyToOne; use Doctrine\ORM\Mapping\OneToMany; use Doctrine\ORM\Mapping\OneToOne; +use Symfony\Component\Uid\Uuid; /** * Class PlayedGame @@ -40,6 +41,9 @@ class PlayedGame #[Id, GeneratedValue, Column] private ?int $id = null; + #[Column(type: 'uuid', unique: true)] + private ?Uuid $uuid = null; + #[Column(length: 50)] private ?string $gameAssoc = null; @@ -90,6 +94,7 @@ class PlayedGame public function __construct() { $this->steps = new ArrayCollection(); + $this->uuid = Uuid::v4(); } public function getId(): ?int @@ -97,6 +102,16 @@ class PlayedGame return $this->id; } + public function getUuid(): ?Uuid + { + return $this->uuid; + } + + public function setUuid(?Uuid $uuid): void + { + $this->uuid = $uuid; + } + public function getGameAssoc(): ?string { return $this->gameAssoc; diff --git a/src/Migrations/2026/04/Version20260414000000.php b/src/Migrations/2026/04/Version20260414000000.php new file mode 100644 index 0000000..fbbc48e --- /dev/null +++ b/src/Migrations/2026/04/Version20260414000000.php @@ -0,0 +1,47 @@ + + * @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'); + } +} + diff --git a/src/Service/BattleCardGenerator.php b/src/Service/BattleCardGenerator.php new file mode 100644 index 0000000..5c550d9 --- /dev/null +++ b/src/Service/BattleCardGenerator.php @@ -0,0 +1,180 @@ + + * @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); + } +} diff --git a/templates/Game/battle_share.html.twig b/templates/Game/battle_share.html.twig index 261a49d..7cf5338 100644 --- a/templates/Game/battle_share.html.twig +++ b/templates/Game/battle_share.html.twig @@ -3,32 +3,33 @@ {% block title %} - Battle Report{% endblock %} {% 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 }) -%} + + + + + - - + + - - + + + + - + + {% endblock %} {% block body %}
-
-
Battle Report
- - {# VS Header #}
-
{{ redName|slice(0,2)|upper }} @@ -36,7 +37,6 @@ {{ redName }} Red
-
{% if redPts is not null and bluePts is not null %}
@@ -48,8 +48,6 @@
— : —
{% endif %}
VS
- - {# Result badge #} {% if resign == 'red' %}
Blue wins @@ -74,7 +72,6 @@ {% endif %} {% endif %}
-
{{ blueName|slice(0,2)|upper }} @@ -82,10 +79,7 @@ {{ blueName }} Blue
-
- - {# Details #}
{% if resign %}
@@ -112,7 +106,6 @@
{% endif %}
- -
-
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/Game/index.html.twig b/templates/Game/index.html.twig index e8781da..de77929 100644 --- a/templates/Game/index.html.twig +++ b/templates/Game/index.html.twig @@ -3,12 +3,23 @@ {% block title %} - The Game{% endblock %} {% block metas %} + {%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%} - - - + + + + + + + + + + + + {% endblock %} {% block header %} diff --git a/templates/Official/contact.html.twig b/templates/Official/contact.html.twig index c98a896..486b1f2 100644 --- a/templates/Official/contact.html.twig +++ b/templates/Official/contact.html.twig @@ -2,12 +2,26 @@ {% block title %} - Contact{% endblock %} -{% block body %} -
-

Contact and user support

- -

Under construction

- - langlasz@gmail.com -
+{% block metas %} + {%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%} + + + + + + + + + + + + +{% endblock %} + +{% block body %} +
+

Contact and user support

+

Under construction

+ langlasz@gmail.com +
{% endblock %} diff --git a/templates/Official/privacy.html.twig b/templates/Official/privacy.html.twig index e88cd97..603eba7 100644 --- a/templates/Official/privacy.html.twig +++ b/templates/Official/privacy.html.twig @@ -2,51 +2,67 @@ {% block title %} - Privacy Policy{% endblock %} -{% block body %} -
-

MineSeeker Privacy Policy

- -

Your privacy is important to us.

- -

It is MineSeeker's policy to respect your privacy regarding any information we may collect while operating - our - website. Accordingly, we have developed this privacy policy in order for you to understand how we collect, - use, - communicate, disclose and otherwise make use of personal information. We have outlined our privacy policy - below.

- -
    -
  • We will collect personal information by lawful and fair means and, where appropriate, with the knowledge - or - consent of the individual concerned. -
  • -
  • Before or at the time of collecting personal information, we will identify the purposes for which - information is being collected. -
  • -
  • We will collect and use personal information solely for fulfilling those purposes specified by us and - for - other ancillary purposes, unless we obtain the consent of the individual concerned or as required by - law. -
  • -
  • Personal data should be relevant to the purposes for which it is to be used, and, to the extent - necessary - for those purposes, should be accurate, complete, and up-to-date. -
  • -
  • We will protect personal information by using reasonable security safeguards against loss or theft, as - well - as unauthorized access, disclosure, copying, use or modification. -
  • -
  • We will make readily available to customers information about our policies and practices relating to the - management of personal information. -
  • -
  • We will only retain personal information for as long as necessary for the fulfilment of those - purposes. -
  • -
- -

We are committed to conducting our business in accordance with these principles in order to ensure that the - confidentiality of personal information is protected and maintained. MineSeeker may change this privacy - policy - from time to time at MineSeeker's sole discretion.

-
+{% block metas %} + {%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%} + + + + + + + + + + + + +{% endblock %} + +{% block body %} +
+

MineSeeker Privacy Policy

+ +

Your privacy is important to us.

+ +

It is MineSeeker's policy to respect your privacy regarding any information we may collect while operating + our + website. Accordingly, we have developed this privacy policy in order for you to understand how we collect, + use, + communicate, disclose and otherwise make use of personal information. We have outlined our privacy policy + below.

+ +
    +
  • We will collect personal information by lawful and fair means and, where appropriate, with the knowledge + or + consent of the individual concerned. +
  • +
  • Before or at the time of collecting personal information, we will identify the purposes for which + information is being collected. +
  • +
  • We will collect and use personal information solely for fulfilling those purposes specified by us and + for + other ancillary purposes, unless we obtain the consent of the individual concerned or as required by + law. +
  • +
  • Personal data should be relevant to the purposes for which it is to be used, and, to the extent + necessary + for those purposes, should be accurate, complete, and up-to-date. +
  • +
  • We will protect personal information by using reasonable security safeguards against loss or theft, as + well + as unauthorized access, disclosure, copying, use or modification. +
  • +
  • We will make readily available to customers information about our policies and practices relating to the + management of personal information. +
  • +
  • We will only retain personal information for as long as necessary for the fulfilment of those + purposes. +
  • +
+ +

We are committed to conducting our business in accordance with these principles in order to ensure that the + confidentiality of personal information is protected and maintained. MineSeeker may change this privacy + policy + from time to time at MineSeeker's sole discretion.

+
{% endblock %} diff --git a/templates/Official/terms.html.twig b/templates/Official/terms.html.twig index 3eb64a6..d0cc74b 100644 --- a/templates/Official/terms.html.twig +++ b/templates/Official/terms.html.twig @@ -2,99 +2,115 @@ {% block title %} - Terms of Service{% endblock %} -{% block body %} -
-

MineSeeker Terms of Service

- -

1. Terms

- -

By accessing the website at https://www.mineseeker.hu, you are - agreeing to be bound by these terms of service, all applicable laws and regulations, and agree that you are - responsible for compliance with any applicable local laws. If you do not agree with any of these terms, you - are - prohibited from using or accessing this site. The materials contained in this website are protected by - applicable copyright and trademark law.

- -

2. Use License

- -
    -
  1. - Permission is granted to temporarily download one copy of the materials (information or software) on - MineSeeker's website for personal, non-commercial transitory viewing only. This is the grant of a - license, - not a transfer of title, and under this license you may not: - -
      -
    1. modify or copy the materials;
    2. -
    3. use the materials for any commercial purpose, or for any public display (commercial or - non-commercial); -
    4. -
    5. attempt to decompile or reverse engineer any software contained on MineSeeker's website;
    6. -
    7. remove any copyright or other proprietary notations from the materials; or
    8. -
    9. transfer the materials to another person or "mirror" the materials on any other server.
    10. -
    -
  2. -
  3. This license shall automatically terminate if you violate any of these restrictions and may be - terminated by - MineSeeker at any time. Upon terminating your viewing of these materials or upon the termination of this - license, you must destroy any downloaded materials in your possession whether in electronic or printed - format. -
  4. -
- -

3. Disclaimer

- -
    -
  1. The materials on MineSeeker's website are provided on an 'as is' basis. MineSeeker makes no warranties, - expressed or implied, and hereby disclaims and negates all other warranties including, without - limitation, - implied warranties or conditions of merchantability, fitness for a particular purpose, or - non-infringement - of intellectual property or other violation of rights. -
  2. -
  3. Further, MineSeeker does not warrant or make any representations concerning the accuracy, likely - results, or - reliability of the use of the materials on its website or otherwise relating to such materials or on any - sites linked to this site. -
  4. -
- -

4. Limitations

- -

In no event shall MineSeeker or its suppliers be liable for any damages (including, without limitation, - damages - for loss of data or profit, or due to business interruption) arising out of the use or inability to use the - materials on MineSeeker's website, even if MineSeeker or a MineSeeker authorized representative has been - notified orally or in writing of the possibility of such damage. Because some jurisdictions do not allow - limitations on implied warranties, or limitations of liability for consequential or incidental damages, - these - limitations may not apply to you.

- -

5. Accuracy of materials

- -

The materials appearing on MineSeeker's website could include technical, typographical, or photographic - errors. - MineSeeker does not warrant that any of the materials on its website are accurate, complete or current. - MineSeeker may make changes to the materials contained on its website at any time without notice. However - MineSeeker does not make any commitment to update the materials.

- -

6. Links

- -

MineSeeker has not reviewed all of the sites linked to its website and is not responsible for the contents of - any - such linked site. The inclusion of any link does not imply endorsement by MineSeeker of the site. Use of any - such linked website is at the user's own risk.

- -

7. Modifications

- -

MineSeeker may revise these terms of service for its website at any time without notice. By using this - website - you are agreeing to be bound by the then current version of these terms of service.

- -

8. Governing Law

- -

These terms and conditions are governed by and construed in accordance with the laws of Budapest, Hungary and - you - irrevocably submit to the exclusive jurisdiction of the courts in that State or location.

-
+{% block metas %} + {%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%} + + + + + + + + + + + + +{% endblock %} + +{% block body %} +
+

MineSeeker Terms of Service

+ +

1. Terms

+ +

By accessing the website at https://www.mineseeker.hu, you are + agreeing to be bound by these terms of service, all applicable laws and regulations, and agree that you are + responsible for compliance with any applicable local laws. If you do not agree with any of these terms, you + are + prohibited from using or accessing this site. The materials contained in this website are protected by + applicable copyright and trademark law.

+ +

2. Use License

+ +
    +
  1. + Permission is granted to temporarily download one copy of the materials (information or software) on + MineSeeker's website for personal, non-commercial transitory viewing only. This is the grant of a + license, + not a transfer of title, and under this license you may not: + +
      +
    1. modify or copy the materials;
    2. +
    3. use the materials for any commercial purpose, or for any public display (commercial or + non-commercial); +
    4. +
    5. attempt to decompile or reverse engineer any software contained on MineSeeker's website;
    6. +
    7. remove any copyright or other proprietary notations from the materials; or
    8. +
    9. transfer the materials to another person or "mirror" the materials on any other server.
    10. +
    +
  2. +
  3. This license shall automatically terminate if you violate any of these restrictions and may be + terminated by + MineSeeker at any time. Upon terminating your viewing of these materials or upon the termination of this + license, you must destroy any downloaded materials in your possession whether in electronic or printed + format. +
  4. +
+ +

3. Disclaimer

+ +
    +
  1. The materials on MineSeeker's website are provided on an 'as is' basis. MineSeeker makes no warranties, + expressed or implied, and hereby disclaims and negates all other warranties including, without + limitation, + implied warranties or conditions of merchantability, fitness for a particular purpose, or + non-infringement + of intellectual property or other violation of rights. +
  2. +
  3. Further, MineSeeker does not warrant or make any representations concerning the accuracy, likely + results, or + reliability of the use of the materials on its website or otherwise relating to such materials or on any + sites linked to this site. +
  4. +
+ +

4. Limitations

+ +

In no event shall MineSeeker or its suppliers be liable for any damages (including, without limitation, + damages + for loss of data or profit, or due to business interruption) arising out of the use or inability to use the + materials on MineSeeker's website, even if MineSeeker or a MineSeeker authorized representative has been + notified orally or in writing of the possibility of such damage. Because some jurisdictions do not allow + limitations on implied warranties, or limitations of liability for consequential or incidental damages, + these + limitations may not apply to you.

+ +

5. Accuracy of materials

+ +

The materials appearing on MineSeeker's website could include technical, typographical, or photographic + errors. + MineSeeker does not warrant that any of the materials on its website are accurate, complete or current. + MineSeeker may make changes to the materials contained on its website at any time without notice. However + MineSeeker does not make any commitment to update the materials.

+ +

6. Links

+ +

MineSeeker has not reviewed all of the sites linked to its website and is not responsible for the contents of + any + such linked site. The inclusion of any link does not imply endorsement by MineSeeker of the site. Use of any + such linked website is at the user's own risk.

+ +

7. Modifications

+ +

MineSeeker may revise these terms of service for its website at any time without notice. By using this + website + you are agreeing to be bound by the then current version of these terms of service.

+ +

8. Governing Law

+ +

These terms and conditions are governed by and construed in accordance with the laws of Budapest, Hungary and + you + irrevocably submit to the exclusive jurisdiction of the courts in that State or location.

+
{% endblock %} diff --git a/templates/Security/2fa.html.twig b/templates/Security/2fa.html.twig index 93ea88d..206f25e 100644 --- a/templates/Security/2fa.html.twig +++ b/templates/Security/2fa.html.twig @@ -2,6 +2,17 @@ {% block title %} - Two-Factor Authentication{% endblock %} +{% block metas %} + + + + + + + + +{% endblock %} + {% block body %}
@@ -51,4 +62,4 @@
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/Security/2fa_setup.html.twig b/templates/Security/2fa_setup.html.twig index 9bf1181..c017363 100644 --- a/templates/Security/2fa_setup.html.twig +++ b/templates/Security/2fa_setup.html.twig @@ -2,6 +2,17 @@ {% block title %} - Enable Two-Factor Authentication{% endblock %} +{% block metas %} + + + + + + + + +{% endblock %} + {% block body %}
diff --git a/templates/Security/forgot_password.html.twig b/templates/Security/forgot_password.html.twig index 3223fa1..5597995 100644 --- a/templates/Security/forgot_password.html.twig +++ b/templates/Security/forgot_password.html.twig @@ -2,6 +2,19 @@ {% block title %} - Forgot Password{% endblock %} +{% block metas %} + {%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%} + + + + + + + + + +{% endblock %} + {% block body %}
{% for email in app.flashes('reset_sent') %} @@ -26,28 +39,28 @@ {{ form_start(form, {attr: {class: 'auth-form'}}) }} -
- -
- - {{ form_widget(form.email, { - attr: { - class: 'auth-input' ~ (not form.email.vars.valid ? ' auth-input--error' : ''), - autocomplete: 'email', - autofocus: true, - } - }) }} -
- {% if not form.email.vars.valid %} - {% for error in form.email.vars.errors %} -

{{ error.message }}

- {% endfor %} - {% endif %} +
+ +
+ + {{ form_widget(form.email, { + attr: { + class: 'auth-input' ~ (not form.email.vars.valid ? ' auth-input--error' : ''), + autocomplete: 'email', + autofocus: true, + } + }) }}
+ {% if not form.email.vars.valid %} + {% for error in form.email.vars.errors %} +

{{ error.message }}

+ {% endfor %} + {% endif %} +
- + {{ form_end(form) }} diff --git a/templates/Security/login.html.twig b/templates/Security/login.html.twig index 1ffd440..99765f7 100644 --- a/templates/Security/login.html.twig +++ b/templates/Security/login.html.twig @@ -2,6 +2,25 @@ {% block title %} - Sign In{% endblock %} +{% block metas %} + {%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%} + + + + + + + + + + + + + +{% endblock %} + {% block body %}
{% for message in app.flashes('success') %} diff --git a/templates/Security/profile.html.twig b/templates/Security/profile.html.twig index 76bbbb3..cf589e8 100644 --- a/templates/Security/profile.html.twig +++ b/templates/Security/profile.html.twig @@ -2,6 +2,25 @@ {% block title %} - Profile{% endblock %} +{% block metas %} + {%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%} + + + + + + + + + + + + + +{% endblock %} + {% block body %}
diff --git a/templates/Security/profile_security.html.twig b/templates/Security/profile_security.html.twig index 71e7e7d..8a7959b 100644 --- a/templates/Security/profile_security.html.twig +++ b/templates/Security/profile_security.html.twig @@ -2,6 +2,25 @@ {% block title %} - Security Settings{% endblock %} +{% block metas %} + {%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%} + + + + + + + + + + + + + +{% endblock %} + {% block body %}
diff --git a/templates/Security/register.html.twig b/templates/Security/register.html.twig index b7e7862..8926717 100644 --- a/templates/Security/register.html.twig +++ b/templates/Security/register.html.twig @@ -2,6 +2,24 @@ {% block title %} - Register{% endblock %} +{% block metas %} + {%- set _ogImage = app.request.getSchemeAndHttpHost() ~ asset('images/mine-1600x627.png') -%} + + + + + + + + + + + + +{% endblock %} + {% block body %}
{% for email in app.flashes('verify_email') %} diff --git a/templates/Security/reset_password.html.twig b/templates/Security/reset_password.html.twig index 6f67081..33f85bc 100644 --- a/templates/Security/reset_password.html.twig +++ b/templates/Security/reset_password.html.twig @@ -2,6 +2,17 @@ {% block title %} - Reset Password{% endblock %} +{% block metas %} + + + + + + + + +{% endblock %} + {% block body %}