This commit is contained in:
talorr
2026-03-27 03:36:08 +03:00
parent 8a97ce6d54
commit cda36918e8
225 changed files with 35641 additions and 0 deletions

View File

@@ -0,0 +1,153 @@
<script setup lang="ts">
import Tag from "primevue/tag";
import type { Signal } from "~/types";
const props = defineProps<{
signal: Signal;
}>();
const { formatDateTime } = useBrowserDateTime();
const { copyText } = useClipboard();
const botName = computed(() => props.signal.rawPayload?.botName || null);
const isInactiveForecast = computed(() => props.signal.rawPayload?.forecastInactive === true);
const forecast = computed(() => props.signal.forecast || props.signal.rawPayload?.forecast || null);
const forecastImageUrl = computed(() => props.signal.rawPayload?.forecastImageUrl || null);
const forecastImageFailed = ref(false);
const copiedMatch = ref(false);
let copiedMatchResetTimeout: ReturnType<typeof setTimeout> | null = null;
const statusLabels: Record<Signal["status"], string> = {
pending: "LIVE",
win: "WIN",
lose: "LOSE",
void: "VOID",
manual_review: "CHECK",
unpublished: "OFF"
};
const displayStatusLabel = computed(() => {
if (props.signal.status === "pending" && isInactiveForecast.value) return "OFF";
return statusLabels[props.signal.status];
});
const statusSeverity = computed(() => {
if (props.signal.status === "pending" && isInactiveForecast.value) return "secondary";
switch (props.signal.status) {
case "win":
return "success";
case "lose":
return "danger";
case "manual_review":
return "warn";
case "pending":
return "info";
default:
return "secondary";
}
});
const formattedDate = computed(() =>
formatDateTime(props.signal.signalTime, {
day: "2-digit",
month: "2-digit",
hour: "2-digit",
minute: "2-digit"
})
);
const shouldShowForecastImage = computed(() => Boolean(forecastImageUrl.value) && !forecastImageFailed.value);
const copyMatchName = async (event?: Event) => {
event?.preventDefault();
event?.stopPropagation();
const copied = await copyText(`${props.signal.homeTeam} - ${props.signal.awayTeam}`);
if (!copied) {
return;
}
copiedMatch.value = true;
if (copiedMatchResetTimeout) {
clearTimeout(copiedMatchResetTimeout);
}
copiedMatchResetTimeout = setTimeout(() => {
copiedMatch.value = false;
}, 1600);
};
watch(
() => props.signal.rawPayload?.forecastImageUrl,
() => {
forecastImageFailed.value = false;
}
);
</script>
<template>
<article class="sakai-signal-card">
<div class="sakai-signal-card__main">
<div class="sakai-signal-card__meta">
<span>
<i class="pi pi-clock" />
{{ formattedDate }}
</span>
<span>
<i class="pi pi-flag" />
{{ signal.leagueName }}
</span>
<span v-if="botName">
<AppLogo size="14px" />
{{ botName }}
</span>
</div>
<div class="sakai-signal-card__teams">
<strong>{{ signal.homeTeam }}</strong>
<strong>{{ signal.awayTeam }}</strong>
</div>
<div class="sakai-signal-card__actions">
<button
type="button"
class="sakai-copy-button sakai-copy-button--wide"
:aria-label="`Скопировать матч ${signal.homeTeam} - ${signal.awayTeam}`"
@click="copyMatchName($event)"
>
<i class="pi" :class="copiedMatch ? 'pi-check' : 'pi-copy'" aria-hidden="true" />
<span>{{ copiedMatch ? "Скопировано" : "Скопировать матч" }}</span>
</button>
</div>
<div class="sakai-signal-card__market">
<span>{{ signal.marketType }}</span>
<span>{{ signal.selection }}</span>
</div>
</div>
<div class="sakai-signal-card__side">
<div v-if="forecastImageUrl || forecast" class="sakai-signal-card__forecast">
<img
v-if="shouldShowForecastImage"
:src="forecastImageUrl"
:alt="forecast || `${signal.homeTeam} - ${signal.awayTeam}`"
class="sakai-signal-card__forecast-image"
loading="lazy"
@error="forecastImageFailed = true"
>
<p v-else-if="forecast" class="sakai-signal-card__forecast-text">
{{ forecast }}
</p>
<p v-else class="sakai-signal-card__forecast-text sakai-signal-card__forecast-text--empty">
Прогноз недоступен
</p>
</div>
<Tag class="rounded" :value="displayStatusLabel" :severity="statusSeverity" />
<div class="sakai-signal-card__odds">{{ signal.odds.toFixed(2) }}</div>
</div>
</article>
</template>