521 lines
13 KiB
Vue
521 lines
13 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, onUnmounted, ref } from "vue";
|
|
import { mdiPlay } from "@mdi/js";
|
|
|
|
const { t } = useI18n();
|
|
const config = useRuntimeConfig();
|
|
const muxAccentColor = "#00f0ff";
|
|
const muxPrimaryColor = "#e6fbff";
|
|
const muxSecondaryColor = "#020617";
|
|
|
|
const muxPlaybackId = computed(() => String(config.public.muxPlaybackId || "").trim());
|
|
const videoTitle = computed(() => t("hero.demoVideoTitle"));
|
|
const muxVideoTitle = computed(() => t("hero.demoTitle"));
|
|
const muxPlayerUrl = computed(() => {
|
|
if (!muxPlaybackId.value) return "";
|
|
|
|
const url = new URL(`https://player.mux.com/${encodeURIComponent(muxPlaybackId.value)}`);
|
|
url.searchParams.set("accent-color", muxAccentColor);
|
|
url.searchParams.set("primary-color", muxPrimaryColor);
|
|
url.searchParams.set("secondary-color", muxSecondaryColor);
|
|
url.searchParams.set("metadata-video-id", "agent-teams-demo");
|
|
url.searchParams.set("metadata-video-title", muxVideoTitle.value);
|
|
url.searchParams.set("metadata-player-name", "Landing hero");
|
|
url.searchParams.set("title", muxVideoTitle.value);
|
|
url.searchParams.set("video-title", muxVideoTitle.value);
|
|
return url.toString();
|
|
});
|
|
const muxPosterUrl = computed(() => {
|
|
if (!muxPlaybackId.value) return "";
|
|
|
|
const url = new URL(`https://image.mux.com/${encodeURIComponent(muxPlaybackId.value)}/thumbnail.webp`);
|
|
url.searchParams.set("time", "0.1");
|
|
url.searchParams.set("width", "900");
|
|
url.searchParams.set("fit_mode", "preserve");
|
|
return url.toString();
|
|
});
|
|
const isLoaded = ref(false);
|
|
const hasError = ref(false);
|
|
const isMobileViewport = ref(false);
|
|
const playerActivated = ref(false);
|
|
const shouldShowMobilePoster = computed(() => (
|
|
Boolean(muxPlayerUrl.value) &&
|
|
!hasError.value &&
|
|
isMobileViewport.value &&
|
|
!playerActivated.value
|
|
));
|
|
const shouldShowPlayer = computed(() => Boolean(muxPlayerUrl.value) && !hasError.value && !shouldShowMobilePoster.value);
|
|
|
|
let loadFallbackTimer: ReturnType<typeof setTimeout> | null = null;
|
|
let mobileQuery: MediaQueryList | null = null;
|
|
|
|
function clearLoadFallback() {
|
|
if (!loadFallbackTimer) return;
|
|
clearTimeout(loadFallbackTimer);
|
|
loadFallbackTimer = null;
|
|
}
|
|
|
|
function markLoaded() {
|
|
if (hasError.value) return;
|
|
isLoaded.value = true;
|
|
clearLoadFallback();
|
|
}
|
|
|
|
function markError() {
|
|
hasError.value = true;
|
|
clearLoadFallback();
|
|
}
|
|
|
|
function syncMobileViewport() {
|
|
isMobileViewport.value = Boolean(mobileQuery?.matches);
|
|
}
|
|
|
|
function activatePlayer() {
|
|
playerActivated.value = true;
|
|
}
|
|
|
|
onMounted(() => {
|
|
mobileQuery = window.matchMedia("(max-width: 700px)");
|
|
syncMobileViewport();
|
|
mobileQuery.addEventListener("change", syncMobileViewport);
|
|
loadFallbackTimer = setTimeout(markLoaded, 2500);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
mobileQuery?.removeEventListener("change", syncMobileViewport);
|
|
clearLoadFallback();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="hero-video">
|
|
<div class="hero-video__ambient" aria-hidden="true" />
|
|
<div class="hero-video__edge hero-video__edge--top" aria-hidden="true" />
|
|
<div class="hero-video__edge hero-video__edge--bottom" aria-hidden="true" />
|
|
<div class="hero-video__corner hero-video__corner--tl" aria-hidden="true" />
|
|
<div class="hero-video__corner hero-video__corner--tr" aria-hidden="true" />
|
|
<div class="hero-video__corner hero-video__corner--bl" aria-hidden="true" />
|
|
<div class="hero-video__corner hero-video__corner--br" aria-hidden="true" />
|
|
|
|
<ClientOnly>
|
|
<iframe
|
|
v-if="shouldShowPlayer"
|
|
class="hero-video__player"
|
|
:class="{ 'hero-video__player--loaded': isLoaded }"
|
|
:src="muxPlayerUrl"
|
|
:title="videoTitle"
|
|
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture"
|
|
allowfullscreen
|
|
loading="lazy"
|
|
referrerpolicy="strict-origin-when-cross-origin"
|
|
@load="markLoaded"
|
|
@error="markError"
|
|
/>
|
|
|
|
<template #fallback>
|
|
<div class="hero-video__skeleton">
|
|
<div class="hero-video__skeleton-pulse" />
|
|
<div class="hero-video__skeleton-content">
|
|
<div class="hero-video__skeleton-spinner" />
|
|
<span class="hero-video__skeleton-label">{{ t("hero.watchDemo") }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</ClientOnly>
|
|
|
|
<button
|
|
v-if="shouldShowMobilePoster"
|
|
type="button"
|
|
class="hero-video__poster"
|
|
:style="{ '--hero-video-poster': muxPosterUrl ? `url(${muxPosterUrl})` : 'url(/screenshots/2.jpg)' }"
|
|
:aria-label="videoTitle"
|
|
@click="activatePlayer"
|
|
>
|
|
<span class="hero-video__poster-play">
|
|
<v-icon :icon="mdiPlay" size="40" />
|
|
</span>
|
|
<span class="hero-video__poster-label">{{ t("hero.watchDemo") }}</span>
|
|
</button>
|
|
|
|
<div v-if="!isLoaded && !hasError && shouldShowPlayer" class="hero-video__skeleton">
|
|
<div class="hero-video__skeleton-pulse" />
|
|
<div class="hero-video__skeleton-content">
|
|
<div class="hero-video__skeleton-spinner" />
|
|
<span class="hero-video__skeleton-label">{{ t("hero.watchDemo") }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="hasError || !muxPlayerUrl" class="hero-video__error">
|
|
<v-icon :icon="mdiPlay" size="36" class="hero-video__error-icon" />
|
|
<span class="hero-video__error-text">{{ t("hero.videoUnavailable") }}</span>
|
|
</div>
|
|
|
|
<div class="hero-video__scan" aria-hidden="true" />
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.hero-video {
|
|
--hero-video-cyan: #00f0ff;
|
|
--hero-video-cyan-soft: rgba(0, 240, 255, 0.22);
|
|
--hero-video-magenta: #ff2bff;
|
|
--hero-video-magenta-soft: rgba(255, 43, 255, 0.18);
|
|
--hero-video-dark: rgba(2, 6, 23, 0.96);
|
|
|
|
position: relative;
|
|
z-index: 1;
|
|
aspect-ratio: 16 / 9;
|
|
border-radius: 16px;
|
|
background:
|
|
radial-gradient(circle at 18% 0%, var(--hero-video-cyan-soft), transparent 42%),
|
|
radial-gradient(circle at 88% 100%, var(--hero-video-magenta-soft), transparent 38%),
|
|
var(--hero-video-dark);
|
|
backdrop-filter: blur(20px);
|
|
-webkit-backdrop-filter: blur(20px);
|
|
border: 1px solid rgba(0, 240, 255, 0.34);
|
|
overflow: hidden;
|
|
box-shadow:
|
|
0 20px 60px rgba(0, 0, 0, 0.6),
|
|
0 0 34px rgba(0, 240, 255, 0.18),
|
|
0 0 52px rgba(255, 43, 255, 0.1),
|
|
inset 0 1px 0 rgba(230, 251, 255, 0.18);
|
|
}
|
|
|
|
.hero-video::before,
|
|
.hero-video::after {
|
|
content: "";
|
|
position: absolute;
|
|
pointer-events: none;
|
|
z-index: 4;
|
|
}
|
|
|
|
.hero-video::before {
|
|
inset: 0;
|
|
border: 1px solid rgba(230, 251, 255, 0.16);
|
|
border-radius: inherit;
|
|
box-shadow:
|
|
inset 0 0 28px rgba(0, 240, 255, 0.08),
|
|
inset 0 -26px 42px rgba(2, 6, 23, 0.44);
|
|
}
|
|
|
|
.hero-video::after {
|
|
inset: 0;
|
|
background:
|
|
linear-gradient(90deg, transparent 0 7%, rgba(0, 240, 255, 0.1) 7.2% 7.55%, transparent 7.8%),
|
|
linear-gradient(180deg, rgba(255, 255, 255, 0.1), transparent 18%, transparent 78%, rgba(0, 240, 255, 0.1));
|
|
mix-blend-mode: screen;
|
|
opacity: 0.32;
|
|
}
|
|
|
|
.hero-video__ambient,
|
|
.hero-video__scan,
|
|
.hero-video__edge,
|
|
.hero-video__corner {
|
|
position: absolute;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.hero-video__ambient {
|
|
inset: 0;
|
|
z-index: 1;
|
|
background:
|
|
linear-gradient(135deg, rgba(0, 240, 255, 0.14), transparent 28%, transparent 68%, rgba(255, 43, 255, 0.16)),
|
|
radial-gradient(circle at 50% 50%, transparent 58%, rgba(0, 0, 0, 0.34));
|
|
mix-blend-mode: screen;
|
|
opacity: 0.74;
|
|
}
|
|
|
|
.hero-video__scan {
|
|
inset: 0;
|
|
z-index: 5;
|
|
background:
|
|
repeating-linear-gradient(to bottom, rgba(230, 251, 255, 0.1) 0 1px, transparent 1px 5px),
|
|
linear-gradient(90deg, transparent, rgba(0, 240, 255, 0.12), transparent);
|
|
mix-blend-mode: soft-light;
|
|
opacity: 0.22;
|
|
}
|
|
|
|
.hero-video__edge {
|
|
left: 18px;
|
|
right: 18px;
|
|
z-index: 6;
|
|
height: 1px;
|
|
background: linear-gradient(90deg, transparent, var(--hero-video-cyan), var(--hero-video-magenta), transparent);
|
|
box-shadow: 0 0 16px rgba(0, 240, 255, 0.45);
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.hero-video__edge--top {
|
|
top: 9px;
|
|
}
|
|
|
|
.hero-video__edge--bottom {
|
|
bottom: 9px;
|
|
opacity: 0.58;
|
|
}
|
|
|
|
.hero-video__corner {
|
|
z-index: 6;
|
|
width: 28px;
|
|
height: 28px;
|
|
border-color: var(--hero-video-cyan);
|
|
filter: drop-shadow(0 0 10px rgba(0, 240, 255, 0.54));
|
|
}
|
|
|
|
.hero-video__corner--tl {
|
|
top: 8px;
|
|
left: 8px;
|
|
border-top: 2px solid;
|
|
border-left: 2px solid;
|
|
}
|
|
|
|
.hero-video__corner--tr {
|
|
top: 8px;
|
|
right: 8px;
|
|
border-top: 2px solid;
|
|
border-right: 2px solid;
|
|
}
|
|
|
|
.hero-video__corner--bl {
|
|
bottom: 8px;
|
|
left: 8px;
|
|
border-bottom: 2px solid;
|
|
border-left: 2px solid;
|
|
}
|
|
|
|
.hero-video__corner--br {
|
|
right: 8px;
|
|
bottom: 8px;
|
|
border-right: 2px solid;
|
|
border-bottom: 2px solid;
|
|
}
|
|
|
|
.hero-video__player {
|
|
position: relative;
|
|
z-index: 2;
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
aspect-ratio: 16 / 9;
|
|
border-radius: 14px;
|
|
opacity: 0;
|
|
transition: opacity 0.5s ease;
|
|
border: none;
|
|
background: #020617;
|
|
}
|
|
|
|
.hero-video__player--loaded {
|
|
opacity: 1;
|
|
}
|
|
|
|
.hero-video__poster {
|
|
position: relative;
|
|
z-index: 2;
|
|
display: flex;
|
|
width: 100%;
|
|
height: 100%;
|
|
min-height: 100%;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
border: none;
|
|
border-radius: 14px;
|
|
color: rgba(230, 251, 255, 0.94);
|
|
background:
|
|
linear-gradient(90deg, rgba(2, 6, 16, 0.18), rgba(2, 6, 16, 0.36)),
|
|
linear-gradient(180deg, rgba(0, 234, 255, 0.06), rgba(255, 43, 255, 0.08)),
|
|
var(--hero-video-poster) center / cover;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.hero-video__poster::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
background:
|
|
radial-gradient(circle at 50% 50%, rgba(0, 240, 255, 0.16), transparent 34%),
|
|
repeating-linear-gradient(to bottom, rgba(255, 255, 255, 0.08) 0 1px, transparent 1px 4px);
|
|
mix-blend-mode: screen;
|
|
opacity: 0.38;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.hero-video__poster-play,
|
|
.hero-video__poster-label {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.hero-video__poster-play {
|
|
display: grid;
|
|
width: 70px;
|
|
height: 70px;
|
|
place-items: center;
|
|
border: 1px solid rgba(0, 240, 255, 0.58);
|
|
border-radius: 50%;
|
|
color: #ffffff;
|
|
background: rgba(2, 10, 24, 0.68);
|
|
box-shadow:
|
|
0 0 0 1px rgba(255, 255, 255, 0.08) inset,
|
|
0 0 24px rgba(0, 240, 255, 0.3);
|
|
}
|
|
|
|
.hero-video__poster-label {
|
|
font-size: 12px;
|
|
font-weight: 800;
|
|
color: rgba(0, 240, 255, 0.9);
|
|
font-family: "JetBrains Mono", monospace;
|
|
letter-spacing: 0.05em;
|
|
text-transform: uppercase;
|
|
text-shadow: 0 0 16px rgba(0, 240, 255, 0.42);
|
|
}
|
|
|
|
.hero-video__skeleton {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 16px;
|
|
background: rgba(6, 10, 18, 0.96);
|
|
z-index: 7;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.hero-video__skeleton::before,
|
|
.hero-video__skeleton::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.hero-video__skeleton::before {
|
|
background:
|
|
linear-gradient(90deg, rgba(2, 6, 16, 0.18), rgba(2, 6, 16, 0.36)),
|
|
linear-gradient(180deg, rgba(0, 234, 255, 0.08), rgba(255, 43, 255, 0.08)),
|
|
url("/screenshots/2.jpg") center / cover;
|
|
opacity: 0.82;
|
|
filter: saturate(0.98) contrast(1.14) brightness(0.72);
|
|
transform: scale(1.035);
|
|
}
|
|
|
|
.hero-video__skeleton::after {
|
|
background:
|
|
linear-gradient(90deg, transparent 0 48%, rgba(0, 234, 255, 0.14) 48.2% 48.6%, transparent 48.8%),
|
|
repeating-linear-gradient(to bottom, rgba(255, 255, 255, 0.08) 0 1px, transparent 1px 4px);
|
|
mix-blend-mode: screen;
|
|
opacity: 0.34;
|
|
}
|
|
|
|
.hero-video__skeleton-pulse {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: linear-gradient(
|
|
135deg,
|
|
rgba(0, 240, 255, 0.12) 0%,
|
|
rgba(255, 0, 255, 0.08) 50%,
|
|
rgba(0, 240, 255, 0.1) 100%
|
|
);
|
|
mix-blend-mode: screen;
|
|
animation: skeletonPulse 2s ease-in-out infinite;
|
|
}
|
|
|
|
.hero-video__skeleton-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 12px;
|
|
z-index: 1;
|
|
}
|
|
|
|
.hero-video__skeleton-spinner {
|
|
width: 58px;
|
|
height: 58px;
|
|
border-radius: 50%;
|
|
border: 2px solid rgba(0, 240, 255, 0.28);
|
|
border-top-color: rgba(0, 240, 255, 0.92);
|
|
background: rgba(2, 8, 18, 0.56);
|
|
box-shadow:
|
|
0 0 0 1px rgba(0, 240, 255, 0.14) inset,
|
|
0 0 28px rgba(0, 240, 255, 0.34);
|
|
animation: spinnerRotate 0.8s linear infinite;
|
|
}
|
|
|
|
.hero-video__skeleton-label {
|
|
font-size: 13px;
|
|
font-weight: 800;
|
|
color: rgba(0, 240, 255, 0.88);
|
|
font-family: "JetBrains Mono", monospace;
|
|
letter-spacing: 0.05em;
|
|
text-transform: uppercase;
|
|
text-shadow: 0 0 16px rgba(0, 240, 255, 0.42);
|
|
}
|
|
|
|
@keyframes skeletonPulse {
|
|
0%,
|
|
100% { opacity: 0.3; }
|
|
50% { opacity: 0.8; }
|
|
}
|
|
|
|
@keyframes spinnerRotate {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.hero-video__error {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 7;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 12px;
|
|
padding: 32px;
|
|
background:
|
|
linear-gradient(135deg, rgba(0, 234, 255, 0.08), rgba(255, 43, 255, 0.05)),
|
|
rgba(2, 6, 16, 0.94);
|
|
}
|
|
|
|
.hero-video__error-icon {
|
|
color: rgba(0, 240, 255, 0.3);
|
|
}
|
|
|
|
.hero-video__error-text {
|
|
font-size: 13px;
|
|
color: #8892b0;
|
|
font-family: "JetBrains Mono", monospace;
|
|
text-align: center;
|
|
}
|
|
|
|
@media (max-width: 960px) {
|
|
.hero-video {
|
|
max-width: 100%;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.hero-video {
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.hero-video__player {
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.hero-video__poster {
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.hero-video__edge {
|
|
left: 12px;
|
|
right: 12px;
|
|
}
|
|
|
|
.hero-video__corner {
|
|
width: 20px;
|
|
height: 20px;
|
|
}
|
|
}
|
|
</style>
|