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
60 changes: 47 additions & 13 deletions components/PlayerSanctions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
Edit2,
Trash2,
Calendar as CalendarIcon,
AlertTriangle,
ExternalLink,
Ban,
MicOff,
Expand All @@ -15,6 +14,7 @@ import {
ShieldAlert,
ShieldCheck,
ShieldX,
TriangleAlert,
} from "lucide-vue-next";
import { Button } from "~/components/ui/button";
import {
Expand Down Expand Up @@ -84,7 +84,7 @@ const tabCountClasses =
'text-destructive hover:text-destructive': activeSanctions > 0,
}"
>
<AlertTriangle class="h-3.5 w-3.5" />
<TriangleAlert class="h-3.5 w-3.5" />
<span>{{ $t("player.sanctions.title") }}</span>
<ShieldX
v-if="vacBanned"
Expand Down Expand Up @@ -276,6 +276,11 @@ const tabCountClasses =
class="h-4 w-4 shrink-0"
:class="accentTextClass(sanction)"
/>
<TriangleAlert
v-else-if="isWarning(sanction)"
class="h-4 w-4 shrink-0"
:class="accentTextClass(sanction)"
/>
<VolumeX
v-else
class="h-4 w-4 shrink-0"
Expand All @@ -286,17 +291,13 @@ const tabCountClasses =
<span
class="text-sm font-semibold uppercase tracking-wider"
>
{{ sanction.type }}
{{ typeLabel(sanction.type) }}
</span>
<span
class="inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide"
:class="statusPillClass(sanction)"
>
{{
isExpired(sanction)
? $t("player.sanctions.expired")
: $t("player.sanctions.active")
}}
{{ statusLabel(sanction) }}
</span>
</div>
<p
Expand All @@ -311,13 +312,14 @@ const tabCountClasses =
v-if="canManageSanctions"
class="flex gap-1 items-center shrink-0"
>
<TooltipProvider v-if="!isExpired(sanction)">
<TooltipProvider v-if="canEditEndDate(sanction)">
<Tooltip>
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon"
class="h-7 w-7"
:aria-label="$t('player.sanctions.edit')"
@click="openEditDialog(sanction)"
>
<Edit2 class="h-3.5 w-3.5" />
Expand All @@ -335,6 +337,7 @@ const tabCountClasses =
variant="ghost"
size="icon"
class="h-7 w-7 text-destructive hover:text-destructive"
:aria-label="$t('player.sanctions.remove')"
@click="removeSanction(sanction)"
>
<Trash2 class="h-3.5 w-3.5" />
Expand All @@ -356,6 +359,7 @@ const tabCountClasses =
</p>

