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
12 changes: 12 additions & 0 deletions assets/css/tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,18 @@ mark {
pointer-events: none;
opacity: 0.6;
}

/* Faint HUD scanlines laid over event banner media, so photography/video
reads as composed into the tactical chrome instead of pasted above it. */
.tac-scanlines {
background-image: repeating-linear-gradient(
180deg,
hsl(0 0% 100% / 0.03) 0px,
hsl(0 0% 100% / 0.03) 1px,
transparent 1px,
transparent 4px
);
}
/* One-shot pulse for clip share buttons — toast is far from the click
target, so the button itself flashes amber + scales briefly. Used
by MatchHighlightsReel queue/featured share, HighlightCard, and
Expand Down
11 changes: 9 additions & 2 deletions cloudflare-workers/backblaze-proxy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,9 @@ export default {
},
};

const UPLOAD_PREFIX = "demo-uploads/";
// Every write is still individually authorized by the API-minted HMAC token
// below; the prefix list only bounds which trees are writable at all.
const UPLOAD_PREFIXES = ["demo-uploads/", "events/"];
// B2 multipart part ceiling we accept; the API chunks at 64MiB so anything
// larger is a malformed/abusive request.
const MAX_PART_BYTES = 64 * 1024 * 1024;
Expand All @@ -462,7 +464,12 @@ async function handleUpload(
const uploadId = url.searchParams.get("uploadId");
const token = url.searchParams.get("token");

if (!key.startsWith(UPLOAD_PREFIX) || !partNumber || !uploadId || !token) {
if (
!UPLOAD_PREFIXES.some((prefix) => key.startsWith(prefix)) ||
!partNumber ||
!uploadId ||
!token
) {
return new Response("forbidden", {
status: 403,
headers: corsHeaders(reqOrigin),
Expand Down
18 changes: 18 additions & 0 deletions components/BreadCrumbs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

<script lang="ts">
import { useTournamentContext } from "~/composables/useTournamentContext";
import { useEventContext } from "~/composables/useEventContext";
import { useMatchContext } from "~/composables/useMatchContext";
import { usePlayerContext } from "~/composables/usePlayerContext";
import { useTeamContext } from "~/composables/useTeamContext";
Expand All @@ -58,6 +59,7 @@ export default {
});

const tc = useTournamentContext();
const ec = useEventContext();
const mc = useMatchContext();
const pc = usePlayerContext();
const teamc = useTeamContext();
Expand Down Expand Up @@ -107,6 +109,22 @@ export default {
return;
}

// Events: show the event name once its context matches the route
// segment. Until then (still loading, not found/no access, or the
// create/manage subpages) skip the crumb rather than leaking a raw
// uuid or a literal "create"/"manage" segment, matching the
// tournaments branch above.
if (segments[0] === "events" && index === 1) {
if (ec.value?.id !== segment) {
return;
}
breadcrumbs.push({
text: ec.value.name,
to: path,
});
return;
}

if (segments[0] === "matches" && index === 1) {
if (mc.value?.id !== segment) {
return;
Expand Down
104 changes: 94 additions & 10 deletions components/PlayerSearch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,23 @@ const { height: viewportHeight } = useVisualViewport();
<div
v-for="player in group.players"
:key="`g-${group.key}-${player.steam_id}`"
class="px-3 py-2 hover:bg-accent cursor-pointer"
:ref="
player.steam_id === activeSteamId
? setActiveRow
: undefined
"
class="px-3 py-2 cursor-pointer"
:class="
player.steam_id === activeSteamId
? 'bg-accent'
: 'hover:bg-accent'
"
@click="select(player)"
@mouseenter="
selectedIndex = flatResults.findIndex(
(p) => p.steam_id === player.steam_id,
)
"
>
<PlayerDisplay :player="player" />
</div>
Expand All @@ -95,10 +110,15 @@ const { height: viewportHeight } = useVisualViewport();

<div v-else class="divide-y">
<div
v-for="player in displayPlayers"
v-for="(player, index) in displayPlayers"
:key="`player-${player.steam_id}}`"
class="px-3 py-2 hover:bg-accent cursor-pointer"
:ref="index === selectedIndex ? setActiveRow : undefined"
class="px-3 py-2 cursor-pointer"
:class="
index === selectedIndex ? 'bg-accent' : 'hover:bg-accent'
"
@click="select(player)"
@mouseenter="selectedIndex = index"
>
<PlayerDisplay :player="player" />
</div>
Expand All @@ -111,7 +131,9 @@ const { height: viewportHeight } = useVisualViewport();
class="px-4 py-2 text-xs text-muted-foreground border-t"
>
<template v-if="groupByFriends">
{{ playerGroups[0].players.length + playerGroups[1].players.length }}
{{
playerGroups[0].players.length + playerGroups[1].players.length
}}
{{ $t("player.search.found_players") }}
</template>
<template v-else>
Expand All @@ -136,6 +158,7 @@ const { height: viewportHeight } = useVisualViewport();
(e: Event) =>
debouncedSearch((e.target as HTMLInputElement).value)
"
@keydown="onKeydown"
/>
<div class="flex items-center gap-2 ml-4">
<Switch
Expand Down Expand Up @@ -190,6 +213,7 @@ const { height: viewportHeight } = useVisualViewport();
(e: Event) =>
debouncedSearch((e.target as HTMLInputElement).value)
"
@keydown="onKeydown"
/>
<div class="flex items-center gap-2 ml-4">
<Switch
Expand Down Expand Up @@ -227,8 +251,23 @@ const { height: viewportHeight } = useVisualViewport();
<div
v-for="player in group.players"
:key="`g-${group.key}-${player.steam_id}`"
class="px-3 py-2 hover:bg-accent cursor-pointer"
:ref="
player.steam_id === activeSteamId
? setActiveRow
: undefined
"
class="px-3 py-2 cursor-pointer"
:class="
player.steam_id === activeSteamId
? 'bg-accent'
: 'hover:bg-accent'
"
@click="select(player)"
@mouseenter="
selectedIndex = flatResults.findIndex(
(p) => p.steam_id === player.steam_id,
)
"
>
<PlayerDisplay :player="player" />
</div>
Expand All @@ -253,10 +292,15 @@ const { height: viewportHeight } = useVisualViewport();

<div class="divide-y">
<div
v-for="player in displayPlayers"
v-for="(player, index) in displayPlayers"
:key="`player-${player.steam_id}}`"
class="px-3 py-2 hover:bg-accent cursor-pointer"
:ref="index === selectedIndex ? setActiveRow : undefined"
class="px-3 py-2 cursor-pointer"
:class="
index === selectedIndex ? 'bg-accent' : 'hover:bg-accent'
"
@click="select(player)"
@mouseenter="selectedIndex = index"
>
<PlayerDisplay :player="player" />
</div>
Expand Down Expand Up @@ -338,6 +382,8 @@ export default {
open: false,
query: "",
players: undefined as Player[] | undefined,
selectedIndex: 0,
activeRow: null as HTMLElement | null,
debouncedSearch: debounce((query: string) => {
this.searchPlayers(query);
}, 300),
Expand Down Expand Up @@ -431,9 +477,7 @@ export default {
if (!q) return true;
return f.name?.toLowerCase().includes(q) || id.includes(this.query);
})
.sort((a: any, b: any) =>
(a.name || "").localeCompare(b.name || ""),
);
.sort((a: any, b: any) => (a.name || "").localeCompare(b.name || ""));
},
// Normal search results, minus anyone already shown in the Friends section.
otherPlayers(): Player[] {
Expand Down Expand Up @@ -463,8 +507,46 @@ export default {
hasGroupResults(): boolean {
return this.playerGroups.some((g) => g.players.length > 0);
},
// Single ordered list to drive arrow-key navigation across whichever
// rendering mode is active (grouped Friends/Others or a flat list).
flatResults(): Player[] {
if (this.groupByFriends) {
return [
...this.playerGroups[0].players,
...this.playerGroups[1].players,
];
}
return this.displayPlayers;
},
activeSteamId(): string | undefined {
return this.flatResults[this.selectedIndex]?.steam_id;
},
},
methods: {
setActiveRow(el: HTMLElement | null) {
this.activeRow = el;
},
scrollActiveIntoView() {
this.$nextTick(() => {
this.activeRow?.scrollIntoView({ block: "nearest" });
});
},
onKeydown(event: KeyboardEvent) {
const list = this.flatResults;
if (!list.length) return;
if (event.key === "ArrowDown") {
event.preventDefault();
this.selectedIndex = Math.min(this.selectedIndex + 1, list.length - 1);
this.scrollActiveIntoView();
} else if (event.key === "ArrowUp") {
event.preventDefault();
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
this.scrollActiveIntoView();
} else if (event.key === "Enter") {
event.preventDefault();
if (list[this.selectedIndex]) this.select(list[this.selectedIndex]);
}
},
toggleOnlineOnly() {
this.onlineOnly = !this.onlineOnly;
this.searchPlayers();
Expand All @@ -489,6 +571,8 @@ export default {
? (this.exclude as string[]).concat(this.me.steam_id)
: (this.exclude as string[]);

this.selectedIndex = 0;

if (this.onlineOnly) {
this.players = useSearchStore().search(this.query, exclude);
return;
Expand Down
22 changes: 20 additions & 2 deletions components/draft-games/DraftRoom.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useAuthStore } from "~/stores/AuthStore";
import { useApplicationSettingsStore } from "~/stores/ApplicationSettings";
import { Button } from "~/components/ui/button";
import { toast } from "~/components/ui/toast";
import { Alert, AlertTitle, AlertDescription } from "~/components/ui/alert";
import {
AlertDialog,
AlertDialogContent,
Expand Down Expand Up @@ -551,6 +552,10 @@ const sideFull = (lineup: number) =>

const showStart = computed(() => isOrganizer.value && notStarted.value);

const regionsAvailable = computed(
() => appSettings.availableRegions.length > 0,
);

const startReady = computed(() => {
if (props.room.mode === "Teams") {
return !!props.room.team_1_id;
Expand Down Expand Up @@ -608,6 +613,19 @@ const start = () => {
@close="settingsOpen = false"
/>

<Alert
v-if="!regionsAvailable && notStarted"
variant="destructive"
class="bg-red-600 text-white"
>
<AlertTitle>
{{ $t("match.region_veto.no_regions_available") }}
</AlertTitle>
<AlertDescription>
{{ $t("draft_games.room.no_regions_available_description") }}
</AlertDescription>
</Alert>

<div
v-if="canJoin || hasRequested || isWaitlisted"
class="flex flex-col items-center gap-2 rounded-xl border border-[hsl(var(--tac-amber)/0.4)] bg-[hsl(var(--tac-amber)/0.06)] p-4 sm:flex-row sm:justify-between"
Expand Down Expand Up @@ -776,7 +794,7 @@ const start = () => {
v-if="showStart && startReady"
variant="tactical"
type="button"
:disabled="isPending('start')"
:disabled="isPending('start') || !regionsAvailable"
:class="[
tacticalCtaButtonClasses,
'justify-center px-10 py-3.5 text-base',
Expand Down Expand Up @@ -988,7 +1006,7 @@ const start = () => {
v-if="showStart && startReady"
variant="tactical"
type="button"
:disabled="isPending('start')"
:disabled="isPending('start') || !regionsAvailable"
:class="[
tacticalCtaButtonClasses,
'justify-center px-10 py-3.5 text-base',
Expand Down
Loading