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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Changelog
=========

1.0.6 (Unreleased)
------------------
- Enh: Match card meta row split into date+time / stage / status; team badges enlarged with rectangular corners; probability tooltip scoped to text; "Show all tips" tied to actual tip existence.
- Enh: The "Show all tips" modal now shows each player's profile picture, matching the leaderboard and Top 10.
- Enh: URL-sourced team flag images keep their own aspect ratio and get a hairline frame drawn on the image, separating white-edged flags (Japan, England, France, …) from the card background.
- Enh: Team badges now prefer the bundled Twemoji flag over the data provider's logo (resolution order: flag → logo → initials), for consistent flags across browsers.

1.0.5 (June 7, 2026)
--------------------
- Fix: Top menu competition entries caused an `UnknownMethodException` on HumHub 1.19, where the deprecated `addItem()` method was removed — replaced with `addEntry()`.
Expand Down
2 changes: 1 addition & 1 deletion module.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"world cup",
"leaderboard"
],
"version": "1.0.5",
"version": "1.0.6",
"humhub": {
"minVersion": "1.18"
},
Expand Down
50 changes: 44 additions & 6 deletions resources/css/kickoff.css
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,13 @@
}
.kickoff-match-card.is-tipped { border-left: 3px solid #28a745; }
.kickoff-match-card-meta {
display: flex; justify-content: space-between;
display: grid; grid-template-columns: 1fr auto 1fr;
align-items: baseline;
font-size: 12px; margin-bottom: 8px;
}
.kickoff-match-card-meta-time { text-align: left; color: #555; }
.kickoff-match-card-meta-stage { text-align: center; }
.kickoff-match-card-meta-status { text-align: right; }
.kickoff-match-card-row {
display: flex; align-items: center; gap: 12px;
}
Expand All @@ -177,18 +181,46 @@
width: 100%; height: 100%; object-fit: contain;
background: #fff;
}
.kickoff-team-badge--image,
.kickoff-team-badge--flag {
width: 36px; height: 36px;
border-radius: 3px;
overflow: visible;
background: transparent !important;
font-size: 28px;
line-height: 36px;
text-align: center;
margin-block: -4px;
}
/* URL-sourced flag images: shrink-wrap the img to the image's own ratio
(instead of letterboxing in the square badge) and draw a hairline frame on
top of it — no layout impact, barely visible on colourful flags, but
separates white-edged ones (Japan, England, France, …) from the card
background. An inset box-shadow won't paint over an <img>'s content, so a
negative-offset outline is used; it follows the border-radius. */
.kickoff-team-badge--image img {
width: auto; height: auto;
max-width: 100%; max-height: 100%;
background: transparent;
border-radius: 3px;
outline: 1px solid rgba(0, 0, 0, 0.12);
outline-offset: -1px;
}
.kickoff-match-score {
display: flex; align-items: center; gap: 4px;
flex-shrink: 0;
}
.kickoff-score-input { width: 50px !important; }

.kickoff-match-card-probabilities {
margin-top: 4px; font-size: 11px; color: #6c757d;
text-align: center; letter-spacing: 0.02em;
margin-top: 4px; text-align: center;
}
.kickoff-probabilities-content {
font-size: 11px; color: #6c757d;
letter-spacing: 0.02em;
cursor: help;
}
.kickoff-match-card-probabilities span { display: inline-block; padding: 0 2px; }
.kickoff-probabilities-content span { display: inline-block; padding: 0 2px; }
.kickoff-live-badge {
color: #dc3545; font-weight: 700;
letter-spacing: 0.04em;
Expand All @@ -209,14 +241,20 @@
}
.kickoff-match-card-large-score {
margin: 4px 0 2px;
text-align: center;
/* 1fr auto 1fr keeps the separator dead-centre so the colon stays put even
when a score reaches two digits (the scores grow outward, not the colon
sideways). */
display: grid; grid-template-columns: 1fr auto 1fr;
align-items: baseline;
font-size: 2rem; font-weight: 700;
color: #212529; line-height: 1.1;
letter-spacing: 0.04em;
}
.kickoff-match-card-large-score-home { text-align: right; }
.kickoff-match-card-large-score-away { text-align: left; }
.kickoff-match-card-large-score-sep {
color: #adb5bd;
margin: 0 4px;
margin: 0 16px;
}
.kickoff-match-card-footer {
display: flex; align-items: center; gap: 8px;
Expand Down
124 changes: 68 additions & 56 deletions views/competition/_match_card.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
/** @var Game $game */
/** @var \humhub\modules\kickoff\models\Tip|null $tip */
/** @var bool $editable */
/** @var bool $canParticipate */
/** @var bool $hasTips */
/** @var bool $showOtherTipsLink */
/** @var \humhub\modules\kickoff\models\Competition $competition */

Expand Down Expand Up @@ -35,6 +37,9 @@
$kickoffTime = $kickoffEpoch !== null
? Yii::$app->formatter->asTime($kickoffEpoch, 'short')
: '';
$kickoffDate = $kickoffEpoch !== null
? Yii::$app->formatter->asDate($kickoffEpoch, 'short')
: '';
$relativeTime = $kickoffEpoch !== null
? Yii::$app->formatter->asRelativeTime($kickoffEpoch)
: '';
Expand All @@ -57,27 +62,32 @@
?>
<div class="kickoff-match-card<?= $isTipped && $canTip ? ' is-tipped' : '' ?><?= $isLive ? ' is-live' : '' ?>" data-game-id="<?= (int) $game->id ?>">
<div class="kickoff-match-card-meta">
<span>
<?= Html::encode($kickoffTime) ?>
<span class="kickoff-match-card-meta-time">
<?= Html::encode($kickoffDate) ?>
<?php if ($kickoffTime !== ''): ?>· <?= Html::encode($kickoffTime) ?><?php endif; ?>
</span>
<span class="kickoff-match-card-meta-stage">
<?php if ($stageBadge !== null): ?>
· <span class="text-muted"><?= Html::encode($stageBadge) ?></span>
<span class="text-muted"><?= Html::encode($stageBadge) ?></span>
<?php endif; ?>
</span>
<span class="kickoff-match-card-meta-status">
<?php if ($isLive): ?>
<span class="kickoff-live-badge">
<?= Yii::t('KickoffModule.base', 'LIVE') ?>
<?php $liveMinute = $game->getFormattedLiveMinute(); ?>
<?php if ($liveMinute !== null): ?>
· <?= Html::encode($liveMinute) ?>
<?php endif; ?>
</span>
<?php elseif ($isFinished): ?>
<span class="text-success"><?= Yii::t('KickoffModule.base', 'Finished') ?></span>
<?php elseif (!$canTip): ?>
<span class="text-muted"><?= Yii::t('KickoffModule.base', 'Awaiting result') ?></span>
<?php else: ?>
<span class="text-muted"><?= Html::encode($relativeTime) ?></span>
<?php endif; ?>
</span>
<?php if ($isLive): ?>
<span class="kickoff-live-badge">
<?= Yii::t('KickoffModule.base', 'LIVE') ?>
<?php $liveMinute = $game->getFormattedLiveMinute(); ?>
<?php if ($liveMinute !== null): ?>
· <?= Html::encode($liveMinute) ?>
<?php endif; ?>
</span>
<?php elseif ($isFinished): ?>
<span class="text-success"><?= Yii::t('KickoffModule.base', 'Finished') ?></span>
<?php elseif (!$canTip): ?>
<span class="text-muted"><?= Yii::t('KickoffModule.base', 'Awaiting result') ?></span>
<?php else: ?>
<span class="text-muted"><?= Html::encode($relativeTime) ?></span>
<?php endif; ?>
</div>
<div class="kickoff-match-card-row">
<div class="kickoff-match-team kickoff-match-team-home">
Expand Down Expand Up @@ -110,54 +120,52 @@ class="form-control form-control-sm text-center kickoff-score-input"
</div>
<?php if ($showLargeScoreBlock): ?>
<div class="kickoff-match-card-large-score">
<?= (int) $displayHomeScore ?>
<span class="kickoff-match-card-large-score-home"><?= (int) $displayHomeScore ?></span>
<span class="kickoff-match-card-large-score-sep">:</span>
<?= (int) $displayAwayScore ?>
<span class="kickoff-match-card-large-score-away"><?= (int) $displayAwayScore ?></span>
</div>
<?php endif; ?>
<?php if ($probabilities !== null): ?>
<div class="kickoff-match-card-probabilities" title="<?= Html::encode(Yii::t('KickoffModule.base', 'Estimated chances based on team strength — for orientation only, not betting odds.')) ?>">
<span><?= number_format($probabilities['home'], 0) ?>%</span>
<?php if ($probabilities['draw'] > 0): ?>
<div class="kickoff-match-card-probabilities">
<span class="kickoff-probabilities-content" title="<?= Html::encode(Yii::t('KickoffModule.base', 'Estimated chances based on team strength — for orientation only, not betting odds.')) ?>">
<span><?= number_format($probabilities['home'], 0) ?>%</span>
<?php if ($probabilities['draw'] > 0): ?>
<span class="text-muted">·</span>
<span><?= number_format($probabilities['draw'], 0) ?>%</span>
<?php endif; ?>
<span class="text-muted">·</span>
<span><?= number_format($probabilities['draw'], 0) ?>%</span>
<?php endif; ?>
<span class="text-muted">·</span>
<span><?= number_format($probabilities['away'], 0) ?>%</span>
<span><?= number_format($probabilities['away'], 0) ?>%</span>
</span>
</div>
<?php endif; ?>
<?php
$hasFooterTip = !$showInputs;
$hasFooterTip = !$showInputs && $isTipped;
$hasFooterVenue = !empty($game->venue);
$hasFooterActions = !empty($showOtherTipsLink);
?>
<?php if ($hasFooterTip || $hasFooterVenue || $hasFooterActions): ?>
<div class="kickoff-match-card-footer">
<div class="kickoff-match-card-footer-tip">
<?php if ($hasFooterTip): ?>
<?php if ($tip !== null): ?>
<?= Yii::t('KickoffModule.base', 'Your tip:') ?>
<strong><?= (int) $tip->home_score ?>:<?= (int) $tip->away_score ?></strong>
<?php if ($isFinished && $tip->points !== null): ?>
<?php
$scheme = $game->competition->scoringScheme ?? null;
$pointsClass = 'points-zero';
if ($scheme !== null) {
if ($tip->points === $scheme->points_exact) {
$pointsClass = 'points-exact';
} elseif ($tip->points === $scheme->points_goal_diff) {
$pointsClass = 'points-diff';
} elseif ($tip->points === $scheme->points_tendency) {
$pointsClass = 'points-tendency';
}
<?= Yii::t('KickoffModule.base', 'Your tip:') ?>
<strong><?= (int) $tip->home_score ?>:<?= (int) $tip->away_score ?></strong>
<?php if ($isFinished && $tip->points !== null): ?>
<?php
$scheme = $game->competition->scoringScheme ?? null;
$pointsClass = 'points-zero';
if ($scheme !== null) {
if ($tip->points === $scheme->points_exact) {
$pointsClass = 'points-exact';
} elseif ($tip->points === $scheme->points_goal_diff) {
$pointsClass = 'points-diff';
} elseif ($tip->points === $scheme->points_tendency) {
$pointsClass = 'points-tendency';
}
?>
· <span class="kickoff-points-badge <?= $pointsClass ?>">
<?= Yii::t('KickoffModule.base', '{n} pts', ['n' => (int) $tip->points]) ?>
</span>
<?php endif; ?>
<?php else: ?>
<span class="text-muted"><?= Yii::t('KickoffModule.base', 'No tip placed.') ?></span>
}
?>
· <span class="kickoff-points-badge <?= $pointsClass ?>">
<?= Yii::t('KickoffModule.base', '{n} pts', ['n' => (int) $tip->points]) ?>
</span>
<?php endif; ?>
<?php endif; ?>
</div>
Expand All @@ -168,12 +176,16 @@ class="form-control form-control-sm text-center kickoff-score-input"
</div>
<div class="kickoff-match-card-footer-actions">
<?php if ($hasFooterActions): ?>
<a href="#"
data-kickoff-modal
data-modal-url="<?= \yii\helpers\Url::to(['/kickoff/competition/match-tips', 'slug' => $competition->slug, 'gameId' => $game->id]) ?>"
data-modal-title="<?= Html::encode(($home ? $home->getDisplayName() : '?') . ' – ' . ($away ? $away->getDisplayName() : '?')) ?>">
<?= Yii::t('KickoffModule.base', 'Show all tips') ?> →
</a>
<?php if ($hasTips): ?>
<a href="#"
data-kickoff-modal
data-modal-url="<?= \yii\helpers\Url::to(['/kickoff/competition/match-tips', 'slug' => $competition->slug, 'gameId' => $game->id]) ?>"
data-modal-title="<?= Html::encode(($home ? $home->getDisplayName() : '?') . ' – ' . ($away ? $away->getDisplayName() : '?')) ?>">
<?= Yii::t('KickoffModule.base', 'Show all tips') ?> →
</a>
<?php else: ?>
<span class="text-muted"><?= Yii::t('KickoffModule.base', 'No tips placed') ?></span>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
Expand Down
7 changes: 7 additions & 0 deletions views/competition/_match_tips.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use humhub\modules\kickoff\models\Game;
use humhub\modules\kickoff\services\KickoffTime;
use humhub\modules\user\widgets\Image as UserImage;
use yii\helpers\Html;
use yii\helpers\Url;

Expand Down Expand Up @@ -57,6 +58,7 @@
<table class="table table-sm">
<thead>
<tr>
<th></th>
<th><?= Yii::t('KickoffModule.base', 'Player') ?></th>
<th class="text-center"><?= Yii::t('KickoffModule.base', 'Tip') ?></th>
<th class="text-end"><?= Yii::t('KickoffModule.base', 'Points') ?></th>
Expand All @@ -67,6 +69,11 @@
$user = $tip->user;
?>
<tr>
<td style="width:38px">
<?php if ($user): ?>
<?= UserImage::widget(['user' => $user, 'width' => 34]) ?>
<?php endif; ?>
</td>
<td><?= $user ? Html::encode($user->displayName) : '<span class="text-muted">' . Yii::t('KickoffModule.base', '(deleted user)') . '</span>' ?></td>
<td class="text-center"><?= (int) $tip->home_score ?>:<?= (int) $tip->away_score ?></td>
<td class="text-end">
Expand Down
18 changes: 10 additions & 8 deletions views/competition/_team_badge.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
$logo = $team && $team->logo_url !== null && $team->logo_url !== '' ? $team->logo_url : null;
$flagUrl = null;

if (!$logo && $team) {
if ($team) {
// The codepoint sequence doubles as the Twemoji filename: regional
// indicator pairs for countries, tag sequences for England/Scotland/Wales.
$codepoints = \humhub\modules\kickoff\services\TeamNameLocalizer::flagCodepoints($team->country_code);
Expand Down Expand Up @@ -44,18 +44,20 @@
$color = $team ? $palette[$team->id % count($palette)] : '#9ca3af';

?>
<?php /* Explicit width/height: the Twemoji SVGs carry no intrinsic size, so
without attributes the image briefly renders at full content width
<?php /* Resolution order: bundled Twemoji flag first (consistent across
browsers), then the data provider's logo, then a coloured initials
chip. Explicit width/height: the Twemoji SVGs carry no intrinsic size,
so without attributes the image briefly renders at full content width
before the stylesheet applies (visible flash in Firefox). The badge
CSS still overrides the final size. */ ?>
<?php if ($logo): ?>
<span class="kickoff-team-badge" title="<?= Html::encode($name) ?>">
<img src="<?= Html::encode($logo) ?>" alt="<?= Html::encode($name) ?>" width="28" height="28">
</span>
<?php elseif ($flagUrl !== null): ?>
<?php if ($flagUrl !== null): ?>
<span class="kickoff-team-badge kickoff-team-badge--flag" title="<?= Html::encode($name) ?>">
<img src="<?= Html::encode($flagUrl) ?>" alt="<?= Html::encode($name) ?>" width="28" height="28">
</span>
<?php elseif ($logo): ?>
<span class="kickoff-team-badge kickoff-team-badge--image" title="<?= Html::encode($name) ?>">
<img src="<?= Html::encode($logo) ?>" alt="<?= Html::encode($name) ?>" width="28" height="28">
</span>
<?php else: ?>
<span class="kickoff-team-badge" title="<?= Html::encode($name) ?>" style="background: <?= $color ?>;">
<?= Html::encode($initials) ?>
Expand Down
11 changes: 11 additions & 0 deletions views/competition/view.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@
}
}

// Games that have at least one tip from any player (one query for the whole matchday).
$gamesWithTips = [];
if ($matchdayGames !== []) {
$gameIds = array_map(fn(Game $g) => $g->id, $matchdayGames);
$gamesWithTips = array_flip(
Tip::find()->select('game_id')->where(['game_id' => $gameIds])->distinct()->column()
);
}

$this->registerAssetBundle(\humhub\modules\kickoff\assets\Assets::class);

$autosaveMessages = [
Expand Down Expand Up @@ -430,6 +439,8 @@ class="form-control"
'game' => $g,
'tip' => $tipsByGame[$g->id] ?? null,
'editable' => $canParticipate && !$g->isKickoffPassed(),
'canParticipate' => $canParticipate,
'hasTips' => isset($gamesWithTips[$g->id]),
'showOtherTipsLink' => $competition->tipsVisibleForGame($g),
'competition' => $competition,
]) ?>
Expand Down
Loading