<div
v-if="!isWarning(sanction)"
class="flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs"
>
<span
Expand Down Expand Up @@ -746,6 +750,9 @@ export default {
computed: {
activeSanctions() {
return this.sanctions.filter((sanction) => {
if (this.isWarning(sanction)) {
return false;
}
if (sanction.remove_sanction_date) {
return new Date(sanction.remove_sanction_date) > new Date();
}
Expand Down Expand Up @@ -855,12 +862,33 @@ export default {
},
},
methods: {
isWarning(sanction: any) {
return sanction.type === "warning";
},
isExpired(sanction: any) {
if (this.isWarning(sanction)) {
return false;
}
return (
!!sanction.remove_sanction_date &&
new Date(sanction.remove_sanction_date) <= new Date()
);
},
canEditEndDate(sanction: any) {
return !this.isWarning(sanction) && !this.isExpired(sanction);
},
typeLabel(type: string) {
const key = `player.sanctions.type_labels.${type}`;
return this.$te(key) ? this.$t(key) : type;
},
statusLabel(sanction: any) {
if (this.isWarning(sanction)) {
return this.$t("player.sanctions.on_record");
}
return this.isExpired(sanction)
? this.$t("player.sanctions.expired")
: this.$t("player.sanctions.active");
},
isAutoSteamBan(sanction: any) {
return (
sanction.type === "ban" &&
Expand All @@ -881,12 +909,15 @@ export default {
: "text-[hsl(var(--tac-amber))]";
},
statusPillClass(sanction: any) {
return this.isExpired(sanction)
? "border-border text-muted-foreground"
: "border-destructive/40 bg-destructive/10 text-destructive";
if (this.isExpired(sanction)) {
return "border-border text-muted-foreground";
}
return sanction.type === "ban"
? "border-destructive/40 bg-destructive/10 text-destructive"
: "border-[hsl(var(--tac-amber)/0.5)] bg-[hsl(var(--tac-amber)/0.12)] text-[hsl(var(--tac-amber))]";
},
openEditDialog(sanction: any) {
if (this.isExpired(sanction)) {
if (!this.canEditEndDate(sanction)) {
return;
}
this.editingSanction = sanction;
Expand Down Expand Up @@ -991,11 +1022,13 @@ export default {
$serverId: String
$steam_id: String!
$type: String!
$sanction_id: uuid
) {
unsanctionServerPlayer(
serverId: $serverId
steam_id: $steam_id
type: $type
sanction_id: $sanction_id
) {
enforced
message
Expand All @@ -1006,6 +1039,7 @@ export default {
serverId: this.serverId ?? null,
steam_id: this.playerId,
type: this.sanctionToDelete.type,
sanction_id: this.sanctionToDelete.id,
},
});

Expand Down
50 changes: 40 additions & 10 deletions components/SanctionPlayer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ import SettingHeader from "~/components/match/SettingHeader.vue";
>
<div class="flex items-center gap-2">
<component :is="sanction.icon" class="h-4 w-4" />
<span class="font-medium capitalize">{{ type }}</span>
<span class="font-medium">
{{ $t(`player.sanctions.type_labels.${type}`) }}
</span>
</div>
<p class="text-sm text-muted-foreground mt-1">
{{ sanction.description }}
Expand Down Expand Up @@ -103,16 +105,24 @@ import SettingHeader from "~/components/match/SettingHeader.vue";
<Separator class="my-2" />

<DrawerDescription
class="text-lg flex gap-4 items-center text-red-500"
class="text-lg flex gap-4 items-center"
:class="
sanctionType === 'ban'
? 'text-destructive'
: 'text-[hsl(var(--tac-amber))]'
"
v-if="sanctionType"
>
<component :is="sanctions[sanctionType].icon" class="h-4 w-4" />
{{ sanctions[sanctionType].description }}
</DrawerDescription>

<p v-if="isWarning" class="text-sm text-muted-foreground">
{{ $t("player.sanction.warning_note") }}
</p>
<div
v-if="serverId && sanctionType && sanctionType !== 'ban'"
class="flex gap-2 items-start text-sm text-yellow-500"
v-else-if="serverId && sanctionType && sanctionType !== 'ban'"
class="flex gap-2 items-start text-sm text-[hsl(var(--tac-amber))]"
>
<TriangleAlert class="mt-0.5 h-4 w-4 shrink-0" />
{{ $t("pages.dedicated_servers.detail.sanctions_wip") }}
Expand All @@ -127,7 +137,7 @@ import SettingHeader from "~/components/match/SettingHeader.vue";
</SettingHeader>

<FormField v-slot="{ componentField }" name="reason">
<FormItem>
<FormItem :class="{ 'col-span-2': isWarning }">
<FormLabel>{{ $t("player.sanction.reason_label") }}</FormLabel>
<FormControl>
<Input
Expand All @@ -142,7 +152,11 @@ import SettingHeader from "~/components/match/SettingHeader.vue";
</FormItem>
</FormField>

<FormField v-slot="{ componentField }" name="duration">
<FormField
v-if="!isWarning"
v-slot="{ componentField }"
name="duration"
>
<FormItem>
<FormLabel>{{ $t("player.sanctions.duration_label") }}</FormLabel>
<FormControl>
Expand Down Expand Up @@ -205,7 +219,7 @@ export default {
form: useForm({
validationSchema: toTypedSchema(
z.object({
reason: z.string().min(1),
reason: z.string().optional(),
duration: z.string().min(1),
}),
),
Expand All @@ -215,6 +229,9 @@ export default {
};
},
computed: {
isWarning(): boolean {
return this.sanctionType === "warning";
},
sanctions(): Record<string, { icon: any; description: string }> {
return {
ban: {
Expand All @@ -233,6 +250,10 @@ export default {
icon: BellOff,
description: this.$t("player.sanction.types.silence_description"),
},
warning: {
icon: TriangleAlert,
description: this.$t("player.sanction.types.warning_description"),
},
};
},
durations(): Array<{ label: string; duration: number }> {
Expand Down Expand Up @@ -275,6 +296,14 @@ export default {
return;
}

if (this.isWarning && !this.form.values.reason?.trim()) {
this.form.setFieldError(
"reason",
this.$t("player.sanction.reason_required"),
);
return;
}

this.submitting = true;
try {
await this.$apollo.mutate({
Expand Down Expand Up @@ -304,9 +333,10 @@ export default {
steam_id: this.player.steam_id,
type: this.sanctionType,
reason: this.form.values.reason,
duration: this.form.values.duration
? parseInt(this.form.values.duration)
: 0,
duration:
!this.isWarning && this.form.values.duration
? parseInt(this.form.values.duration)
: 0,
},
});

Expand Down
41 changes: 37 additions & 4 deletions components/notification/NotificationItem.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
<script setup lang="ts">
import { computed, ref, onBeforeUnmount } from "vue";
import { Trash2, Check } from "lucide-vue-next";
import { Trash2, Check, TriangleAlert, Ban } from "lucide-vue-next";
import { Button } from "~/components/ui/button";
import TimeAgo from "~/components/TimeAgo.vue";
import NotificationContext from "~/components/notification/NotificationContext.vue";
import NotificationMessage from "~/components/notification/NotificationMessage.vue";
import { useOrphanedScan } from "~/composables/useOrphanedScan";
import {
sanctionNotificationTone,
sanctionToneBarClasses,
sanctionToneTextClasses,
} from "~/utilities/sanctionNotificationTone";

// The orphaned-uploads dialog is mounted globally (default layout), so a
// StorageScan notification can open it in place instead of navigating away.
Expand All @@ -27,6 +32,7 @@ type NotificationItemProps = {
title: string;
message: string;
type: string;
role?: string | null;
steam_id?: string | null;
entity_id?: string | null;
is_read: boolean;
Expand All @@ -53,11 +59,21 @@ const emit = defineEmits<{
): void;
}>();

const wrapperClass = computed(() =>
const tone = computed(() => sanctionNotificationTone(props.notification));

const wrapperClass = computed(() => [
props.variant === "sheet"
? "mb-4 p-4 rounded-lg shadow-md relative"
: "mb-3 p-3 rounded-md border border-border bg-card/40 relative",
);
tone.value ? "overflow-hidden" : "",
]);

const titleToneClass = computed(() => {
if (props.notification.is_read) {
return "text-muted-foreground";
}
return tone.value ? sanctionToneTextClasses[tone.value] : "";
});

const deleting = ref(false);
const dismissed = ref(false);
Expand Down Expand Up @@ -112,6 +128,12 @@ onBeforeUnmount(() => {

<template>
<div :class="wrapperClass">
<span
v-if="tone"
aria-hidden="true"
class="absolute inset-y-0 left-0 w-1"
:class="sanctionToneBarClasses[tone]"
/>
<Button
v-if="notification.deletable !== false"
size="icon"
Expand All @@ -126,9 +148,20 @@ onBeforeUnmount(() => {
<h3
:class="[
'text-lg font-semibold mb-2',
notification.is_read ? 'text-muted-foreground' : '',
tone ? 'flex items-center gap-2' : '',
titleToneClass,
]"
>
<TriangleAlert
v-if="tone === 'warning'"
aria-hidden="true"
class="h-4 w-4 shrink-0"
/>
<Ban
v-else-if="tone === 'ban'"
aria-hidden="true"
class="h-4 w-4 shrink-0"
/>
{{ notification.title }}
</h3>

Expand Down
Loading
Loading