개선: 토너먼트 베팅 입력 가독성을 높임

This commit is contained in:
2026-08-31 13:54:29 +00:00
parent 3ef86448d7
commit 96f6d85cf4
4 changed files with 231 additions and 96 deletions
+79 -53
View File
@@ -465,27 +465,7 @@ test('desktop bracket connects every real general slot to the next round', async
expect(controls.join).toEqual({ width: 72, height: 44 }); expect(controls.join).toEqual({ width: 72, height: 44 });
expect(controls.close).toEqual({ width: 88, height: 44 }); expect(controls.close).toEqual({ width: 88, height: 44 });
const firstSlot = page.locator('.desktop-bracket-name').first(); await expect(page.locator('.desktop-bracket .bracket-bet-summary')).toHaveCount(0);
const oddsContainment = await firstSlot.evaluate((slot) => {
const card = slot.getBoundingClientRect();
const stat = slot.querySelector<HTMLElement>('.bracket-core-stat')!.getBoundingClientRect();
const odds = slot.querySelector<HTMLElement>('.bracket-odds')!.getBoundingClientRect();
return {
cardTop: card.top,
cardBottom: card.bottom,
statTop: stat.top,
statBottom: stat.bottom,
oddsTop: odds.top,
oddsBottom: odds.bottom,
cardHeight: card.height,
};
});
expect(oddsContainment.cardHeight).toBeGreaterThanOrEqual(82);
expect(oddsContainment.statTop).toBeGreaterThanOrEqual(oddsContainment.cardTop);
expect(oddsContainment.statBottom).toBeLessThanOrEqual(oddsContainment.cardBottom);
expect(oddsContainment.oddsTop).toBeGreaterThanOrEqual(oddsContainment.cardTop);
expect(oddsContainment.oddsBottom).toBeLessThanOrEqual(oddsContainment.cardBottom);
await expect(firstSlot.locator('.bracket-my-bet')).toHaveText('내 투자 금120');
const preliminaryGroups = page.locator('.preliminary-grid .tournament-group-card'); const preliminaryGroups = page.locator('.preliminary-grid .tournament-group-card');
await expect(preliminaryGroups).toHaveCount(8); await expect(preliminaryGroups).toHaveCount(8);
@@ -768,25 +748,7 @@ test('mobile bracket exposes every round through tabs with standard horizontal i
expect(identity.nameLeft).toBeGreaterThanOrEqual(identity.iconRight - 1); expect(identity.nameLeft).toBeGreaterThanOrEqual(identity.iconRight - 1);
expect(identity.nameTop).toBeLessThan(identity.iconBottom); expect(identity.nameTop).toBeLessThan(identity.iconBottom);
expect(identity.nameBottom).toBeGreaterThan(identity.iconTop); expect(identity.nameBottom).toBeGreaterThan(identity.iconTop);
const firstMobileSlot = bracket.locator('.mobile-bracket-name').first(); await expect(bracket.locator('.mobile-bracket .bracket-bet-summary')).toHaveCount(0);
const mobileOddsContainment = await firstMobileSlot.evaluate((slot) => {
const card = slot.getBoundingClientRect();
const stat = slot.querySelector<HTMLElement>('.bracket-core-stat')!.getBoundingClientRect();
const odds = slot.querySelector<HTMLElement>('.bracket-odds')!.getBoundingClientRect();
return {
cardTop: card.top,
cardBottom: card.bottom,
statTop: stat.top,
statBottom: stat.bottom,
oddsBottom: odds.bottom,
cardHeight: card.height,
};
});
expect(mobileOddsContainment.cardHeight).toBeGreaterThanOrEqual(82);
expect(mobileOddsContainment.statTop).toBeGreaterThanOrEqual(mobileOddsContainment.cardTop);
expect(mobileOddsContainment.statBottom).toBeLessThanOrEqual(mobileOddsContainment.cardBottom);
expect(mobileOddsContainment.oddsBottom).toBeLessThanOrEqual(mobileOddsContainment.cardBottom);
await expect(firstMobileSlot.locator('.bracket-my-bet')).toHaveText('내 투자 금120');
await expect(page.getByRole('tablist', { name: '본선 조 선택' })).toBeVisible(); await expect(page.getByRole('tablist', { name: '본선 조 선택' })).toBeVisible();
await page.getByRole('tab', { name: '二조' }).first().click(); await page.getByRole('tab', { name: '二조' }).first().click();
await expect(page.getByRole('tab', { name: '二조' }).first()).toHaveAttribute('aria-selected', 'true'); await expect(page.getByRole('tab', { name: '二조' }).first()).toHaveAttribute('aria-selected', 'true');
@@ -976,7 +938,7 @@ test('tournament and betting close only their script-opened popup window', async
} }
}); });
test('mobile betting rankings use tabs and keep dedicated icons beside general names', async ({ page }, testInfo) => { test('mobile betting cards prioritize readable values and retain the amount draft', async ({ page }, testInfo) => {
await page.setViewportSize({ width: 390, height: 844 }); await page.setViewportSize({ width: 390, height: 844 });
const { placedBets } = await installFixture(page, { tournamentStage: 6 }); const { placedBets } = await installFixture(page, { tournamentStage: 6 });
await page.goto('betting'); await page.goto('betting');
@@ -989,20 +951,70 @@ test('mobile betting rankings use tabs and keep dedicated icons beside general n
const firstCard = page.locator('.mobile-bracket-name[data-general-id="1"]'); const firstCard = page.locator('.mobile-bracket-name[data-general-id="1"]');
const firstBetButton = page.getByRole('button', { name: '관우에게 베팅하기' }); const firstBetButton = page.getByRole('button', { name: '관우에게 베팅하기' });
const corner = await firstCard.evaluate((card) => { await expect(firstCard.locator('.general-identity-icon:visible')).toHaveCount(0);
const layout = await firstCard.evaluate((card) => {
const own = card.getBoundingClientRect(); const own = card.getBoundingClientRect();
const button = card.querySelector<HTMLElement>('.bracket-bet-button')!.getBoundingClientRect(); const button = card.querySelector<HTMLElement>('.bracket-bet-button')!.getBoundingClientRect();
const fields = [
'.general-identity-name',
'.bracket-core-stat',
'.bracket-odds',
'.bracket-my-bet',
'.bracket-bet-button',
].map((selector) => card.querySelector<HTMLElement>(selector)!.getBoundingClientRect());
const overlaps = fields.flatMap((rect, index) =>
fields
.slice(index + 1)
.map(
(other) =>
Math.min(rect.right, other.right) > Math.max(rect.left, other.left) &&
Math.min(rect.bottom, other.bottom) > Math.max(rect.top, other.top)
)
);
return { return {
topOffset: button.top - own.top, topOffset: button.top - own.top,
rightOffset: own.right - button.right, rightOffset: own.right - button.right,
contained: button.top >= own.top && button.right <= own.right && button.bottom <= own.bottom, contained: button.top >= own.top && button.right <= own.right && button.bottom <= own.bottom,
allFieldsContained: fields.every(
(rect) =>
rect.left >= own.left && rect.right <= own.right && rect.top >= own.top && rect.bottom <= own.bottom
),
overlaps,
cardHeight: own.height,
}; };
}); });
expect(corner.topOffset).toBeGreaterThanOrEqual(2); expect(layout.topOffset).toBeGreaterThanOrEqual(2);
expect(corner.topOffset).toBeLessThanOrEqual(4); expect(layout.topOffset).toBeLessThanOrEqual(6);
expect(corner.rightOffset).toBeGreaterThanOrEqual(2); expect(layout.rightOffset).toBeGreaterThanOrEqual(2);
expect(corner.rightOffset).toBeLessThanOrEqual(4); expect(layout.rightOffset).toBeLessThanOrEqual(6);
expect(corner.contained).toBe(true); expect(layout.contained).toBe(true);
expect(layout.allFieldsContained).toBe(true);
expect(layout.overlaps.every((overlap) => !overlap)).toBe(true);
expect(layout.cardHeight).toBeGreaterThanOrEqual(92);
const longNameMetrics = await page
.locator('[data-rich-tooltip="mobile-candidate-icon-16"] .general-identity-name')
.evaluate((element) => ({
clientWidth: element.clientWidth,
scrollWidth: element.scrollWidth,
whiteSpace: getComputedStyle(element).whiteSpace,
overflow: getComputedStyle(element).overflow,
textOverflow: getComputedStyle(element).textOverflow,
}));
expect(longNameMetrics.scrollWidth).toBeLessThanOrEqual(longNameMetrics.clientWidth);
expect(longNameMetrics).toMatchObject({ whiteSpace: 'normal', overflow: 'visible', textOverflow: 'clip' });
const iconTrigger = firstCard.locator('[data-rich-tooltip="mobile-candidate-icon-1"]');
await iconTrigger.hover();
const iconTooltip = page.locator('.tippy-box[data-state="visible"]');
await expect(iconTooltip).toContainText('관우');
await expect(iconTooltip.locator('.general-identity-icon')).toBeVisible();
await page.keyboard.press('Escape');
const returnTrigger = firstCard.locator('[data-rich-tooltip="mobile-candidate-return-1"]');
await returnTrigger.hover();
await expect(page.locator('.tippy-box[data-state="visible"]')).toContainText(
'현재 배당 28.00 × 내 투자 금120 = 금3,360'
);
await firstBetButton.hover(); await firstBetButton.hover();
await expect(firstBetButton).toHaveCSS('filter', 'brightness(1.25)'); await expect(firstBetButton).toHaveCSS('filter', 'brightness(1.25)');
@@ -1035,19 +1047,25 @@ test('mobile betting rankings use tabs and keep dedicated icons beside general n
await page.screenshot({ path: testInfo.outputPath('mobile-betting-dialog-viewport.png') }); await page.screenshot({ path: testInfo.outputPath('mobile-betting-dialog-viewport.png') });
await expect(dialog.getByText('배당 28.00')).toBeVisible(); await expect(dialog.getByText('배당 28.00')).toBeVisible();
await expect(dialog.getByText('예상 환수금 280')).toBeVisible(); await expect(dialog.getByText('예상 환수금 280')).toBeVisible();
await dialog.getByLabel('베팅 금액').selectOption('50'); await dialog.getByLabel('베팅 금액').selectOption('100');
await expect(dialog.getByText('예상 환수금 1,400')).toBeVisible(); await expect(dialog.getByText('예상 환수금 2,800')).toBeVisible();
await persistScreenshot( await persistScreenshot(
page, page,
'tournament-betting-dialog-mobile', 'tournament-betting-dialog-mobile',
testInfo.outputPath('betting-dialog-mobile.webp') testInfo.outputPath('betting-dialog-mobile.webp')
); );
await dialog.getByRole('button', { name: '취소' }).click();
await page.getByRole('button', { name: '장료에게 베팅하기' }).click();
await expect(dialog.getByLabel('베팅 금액')).toHaveValue('100');
await dialog.getByRole('button', { name: '베팅 등록' }).click(); await dialog.getByRole('button', { name: '베팅 등록' }).click();
await expect(dialog).not.toBeVisible(); await expect(dialog).not.toBeVisible();
await expect(page.locator('[data-testid="game-toast"][data-feedback-kind="success"]')).toContainText( await expect(page.locator('[data-testid="game-toast"][data-feedback-kind="success"]')).toContainText(
'베팅이 등록되었습니다.' '베팅이 등록되었습니다.'
); );
expect(placedBets).toEqual([{ targetId: 1, amount: 50 }]); expect(placedBets).toEqual([{ targetId: 2, amount: 100 }]);
await page.getByRole('button', { name: '조운에게 베팅하기' }).click();
await expect(dialog.getByLabel('베팅 금액')).toHaveValue('100');
await dialog.getByRole('button', { name: '취소' }).click();
await expect(page.getByRole('tablist', { name: '토너먼트 랭킹 종목 선택' })).toBeVisible(); await expect(page.getByRole('tablist', { name: '토너먼트 랭킹 종목 선택' })).toBeVisible();
await expect(page.locator('.ranking-table:visible')).toHaveCount(1); await expect(page.locator('.ranking-table:visible')).toHaveCount(1);
@@ -1081,7 +1099,7 @@ test('betting bracket shows intelligence for debate tournament candidates', asyn
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390); expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
}); });
test('desktop betting presents icon-and-name cards and all four rankings without document overflow', async ({ test('desktop betting widens icon-free candidate cards while rankings keep dedicated icons', async ({
page, page,
}, testInfo) => { }, testInfo) => {
await page.setViewportSize({ width: 1365, height: 900 }); await page.setViewportSize({ width: 1365, height: 900 });
@@ -1091,8 +1109,16 @@ test('desktop betting presents icon-and-name cards and all four rankings without
await expect(page.locator('.candidate-table')).toHaveCount(0); await expect(page.locator('.candidate-table')).toHaveCount(0);
await expect(page.locator('.desktop-bracket .bracket-bet-button:visible')).toHaveCount(16); await expect(page.locator('.desktop-bracket .bracket-bet-button:visible')).toHaveCount(16);
await expect(page.locator('.ranking-table:visible')).toHaveCount(4); await expect(page.locator('.ranking-table:visible')).toHaveCount(4);
await expect(page.locator('.general-identity-icon').first()).toHaveCSS('width', '64px'); const firstCandidate = page.locator('.desktop-bracket-name.betting-candidate[data-general-id="1"]');
await expect(page.locator('.general-identity-icon').first()).toHaveCSS('height', '64px'); await expect(firstCandidate.locator('.general-identity-icon:visible')).toHaveCount(0);
await expect(page.locator('.ranking-table:visible .general-identity-icon').first()).toHaveCSS('width', '64px');
await expect(page.locator('.ranking-table:visible .general-identity-icon').first()).toHaveCSS('height', '64px');
const candidateGeometry = await firstCandidate.evaluate((card) => {
const rect = card.getBoundingClientRect();
return { width: rect.width, height: rect.height };
});
expect(candidateGeometry.width).toBeGreaterThanOrEqual(210);
expect(candidateGeometry.height).toBeGreaterThanOrEqual(92);
const firstCardCorner = await page const firstCardCorner = await page
.locator('.desktop-bracket-name.betting-target[data-general-id="1"]') .locator('.desktop-bracket-name.betting-target[data-general-id="1"]')
.evaluate((card) => { .evaluate((card) => {
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import GeneralIdentity from '../ui/GeneralIdentity.vue'; import GeneralIdentity from '../ui/GeneralIdentity.vue';
import RichTooltip from '../ui/RichTooltip.vue';
import { import {
buildTournamentBracket, buildTournamentBracket,
resolveTournamentCoreStat, resolveTournamentCoreStat,
@@ -9,17 +10,28 @@ import {
type TournamentBracketSlot, type TournamentBracketSlot,
} from '../../utils/tournamentBracket'; } from '../../utils/tournamentBracket';
const props = defineProps<{ const props = withDefaults(
participants: TournamentBracketParticipant[]; defineProps<{
matches: TournamentBracketMatch[]; participants: TournamentBracketParticipant[];
winnerId?: number; matches: TournamentBracketMatch[];
betTotals?: Record<number, number>; winnerId?: number;
myBetTotals?: Record<number, number>; betTotals?: Record<number, number>;
totalBet: number; myBetTotals?: Record<number, number>;
tournamentType?: number; totalBet?: number;
showLegend?: boolean; tournamentType?: number;
bettingOpen?: boolean; showLegend?: boolean;
}>(); bettingOpen?: boolean;
bettingMode?: boolean;
}>(),
{
winnerId: undefined,
betTotals: undefined,
myBetTotals: undefined,
totalBet: 0,
tournamentType: undefined,
bettingMode: false,
}
);
const emit = defineEmits<{ const emit = defineEmits<{
requestBet: [slot: TournamentBracketSlot]; requestBet: [slot: TournamentBracketSlot];
@@ -37,10 +49,10 @@ const roundColumns = computed(() => [
]); ]);
const desktopX = [110, 355, 600, 845, 1090]; const desktopX = [110, 355, 600, 845, 1090];
const cardWidth = 190; const cardWidth = 190;
const desktopSlotHeight = 88; const desktopSlotHeight = computed(() => (props.bettingMode ? 100 : 88));
const desktopCanvasHeight = desktopSlotHeight * 16; const desktopCanvasHeight = computed(() => desktopSlotHeight.value * 16);
const slotY = (columnIndex: number, slotIndex: number) => { const slotY = (columnIndex: number, slotIndex: number) => {
const slotHeight = desktopSlotHeight * 2 ** columnIndex; const slotHeight = desktopSlotHeight.value * 2 ** columnIndex;
return slotHeight / 2 + slotIndex * slotHeight; return slotHeight / 2 + slotIndex * slotHeight;
}; };
const connections = computed(() => const connections = computed(() =>
@@ -76,6 +88,18 @@ const odds = (id: number | null) => {
return (props.totalBet / amount).toFixed(2); return (props.totalBet / amount).toFixed(2);
}; };
const myBet = (id: number | null) => (id === null ? 0 : (props.myBetTotals?.[id] ?? 0)); const myBet = (id: number | null) => (id === null ? 0 : (props.myBetTotals?.[id] ?? 0));
const myExpectedReturn = (id: number | null): number | null => {
if (id === null) return null;
const invested = myBet(id);
const targetTotal = props.betTotals?.[id] ?? 0;
if (invested <= 0 || targetTotal <= 0) return null;
return Math.round((invested * props.totalBet) / targetTotal);
};
const myExpectedReturnDescription = (id: number | null): string | null => {
const expectedReturn = myExpectedReturn(id);
if (id === null || expectedReturn === null) return null;
return `현재 배당 ${odds(id)} × 내 투자 금${myBet(id).toLocaleString('ko-KR')} = 금${expectedReturn.toLocaleString('ko-KR')}`;
};
const coreStat = (slot: (typeof bracket.value.top16.slots)[number]) => const coreStat = (slot: (typeof bracket.value.top16.slots)[number]) =>
resolveTournamentCoreStat(slot, props.tournamentType ?? 0); resolveTournamentCoreStat(slot, props.tournamentType ?? 0);
const requestBet = (slot: TournamentBracketSlot) => { const requestBet = (slot: TournamentBracketSlot) => {
@@ -127,6 +151,7 @@ const mobilePairs = computed(() => {
:class="{ :class="{
advanced: slot.advanced, advanced: slot.advanced,
'betting-target': columnIndex === 0 && bettingOpen && slot.id !== null, 'betting-target': columnIndex === 0 && bettingOpen && slot.id !== null,
'betting-candidate': columnIndex === 0 && bettingMode,
}" }"
:data-general-id="slot.id ?? undefined" :data-general-id="slot.id ?? undefined"
:style="{ :style="{
@@ -134,7 +159,24 @@ const mobilePairs = computed(() => {
top: `${slotY(columnIndex, slotIndex)}px`, top: `${slotY(columnIndex, slotIndex)}px`,
}" }"
> >
<span
v-if="columnIndex === 0 && bettingMode && slot.id !== null"
class="bracket-candidate-identity"
>
<RichTooltip placement="right" :max-width="180" :test-id="`candidate-icon-${slot.id}`">
<GeneralIdentity :name="slot.name" :hide-icon="true" :npc-state="slot.npcState" />
<template #content>
<GeneralIdentity
:name="slot.name"
:picture="slot.picture"
:image-server="slot.imageServer"
:npc-state="slot.npcState"
/>
</template>
</RichTooltip>
</span>
<GeneralIdentity <GeneralIdentity
v-else
:name="slot.name" :name="slot.name"
:picture="slot.picture" :picture="slot.picture"
:image-server="slot.imageServer" :image-server="slot.imageServer"
@@ -149,12 +191,24 @@ const mobilePairs = computed(() => {
> >
베팅하기 베팅하기
</button> </button>
<div v-if="columnIndex === 0" class="bracket-bet-summary"> <div v-if="columnIndex === 0 && bettingMode" class="bracket-bet-summary">
<small v-if="coreStat(slot)" class="bracket-core-stat"> <small v-if="coreStat(slot)" class="bracket-core-stat">
{{ coreStat(slot)?.label }} {{ coreStat(slot)?.value }} {{ coreStat(slot)?.label }} {{ coreStat(slot)?.value }}
</small> </small>
<small class="bracket-odds">배당 {{ odds(slot.id) }}</small> <small class="bracket-odds">배당 {{ odds(slot.id) }}</small>
<small class="bracket-my-bet"> 투자 {{ myBet(slot.id) }}</small> <span class="bracket-my-bet-tooltip">
<RichTooltip
title="현재 배당 기준 예상 환수금"
:description="myExpectedReturnDescription(slot.id)"
placement="bottom"
:max-width="260"
:test-id="slot.id === null ? undefined : `candidate-return-${slot.id}`"
>
<small class="bracket-my-bet"
> 투자 {{ myBet(slot.id).toLocaleString('ko-KR') }}</small
>
</RichTooltip>
</span>
</div> </div>
</article> </article>
</template> </template>
@@ -184,10 +238,32 @@ const mobilePairs = computed(() => {
:class="{ :class="{
advanced: slot.advanced, advanced: slot.advanced,
'betting-target': activeMobileRound === 0 && bettingOpen && slot.id !== null, 'betting-target': activeMobileRound === 0 && bettingOpen && slot.id !== null,
'betting-candidate': activeMobileRound === 0 && bettingMode,
}" }"
:data-general-id="slot.id ?? undefined" :data-general-id="slot.id ?? undefined"
> >
<span
v-if="activeMobileRound === 0 && bettingMode && slot.id !== null"
class="bracket-candidate-identity"
>
<RichTooltip
placement="bottom"
:max-width="180"
:test-id="`mobile-candidate-icon-${slot.id}`"
>
<GeneralIdentity :name="slot.name" :hide-icon="true" :npc-state="slot.npcState" />
<template #content>
<GeneralIdentity
:name="slot.name"
:picture="slot.picture"
:image-server="slot.imageServer"
:npc-state="slot.npcState"
/>
</template>
</RichTooltip>
</span>
<GeneralIdentity <GeneralIdentity
v-else
:name="slot.name" :name="slot.name"
:picture="slot.picture" :picture="slot.picture"
:image-server="slot.imageServer" :image-server="slot.imageServer"
@@ -202,12 +278,24 @@ const mobilePairs = computed(() => {
> >
베팅하기 베팅하기
</button> </button>
<div v-if="activeMobileRound === 0" class="bracket-bet-summary"> <div v-if="activeMobileRound === 0 && bettingMode" class="bracket-bet-summary">
<small v-if="coreStat(slot)" class="bracket-core-stat"> <small v-if="coreStat(slot)" class="bracket-core-stat">
{{ coreStat(slot)?.label }} {{ coreStat(slot)?.value }} {{ coreStat(slot)?.label }} {{ coreStat(slot)?.value }}
</small> </small>
<small class="bracket-odds">배당 {{ odds(slot.id) }}</small> <small class="bracket-odds">배당 {{ odds(slot.id) }}</small>
<small class="bracket-my-bet"> 투자 {{ myBet(slot.id) }}</small> <span class="bracket-my-bet-tooltip">
<RichTooltip
title="현재 배당 기준 예상 환수금"
:description="myExpectedReturnDescription(slot.id)"
placement="bottom"
:max-width="260"
:test-id="slot.id === null ? undefined : `mobile-candidate-return-${slot.id}`"
>
<small class="bracket-my-bet"
> 투자 {{ myBet(slot.id).toLocaleString('ko-KR') }}</small
>
</RichTooltip>
</span>
</div> </div>
</div> </div>
<strong v-if="pair.length === 2" class="versus" aria-hidden="true">VS</strong> <strong v-if="pair.length === 2" class="versus" aria-hidden="true">VS</strong>
@@ -272,6 +360,13 @@ const mobilePairs = computed(() => {
color: #fff; color: #fff;
padding: 1px 3px; padding: 1px 3px;
} }
.desktop-bracket-name.betting-candidate {
width: clamp(150px, 18vw, 216px);
min-height: 92px;
align-content: center;
gap: 5px;
padding: 5px 6px;
}
.betting-target { .betting-target {
position: absolute; position: absolute;
} }
@@ -312,12 +407,30 @@ const mobilePairs = computed(() => {
width: 100%; width: 100%;
justify-content: flex-start; justify-content: flex-start;
} }
.bracket-bet-summary { .bracket-candidate-identity {
display: flex; display: flex;
min-width: 0; min-width: 0;
min-height: 24px;
align-items: center; align-items: center;
justify-content: space-between; padding-right: 62px;
gap: 4px; text-align: left;
}
.bracket-candidate-identity :deep(.rich-tooltip-trigger) {
display: block;
min-width: 0;
}
.betting-candidate .bracket-candidate-identity :deep(.general-identity-name) {
overflow: visible;
text-overflow: clip;
white-space: normal;
overflow-wrap: anywhere;
}
.bracket-bet-summary {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
min-width: 0;
align-items: center;
gap: 3px 7px;
white-space: nowrap; white-space: nowrap;
} }
.bracket-core-stat, .bracket-core-stat,
@@ -331,11 +444,20 @@ const mobilePairs = computed(() => {
} }
.bracket-core-stat { .bracket-core-stat {
color: #fff; color: #fff;
text-align: left;
}
.bracket-odds {
text-align: right;
}
.bracket-my-bet-tooltip {
grid-column: 1 / -1;
min-width: 0;
color: orange;
text-align: left;
} }
.bracket-my-bet { .bracket-my-bet {
overflow: hidden;
color: orange; color: orange;
text-overflow: ellipsis; font-variant-numeric: tabular-nums;
} }
.mobile-bracket { .mobile-bracket {
display: none; display: none;
@@ -382,6 +504,10 @@ const mobilePairs = computed(() => {
background: rgb(58 33 24 / 94%); background: rgb(58 33 24 / 94%);
padding: 1px 3px; padding: 1px 3px;
} }
.mobile-bracket-name.betting-candidate {
min-height: 92px;
padding: 5px 6px;
}
.versus { .versus {
position: absolute; position: absolute;
top: 50%; top: 50%;
+2 -13
View File
@@ -12,7 +12,7 @@ import { trpc } from '../utils/trpc';
const tournamentPages = useTournamentPagesStore(); const tournamentPages = useTournamentPagesStore();
const { snapshot, betting: summary, rankings, loading, error } = storeToRefs(tournamentPages); const { snapshot, betting: summary, rankings, loading, error } = storeToRefs(tournamentPages);
const amounts = ref<Record<number, number>>({}); const selectedAmount = ref(10);
const selectedTarget = ref<TournamentBracketSlot | null>(null); const selectedTarget = ref<TournamentBracketSlot | null>(null);
const betDialog = ref<HTMLDialogElement | null>(null); const betDialog = ref<HTMLDialogElement | null>(null);
const betAmountSelect = ref<HTMLSelectElement | null>(null); const betAmountSelect = ref<HTMLSelectElement | null>(null);
@@ -55,17 +55,6 @@ const ratio = (id: number) => {
const openingTime = computed(() => const openingTime = computed(() =>
formatServerDateTime(snapshot.value?.state?.nextAt, { format: 'hourMinute', fallback: '--:--' }) formatServerDateTime(snapshot.value?.state?.nextAt, { format: 'hourMinute', fallback: '--:--' })
); );
const selectedAmount = computed({
get: () => {
const targetId = selectedTarget.value?.id;
return targetId === null || targetId === undefined ? 10 : (amounts.value[targetId] ?? 10);
},
set: (amount: number) => {
const targetId = selectedTarget.value?.id;
if (targetId === null || targetId === undefined) return;
amounts.value[targetId] = amount;
},
});
const selectedRatio = computed(() => { const selectedRatio = computed(() => {
const targetId = selectedTarget.value?.id; const targetId = selectedTarget.value?.id;
return targetId === null || targetId === undefined ? '0' : ratio(targetId); return targetId === null || targetId === undefined ? '0' : ratio(targetId);
@@ -85,7 +74,6 @@ const openBetDialog = async (target: TournamentBracketSlot) => {
if (target.id === null || !bettingOpen.value) return; if (target.id === null || !bettingOpen.value) return;
selectedTarget.value = target; selectedTarget.value = target;
betError.value = null; betError.value = null;
if (amounts.value[target.id] === undefined) amounts.value[target.id] = 10;
await nextTick(); await nextTick();
betDialog.value?.showModal(); betDialog.value?.showModal();
betAmountSelect.value?.focus(); betAmountSelect.value?.focus();
@@ -147,6 +135,7 @@ const placeBet = async () => {
:tournament-type="snapshot?.state?.type ?? 0" :tournament-type="snapshot?.state?.type ?? 0"
:show-legend="false" :show-legend="false"
:betting-open="bettingOpen" :betting-open="bettingOpen"
:betting-mode="true"
@request-bet="openBetDialog" @request-bet="openBetDialog"
/> />
@@ -12,7 +12,7 @@ import { trpc } from '../utils/trpc';
import { resolveTournamentSectionVisibility, resolveTournamentStageName } from '../utils/tournamentStatus'; import { resolveTournamentSectionVisibility, resolveTournamentStageName } from '../utils/tournamentStatus';
const tournamentPages = useTournamentPagesStore(); const tournamentPages = useTournamentPagesStore();
const { snapshot, betting, loading, error } = storeToRefs(tournamentPages); const { snapshot, loading, error } = storeToRefs(tournamentPages);
type Snapshot = NonNullable<typeof snapshot.value>; type Snapshot = NonNullable<typeof snapshot.value>;
const myGeneralId = ref(0); const myGeneralId = ref(0);
const adminEnabled = ref(false); const adminEnabled = ref(false);
@@ -54,12 +54,9 @@ const matchesAt = (stage: number) =>
.filter((match) => match.stage === stage) .filter((match) => match.stage === stage)
.sort((a, b) => a.roundIndex - b.roundIndex); .sort((a, b) => a.roundIndex - b.roundIndex);
const nameOf = (id?: number) => (id ? (participantsById.value.get(id)?.name ?? `#${id}`) : '-'); const nameOf = (id?: number) => (id ? (participantsById.value.get(id)?.name ?? `#${id}`) : '-');
const totalBet = computed(() => betting.value?.totalAmount ?? 0);
const openingTime = computed(() => const openingTime = computed(() =>
formatServerDateTime(snapshot.value?.state?.nextAt, { format: 'hourMinute', fallback: '--:--' }) formatServerDateTime(snapshot.value?.state?.nextAt, { format: 'hourMinute', fallback: '--:--' })
); );
const betTotals = computed(() => betting.value?.totals as Record<number, number> | undefined);
const myBetTotals = computed(() => betting.value?.myTotals as Record<number, number> | undefined);
const isParticipant = computed(() => const isParticipant = computed(() =>
(snapshot.value?.participants ?? []).some((participant) => participant.id === myGeneralId.value) (snapshot.value?.participants ?? []).some((participant) => participant.id === myGeneralId.value)
); );
@@ -230,9 +227,6 @@ const start = async () => {
:participants="snapshot?.participants ?? []" :participants="snapshot?.participants ?? []"
:matches="snapshot?.matches ?? []" :matches="snapshot?.matches ?? []"
:winner-id="snapshot?.state?.winnerId" :winner-id="snapshot?.state?.winnerId"
:bet-totals="betTotals"
:my-bet-totals="myBetTotals"
:total-bet="totalBet"
:tournament-type="snapshot?.state?.type ?? 0" :tournament-type="snapshot?.state?.type ?? 0"
/> />