Skip to content

Commit de6b375

Browse files
authored
feature: ready popup for matches (#378)
1 parent dcc71f3 commit de6b375

10 files changed

Lines changed: 411 additions & 4 deletions

File tree

app.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import { useAuthStore } from "~/stores/AuthStore";
88
const MatchmakingConfirm = defineAsyncComponent(
99
() => import("~/components/matchmaking/MatchmakingConfirm.vue"),
1010
);
11+
const MatchActiveAlert = defineAsyncComponent(
12+
() => import("~/components/match/MatchActiveAlert.vue"),
13+
);
1114
const PlayerNameRegistration = defineAsyncComponent(
1215
() => import("~/components/PlayerNameRegistration.vue"),
1316
);
@@ -63,6 +66,7 @@ function pageKeyWithoutTabQuery(route: {
6366
<template v-if="me">
6467
<PlayerNameRegistration />
6568
<MatchmakingConfirm />
69+
<MatchActiveAlert />
6670
</template>
6771

6872
<NuxtLayout>
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<script setup lang="ts">
2+
import { AlertDialog, AlertDialogContent } from "@/components/ui/alert-dialog";
3+
import { X } from "lucide-vue-next";
4+
</script>
5+
6+
<template>
7+
<AlertDialog :open="!!shouldShow">
8+
<AlertDialogContent
9+
class="!max-w-md !gap-0 overflow-visible !border-0 !bg-transparent !p-0 !shadow-none"
10+
>
11+
<div
12+
v-if="match"
13+
class="relative overflow-hidden rounded-lg border border-border px-6 py-8 [backdrop-filter:blur(10px)] [background:linear-gradient(180deg,hsl(var(--card)/0.95)_0%,hsl(var(--card)/0.85)_100%)] [box-shadow:0_0_0_1px_hsl(var(--tac-amber)/0.3),0_0_40px_hsl(var(--tac-amber)/0.18)]"
14+
>
15+
<span
16+
aria-hidden="true"
17+
class="pointer-events-none absolute left-2 top-2 h-[14px] w-[14px] border-l-2 border-t-2 border-[hsl(var(--tac-amber))]"
18+
></span>
19+
<span
20+
aria-hidden="true"
21+
class="pointer-events-none absolute bottom-2 right-2 h-[14px] w-[14px] border-b-2 border-r-2 border-[hsl(var(--tac-amber))]"
22+
></span>
23+
<span
24+
aria-hidden="true"
25+
class="pointer-events-none absolute inset-0 opacity-30 [background-image:repeating-linear-gradient(180deg,transparent_0,transparent_3px,hsl(var(--tac-amber)/0.04)_3px,hsl(var(--tac-amber)/0.04)_4px)]"
26+
></span>
27+
28+
<div class="relative z-10 flex flex-col items-center gap-5 text-center">
29+
<div class="flex flex-col items-center gap-1.5">
30+
<div
31+
class="inline-flex items-center gap-2 font-mono text-[0.72rem] font-bold uppercase tracking-[0.28em] text-[hsl(var(--tac-amber))]"
32+
>
33+
<span
34+
class="inline-block h-[2px] w-[10px] bg-[hsl(var(--tac-amber))]"
35+
></span>
36+
{{ statusLabel }}
37+
<span
38+
class="h-1 w-1 rounded-full animate-soft-pulse bg-[hsl(var(--tac-amber))]"
39+
></span>
40+
</div>
41+
</div>
42+
43+
<div class="flex flex-col items-center gap-1">
44+
<div
45+
class="font-sans text-base font-semibold leading-snug text-foreground"
46+
>
47+
{{ matchTitle }}
48+
</div>
49+
</div>
50+
51+
<NuxtLink
52+
:to="`/matches/${match.id}`"
53+
class="tac-amber-cta relative isolate mt-2 inline-flex w-full items-center justify-center gap-3 overflow-hidden rounded-md border px-6 py-4 font-sans text-sm font-bold uppercase leading-none tracking-[0.22em]"
54+
@click="acknowledge"
55+
>
56+
<span
57+
class="inline-block h-[2px] w-[12px] bg-[hsl(var(--tac-amber-foreground))]/70"
58+
></span>
59+
{{ $t("matchmaking.go_to_match") }}
60+
<span
61+
class="inline-block h-[2px] w-[12px] bg-[hsl(var(--tac-amber-foreground))]/70"
62+
></span>
63+
</NuxtLink>
64+
65+
<div
66+
class="mt-1 flex flex-col items-center gap-0.5 text-center text-xs leading-snug"
67+
>
68+
<span class="text-muted-foreground">
69+
{{ $t("matchmaking.disable_match_ready_modal_hint") }}
70+
</span>
71+
<NuxtLink
72+
to="/settings/matchmaking"
73+
class="text-foreground underline underline-offset-4 decoration-muted-foreground/60 transition-colors hover:text-[hsl(var(--tac-amber))] hover:decoration-[hsl(var(--tac-amber))]"
74+
@click="acknowledge"
75+
>
76+
{{ $t("matchmaking.disable_match_ready_modal_action") }}
77+
</NuxtLink>
78+
</div>
79+
</div>
80+
81+
<button
82+
type="button"
83+
class="absolute right-3 top-3 z-20 inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground"
84+
:aria-label="$t('common.close')"
85+
@click="acknowledge"
86+
>
87+
<X class="h-4 w-4" />
88+
</button>
89+
</div>
90+
</AlertDialogContent>
91+
</AlertDialog>
92+
</template>
93+
94+
<script lang="ts">
95+
import { e_match_status_enum } from "~/generated/zeus";
96+
import { useMatchLobbyStore } from "~/stores/MatchLobbyStore";
97+
import { useMatchmakingStore } from "~/stores/MatchmakingStore";
98+
import { useAuthStore } from "~/stores/AuthStore";
99+
import { useMatchReadyModal } from "~/composables/useMatchReadyModal";
100+
101+
const ALERT_STATUSES: string[] = [
102+
e_match_status_enum.WaitingForCheckIn,
103+
e_match_status_enum.Veto,
104+
e_match_status_enum.Live,
105+
];
106+
107+
export default {
108+
setup() {
109+
const { manuallyOpened } = useMatchReadyModal();
110+
return { manuallyOpened };
111+
},
112+
data() {
113+
return {
114+
acknowledgedKey: null as string | null,
115+
};
116+
},
117+
computed: {
118+
match(): any {
119+
return useMatchLobbyStore().currentMatch;
120+
},
121+
matchKey(): string | null {
122+
if (!this.match) return null;
123+
return `${this.match.id}:${this.match.status}`;
124+
},
125+
isAlertable(): boolean {
126+
return !!this.match && ALERT_STATUSES.includes(this.match.status);
127+
},
128+
hasActiveMatchmakingConfirmation(): boolean {
129+
const confirmation =
130+
useMatchmakingStore().joinedMatchmakingQueues?.confirmation;
131+
return !!confirmation && !confirmation.matchId;
132+
},
133+
showPref(): boolean {
134+
return useAuthStore().me?.show_match_ready_modal !== false;
135+
},
136+
isOnMatchPage(): boolean {
137+
const path = this.$route?.path || "";
138+
return !!this.match && path.startsWith(`/matches/${this.match.id}`);
139+
},
140+
shouldShow(): boolean {
141+
if (!this.isAlertable) return false;
142+
if (this.hasActiveMatchmakingConfirmation) return false;
143+
if (this.isOnMatchPage) return false;
144+
if (this.acknowledgedKey && this.acknowledgedKey === this.matchKey) {
145+
return false;
146+
}
147+
return this.showPref || this.manuallyOpened;
148+
},
149+
statusLabel(): string {
150+
return (
151+
this.match?.e_match_status?.description ||
152+
this.match?.status ||
153+
""
154+
);
155+
},
156+
matchTitle(): string {
157+
const a = this.match?.lineup_1?.name || this.$t("common.tbd");
158+
const b = this.match?.lineup_2?.name || this.$t("common.tbd");
159+
return `${a} vs ${b}`;
160+
},
161+
},
162+
watch: {
163+
matchKey(next, prev) {
164+
if (next !== prev && this.acknowledgedKey !== next) {
165+
this.acknowledgedKey = null;
166+
}
167+
},
168+
},
169+
methods: {
170+
acknowledge() {
171+
this.acknowledgedKey = this.matchKey;
172+
useMatchReadyModal().closeMatchReadyModal();
173+
},
174+
},
175+
};
176+
</script>

components/matchmaking/MatchmakingConfirm.vue

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import { AlertDialog, AlertDialogContent } from "@/components/ui/alert-dialog";
3+
import { X } from "lucide-vue-next";
34
</script>
45

56
<template>
@@ -152,18 +153,49 @@ import { AlertDialog, AlertDialogContent } from "@/components/ui/alert-dialog";
152153
></span>
153154
{{ $t("matchmaking.locked_in") }}
154155
</div>
156+
157+
<div
158+
class="mt-1 flex flex-col items-center gap-0.5 text-center text-xs leading-snug"
159+
>
160+
<span class="text-muted-foreground">
161+
{{ $t("matchmaking.disable_match_ready_modal_hint") }}
162+
</span>
163+
<NuxtLink
164+
to="/settings/matchmaking"
165+
class="text-foreground underline underline-offset-4 decoration-muted-foreground/60 transition-colors hover:text-[hsl(var(--tac-amber))] hover:decoration-[hsl(var(--tac-amber))]"
166+
@click="dismiss"
167+
>
168+
{{ $t("matchmaking.disable_match_ready_modal_action") }}
169+
</NuxtLink>
170+
</div>
155171
</div>
172+
173+
<button
174+
v-if="manuallyOpened"
175+
type="button"
176+
class="absolute right-3 top-3 z-20 inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground"
177+
:aria-label="$t('common.close')"
178+
@click="dismiss"
179+
>
180+
<X class="h-4 w-4" />
181+
</button>
156182
</div>
157183
</AlertDialogContent>
158184
</AlertDialog>
159185
</template>
160186

161187
<script lang="ts">
162188
import { useMatchmakingStore } from "~/stores/MatchmakingStore";
189+
import { useAuthStore } from "~/stores/AuthStore";
190+
import { useMatchReadyModal } from "~/composables/useMatchReadyModal";
163191
import socket from "~/web-sockets/Socket";
164192
import { useSound } from "~/composables/useSound";
165193
166194
export default {
195+
setup() {
196+
const { manuallyOpened } = useMatchReadyModal();
197+
return { manuallyOpened };
198+
},
167199
data() {
168200
return {
169201
remainingSeconds: 0,
@@ -178,11 +210,17 @@ export default {
178210
confirmation() {
179211
return useMatchmakingStore().joinedMatchmakingQueues?.confirmation;
180212
},
213+
hasPendingConfirmation(): boolean {
214+
return !!this.confirmation && !this.confirmation.matchId;
215+
},
216+
showMatchReadyModalPref(): boolean {
217+
return useAuthStore().me?.show_match_ready_modal !== false;
218+
},
181219
shouldShow() {
182-
if (!this.confirmation || this.confirmation.matchId) {
220+
if (!this.hasPendingConfirmation) {
183221
return false;
184222
}
185-
return true;
223+
return this.showMatchReadyModalPref || this.manuallyOpened;
186224
},
187225
formattedCountdown(): string {
188226
const total = Math.max(0, this.remainingSeconds);
@@ -201,6 +239,7 @@ export default {
201239
immediate: true,
202240
handler(confirmation, oldConfirmation) {
203241
if (!confirmation) {
242+
useMatchReadyModal().closeMatchReadyModal();
204243
return;
205244
}
206245
@@ -235,6 +274,9 @@ export default {
235274
confirmationId: this.confirmation.confirmationId,
236275
});
237276
},
277+
dismiss() {
278+
useMatchReadyModal().closeMatchReadyModal();
279+
},
238280
updateCountdown() {
239281
if (
240282
this.confirmation?.expiresAt &&

components/matchmaking/MatchmakingSettings.vue

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,39 @@
11
<script setup lang="ts">
22
import { useApplicationSettingsStore } from "~/stores/ApplicationSettings";
33
import { Button } from "~/components/ui/button";
4+
import { Switch } from "~/components/ui/switch";
45
import { RefreshCw } from "lucide-vue-next";
56
import { Loader2 } from "lucide-vue-next";
67
</script>
78

89
<template>
910
<div class="space-y-4">
11+
<div
12+
class="flex items-center justify-between gap-4 p-8 border border-border rounded-lg bg-card"
13+
>
14+
<div class="flex-1">
15+
<Label class="text-lg font-semibold">
16+
{{ $t("pages.settings.matchmaking.show_match_ready_modal.title") }}
17+
</Label>
18+
<p class="text-sm text-muted-foreground mt-1">
19+
{{
20+
$t("pages.settings.matchmaking.show_match_ready_modal.description")
21+
}}
22+
</p>
23+
</div>
24+
<div class="flex items-center gap-2">
25+
<Loader2
26+
v-if="savingShowMatchReadyModal"
27+
class="h-4 w-4 animate-spin text-muted-foreground"
28+
/>
29+
<Switch
30+
:model-value="showMatchReadyModal"
31+
:disabled="savingShowMatchReadyModal || !me"
32+
@update:model-value="updateShowMatchReadyModal"
33+
/>
34+
</div>
35+
</div>
36+
1037
<div class="flex items-center p-8 border border-border rounded-lg bg-card">
1138
<div class="flex-1">
1239
<div class="flex justify-between mb-4">
@@ -127,10 +154,16 @@ import { Loader2 } from "lucide-vue-next";
127154
</template>
128155

129156
<script lang="ts">
157+
import { generateMutation } from "~/graphql/graphqlGen";
158+
import { $ } from "~/generated/zeus";
159+
import { useAuthStore } from "~/stores/AuthStore";
160+
import { toast } from "@/components/ui/toast";
161+
130162
export default {
131163
data() {
132164
return {
133165
playerMaxAcceptablelatnecy: 75,
166+
savingShowMatchReadyModal: false,
134167
};
135168
},
136169
mounted() {
@@ -141,6 +174,27 @@ export default {
141174
async refreshLatencies() {
142175
await useMatchmakingStore().refreshLatencies();
143176
},
177+
async updateShowMatchReadyModal(value: boolean) {
178+
if (!this.me) return;
179+
this.savingShowMatchReadyModal = true;
180+
try {
181+
await this.$apollo.mutate({
182+
variables: { show: value },
183+
mutation: generateMutation({
184+
update_players_by_pk: [
185+
{
186+
pk_columns: { steam_id: this.me.steam_id },
187+
_set: { show_match_ready_modal: $("show", "Boolean!") },
188+
},
189+
{ steam_id: true, show_match_ready_modal: true },
190+
],
191+
}),
192+
});
193+
toast({ title: this.$t("pages.settings.account.update_success") });
194+
} finally {
195+
this.savingShowMatchReadyModal = false;
196+
}
197+
},
144198
togglePreferredRegion(region: string) {
145199
useMatchmakingStore().togglePreferredRegion(region);
146200
},
@@ -193,6 +247,12 @@ export default {
193247
},
194248
},
195249
computed: {
250+
me() {
251+
return useAuthStore().me;
252+
},
253+
showMatchReadyModal(): boolean {
254+
return this.me?.show_match_ready_modal !== false;
255+
},
196256
isRefreshing() {
197257
return useMatchmakingStore().isRefreshing;
198258
},

0 commit comments

Comments
 (0)