Files
thebet365/apps/player/src/views/HomeView.vue

575 lines
12 KiB
Vue

<script setup lang="ts">
import { onActivated, computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import emptyMatchesImg from '../assets/images/empty-matches.svg';
import vsImg from '../assets/images/vs.png';
import cardBg from '../assets/images/card-bg.webp';
import BannerCarousel from '../components/BannerCarousel.vue';
import { usePlayerHome } from '../composables/usePlayerHome';
import TeamEmblem from '../components/TeamEmblem.vue';
import GoldSpinner from '../components/GoldSpinner.vue';
import { usePullToRefresh } from '../composables/usePullToRefresh';
import { formatLocalMatchDateTime } from '@thebet365/shared';
import type { PlayerHomeMatch } from '../composables/usePlayerHome';
type HotTab = 'hot' | 'upcoming';
const matchCardBg = `url(${cardBg})`;
const { t, locale } = useI18n();
const router = useRouter();
const { banners, hotMatches, upcomingMatches, loading, load, announcementItems } = usePlayerHome();
const activeTab = ref<HotTab>('hot');
const bannerFallbackTo = computed(() => {
const id = announcementItems.value[0]?.id;
return id ? `/announcements/${id}` : '/announcements';
});
const displayedMatches = computed<PlayerHomeMatch[]>(() =>
activeTab.value === 'hot' ? hotMatches.value : upcomingMatches.value,
);
const emptyMessage = computed(() =>
activeTab.value === 'hot' ? t('home.no_matches') : t('home.upcoming_empty'),
);
const { pullDistance, refreshing, spinning, progress } = usePullToRefresh({
onRefresh: async () => { await load(true); },
});
onActivated(() => { void load(true); });
const pullIndicatorStyle = () => ({
height: `${pullDistance.value}px`,
opacity: Math.min(pullDistance.value / 48, 1),
});
function goMatch(id: string) {
router.push(`/match/${id}`);
}
function formatKickoff(startTime: string) {
return formatLocalMatchDateTime(startTime, locale.value, { variant: 'full' });
}
</script>
<template>
<div>
<div
class="pull-indicator"
:style="pullIndicatorStyle()"
>
<GoldSpinner v-if="spinning" :size="28" :progress="progress" :active="spinning" />
</div>
<BannerCarousel :banners="banners" :fallback-to="bannerFallbackTo" />
<section class="hot-panel">
<div class="hot-tabs" role="tablist" :aria-label="t('home.hot_matches')">
<button
type="button"
role="tab"
class="hot-tab"
:class="{ active: activeTab === 'hot' }"
:aria-selected="activeTab === 'hot'"
@click="activeTab = 'hot'"
>
{{ t('home.hot_tab') }}
</button>
<button
type="button"
role="tab"
class="hot-tab"
:class="{ active: activeTab === 'upcoming' }"
:aria-selected="activeTab === 'upcoming'"
@click="activeTab = 'upcoming'"
>
{{ t('home.upcoming_tab') }}
</button>
</div>
<div
v-for="(match, index) in displayedMatches"
:key="match.id"
class="match-card"
:class="{ 'match-card--live-anim': activeTab === 'hot' && index < 3 }"
@click="goMatch(match.id)"
>
<div class="match-info">
<div class="match-teams">{{ match.homeTeamName }} vs {{ match.awayTeamName }}</div>
<div class="match-time">{{ formatKickoff(match.startTime) }}</div>
</div>
<div class="match-flags" aria-hidden="true">
<TeamEmblem
size="md"
:team-code="match.homeTeamCode"
:team-name="match.homeTeamName"
:logo-url="match.homeTeamLogoUrl"
/>
<div class="vs-arena">
<svg class="hz-lightning" viewBox="0 0 72 28" aria-hidden="true">
<defs>
<linearGradient :id="`hzBoltGrad-${match.id}`" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#FFB980" stop-opacity="0.2" />
<stop offset="35%" stop-color="#FFDDD2" stop-opacity="1" />
<stop offset="50%" stop-color="#FFFFFF" stop-opacity="1" />
<stop offset="65%" stop-color="#FFDDD2" stop-opacity="1" />
<stop offset="100%" stop-color="#FFDDD2" stop-opacity="0.2" />
</linearGradient>
</defs>
<path
class="hz-path hz-path-main"
:stroke="`url(#hzBoltGrad-${match.id})`"
d="M1 14 H16 L20 5 L24 23 L28 9 L32 14 H40 L44 6 L48 22 L52 12 L56 14 H71"
/>
<path
class="hz-path hz-path-sub"
:stroke="`url(#hzBoltGrad-${match.id})`"
d="M3 19 H14 L18 15 L22 19 H50 L54 16 L58 19 H69"
/>
</svg>
<span class="hz-beam" aria-hidden="true" />
<img :src="vsImg" alt="" class="vs-img" />
</div>
<TeamEmblem
size="md"
:team-code="match.awayTeamCode"
:team-name="match.awayTeamName"
:logo-url="match.awayTeamLogoUrl"
/>
</div>
</div>
<div v-if="!loading && !displayedMatches.length" class="empty">
<img :src="emptyMatchesImg" alt="" class="empty-icon" />
<p>{{ emptyMessage }}</p>
</div>
</section>
</div>
</template>
<style scoped>
.pull-indicator {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
transition: height 0.15s ease;
}
.hot-panel {
background: var(--glass-bg);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.hot-tabs {
display: flex;
gap: 0;
margin-bottom: 0;
background: rgba(15, 32, 39, 0.55);
border-bottom: 1px solid var(--border);
}
.hot-tab {
flex: 1;
padding: 12px 6px;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: var(--text-muted);
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: color 0.15s, border-color 0.15s;
}
.hot-tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
.quick-entries {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-areas:
"a a b b"
"c d e f";
gap: 8px;
margin-bottom: 16px;
padding: 0 2px;
}
.qe-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 12px 4px 10px;
border-radius: var(--radius-lg);
min-width: 0;
overflow: hidden;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
}
.qe-item:active {
transform: scale(0.96);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.qe-item:nth-child(1) { grid-area: a; }
.qe-item:nth-child(2) { grid-area: b; }
.qe-item:nth-child(3) { grid-area: c; }
.qe-item:nth-child(4) { grid-area: d; }
.qe-item:nth-child(5) { grid-area: e; }
.qe-item:nth-child(6) { grid-area: f; }
.qe-item--wide {
flex-direction: row;
gap: 10px;
padding: 14px 12px;
justify-content: flex-start;
}
.qe-item.qe-item--wide .qe-icon {
width: 36px;
height: 36px;
border-radius: 8px;
}
.qe-item.qe-item--wide .qe-label {
font-size: 13px;
font-weight: 700;
}
.qe-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.qe-icon--bet {
background: rgba(244, 162, 97, 0.15);
color: #FFB980;
}
.qe-icon--recharge {
background: rgba(16, 185, 129, 0.15);
color: #10B981;
}
.qe-icon--bill {
background: rgba(255, 179, 0, 0.15);
color: #FFB300;
}
.qe-icon--history {
background: rgba(231, 111, 81, 0.15);
color: #E76F51;
}
.qe-label {
font-size: 11px;
font-weight: 600;
color: var(--text);
text-align: center;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.section-title {
font-size: 15px;
font-weight: 700;
color: var(--text);
margin: 0 0 8px;
padding-left: 10px;
border-left: 3px solid var(--primary);
line-height: 1.3;
}
.hot-panel .match-card {
background: transparent;
border: none;
border-bottom: 1px solid var(--border);
box-shadow: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.match-card {
position: relative;
isolation: isolate;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 0;
padding: 10px 12px;
min-height: 60px;
border-radius: 0;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
overflow: hidden;
}
.hot-panel .match-card:last-of-type {
border-bottom: none;
}
.match-card::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(244, 162, 97, 0.06) 0%, transparent 60%);
z-index: 0;
pointer-events: none;
}
.match-card:active {
transform: scale(0.995);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.match-info,
.match-flags {
position: relative;
z-index: 1;
}
.match-info {
flex: 1;
min-width: 0;
}
.match-teams {
font-weight: 700;
margin-bottom: 4px;
font-size: 14px;
line-height: 1.3;
color: var(--text);
}
.match-time {
font-size: 12px;
color: var(--text-muted);
font-weight: 500;
}
.match-flags {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 3px;
max-width: 46%;
}
.vs-arena {
position: relative;
flex-shrink: 0;
width: 52px;
height: 42px;
display: flex;
align-items: center;
justify-content: center;
}
.hz-lightning {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 1;
pointer-events: none;
overflow: visible;
}
.hz-path {
fill: none;
stroke-width: 2.2;
stroke-linecap: round;
stroke-linejoin: round;
filter: drop-shadow(0 0 4px rgba(244, 162, 97, 0.6)) drop-shadow(0 0 8px rgba(255, 185, 128, 0.35));
opacity: 0;
}
.match-card--live-anim .hz-path-main {
animation: hz-strike-main 2.6s ease-in-out infinite;
}
.match-card--live-anim .hz-path-sub {
stroke-width: 1.6;
animation: hz-strike-sub 2.6s ease-in-out infinite;
animation-delay: 0.12s;
}
.match-card--live-anim .hz-beam {
position: absolute;
left: 0;
right: 0;
top: 50%;
height: 2px;
transform: translateY(-50%);
pointer-events: none;
z-index: 1;
background: linear-gradient(
90deg,
rgba(255, 185, 128, 0) 0%,
rgba(244, 162, 97, 0.7) 28%,
#F4A261 50%,
rgba(255, 185, 128, 0.7) 72%,
rgba(109, 40, 217, 0) 100%
);
opacity: 0;
filter: blur(0.4px);
animation: hz-beam-flash 2.6s ease-in-out infinite;
}
.hz-beam {
display: none;
}
.vs-img {
position: relative;
z-index: 0;
width: 40px;
height: auto;
object-fit: contain;
filter: drop-shadow(0 0 3px rgba(244, 162, 97, 0.25));
}
.match-card--live-anim .vs-img {
animation: vs-glow 2.4s ease-in-out infinite;
}
@keyframes hz-strike-main {
0%,
72%,
100% {
opacity: 0;
}
74% {
opacity: 1;
}
75% {
opacity: 0.25;
}
76% {
opacity: 0.95;
}
78% {
opacity: 0;
}
}
@keyframes hz-strike-sub {
0%,
74%,
100% {
opacity: 0;
}
76% {
opacity: 0.85;
}
77% {
opacity: 0.2;
}
78% {
opacity: 0.7;
}
80% {
opacity: 0;
}
}
@keyframes hz-beam-flash {
0%,
71%,
100% {
opacity: 0;
transform: translateY(-50%) scaleX(0.6);
}
73% {
opacity: 0.85;
transform: translateY(-50%) scaleX(1);
}
75% {
opacity: 0.15;
transform: translateY(-50%) scaleX(0.95);
}
76% {
opacity: 0.75;
transform: translateY(-50%) scaleX(1);
}
78% {
opacity: 0;
transform: translateY(-50%) scaleX(1.05);
}
}
@keyframes vs-glow {
0%,
100% {
opacity: 0.82;
filter: drop-shadow(0 0 2px rgba(244, 162, 97, 0.2));
}
50% {
opacity: 1;
filter:
drop-shadow(0 0 3px rgba(244, 162, 97, 0.5))
drop-shadow(0 0 6px rgba(255, 185, 128, 0.25));
}
}
@media (prefers-reduced-motion: reduce) {
.vs-img {
animation: none;
filter: drop-shadow(0 0 3px rgba(244, 162, 97, 0.25));
}
.hz-path,
.hz-beam {
animation: none;
opacity: 0;
}
}
.empty {
text-align: center;
color: var(--text-muted);
padding: 28px 16px;
font-weight: 600;
border-top: 1px solid var(--border);
}
.empty-icon {
width: 72px;
height: 72px;
margin-bottom: 10px;
}
</style>