feat(theme-2): sync match search feature from main
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onActivated, watch } from 'vue';
|
import { ref, computed, onActivated, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { usePlayerMatches } from '../composables/usePlayerMatches';
|
import { usePlayerMatches } from '../composables/usePlayerMatches';
|
||||||
import LeagueAccordionItem from '../components/LeagueAccordionItem.vue';
|
import LeagueAccordionItem from '../components/LeagueAccordionItem.vue';
|
||||||
|
import MatchBetCard from '../components/MatchBetCard.vue';
|
||||||
import OutrightPanel from '../components/outright/OutrightPanel.vue';
|
import OutrightPanel from '../components/outright/OutrightPanel.vue';
|
||||||
import emptyMatchesImg from '../assets/images/empty-matches.svg';
|
import emptyMatchesImg from '../assets/images/empty-matches.svg';
|
||||||
import { useOnLocaleChange } from '../composables/useOnLocaleChange';
|
import { useOnLocaleChange } from '../composables/useOnLocaleChange';
|
||||||
@@ -59,8 +60,10 @@ interface LeagueGroup {
|
|||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
const mainTab = ref<MainTab>('matches');
|
const mainTab = ref<MainTab>('matches');
|
||||||
|
const searchQuery = ref('');
|
||||||
const filterState = ref({
|
const filterState = ref({
|
||||||
time: 'all' as 'all' | 'today' | 'early',
|
time: 'all' as 'all' | 'today' | 'early',
|
||||||
status: 'open' as 'all' | 'open' | 'settled',
|
status: 'open' as 'all' | 'open' | 'settled',
|
||||||
@@ -96,10 +99,23 @@ function normalizeLeagueName(name: string): string {
|
|||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncSearchFromRoute() {
|
||||||
|
const q = route.query.search;
|
||||||
|
searchQuery.value = typeof q === 'string' ? q : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesSearchKeyword(m: Match, keyword: string) {
|
||||||
|
if (!keyword) return true;
|
||||||
|
const haystack = `${m.homeTeamName} ${m.awayTeamName} ${m.leagueName}`.toLowerCase();
|
||||||
|
return haystack.includes(keyword);
|
||||||
|
}
|
||||||
|
|
||||||
const filteredMatches = computed(() => {
|
const filteredMatches = computed(() => {
|
||||||
if (mainTab.value !== 'matches') return [];
|
if (mainTab.value !== 'matches') return [];
|
||||||
const now = filterNow.value;
|
const now = filterNow.value;
|
||||||
|
const keyword = searchQuery.value.trim().toLowerCase();
|
||||||
return matches.value.filter((m) => {
|
return matches.value.filter((m) => {
|
||||||
|
if (!matchesSearchKeyword(m, keyword)) return false;
|
||||||
let timeMatch = true;
|
let timeMatch = true;
|
||||||
if (filterState.value.time === 'today') {
|
if (filterState.value.time === 'today') {
|
||||||
timeMatch = isInTodayMatchWindow(m.startTime, now);
|
timeMatch = isInTodayMatchWindow(m.startTime, now);
|
||||||
@@ -196,6 +212,16 @@ function buildLeagueGroups(source: Match[]): LeagueGroup[] {
|
|||||||
|
|
||||||
const leagueGroups = computed(() => buildLeagueGroups(filteredMatches.value));
|
const leagueGroups = computed(() => buildLeagueGroups(filteredMatches.value));
|
||||||
|
|
||||||
|
const isSearchActive = computed(() => searchQuery.value.trim().length > 0);
|
||||||
|
|
||||||
|
const sortedFilteredMatches = computed(() =>
|
||||||
|
[...filteredMatches.value].sort(
|
||||||
|
(a, b) =>
|
||||||
|
(a.displayOrder ?? 0) - (b.displayOrder ?? 0) ||
|
||||||
|
new Date(a.startTime).getTime() - new Date(b.startTime).getTime(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const expandedLeagues = ref(new Set<string>());
|
const expandedLeagues = ref(new Set<string>());
|
||||||
|
|
||||||
watch(leagueGroups, (groups) => {
|
watch(leagueGroups, (groups) => {
|
||||||
@@ -226,9 +252,15 @@ function selectMainTab(tab: MainTab) {
|
|||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
filterNow.value = new Date();
|
filterNow.value = new Date();
|
||||||
filterState.value.time = 'all';
|
filterState.value.time = 'all';
|
||||||
|
syncSearchFromRoute();
|
||||||
void loadSummary(true, true);
|
void loadSummary(true, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query.search,
|
||||||
|
() => syncSearchFromRoute(),
|
||||||
|
);
|
||||||
|
|
||||||
function goMatch(id: string) {
|
function goMatch(id: string) {
|
||||||
router.push(`/match/${id}`);
|
router.push(`/match/${id}`);
|
||||||
}
|
}
|
||||||
@@ -265,6 +297,17 @@ function goMatch(id: string) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :class="['tab-panel', { 'tab-panel--hidden': mainTab !== 'matches' }]">
|
<div :class="['tab-panel', { 'tab-panel--hidden': mainTab !== 'matches' }]">
|
||||||
|
<div class="search-bar">
|
||||||
|
<span class="search-icon" aria-hidden="true">⌕</span>
|
||||||
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
type="search"
|
||||||
|
:placeholder="t('search.placeholder')"
|
||||||
|
enterkeyhint="search"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="filters-bar">
|
<div class="filters-bar">
|
||||||
<div class="phase-filter">
|
<div class="phase-filter">
|
||||||
<div class="league-dropdown">
|
<div class="league-dropdown">
|
||||||
@@ -340,7 +383,22 @@ function goMatch(id: string) {
|
|||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="leagueGroups.length" class="league-list">
|
<div v-if="isSearchActive && sortedFilteredMatches.length" class="search-results">
|
||||||
|
<p class="search-results-meta">
|
||||||
|
{{ t('search.results_count', { count: sortedFilteredMatches.length }) }}
|
||||||
|
</p>
|
||||||
|
<div class="search-match-list">
|
||||||
|
<article
|
||||||
|
v-for="match in sortedFilteredMatches"
|
||||||
|
:key="match.id"
|
||||||
|
class="search-result-item"
|
||||||
|
>
|
||||||
|
<p class="search-result-league">{{ normalizeLeagueName(match.leagueName) }}</p>
|
||||||
|
<MatchBetCard :match="match" @bet="goMatch" />
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!isSearchActive && leagueGroups.length" class="league-list">
|
||||||
<LeagueAccordionItem
|
<LeagueAccordionItem
|
||||||
v-for="group in leagueGroups"
|
v-for="group in leagueGroups"
|
||||||
:key="group.leagueId"
|
:key="group.leagueId"
|
||||||
@@ -355,7 +413,7 @@ function goMatch(id: string) {
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="empty">
|
<div v-else class="empty">
|
||||||
<img :src="emptyMatchesImg" alt="" class="empty-icon" />
|
<img :src="emptyMatchesImg" alt="" class="empty-icon" />
|
||||||
<p>{{ t('bet.no_matches') }}</p>
|
<p>{{ isSearchActive ? t('search.no_results') : t('bet.no_matches') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -637,4 +695,75 @@ function goMatch(id: string) {
|
|||||||
.outright-tab {
|
.outright-tab {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0 12px 8px;
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 32px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
color: var(--primary-light);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.2;
|
||||||
|
outline: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar input::placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results-meta {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-match-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-league {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 2px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary-light);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user