Skip to content
Draft
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
6 changes: 0 additions & 6 deletions app/components/ExitIntentPopup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,6 @@ function close() {
visible.value = false;
}

function submit() {
console.log("Email:", email.value);

close();
}

function handleMouseLeave(e) {
if (
e.clientY <= 0 &&
Expand Down
1 change: 0 additions & 1 deletion app/components/FAQSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ watchEffect(() => {
})

function selectPanel(section) {
console.log(section)
panel.value = section
}
</script>
Expand Down
79 changes: 79 additions & 0 deletions app/components/VideoCustomerStories.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<template>
<div class="stories-section">
<div v-if="pending" class="stories-status">Loading customer stories…</div>

<div v-else-if="!stories?.length" class="stories-status">
No customer stories available yet.
</div>

<div v-else class="stories-grid">
<VideoCustomerStory
v-for="story in stories"
:key="story.title"
:slug="story.stem.split('/').at(-1)"
class="stories-grid__item"
/>
</div>
</div>
</template>

<script setup>
import { computed } from 'vue'

const { locale } = useI18n()

// Assumes a plural counterpart to useVideoCustomerStory that returns
// a list of { slug, ... } entries. Rename to match your actual composable.
const { queryVideoCustomerStories } = useVideoCustomerStories(locale)

const { data: stories, pending } = await useAsyncData(
`video-customer-stories-${locale.value}`,
queryVideoCustomerStories,
{ watch: [locale] }
)
</script>

<style scoped>
.stories-section {
width: 100%;
}

.stories-status {
text-align: center;
padding: 3rem 1rem;
color: rgba(0, 0, 0, 0.6);
}

.stories-grid {
--gap: 2rem;
--cols: 3;

display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--gap);
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
}

.stories-grid__item {
aspect-ratio: 9 / 16;
/* 3 per line max: subtract gap share, cap width so items don't grow past that */
flex: 1 1 calc((100% - (var(--cols) - 1) * var(--gap)) / var(--cols));
max-width: calc((100% - (var(--cols) - 1) * var(--gap)) / var(--cols));
}

@media (max-width: 900px) {
.stories-grid {
--cols: 2;
}
}

@media (max-width: 480px) {
.stories-grid {
--cols: 1;
padding: 1.5rem 1rem;
}
}
</style>
131 changes: 131 additions & 0 deletions app/components/VideoCustomerStory.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<template>
<div class="video-wrapper">
<iframe
ref="playerRef"
class="video-iframe"
:src="videoUrl"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen
></iframe>

<button
class="play-pause-overlay"
type="button"
:aria-label="isPlaying ? 'Pause video' : 'Play video'"
@click="togglePlay"
>
<svg v-if="!isPlaying" viewBox="0 0 24 24"><path d="M8 5v14l11-7z" /></svg>
<svg v-else viewBox="0 0 24 24"><path d="M6 5h4v14H6zM14 5h4v14h-4z" /></svg>
</button>
</div>
</template>

<script setup>
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'

const props = defineProps({
slug: String
})

const { locale } = useI18n()
const slug = computed(() => props.slug)

const { queryVideoCustomerStory } = useVideoCustomerStory(slug, locale)

const { data: customerStory } = await useAsyncData(
`video-customer-story-${locale.value}-${slug.value}`,
queryVideoCustomerStory,
{ watch: [locale, slug] }
)

const playerRef = ref(null)
const isPlaying = ref(false)

// Build the embed URL with controls hidden and the JS API enabled
const videoUrl = computed(() => {
const rawUrl = customerStory.value?.meta?.url
if (!rawUrl) return ''
const url = new URL(rawUrl)
url.searchParams.set('controls', '0')
url.searchParams.set('enablejsapi', '1')
url.searchParams.set('modestbranding', '1')
url.searchParams.set('playsinline', '1')
url.searchParams.set('rel', '0')
if (typeof window !== 'undefined') {
// required for the postMessage API to work reliably
url.searchParams.set('origin', window.location.origin)
}
return url.toString()
})

function postCommand(func) {
playerRef.value?.contentWindow?.postMessage(
JSON.stringify({ event: 'command', func, args: [] }),
'*'
)
}

function togglePlay() {
postCommand(isPlaying.value ? 'pauseVideo' : 'playVideo')
isPlaying.value = !isPlaying.value
}

// Keep the button state in sync with actual player state
function handleMessage(event) {
if (typeof event.data !== 'string') return
try {
const data = JSON.parse(event.data)
if (data.event === 'infoDelivery' && typeof data.info?.playerState === 'number') {
// 1 = playing, 2 = paused, 0 = ended
if (data.info.playerState === 1) isPlaying.value = true
else if (data.info.playerState === 2 || data.info.playerState === 0) isPlaying.value = false
}
} catch {
// ignore non-JSON messages from other sources
}
}

onMounted(() => window.addEventListener('message', handleMessage))
onBeforeUnmount(() => window.removeEventListener('message', handleMessage))
</script>

<style scoped>
.video-wrapper {
position: relative;
width: 100%;
max-width: 720px; /* optional: caps how big it can grow, remove if you want truly full width */
aspect-ratio: 9 / 16;
overflow: hidden;
background: #000;
}

.video-iframe {
width: 100%;
height: 100%;
display: block;
}

.play-pause-overlay {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
cursor: pointer;
padding: 0;
}

.play-pause-overlay svg {
width: 72px;
height: 72px;
fill: rgba(255, 255, 255, 0.9);
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.5));
}
</style>
131 changes: 131 additions & 0 deletions app/components/VideoTestimonial.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<template>
<div class="video-wrapper">
<iframe
ref="playerRef"
class="video-iframe"
:src="videoUrl"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen
></iframe>

