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
15 changes: 8 additions & 7 deletions components/AvatarUpload.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script setup lang="ts">
import { ref, computed } from "vue";
import { toast } from "@/components/ui/toast";
import { Upload, Trash2, Loader2, X } from "lucide-vue-next";
import { Upload, Trash2, X } from "lucide-vue-next";
import { Spinner } from "~/components/ui/spinner";
import RosterImageEditor from "@/components/RosterImageEditor.vue";

interface BulkTeam {
Expand Down Expand Up @@ -238,7 +239,7 @@ async function remove() {
:disabled="isUploading || isRemoving"
@click.prevent.stop="remove"
>
<Loader2 v-if="isRemoving" class="h-3 w-3 animate-spin" />
<Spinner v-if="isRemoving" class="h-3 w-3" />
<X v-else class="h-3 w-3" />
</button>
</div>
Expand All @@ -258,9 +259,9 @@ async function remove() {
{{ useEditor ? $t("avatar.roster_hint") : $t("avatar.hint") }}
</div>
</div>
<Loader2
<Spinner
v-if="isUploading"
class="w-5 h-5 animate-spin text-[hsl(var(--tac-amber))]"
class="w-5 h-5 text-[hsl(var(--tac-amber))]"
/>
</div>

Expand All @@ -271,7 +272,7 @@ async function remove() {
:disabled="isUploading || isRemoving"
@click.prevent.stop="remove"
>
<Loader2 v-if="isRemoving" class="w-3.5 h-3.5 animate-spin" />
<Spinner v-if="isRemoving" class="w-3.5 h-3.5" />
<Trash2 v-else class="w-3.5 h-3.5" />
<span>{{ $t(removeKey) }}</span>
</button>
Expand All @@ -292,7 +293,7 @@ async function remove() {
:disabled="isUploading || isRemoving"
@click.prevent="triggerPicker"
>
<Loader2 v-if="isUploading" class="w-4 h-4 animate-spin" />
<Spinner v-if="isUploading" class="w-4 h-4" />
<Upload v-else class="w-4 h-4" />
<span>{{ hasCustom ? $t(changeKey) : $t(uploadKey) }}</span>
</button>
Expand All @@ -303,7 +304,7 @@ async function remove() {
:disabled="isUploading || isRemoving"
@click.prevent="remove"
>
<Loader2 v-if="isRemoving" class="w-4 h-4 animate-spin" />
<Spinner v-if="isRemoving" class="w-4 h-4" />
<Trash2 v-else class="w-4 h-4" />
<span>{{ $t(removeKey) }}</span>
</button>
Expand Down
72 changes: 59 additions & 13 deletions components/ClipBoard.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,36 @@
<script lang="ts" setup>
import { Copy } from "lucide-vue-next";
import { Copy, Check } from "lucide-vue-next";
</script>
<template>
<Button variant="outline" size="icon" @click="copyToClipboard">
<div ref="copy" :data-clipboard-text="data">
<slot>
<Copy></Copy>
</slot>
<div
ref="copy"
:data-clipboard-text="data"
class="relative inline-flex items-center justify-center"
>
<Transition
enter-active-class="transition-all duration-300 ease-out"
leave-active-class="transition-all duration-200 ease-in"
enter-from-class="opacity-0 scale-50 -rotate-90"
enter-to-class="opacity-100 scale-100 rotate-0"
leave-from-class="opacity-100 scale-100"
leave-to-class="opacity-0 scale-50"
>
<span
v-if="copied"
class="absolute inset-0 flex items-center justify-center"
>
<Check class="text-green-500" />
</span>
</Transition>
<span
class="inline-flex items-center justify-center transition-all duration-300 ease-out"
:class="copied ? 'scale-50 opacity-0 blur-[1px]' : ''"
>
<slot>
<Copy></Copy>
</slot>
</span>
</div>
</Button>
</template>
Expand All @@ -21,8 +45,21 @@ export default {
default: "",
},
},
data() {
return {
copied: false,
};
},
beforeUnmount() {
if (this.copiedTimer) {
clearTimeout(this.copiedTimer);
}
},
methods: {
async copyToClipboard() {
copyToClipboard() {
void this.performCopy();
},
async performCopy() {
if (!this.data) {
console.warn("data missing");
return;
Expand All @@ -34,17 +71,26 @@ export default {
} else {
this.fallbackCopyToClipboard(this.data);
}

toast({
title: this.$t("pages.toasts.copied_to_clipboard"),
});
} catch (err) {
console.error("Failed to copy to clipboard:", err);
this.fallbackCopyToClipboard(this.data);
toast({
title: this.$t("pages.toasts.copied_to_clipboard"),
});
}

this.showCopied();
toast({
title: this.$t("pages.toasts.copied_to_clipboard"),
});
},

showCopied() {
this.copied = true;
if (this.copiedTimer) {
clearTimeout(this.copiedTimer);
}
this.copiedTimer = setTimeout(() => {
this.copied = false;
this.copiedTimer = undefined;
}, 1500);
},

fallbackCopyToClipboard(text) {
Expand Down
135 changes: 135 additions & 0 deletions components/KickPlayer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<script setup lang="ts">
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import gql from "graphql-tag";
import { LogOut } from "lucide-vue-next";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import { toast } from "@/components/ui/toast";

const props = defineProps<{
player: Record<string, any>;
serverId: string;
}>();

const emit = defineEmits<{ (e: "kicked"): void }>();

const { t } = useI18n();
const nuxtApp = useNuxtApp();

const open = ref(false);
const reason = ref("");
const submitting = ref(false);

const kickMutation = gql`
mutation KickServerPlayer(
$serverId: String!
$steam_id: String!
$reason: String
) {
kickServerPlayer(
serverId: $serverId
steam_id: $steam_id
reason: $reason
) {
kicked
message
}
}
`;

async function kick() {
if (submitting.value) {
return;
}

submitting.value = true;

try {
const { data } = await nuxtApp.$apollo.defaultClient.mutate({
mutation: kickMutation,
variables: {
serverId: props.serverId,
steam_id: props.player.steam_id,
reason: reason.value || null,
},
});

const result = data?.kickServerPlayer;

if (result?.kicked) {
toast({ title: t("player.kick.kicked", { name: props.player.name }) });
} else {
toast({
variant: "destructive",
title: t("common.error"),
description: result?.message,
});
}

open.value = false;
reason.value = "";
emit("kicked");
} catch (error: any) {
toast({
variant: "destructive",
title: t("common.error"),
description: error?.message,
});
} finally {
submitting.value = false;
}
}
</script>

<template>
<button
type="button"
:title="$t('player.kick.button')"
:aria-label="$t('player.kick.button')"
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded border border-amber-500/45 bg-amber-500/10 text-amber-400 transition-colors hover:border-amber-500/80 hover:bg-amber-500/20 hover:text-amber-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background"
@click="open = true"
>
<LogOut class="h-4 w-4" />
</button>

<Dialog :open="open" @update:open="open = $event">
<DialogContent>
<DialogHeader>
<DialogTitle>
{{ $t("player.kick.title", { name: player.name }) }}
</DialogTitle>
<DialogDescription>
{{ $t("player.kick.description") }}
</DialogDescription>
</DialogHeader>

<div class="grid gap-2 py-2">
<Label>{{ $t("player.kick.reason_label") }}</Label>
<Input
v-model="reason"
:placeholder="$t('player.kick.reason_placeholder')"
@keyup.enter="kick"
/>
</div>

<DialogFooter>
<Button variant="outline" @click="open = false">
{{ $t("common.cancel") }}
</Button>
<Button :loading="submitting" @click="kick">
{{ $t("player.kick.confirm") }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
11 changes: 11 additions & 0 deletions components/MatchTableRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1779,6 +1779,16 @@ export default {

return null;
},
isPartyLeader(): boolean {
const lobby = useMatchmakingStore().currentLobby as any;
if (!lobby) {
return true;
}
const me = lobby.players?.find((p: any) => {
return p.player.steam_id === useAuthStore().me?.steam_id;
});
return !!me?.captain;
},
canJoinMatch(): boolean {
const hasAvailableSpot =
(this.match.lineup_counts?.lineup_1_count ?? 0) <
Expand All @@ -1787,6 +1797,7 @@ export default {
this.match.max_players_per_lineup;

return (
this.isPartyLeader &&
this.match.status === e_match_status_enum.PickingPlayers &&
this.match.options.lobby_access === e_lobby_access_enum.Open &&
!this.match.lineup_1.is_on_lineup &&
Expand Down
5 changes: 3 additions & 2 deletions components/PlayerChangeCountry.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { Check, ChevronsUpDown, Loader2 } from "lucide-vue-next";
import { Check, ChevronsUpDown } from "lucide-vue-next";
import { Spinner } from "~/components/ui/spinner";
import TimezoneFlag from "~/components/TimezoneFlag.vue";
</script>

Expand Down Expand Up @@ -63,7 +64,7 @@ import TimezoneFlag from "~/components/TimezoneFlag.vue";
</Command>
</PopoverContent>
</Popover>
<Loader2 v-if="saving" class="h-4 w-4 animate-spin text-muted-foreground" />
<Spinner v-if="saving" class="h-4 w-4 text-muted-foreground" />
</div>
</template>

Expand Down
5 changes: 3 additions & 2 deletions components/PlayerChangeName.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { Input } from "~/components/ui/input";
import { Button } from "~/components/ui/button";
import { Loader2 } from "lucide-vue-next";
import { Spinner } from "~/components/ui/spinner";
</script>

<template>
Expand All @@ -19,9 +19,10 @@ import { Loader2 } from "lucide-vue-next";
<Button
type="submit"
size="sm"
:loading="saving"
:disabled="saving || !isValid || name === player.name"
>
<Loader2 v-if="saving" class="mr-1 h-4 w-4 animate-spin" />
<Spinner v-if="saving" class="mr-1 h-4 w-4" />
{{ $t("common.save") }}
</Button>
</form>
Expand Down
15 changes: 14 additions & 1 deletion components/PlayerDisplay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import SteamIcon from "~/components/icons/SteamIcon.vue";
import PlayerElo from "~/components/PlayerElo.vue";
import PlayerPremierRank from "~/components/PlayerPremierRank.vue";
import PlayerSkillGroupRank from "~/components/PlayerSkillGroupRank.vue";
import { Crown, Shield, BadgeCheck, BadgeIcon, Podcast } from "lucide-vue-next";
import {
Crown,
Shield,
ShieldHalf,
BadgeCheck,
BadgeIcon,
Podcast,
} from "lucide-vue-next";
import FiveStackToolTip from "./FiveStackToolTip.vue";
</script>
<template>
Expand Down Expand Up @@ -176,6 +183,9 @@ import FiveStackToolTip from "./FiveStackToolTip.vue";
<template v-if="isStreamer">
<Podcast class="w-3 h-3 mr-1 text-green-500" />
</template>
<template v-if="isModerator">
<ShieldHalf class="w-3 h-3 mr-1 text-blue-500" />
</template>
<template v-if="isMatchOrganizer">
<Shield class="w-3 h-3 mr-1 text-yellow-500" />
</template>
Expand Down Expand Up @@ -406,6 +416,9 @@ export default {
isStreamer() {
return this.player?.role === e_player_roles_enum.streamer;
},
isModerator() {
return this.player?.role === e_player_roles_enum.moderator;
},
isMatchOrganizer() {
return this.player?.role === e_player_roles_enum.match_organizer;
},
Expand Down
Loading