:class="{ selected: isSelected(sel.id), 'odds-btn--locked': locked }"
:data-bet-selection-id="sel.id"
:disabled="locked"
+ @mouseenter="onPick(sel.id, $event)"
@click="onPick(sel.id, $event)"
>
{{ label(sel) }}
@@ -147,14 +151,14 @@ const panelStyle = computed(() =>
}
.panel.desktop-card .odds-btn {
- min-height: 38px;
- padding: 4px 6px;
- gap: 2px;
+ min-height: 34px;
+ padding: 3px 4px;
+ gap: 1px;
}
.panel.desktop-card .odds-btn .label {
- font-size: 9px;
- line-height: 1.2;
+ font-size: 8px;
+ line-height: 1.15;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
@@ -162,7 +166,7 @@ const panelStyle = computed(() =>
}
.panel.desktop-card .odds-btn .odds {
- font-size: 13px;
+ font-size: 12px;
line-height: 1.1;
}
diff --git a/apps/player/src/composables/useDesktopBetPopover.ts b/apps/player/src/composables/useDesktopBetPopover.ts
index 982d634..bf23469 100644
--- a/apps/player/src/composables/useDesktopBetPopover.ts
+++ b/apps/player/src/composables/useDesktopBetPopover.ts
@@ -5,27 +5,80 @@ const visible = ref(false);
const anchorX = ref(0);
const anchorY = ref(0);
const pendingItem = ref(null);
+const activeCardRootId = ref(null);
+const placement = ref<'top' | 'bottom'>('bottom');
+let closeTimer: number | undefined;
export function useDesktopBetPopover() {
function openAt(event: MouseEvent, item: SlipItem) {
+ cancelClose();
+
+ const el = (event.currentTarget || event.target) as HTMLElement;
+ const btn = el?.closest?.('button') || el;
+ const rect = btn?.getBoundingClientRect?.();
+ if (!rect) return;
+
+ const popW = 172;
+ const popH = 155;
const pad = 12;
- const popW = 300;
- const popH = 320;
- let x = event.clientX + pad;
- let y = event.clientY + pad;
+
+ const btnCenterX = rect.left + rect.width / 2;
+ let x = btnCenterX - popW / 2;
+
+ let y = rect.bottom + pad;
+ placement.value = 'bottom';
+
if (typeof window !== 'undefined') {
- x = Math.min(x, window.innerWidth - popW - pad);
- y = Math.min(y, window.innerHeight - popH - pad);
+ const spaceBelow = window.innerHeight - rect.bottom;
+ if (spaceBelow < popH + pad) {
+ y = rect.top - popH - pad;
+ placement.value = 'top';
+ }
+
+ x = Math.max(pad, Math.min(x, window.innerWidth - popW - pad));
+ y = Math.max(pad, Math.min(y, window.innerHeight - popH - pad));
}
- anchorX.value = Math.max(pad, x);
- anchorY.value = Math.max(pad, y);
+
+ anchorX.value = x;
+ anchorY.value = y;
pendingItem.value = item;
+ const root = el?.closest?.('[data-bet-card-root]') as HTMLElement | null;
+ activeCardRootId.value = root?.dataset.betCardRoot ?? null;
visible.value = true;
}
function close() {
visible.value = false;
pendingItem.value = null;
+ activeCardRootId.value = null;
+ if (closeTimer !== undefined) {
+ window.clearTimeout(closeTimer);
+ closeTimer = undefined;
+ }
+ }
+
+ function scheduleClose(delay = 200) {
+ if (closeTimer !== undefined) {
+ window.clearTimeout(closeTimer);
+ }
+ closeTimer = window.setTimeout(() => {
+ visible.value = false;
+ pendingItem.value = null;
+ activeCardRootId.value = null;
+ closeTimer = undefined;
+ }, delay);
+ }
+
+ function cancelClose() {
+ if (closeTimer !== undefined) {
+ window.clearTimeout(closeTimer);
+ closeTimer = undefined;
+ }
+ }
+
+ function isActiveForCard(cardRootId: string | null | undefined) {
+ if (!cardRootId || !visible.value || !pendingItem.value) return false;
+ return activeCardRootId.value === cardRootId;
}
return {
@@ -33,7 +86,12 @@ export function useDesktopBetPopover() {
anchorX,
anchorY,
pendingItem,
+ activeCardRootId,
+ placement,
openAt,
close,
+ scheduleClose,
+ cancelClose,
+ isActiveForCard,
};
}
diff --git a/apps/player/src/i18n/en-US.ts b/apps/player/src/i18n/en-US.ts
index b6359bf..7ae0841 100644
--- a/apps/player/src/i18n/en-US.ts
+++ b/apps/player/src/i18n/en-US.ts
@@ -32,6 +32,9 @@ export default {
nav: { home: 'Home', sports: 'Sports', football: 'Football', bet: 'Bet', bet_history: 'History', wallet: 'Transactions', my_wallet: 'My Wallet', account_hub: 'Bets & Wallet', profile: 'Profile', search: 'Search teams, leagues…', announcements: 'Announcements', sports_more_soon: 'More sports coming soon' },
home: {
hot_matches: 'Hot matches',
+ section_featured: 'Featured',
+ section_upcoming: 'Upcoming',
+ sidebar_recommend: 'Recommended',
hot_tab: 'Hot',
upcoming_tab: 'Upcoming',
no_matches: 'No matches',
@@ -405,12 +408,14 @@ export default {
filter_league: 'Leagues',
filter_market: 'Markets',
time_all: 'All',
+ all: 'All',
time_today: 'Today',
time_early: 'Early',
market_all: 'All markets',
market_handicap_ou: 'Handicap / O-U',
correct_score: 'Correct score',
no_markets: 'No markets available',
+ no_market_odds: 'No odds available',
no_markets_filtered: 'No markets match the current filter',
popover_title: 'Confirm bet',
place_now: 'Bet now',
@@ -453,6 +458,9 @@ export default {
outright_load_more: 'Load more',
outright_settled: 'Settled',
outright_settled_hint: 'This event is settled. Odds and results are view-only.',
+ quick_bet: 'Quick bet',
+ popular_teams: 'Popular teams',
+ outright_view_all: 'View all teams',
cancel: 'Cancel',
parlay_max_legs: 'Parlay allows up to 5 legs',
parlay_block_outright: 'Outright cannot be parlayed',
diff --git a/apps/player/src/i18n/ms-MY.ts b/apps/player/src/i18n/ms-MY.ts
index 0b248d1..1169d2f 100644
--- a/apps/player/src/i18n/ms-MY.ts
+++ b/apps/player/src/i18n/ms-MY.ts
@@ -45,6 +45,9 @@ export default {
},
home: {
hot_matches: 'Perlawanan popular',
+ section_featured: 'Perlawanan utama',
+ section_upcoming: 'Akan bermula',
+ sidebar_recommend: 'Disyorkan',
hot_tab: 'Popular',
upcoming_tab: 'Terdekat',
no_matches: 'Tiada perlawanan',
@@ -418,12 +421,14 @@ export default {
filter_league: 'Liga',
filter_market: 'Pasaran',
time_all: 'Semua',
+ all: 'Semua',
time_today: 'Hari ini',
time_early: 'Awal',
market_all: 'Semua pasaran',
market_handicap_ou: 'Handicap / O-U',
correct_score: 'Skor tepat',
no_markets: 'Tiada pasaran tersedia',
+ no_market_odds: 'Tiada odds tersedia',
no_markets_filtered: 'Tiada pasaran sepadan dengan penapis semasa',
popover_title: 'Sahkan pertaruhan',
place_now: 'Pertaruhan sekarang',
@@ -466,6 +471,9 @@ export default {
outright_load_more: 'Muat lagi',
outright_settled: 'Selesai',
outright_settled_hint: 'Acara ini telah diselesaikan. Hanya paparan odds dan keputusan.',
+ quick_bet: 'Pertaruhan pantas',
+ popular_teams: 'Pasukan popular',
+ outright_view_all: 'Lihat semua pasukan',
cancel: 'Batal',
parlay_max_legs: 'Maksimum 5 pilihan parlay',
parlay_block_outright: 'Outright tidak boleh parlay',
diff --git a/apps/player/src/i18n/zh-CN.ts b/apps/player/src/i18n/zh-CN.ts
index af2804d..32bf5ca 100644
--- a/apps/player/src/i18n/zh-CN.ts
+++ b/apps/player/src/i18n/zh-CN.ts
@@ -32,6 +32,9 @@ export default {
nav: { home: '主页', sports: '体育赛事', football: '足球', bet: '投注', bet_history: '历史投注', wallet: '资金明细', my_wallet: '我的钱包', account_hub: '投注账单', profile: '我的', search: '搜索球队、联赛…', announcements: '公告', sports_more_soon: '更多体育,敬请期待' },
home: {
hot_matches: '热门赛事',
+ section_featured: '焦点赛事',
+ section_upcoming: '即将开赛',
+ sidebar_recommend: '推荐赛事',
hot_tab: '热门',
upcoming_tab: '近期',
no_matches: '暂无赛事',
@@ -405,12 +408,14 @@ export default {
filter_league: '联赛',
filter_market: '玩法筛选',
time_all: '全部',
+ all: '全部',
time_today: '今天',
time_early: '早盘',
market_all: '所有盘口',
market_handicap_ou: '让球 / 大小',
correct_score: '波胆',
no_markets: '暂无可用盘口',
+ no_market_odds: '暂无盘口赔率',
no_markets_filtered: '当前筛选下暂无盘口',
popover_title: '确认投注',
place_now: '立即下注',
@@ -453,6 +458,9 @@ export default {
outright_load_more: '加载更多',
outright_settled: '已结算',
outright_settled_hint: '本赛事已结算,仅可查看赔率与冠军结果',
+ quick_bet: '快速下注',
+ popular_teams: '热门队伍',
+ outright_view_all: '查看全部队伍',
cancel: '取消',
parlay_max_legs: '串关最多 5 项',
parlay_block_outright: '冠军盘不可串关',
diff --git a/apps/player/src/stores/betSlip.ts b/apps/player/src/stores/betSlip.ts
index f30aac6..2b67219 100644
--- a/apps/player/src/stores/betSlip.ts
+++ b/apps/player/src/stores/betSlip.ts
@@ -255,7 +255,7 @@ export const useBetSlipStore = defineStore('betSlip', () => {
function addToSingleCart(item: SlipItem) {
mode.value = 'single';
if (singleCartItems.value.some((i) => i.selectionId === item.selectionId)) return;
- singleCartItems.value.push(item);
+ singleCartItems.value.unshift(item);
initItemStake(item.selectionId);
lastParlayError.value = null;
}
@@ -298,7 +298,7 @@ export const useBetSlipStore = defineStore('betSlip', () => {
return 'MAX_LEGS';
}
- parlayItems.value.push(item);
+ parlayItems.value.unshift(item);
lastParlayError.value = null;
return null;
}
diff --git a/apps/player/src/views/MobileFootballView.vue b/apps/player/src/views/MobileFootballView.vue
index 442ec24..25c083d 100644
--- a/apps/player/src/views/MobileFootballView.vue
+++ b/apps/player/src/views/MobileFootballView.vue
@@ -282,7 +282,6 @@ function goMatch(id: string) {
:class="{ active: mainTab === 'matches', 'tab-gold-active': mainTab === 'matches' }"
@click="selectMainTab('matches')"
>
- ⚽
{{ t('bet.tab_matches') }}
@@ -494,11 +492,6 @@ function goMatch(id: string) {
color: var(--primary-light);
}
-.tab-icon {
- font-size: 16px;
- line-height: 1;
-}
-
.filters-bar {
display: flex;
align-items: center;
diff --git a/apps/player/src/views/desktop/DesktopFootballView.vue b/apps/player/src/views/desktop/DesktopFootballView.vue
index 83225fc..9b760b1 100644
--- a/apps/player/src/views/desktop/DesktopFootballView.vue
+++ b/apps/player/src/views/desktop/DesktopFootballView.vue
@@ -11,6 +11,9 @@ import {
isAfterLocalTodayMatchWindow as isAfterTodayMatchWindow,
isInLocalTodayMatchWindow as isInTodayMatchWindow,
} from '@thebet365/shared';
+import { useAuthStore } from '../../stores/auth';
+import type { OutrightEvent, OutrightSelection } from '../../components/outright/OutrightEventSection.vue';
+import OutrightBetModal, { type OutrightPick } from '../../components/outright/OutrightBetModal.vue';
type MainTab = 'matches' | 'outright';
@@ -102,6 +105,22 @@ const leagueGroups = computed(() => {
return Object.values(groups).sort((a, b) => a.leagueName.localeCompare(b.leagueName));
});
+const filteredOutrightEvents = computed(() => {
+ if (mainTab.value !== 'outright') return [];
+ const keyword = searchQuery.value.trim().toLowerCase();
+ return outrightEvents.value.filter((e) => {
+ if (keyword) {
+ const haystack = `${e.title} ${e.leagueName || ''}`.toLowerCase();
+ if (!haystack.includes(keyword)) return false;
+ }
+ if (filterState.value.leagueIds.length > 0) {
+ const id = e.leagueId ?? e.leagueName;
+ if (!filterState.value.leagueIds.includes(id)) return false;
+ }
+ return true;
+ });
+});
+
function goMatch(id: string) {
router.push(`/match/${id}`);
}
@@ -110,6 +129,32 @@ function goOutright(id: string) {
router.push(`/outright/${id}`);
}
+const auth = useAuthStore();
+const modalOpen = ref(false);
+const activePick = ref
(null);
+
+function openBet(event: OutrightEvent, sel: OutrightSelection) {
+ if (event.bettingOpen === false || event.status === 'SETTLED') return;
+ if (!auth.token) {
+ auth.showLoginPrompt(route.fullPath);
+ return;
+ }
+ activePick.value = {
+ selectionId: sel.id,
+ oddsVersion: sel.oddsVersion,
+ teamCode: sel.teamCode,
+ teamName: sel.teamName,
+ odds: sel.odds,
+ eventTitle: event.title,
+ };
+ modalOpen.value = true;
+}
+
+function closeModal() {
+ modalOpen.value = false;
+ activePick.value = null;
+}
+
async function refresh() {
if (mainTab.value === 'outright') {
await loadOutrights({ silent: false });
@@ -131,7 +176,7 @@ void loadParlayMatches(true);
:class="{ active: mainTab === 'matches' }"
@click="setMainTab('matches')"
>
- ⚽ {{ t('bet.tab_matches') }}
+ {{ t('bet.tab_matches') }}