init
This commit is contained in:
26
frontend/components/AppLogo.vue
Normal file
26
frontend/components/AppLogo.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
src?: string;
|
||||
alt?: string;
|
||||
size?: number | string;
|
||||
}>(),
|
||||
{
|
||||
src: "/icons/app-icon-192.png",
|
||||
alt: "Антигол",
|
||||
size: 40
|
||||
}
|
||||
);
|
||||
|
||||
const resolvedSize = computed(() => (typeof props.size === "number" ? `${props.size}px` : props.size));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img
|
||||
:src="src"
|
||||
:alt="alt"
|
||||
:style="{ width: resolvedSize, height: resolvedSize }"
|
||||
class="block shrink-0 object-contain"
|
||||
draggable="false"
|
||||
/>
|
||||
</template>
|
||||
153
frontend/components/SignalCard.vue
Normal file
153
frontend/components/SignalCard.vue
Normal 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>
|
||||
Reference in New Issue
Block a user