Skip to content
Open
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
43 changes: 42 additions & 1 deletion app/components/HeroSection.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,43 @@
<script setup>
import { onBeforeUnmount, onMounted, ref } from "vue";

const videoFrame = ref(null);
let observer;

const postToPlayer = (func) => {
const frame = videoFrame.value;
if (!frame || !frame.contentWindow) return;
frame.contentWindow.postMessage(
JSON.stringify({ event: "command", func, args: [] }),
"*"
);
};

onMounted(() => {
const frame = videoFrame.value;
if (!frame) return;

observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
postToPlayer("mute");
postToPlayer("playVideo");
} else {
postToPlayer("pauseVideo");
}
},
{ threshold: 0.4 }
);

observer.observe(frame);
});

onBeforeUnmount(() => {
if (observer && videoFrame.value) observer.unobserve(videoFrame.value);
observer = null;
});
</script>

<template>
<!-- Hero Section -->
<section class="py-32 px-6 ">
Expand Down Expand Up @@ -119,8 +159,9 @@
<div class="rounded-2xl overflow-hidden shadow-2xl">
<div class="w-full aspect-video">
<iframe
ref="videoFrame"
class="w-full h-full"
src="https://www.youtube-nocookie.com/embed/vNWWGWXjybg?rel=0&modestbranding=1&playsinline=1"
src="https://www.youtube-nocookie.com/embed/vNWWGWXjybg?rel=0&modestbranding=1&playsinline=1&enablejsapi=1"
title="DevCongress – Community Highlight"
loading="lazy"
referrerpolicy="strict-origin-when-cross-origin"
Expand Down
153 changes: 140 additions & 13 deletions app/components/TestimonialSection.vue
Original file line number Diff line number Diff line change
@@ -1,34 +1,58 @@
<template>
<section class="py-24 px-6 text-white">
<div class="max-w-7xl mx-auto text-center mb-16">
<h2 class="text-5xl lg:text-7xl font-bold text-center mb-16 tracking-tighter">
See What’s Possible When You <br class="hidden sm:inline-flex" />
<h2
class="text-5xl lg:text-7xl font-bold text-center mb-16 tracking-tighter">
See What’s Possible When You
<br class="hidden sm:inline-flex" />
<span class="text-primary-500">Have the Right Support</span>
</h2>
</div>

<!-- HORIZONTAL SCROLLER -->
<div class="relative mx-auto">
<div class="overflow-x-auto snap-x snap-mandatory scrollbar-none -mx-6 px-6">
<div class="grid grid-flow-col auto-cols-[minmax(16rem,20rem)] gap-8 py-2">
<div class="relative mx-auto max-w-7xl">
<!-- Prev / Next buttons -->
<button
@click="scrollBy(-1)"
aria-label="Previous testimonials"
class="absolute left-4 top-1/2 -translate-y-1/2 z-20 bg-black/50 backdrop-blur-md text-white rounded-full p-4 text-2xl transition-colors duration-150 testimonial-scroll-button focus:outline-none">
</button>
<button
@click="scrollBy(1)"
aria-label="Next testimonials"
class="absolute right-4 top-1/2 -translate-y-1/2 z-20 bg-black/50 backdrop-blur-md text-white rounded-full p-4 text-2xl transition-colors duration-150 testimonial-scroll-button focus:outline-none">
</button>

<div
ref="scroller"
role="region"
aria-label="Testimonials carousel"
tabindex="0"
class="overflow-x-auto snap-x snap-mandatory scrollbar-none -mx-6 px-6 py-2">
<div class="flex gap-8 items-start">
<!-- CARD -->
<div v-for="(t, i) in testimonials" :key="i" class="snap-start group">
<div
v-for="(t, i) in testimonials"
:key="i"
class="snap-start min-w-[14.4rem] w-[18rem] sm:w-[21.6rem] shrink-0 group">
<div
class="relative w-full aspect-4/3 sm:aspect-9/16 rounded-3xl overflow-hidden shadow-lg group hover:scale-[1.02] transition duration-300"
>
class="relative w-full rounded-3xl overflow-hidden shadow-lg group-hover:scale-[1.02] transition-transform duration-300"
style="aspect-ratio: 4/3">
<img
:src="t.image"
:alt="t.name"
class="w-full h-full object-cover grayscale-100 group-hover:grayscale-0 group-hover:sepia-0 transition ease-out duration-300 delay-75"
/>
:data-seed="t.name || i"
@error="onImgError"
class="w-full h-full object-cover grayscale-100 group-hover:grayscale-0 transition ease-out duration-300" />
</div>

<p class="text-base text-gray-100 mt-4 italic leading-relaxed">
“{{ t.quote }}”
<br />
<span
class="not-italic font-semibold text-gray-400 group-hover:text-secondary-400"
>
class="not-italic font-semibold text-gray-400 group-hover:text-secondary-400">
– {{ t.name }}, {{ t.role }}
</span>
</p>
Expand All @@ -40,7 +64,105 @@
</template>

