feat(video): add YouTube lightbox to benefits section
Replaces static video link with a self-contained VideoCard component that opens a native <dialog> lightbox with autoplay on open and iframe src teardown on close. Includes pulsing ring animation on the play button and a rounded-triangle SVG icon. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,8 @@ interface Props { lang?: string }
|
|||||||
const { lang = 'en' } = Astro.props;
|
const { lang = 'en' } = Astro.props;
|
||||||
const es = lang === 'es';
|
const es = lang === 'es';
|
||||||
const entry = await getEntry('pages', es ? 'es/index' : 'en/index');
|
const entry = await getEntry('pages', es ? 'es/index' : 'en/index');
|
||||||
const { title, description, canonical, sections = [] } = entry!.data;
|
if (!entry) throw new Error(`[HomePage] Content entry not found for lang="${lang}". If running in dev, restart the dev server to rebuild the content layer cache.`);
|
||||||
|
const { title, description, canonical, sections = [] } = entry.data;
|
||||||
---
|
---
|
||||||
<BaseLayout {title} {description} {canonical} {lang}>
|
<BaseLayout {title} {description} {canonical} {lang}>
|
||||||
{(sections as HomeSection[]).map((section) => {
|
{(sections as HomeSection[]).map((section) => {
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
image: string;
|
||||||
|
alt: string;
|
||||||
|
videoUrl: string;
|
||||||
|
videoTitle: string;
|
||||||
|
}
|
||||||
|
const { image, alt, videoUrl, videoTitle } = Astro.props;
|
||||||
|
---
|
||||||
|
<button class="video-card" data-video-url={videoUrl} aria-haspopup="dialog" aria-label={`Watch video: ${videoTitle}`}>
|
||||||
|
<img src={`/assets/images/${image}`} alt={alt} />
|
||||||
|
<span aria-hidden="true">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50" height="50" fill="currentColor" aria-hidden="true">
|
||||||
|
<path d="M21 12.88L37.5 22.4A3 3 0 0 1 37.5 27.6L21 37.12A3 3 0 0 1 16.5 34.52L16.5 15.48A3 3 0 0 1 21 12.88Z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<dialog class="video-dialog">
|
||||||
|
<div class="video-dialog-inner">
|
||||||
|
<button class="video-dialog-close" aria-label="Close video" autofocus>✕</button>
|
||||||
|
<div class="video-dialog-frame">
|
||||||
|
<iframe title={videoTitle} allowfullscreen allow="autoplay; encrypted-media"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll('button.video-card[data-video-url]').forEach((card) => {
|
||||||
|
const dialog = card.nextElementSibling as HTMLDialogElement;
|
||||||
|
const iframe = dialog.querySelector('iframe') as HTMLIFrameElement;
|
||||||
|
const base = card.getAttribute('data-video-url')!;
|
||||||
|
const sep = base.includes('?') ? '&' : '?';
|
||||||
|
|
||||||
|
const closeBtn = dialog.querySelector('.video-dialog-close') as HTMLButtonElement;
|
||||||
|
closeBtn.addEventListener('click', () => dialog.close());
|
||||||
|
|
||||||
|
card.addEventListener('click', () => {
|
||||||
|
iframe.src = `${base}${sep}autoplay=1`;
|
||||||
|
dialog.showModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.addEventListener('close', () => {
|
||||||
|
iframe.src = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close on backdrop click
|
||||||
|
dialog.addEventListener('click', (e) => {
|
||||||
|
if (e.target === dialog) dialog.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
import type { BenefitsSection } from '../../types/home-sections';
|
import type { BenefitsSection } from '../../types/home-sections';
|
||||||
|
import VideoCard from '../VideoCard.astro';
|
||||||
interface Props { section: BenefitsSection }
|
interface Props { section: BenefitsSection }
|
||||||
const { section } = Astro.props;
|
const { section } = Astro.props;
|
||||||
---
|
---
|
||||||
@@ -10,9 +11,11 @@ const { section } = Astro.props;
|
|||||||
<h3>{section.subheading}</h3>
|
<h3>{section.subheading}</h3>
|
||||||
<ul class="source-list">{section.items.map((item) => <li>{item}</li>)}</ul>
|
<ul class="source-list">{section.items.map((item) => <li>{item}</li>)}</ul>
|
||||||
</div>
|
</div>
|
||||||
<a class="video-card" href={section.videoHref}>
|
<VideoCard
|
||||||
<img src={`/assets/images/${section.videoImage}`} alt={section.videoImageAlt} />
|
image={section.videoImage}
|
||||||
<span>▶</span>
|
alt={section.videoImageAlt}
|
||||||
</a>
|
videoUrl={section.videoUrl}
|
||||||
|
videoTitle={section.videoTitle}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ sections:
|
|||||||
- Individualized Results
|
- Individualized Results
|
||||||
- Sensory-Friendly Rooms
|
- Sensory-Friendly Rooms
|
||||||
- Convenient and Affordable
|
- Convenient and Affordable
|
||||||
videoHref: /tour
|
videoUrl: "https://www.youtube-nocookie.com/embed/EczPH1jx9mc?si=beestOCaO4tL7Re6"
|
||||||
|
videoTitle: AIA Learner Journey
|
||||||
videoImage: learner-journey.webp
|
videoImage: learner-journey.webp
|
||||||
videoImageAlt: Learner journey for children with autism video
|
videoImageAlt: Learner journey for children with autism video
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ sections:
|
|||||||
- Resultados individualizados
|
- Resultados individualizados
|
||||||
- Salas sensoriales
|
- Salas sensoriales
|
||||||
- Conveniente y asequible
|
- Conveniente y asequible
|
||||||
videoHref: /tour
|
videoUrl: "https://www.youtube-nocookie.com/embed/EczPH1jx9mc?si=beestOCaO4tL7Re6"
|
||||||
|
videoTitle: AIA Learner Journey
|
||||||
videoImage: learner-journey.webp
|
videoImage: learner-journey.webp
|
||||||
videoImageAlt: Video del recorrido del estudiante
|
videoImageAlt: Video del recorrido del estudiante
|
||||||
|
|
||||||
|
|||||||
@@ -105,9 +105,18 @@
|
|||||||
.source-list li > p { margin-block: 0; }
|
.source-list li > p { margin-block: 0; }
|
||||||
.two-column-list { columns: 2; }
|
.two-column-list { columns: 2; }
|
||||||
.benefit-grid { grid-template-columns: 1.1fr .9fr; }
|
.benefit-grid { grid-template-columns: 1.1fr .9fr; }
|
||||||
.video-card { display: block; position: relative; }
|
.video-card { background: none; border: none; cursor: pointer; display: block; padding: 0; position: relative; }
|
||||||
.video-card img { width: 100%; }
|
.video-card img { width: 100%; }
|
||||||
.video-card span { align-items: center; background: var(--color-accent); border-radius: 50%; color: white; display: flex; height: 64px; justify-content: center; left: calc(50% - 32px); position: absolute; top: calc(50% - 32px); width: 64px; }
|
.video-card span { align-items: center; background: var(--color-accent); border-radius: 50%; color: white; display: flex; height: 64px; justify-content: center; left: 50%; position: absolute; top: 50%; transform: translate(-50%, -50%); width: 64px; z-index: 1; }
|
||||||
|
.video-card::before, .video-card::after { animation-duration: 3s; animation-iteration-count: infinite; animation-name: video-ripple; animation-timing-function: cubic-bezier(.65, 0, .34, 1); border: 8px solid var(--color-accent); border-radius: 50%; content: ""; height: 64px; left: 50%; opacity: 0; position: absolute; top: 50%; width: 64px; }
|
||||||
|
.video-card::before { animation-delay: .5s; }
|
||||||
|
@keyframes video-ripple { 0% { opacity: 1; transform: translate(-50%, -50%) scale3d(.75, .75, 1); } to { opacity: 0; transform: translate(-50%, -50%) scale3d(1.5, 1.5, 1); } }
|
||||||
|
.video-dialog { background: none; border: none; box-sizing: border-box; max-width: min(90vw, 900px); padding: 1rem 1rem 0 0; width: 100%; }
|
||||||
|
.video-dialog::backdrop { background: rgba(0, 0, 0, .85); }
|
||||||
|
.video-dialog-inner { position: relative; }
|
||||||
|
.video-dialog-frame { aspect-ratio: 16 / 9; width: 100%; }
|
||||||
|
.video-dialog-frame iframe { border: none; height: 100%; width: 100%; }
|
||||||
|
.video-dialog-close { background: white; border: none; border-radius: 50%; cursor: pointer; font-size: 1rem; height: 2rem; line-height: 2rem; position: absolute; right: -1rem; top: -1rem; width: 2rem; }
|
||||||
.skills-section { background: var(--color-primary); color: white; padding-block: 4rem; text-align: center; }
|
.skills-section { background: var(--color-primary); color: white; padding-block: 4rem; text-align: center; }
|
||||||
.skills-section h2 { color: white; }
|
.skills-section h2 { color: white; }
|
||||||
.skills-grid { display: grid; gap: 1rem; grid-template-columns: repeat(3, 1fr); margin-top: 2.5rem; }
|
.skills-grid { display: grid; gap: 1rem; grid-template-columns: repeat(3, 1fr); margin-top: 2.5rem; }
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ export interface BenefitsSection {
|
|||||||
heading: string;
|
heading: string;
|
||||||
subheading: string;
|
subheading: string;
|
||||||
items: string[];
|
items: string[];
|
||||||
videoHref: string;
|
videoUrl: string;
|
||||||
|
videoTitle: string;
|
||||||
videoImage: string;
|
videoImage: string;
|
||||||
videoImageAlt: string;
|
videoImageAlt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user