Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions components/PlayerDisplay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ import FiveStackToolTip from "./FiveStackToolTip.vue";
</a>
</p>
</div>
<slot name="subline"></slot>
</div>
</slot>
</div>
Expand Down
257 changes: 224 additions & 33 deletions components/PlayerSearch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,30 +49,74 @@ const { height: viewportHeight } = useVisualViewport();
>
<div class="flex-1 overflow-y-auto min-h-0 p-4 flex flex-col">
<div class="flex-1" />
<div
v-if="!players?.length"
class="p-4 text-center text-muted-foreground"
>
{{ $t("player.search.no_players_found") }}
</div>

<div v-else class="divide-y">
<!-- Grouped: Friends / Others -->
<template v-if="groupByFriends">
<div
v-for="player in players"
:key="`player-${player.steam_id}}`"
class="px-3 py-2 hover:bg-accent cursor-pointer"
@click="select(player)"
v-if="!hasGroupResults"
class="p-4 text-center text-muted-foreground"
>
<PlayerDisplay :player="player" />
{{ $t("player.search.no_players_found") }}
</div>
</div>
<template v-for="group in playerGroups" :key="group.key">
<div v-if="group.players.length">
<div
class="sticky top-0 z-20 flex items-center gap-2 border-b border-border bg-background px-3 py-2 font-mono text-[0.6rem] font-bold uppercase tracking-[0.18em] text-muted-foreground"
>
<span class="h-[2px] w-2 bg-[hsl(var(--tac-amber))]" />
{{ group.label }}
<span
class="ml-auto tabular-nums text-[hsl(var(--tac-amber))]"
>
{{ group.players.length }}
</span>
</div>
<div class="divide-y">
<div
v-for="player in group.players"
:key="`g-${group.key}-${player.steam_id}`"
class="px-3 py-2 hover:bg-accent cursor-pointer"
@click="select(player)"
>
<PlayerDisplay :player="player" />
</div>
</div>
</div>
</template>
</template>

<template v-else>
<div
v-if="!displayPlayers.length"
class="p-4 text-center text-muted-foreground"
>
{{ $t("player.search.no_players_found") }}
</div>

<div v-else class="divide-y">
<div
v-for="player in displayPlayers"
:key="`player-${player.steam_id}}`"
class="px-3 py-2 hover:bg-accent cursor-pointer"
@click="select(player)"
>
<PlayerDisplay :player="player" />
</div>
</div>
</template>
</div>

<div
v-if="players?.length"
v-if="groupByFriends ? hasGroupResults : displayPlayers.length"
class="px-4 py-2 text-xs text-muted-foreground border-t"
>
{{ players.length }} {{ $t("player.search.found_players") }}
<template v-if="groupByFriends">
{{ playerGroups[0].players.length + playerGroups[1].players.length }}
{{ $t("player.search.found_players") }}
</template>
<template v-else>
{{ displayPlayers.length }} {{ $t("player.search.found_players") }}
</template>
</div>

<div class="flex items-center justify-between p-4 border-t">
Expand Down Expand Up @@ -158,29 +202,67 @@ const { height: viewportHeight } = useVisualViewport();
</div>

<div class="max-h-[300px] overflow-y-auto">
<div
v-if="!players?.length"
class="p-4 text-center text-muted-foreground"
>
{{ $t("player.search.no_players_found") }}
</div>
<!-- Grouped: Friends / Others -->
<template v-if="groupByFriends">
<div
v-if="!hasGroupResults"
class="p-4 text-center text-muted-foreground"
>
{{ $t("player.search.no_players_found") }}
</div>
<template v-for="group in playerGroups" :key="group.key">
<div v-if="group.players.length">
<div
class="sticky top-0 z-20 flex items-center gap-2 border-b border-border bg-popover px-3 py-2 font-mono text-[0.6rem] font-bold uppercase tracking-[0.18em] text-muted-foreground"
>
<span class="h-[2px] w-2 bg-[hsl(var(--tac-amber))]" />
{{ group.label }}
<span
class="ml-auto tabular-nums text-[hsl(var(--tac-amber))]"
>
{{ group.players.length }}
</span>
</div>
<div class="divide-y">
<div
v-for="player in group.players"
:key="`g-${group.key}-${player.steam_id}`"
class="px-3 py-2 hover:bg-accent cursor-pointer"
@click="select(player)"
>
<PlayerDisplay :player="player" />
</div>
</div>
</div>
</template>
</template>

