diff --git a/components/Navbar.tsx b/components/Navbar.tsx index c5136e3..d2a4d1a 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -166,6 +166,9 @@ export function Navbar() { alt='Vocdoni App' className='aspect-video w-full object-cover' loading='lazy' + decoding='async' + width={640} + height={360} /> {/* Gradient overlay */} diff --git a/components/TrustedBySection.tsx b/components/TrustedBySection.tsx index d8cb8d8..28f56d1 100644 --- a/components/TrustedBySection.tsx +++ b/components/TrustedBySection.tsx @@ -30,6 +30,9 @@ export default function TrustedBySection() { src={logo.url} alt={logo.name} className='h-10 w-auto object-contain opacity-70 grayscale transition-all duration-300 hover:grayscale-0 hover:opacity-100 dark:invert' + width={160} + height={40} + decoding='async' /> ))} diff --git a/components/app/AppHeroWithVideo.tsx b/components/app/AppHeroWithVideo.tsx index 4024217..5e7d2d4 100644 --- a/components/app/AppHeroWithVideo.tsx +++ b/components/app/AppHeroWithVideo.tsx @@ -1,5 +1,4 @@ import { ArrowRight, PlayCircleIcon, ScaleIcon, ShieldCheckIcon } from 'lucide-react' -import { useState } from 'react' import { useTranslation } from 'react-i18next' import CleanYoutubePlayer from '@/components/app/CleanYoutubePlayer' @@ -13,7 +12,6 @@ const THUMBNAIL_URL = `https://img.youtube.com/vi/${VIDEO_ID}/maxresdefault.jpg` export default function AppHeroWithVideo() { const { t } = useTranslation() - const [playing, setPlaying] = useState(false) return (
@@ -60,9 +58,11 @@ export default function AppHeroWithVideo() { - @@ -100,37 +100,15 @@ export default function AppHeroWithVideo() { transition={{ duration: 0.6 }} className='relative w-full mt-6 lg:mt-0' > -
- {playing ? ( -
- -
- ) : ( - - )} +
+
diff --git a/components/app/CleanYoutubePlayer.tsx b/components/app/CleanYoutubePlayer.tsx index 099a563..ad43a64 100644 --- a/components/app/CleanYoutubePlayer.tsx +++ b/components/app/CleanYoutubePlayer.tsx @@ -1,125 +1,27 @@ -import type { APITypes } from 'plyr-react' -import { lazy, Suspense, useEffect, useRef, useState } from 'react' - -// plyr accesses `document` at import time, so it must only load client-side. -const LazyPlyr = lazy(async () => { - await import('plyr/dist/plyr.css') - const mod = await import('plyr-react') - return { default: mod.Plyr } -}) +import { YoutubeFacade } from '@/components/ui/youtube-facade' type Props = { videoId: string title?: string - /** Optional cover image shown until playback has ramped up to HD, masking YouTube's low-quality ABR cold-start. */ + /** Optional poster image URL. Falls back to the YouTube maxresdefault thumbnail. */ coverUrl?: string + /** Unused — kept for backward-compat; the facade derives its own alt from title */ coverAlt?: string } -// How long to keep the cover up at most, even if no qualitychange/playing event arrives. -const MAX_COVER_MS = 2500 -// Quality (in vertical pixels) considered "good enough" to reveal the player. -const MIN_REVEAL_QUALITY = 720 - -export default function CleanYoutubePlayer({ videoId, title, coverUrl, coverAlt }: Props) { - const [mounted, setMounted] = useState(false) - const [revealed, setRevealed] = useState(!coverUrl) - const ref = useRef(null) - - useEffect(() => { - setMounted(true) - }, []) - - useEffect(() => { - if (!mounted || !coverUrl) return - - let fallback: ReturnType | null = null - let cancelled = false - - // Poll briefly for the Plyr instance to become available (lazy import + mount). - const attach = () => { - const plyr = ref.current?.plyr - // The instance exists but has no `.on` until ready; keep polling. - if (!plyr || typeof (plyr as unknown as { on?: unknown }).on !== 'function') { - if (!cancelled) setTimeout(attach, 50) - return - } - - // Force playback — the user already triggered a gesture by clicking the cover. - plyr.play() - - const reveal = () => { - if (cancelled) return - setRevealed(true) - } - - // Reveal once quality has ramped up past our threshold… - plyr.on('qualitychange', (event: unknown) => { - const detail = (event as { detail?: { quality?: number } }).detail - const q = detail?.quality ?? 0 - if (q >= MIN_REVEAL_QUALITY) reveal() - }) - - // …or, worst case, once playback has actually started. - plyr.on('playing', reveal) - - // Hard fallback so the user is never stuck looking at the cover. - fallback = setTimeout(reveal, MAX_COVER_MS) - } - - attach() - - return () => { - cancelled = true - if (fallback) clearTimeout(fallback) - } - }, [mounted, coverUrl]) - - if (!mounted) return null +/** + * A clean YouTube embed that defers iframe creation until user interaction. + * Wraps YoutubeFacade with sensible defaults for poster image derivation. + */ +export default function CleanYoutubePlayer({ videoId, title, coverUrl }: Props) { + const posterUrl = coverUrl || `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg` return ( -
- - - - - {coverUrl && ( - + ) } diff --git a/components/app/SocialProof.tsx b/components/app/SocialProof.tsx index 54b9589..c95164d 100644 --- a/components/app/SocialProof.tsx +++ b/components/app/SocialProof.tsx @@ -67,6 +67,9 @@ export default function SocialProof() { alt={logo.alt} className='h-9 w-auto object-contain opacity-85' loading='lazy' + decoding='async' + width={144} + height={36} /> ))}
diff --git a/components/shadcn-studio/blocks/about-us-page-03/about-us-page-03.tsx b/components/shadcn-studio/blocks/about-us-page-03/about-us-page-03.tsx index b92418f..4e01e97 100644 --- a/components/shadcn-studio/blocks/about-us-page-03/about-us-page-03.tsx +++ b/components/shadcn-studio/blocks/about-us-page-03/about-us-page-03.tsx @@ -125,6 +125,10 @@ const AboutUs = ({ aboutUsData }: { aboutUsData: AboutUsData }) => { src={teamGathering} alt='The Vocdoni team' className='w-full h-full object-cover object-center transition-transform duration-1000 hover:scale-105' + width={1400} + height={1205} + loading='lazy' + decoding='async' /> {/* Subtle gradient to fade into the page */}
diff --git a/components/shadcn-studio/blocks/about-us-page-07/about-us-page-07.tsx b/components/shadcn-studio/blocks/about-us-page-07/about-us-page-07.tsx index 3bc4cf4..714250a 100644 --- a/components/shadcn-studio/blocks/about-us-page-07/about-us-page-07.tsx +++ b/components/shadcn-studio/blocks/about-us-page-07/about-us-page-07.tsx @@ -139,6 +139,10 @@ const AboutUs = ({ src={aboutVocdoniImage} alt={t('about_us.image_alt')} className='h-full w-full object-cover transition-transform duration-1000 ease-out group-hover:scale-110' + width={684} + height={696} + loading='lazy' + decoding='async' />
diff --git a/components/shadcn-studio/blocks/cta-section-09/cta-section-09.tsx b/components/shadcn-studio/blocks/cta-section-09/cta-section-09.tsx index 87e9859..50ea3a8 100644 --- a/components/shadcn-studio/blocks/cta-section-09/cta-section-09.tsx +++ b/components/shadcn-studio/blocks/cta-section-09/cta-section-09.tsx @@ -56,11 +56,23 @@ const CTASection = () => { delay={0.6} transition={{ duration: 0.7 }} > - {t('cta.image_alt')} + {t('cta.image_alt')} {t('cta.image_alt')} diff --git a/components/shadcn-studio/blocks/features-section-02/accordion-with-image.tsx b/components/shadcn-studio/blocks/features-section-02/accordion-with-image.tsx index d0c6ba9..828fd40 100644 --- a/components/shadcn-studio/blocks/features-section-02/accordion-with-image.tsx +++ b/components/shadcn-studio/blocks/features-section-02/accordion-with-image.tsx @@ -22,6 +22,8 @@ const AccordionWithImage = () => { ), image: memberbaseImg, imageAlt: 'Upload member base', + imageWidth: 1400, + imageHeight: 924, }, { id: 'create', @@ -33,6 +35,8 @@ const AccordionWithImage = () => { ), image: createVoteImg, imageAlt: 'Create voting process', + imageWidth: 1400, + imageHeight: 893, }, { id: 'share', @@ -44,6 +48,8 @@ const AccordionWithImage = () => { ), image: publicVoteImg, imageAlt: 'Share and vote', + imageWidth: 1400, + imageHeight: 862, }, ] const [activeAccordion, setActiveAccordion] = useState('upload') @@ -102,6 +108,9 @@ const AccordionWithImage = () => { alt={activeFeature.imageAlt} className='w-full rounded-t-xl object-cover' loading='lazy' + decoding='async' + width={activeFeature.imageWidth} + height={activeFeature.imageHeight} />
diff --git a/components/shadcn-studio/blocks/portfolio-16/portfolio-16.tsx b/components/shadcn-studio/blocks/portfolio-16/portfolio-16.tsx index c5fbf99..a729c44 100644 --- a/components/shadcn-studio/blocks/portfolio-16/portfolio-16.tsx +++ b/components/shadcn-studio/blocks/portfolio-16/portfolio-16.tsx @@ -20,6 +20,8 @@ export type PortfolioItem = { btnColor?: string imageClassName?: string imageWrapperClassName?: string + imageWidth?: number + imageHeight?: number } type PortfolioProps = { @@ -75,6 +77,11 @@ const Portfolio = ({ portfolioItems }: PortfolioProps) => { src={project.imageUrl} alt={project.imageAlt} className={`w-full object-cover transition-transform duration-300 group-hover:scale-105 ${project.imageClassName || ''}`} + {...(project.imageWidth != null && project.imageHeight != null + ? { width: project.imageWidth, height: project.imageHeight } + : {})} + loading='lazy' + decoding='async' />