<button
class="play-pause-overlay"
type="button"
:aria-label="isPlaying ? 'Pause video' : 'Play video'"
@click="togglePlay"
>
<svg v-if="!isPlaying" viewBox="0 0 24 24"><path d="M8 5v14l11-7z" /></svg>
<svg v-else viewBox="0 0 24 24"><path d="M6 5h4v14H6zM14 5h4v14h-4z" /></svg>
</button>
</div>
</template>

<script setup>
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'

const props = defineProps({
slug: String
})

const { locale } = useI18n()
const slug = computed(() => props.slug)

const { queryVideoTestimonial } = useVideoTestimonial(slug, locale)

const { data: customerStory } = await useAsyncData(
`video-testimonial-${locale.value}-${slug.value}`,
queryVideoTestimonial,
{ watch: [locale, slug] }
)

const playerRef = ref(null)
const isPlaying = ref(false)

// Build the embed URL with controls hidden and the JS API enabled
const videoUrl = computed(() => {
const rawUrl = customerStory.value?.meta?.url
if (!rawUrl) return ''
const url = new URL(rawUrl)
url.searchParams.set('controls', '0')
url.searchParams.set('enablejsapi', '1')
url.searchParams.set('modestbranding', '1')
url.searchParams.set('playsinline', '1')
url.searchParams.set('rel', '0')
if (typeof window !== 'undefined') {
// required for the postMessage API to work reliably
url.searchParams.set('origin', window.location.origin)
}
return url.toString()
})

function postCommand(func) {
playerRef.value?.contentWindow?.postMessage(
JSON.stringify({ event: 'command', func, args: [] }),
'*'
)
}

function togglePlay() {
postCommand(isPlaying.value ? 'pauseVideo' : 'playVideo')
isPlaying.value = !isPlaying.value
}

// Keep the button state in sync with actual player state
function handleMessage(event) {
if (typeof event.data !== 'string') return
try {
const data = JSON.parse(event.data)
if (data.event === 'infoDelivery' && typeof data.info?.playerState === 'number') {
// 1 = playing, 2 = paused, 0 = ended
if (data.info.playerState === 1) isPlaying.value = true
else if (data.info.playerState === 2 || data.info.playerState === 0) isPlaying.value = false
}
} catch {
// ignore non-JSON messages from other sources
}
}

onMounted(() => window.addEventListener('message', handleMessage))
onBeforeUnmount(() => window.removeEventListener('message', handleMessage))
</script>

<style scoped>
.video-wrapper {
position: relative;
width: 720px;
height: 1280px;
max-width: 100%;
overflow: hidden;
background: #000;
}

.video-iframe {
width: 100%;
height: 100%;
display: block;
}

.play-pause-overlay {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
cursor: pointer;
padding: 0;
}

.play-pause-overlay svg {
width: 72px;
height: 72px;
fill: rgba(255, 255, 255, 0.9);
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.5));
}
</style>
12 changes: 12 additions & 0 deletions app/composables/useVideoCustomerStories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function useVideoCustomerStories(locale) {
async function queryVideoCustomerStories() {
let res = await queryCollection('jsonPages')
.where('lang', '=', locale.value)
.where('pageType', '=', 'video_testimonials')
.all()

return res
}

return { queryVideoCustomerStories }
}
14 changes: 14 additions & 0 deletions app/composables/useVideoCustomerStory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function useVideoCustomerStory(slug, locale) {
async function queryVideoCustomerStory() {
let res = await queryCollection('jsonPages')
.where('lang', '=', locale.value)
.where('pageType', '=', 'video_testimonials')
.all()

res = res.filter(t => t.stem.split('/')[2] == slug.value)

return res[0]
}

return { queryVideoCustomerStory }
}
Loading