<div v-else>
<div class="px-3 py-2 text-sm text-muted-foreground">
{{ players.length }} {{ $t("player.search.found_players") }}
<template v-else>
<div
v-if="!displayPlayers.length"
class="p-4 text-center text-muted-foreground"
>
{{ $t("player.search.no_players_found") }}
</div>

<div class="divide-y">
<div
v-for="player in players"
:key="`player-${player.steam_id}}`"
class="px-3 py-2 hover:bg-accent cursor-pointer"
@click="select(player)"
>
<PlayerDisplay :player="player" />
<div v-else>
<div class="px-3 py-2 text-sm text-muted-foreground">
{{ displayPlayers.length }}
{{ $t("player.search.found_players") }}
</div>

<div class="divide-y">
<div
v-for="player in displayPlayers"
:key="`player-${player.steam_id}}`"
class="px-3 py-2 hover:bg-accent cursor-pointer"
@click="select(player)"
>
<PlayerDisplay :player="player" />
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</PopoverContent>
Expand Down Expand Up @@ -245,6 +327,11 @@ export default {
required: false,
default: false,
},
groupByFriends: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
Expand All @@ -263,6 +350,44 @@ export default {
canSelectSelf() {
return this.self && this.me && !this.exclude.includes(this.me.steam_id);
},
// The current user, surfaced as a selectable entry (the online presence
// list never contains yourself). Hidden once you're in `exclude`, i.e.
// already in a lineup, and filtered by the active query.
selfPlayer(): Player | null {
if (!this.canSelectSelf || !this.me) return null;
const me = this.me as any;
const q = this.query.toLowerCase();
if (
q &&
!(
me.name?.toLowerCase().includes(q) ||
String(me.steam_id).includes(this.query)
)
) {
return null;
}
return {
steam_id: me.steam_id,
name: me.name,
avatar_url: me.avatar_url,
country: me.country,
role: me.role,
is_banned: me.is_banned,
is_muted: me.is_muted,
is_gagged: me.is_gagged,
elo: me.elo,
} as Player;
},
// Non-grouped results with `me` pinned to the top when selectable.
displayPlayers(): Player[] {
const base = this.players ?? [];
if (!this.selfPlayer) return base as Player[];
const meId = String(this.me?.steam_id);
return [
this.selfPlayer,
...base.filter((p: Player) => String(p.steam_id) !== meId),
];
},
onlineOnly: {
get() {
return useSearchStore().onlineOnly;
Expand All @@ -272,6 +397,72 @@ export default {
useSearchStore().onlineOnly = value;
},
},
friendIds(): Set<string> {
return new Set(
(useMatchmakingStore().friends as any[])
.filter((f: any) => f.status !== "Pending")
.map((f: any) => String(f.steam_id)),
);
},
// Friends list, filtered by query/exclude/self and sorted online-first.
// The online toggle applies here too: when on, only online friends show;
// when off, all friends (online + offline). Built from the full friends
// list so offline friends reliably appear when the toggle is off.
friendsForSearch(): Player[] {
if (!this.groupByFriends) return [];
const store = useMatchmakingStore();
const onlineIds = new Set(
(store.onlinePlayerSteamIds as string[]).map(String),
);
const q = this.query.toLowerCase();
const excluded = new Set((this.exclude as string[]).map(String));
const meId = String(this.me?.steam_id ?? "");

return (store.friends as any[])
.filter((f: any) => {
if (f.status === "Pending") return false;
const id = String(f.steam_id);
if (excluded.has(id)) return false;
if (!this.canSelectSelf && id === meId) return false;
// Strictly respect the toggle: online-only -> only online friends,
// otherwise -> only offline friends.
const online = onlineIds.has(id);
if (this.onlineOnly !== online) return false;
if (!q) return true;
return f.name?.toLowerCase().includes(q) || id.includes(this.query);
})
.sort((a: any, b: any) =>
(a.name || "").localeCompare(b.name || ""),
);
},
// Normal search results, minus anyone already shown in the Friends section.
otherPlayers(): Player[] {
const meId = this.selfPlayer ? String(this.me?.steam_id) : null;
return (this.players ?? []).filter(
(p: Player) =>
!this.friendIds.has(String(p.steam_id)) &&
(meId === null || String(p.steam_id) !== meId),
);
},
playerGroups(): Array<{ key: string; label: string; players: Player[] }> {
return [
{
key: "friends",
label: this.$t("matchmaking.friends.title"),
players: this.selfPlayer
? [this.selfPlayer, ...this.friendsForSearch]
: this.friendsForSearch,
},
{
key: "others",
label: this.$t("matchmaking.others.title"),
players: this.otherPlayers,
},
];
},
hasGroupResults(): boolean {
return this.playerGroups.some((g) => g.players.length > 0);
},
},
methods: {
toggleOnlineOnly() {
Expand Down
16 changes: 16 additions & 0 deletions components/draft-games/DraftActiveAlert.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ const isOnDraftPage = computed(() => route.path.startsWith("/draft-room"));

const acknowledgedKey = ref<string | null>(null);

// Track rooms the user has actually opened the draft page for. If they were on
// the draft page (e.g. landed on it) and then navigated away, we don't nag them
// with the alert — they already know they're in the room.
const visitedRoomId = ref<string | null>(null);

watch(
[isOnDraftPage, draftGameId],
([onPage, id]) => {
if (onPage && id) {
visitedRoomId.value = id;
}
},
{ immediate: true },
);

watch(roomKey, (next) => {
if (next !== acknowledgedKey.value) {
acknowledgedKey.value = null;
Expand All @@ -50,6 +65,7 @@ const rawShow = computed(
() =>
isAlertable.value &&
!isOnDraftPage.value &&
visitedRoomId.value !== draftGameId.value &&
acknowledgedKey.value !== roomKey.value,
);

Expand Down
4 changes: 1 addition & 3 deletions components/draft-games/DraftGames.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { onMounted, onUnmounted, computed, ref, watch } from "vue";
import {
Plus,
Swords,
Search,
ArrowUpDown,
RotateCcw,
Expand Down Expand Up @@ -291,7 +290,7 @@ const rehost = async () => {
<div class="min-w-0">
<div :class="tacticalSectionLabelClasses">
<span :class="tacticalSectionTickClasses"></span>
OPEN.MATCHES
{{ $t("pages.play.draft_games.section_label") }}
</div>
<div :class="tacticalSectionDescriptionClasses">
{{ $t("pages.play.draft_games.description") }}
Expand Down Expand Up @@ -461,7 +460,6 @@ const rehost = async () => {
v-if="filtered.length === 0"
class="flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border bg-card/30 px-6 py-10 text-center"
>
<Swords class="h-7 w-7 text-muted-foreground/50" />
<p class="text-sm text-muted-foreground">
{{
draftGames.length === 0
Expand Down
2 changes: 2 additions & 0 deletions components/draft-games/DraftOpenSlot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const onSelected = (player: { steam_id: string }) => {
<PlayerSearch
:label="$t('draft_games.room.search_player')"
:exclude="exclude"
:group-by-friends="true"
:self="true"
@selected="onSelected"
>
<button type="button" class="open-slot">
Expand Down
Loading