<script setup>
import { testimonials } from '../data/testimonials';
import { onMounted, onBeforeUnmount, ref } from "vue";
import { testimonials as rawTestimonials } from "../data/testimonials";

const scroller = ref(null);
const testimonials = ref([]);

function shuffleArray(array) {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}

function scrollBy(direction = 1) {
const el = scroller.value;
if (!el) return;
const amount = Math.round(el.clientWidth * 0.8);
el.scrollBy({ left: direction * amount, behavior: "smooth" });
}

let pointerDown = false;
let startX = 0;
let scrollLeft = 0;

function onPointerDown(e) {
const el = scroller.value;
if (!el) return;
pointerDown = true;
el.setPointerCapture(e.pointerId);
startX = e.clientX;
scrollLeft = el.scrollLeft;
}

function onPointerMove(e) {
if (!pointerDown) return;
const el = scroller.value;
if (!el) return;
const dx = e.clientX - startX;
el.scrollLeft = scrollLeft - dx;
}

function onPointerUp(e) {
const el = scroller.value;
if (!el) return;
pointerDown = false;
try {
el.releasePointerCapture?.(e.pointerId);
} catch (err) {
// ignore
}
}

function onKeyDown(e) {
if (e.key === "ArrowLeft") {
e.preventDefault();
scrollBy(-1);
} else if (e.key === "ArrowRight") {
e.preventDefault();
scrollBy(1);
}
}

function onImgError(e) {
try {
e.target.onerror = null;
const seed = e.target?.dataset?.seed || String(Date.now());
const style = "adventurer"; // fun avatar style
const params = new URLSearchParams({
seed: seed,
backgroundType: "solid",
}).toString();
e.target.src = `https://api.dicebear.com/6.x/${style}/svg?${params}`;
} catch (err) {
// fallback no-op
}
}

onMounted(() => {
testimonials.value = shuffleArray(rawTestimonials);
const el = scroller.value;
if (!el) return;
el.addEventListener("pointerdown", onPointerDown);
el.addEventListener("pointermove", onPointerMove);
el.addEventListener("pointerup", onPointerUp);
el.addEventListener("pointercancel", onPointerUp);
el.addEventListener("keydown", onKeyDown);
});

onBeforeUnmount(() => {
const el = scroller.value;
if (!el) return;
el.removeEventListener("pointerdown", onPointerDown);
el.removeEventListener("pointermove", onPointerMove);
el.removeEventListener("pointerup", onPointerUp);
el.removeEventListener("pointercancel", onPointerUp);
el.removeEventListener("keydown", onKeyDown);
});
</script>

<style>
Expand All @@ -51,4 +173,9 @@ import { testimonials } from '../data/testimonials';
-ms-overflow-style: none;
scrollbar-width: none;
}

.testimonial-scroll-button:hover {
background: oklch(0.9412 0.1999 105.66) !important;
color: black !important;
}
</style>
33 changes: 32 additions & 1 deletion app/components/WhatWeveBeenUpTo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,38 @@
class="grid grid-cols-1 md:grid-cols-6 gap-4 md:gap-6 auto-rows-[minmax(8rem,auto)] md:grid-flow-dense"
>
<div v-for="(card, i) in cards" :key="i" :class="card.size">
<div
v-if="card.title === 'Echo Podcast'"
class="group block h-full rounded-3xl transition duration-300 bg-(--card-color)/5 hover:bg-(--card-color)/20"
:style="{ '--card-color': card.color }"
>
<div class="flex h-full flex-col gap-4 p-5 md:p-7">
<!-- Copy -->
<div class="w-full">
<h3
class="text-2xl md:text-4xl font-extrabold tracking-tight text-gray-100 transition-colors duration-300 group-hover:text-(--card-color)"
>
{{ card.title }}
</h3>
<p
class="text-base md:text-lg text-white/60 leading-snug mt-2 transition-colors duration-300 group-hover:text-white"
>
{{ card.description }}
</p>
</div>

<iframe
src="https://embed.acast.com/653bf1eb18e0ae00111ac1a1?episode-order=desc&accentColor=161616&bgColor=fcf404&secondaryColor=161616&feed=true"
frameborder="0"
width="100%"
height="280px"
title="Echo Podcast"
></iframe>
</div>
</div>

<a
v-else
:href="card.link"
class="group block h-full rounded-3xl transition duration-300 bg-(--card-color)/5 hover:bg-(--card-color)/20"
:style="{ '--card-color': card.color }"
Expand Down Expand Up @@ -56,4 +87,4 @@

<script setup>
import { activities as cards } from '../data/activities';
</script>
</script>
Loading