feat: add how-it-works, supported platforms and FAQ sections to homepage
Enrich the landing page with reassurance/trust content inspired by competitor downloader sites: a 3-step "how it works" guide, a supported-platforms grid with a non-affiliation disclaimer, and an 8-question FAQ (with FAQPage JSON-LD) -- all translated across en/fr/es/it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
import { SubmitForm } from '@/components/SubmitForm'
|
||||
import { ReassuranceSection } from '@/components/ReassuranceSection'
|
||||
import { HowItWorksSection } from '@/components/HowItWorksSection'
|
||||
import { SupportedPlatformsSection } from '@/components/SupportedPlatformsSection'
|
||||
import { FaqSection } from '@/components/FaqSection'
|
||||
|
||||
export default async function Home({
|
||||
params,
|
||||
@@ -32,6 +35,9 @@ export default async function Home({
|
||||
</section>
|
||||
|
||||
<ReassuranceSection locale={locale} />
|
||||
<HowItWorksSection locale={locale} />
|
||||
<SupportedPlatformsSection locale={locale} />
|
||||
<FaqSection locale={locale} />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
const QUESTION_COUNT = 8
|
||||
|
||||
function ChevronIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden
|
||||
className="shrink-0 transition-transform duration-200 group-open:rotate-180"
|
||||
>
|
||||
<path d="M6 9l6 6 6-6" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export async function FaqSection({ locale }: { locale: string }) {
|
||||
const t = await getTranslations({ locale, namespace: 'faq' })
|
||||
|
||||
const items = Array.from({ length: QUESTION_COUNT }, (_, i) => ({
|
||||
q: t(`q${i + 1}`),
|
||||
a: t(`a${i + 1}`),
|
||||
}))
|
||||
|
||||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: items.map(({ q, a }) => ({
|
||||
'@type': 'Question',
|
||||
name: q,
|
||||
acceptedAnswer: { '@type': 'Answer', text: a },
|
||||
})),
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="py-16 px-6 bg-gray-50 dark:bg-slate-900/50" aria-labelledby="faq-heading">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<h2 id="faq-heading" className="text-2xl md:text-3xl font-bold tracking-tight text-gray-900 dark:text-gray-50 text-center mb-10">
|
||||
{t('heading')}
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
{items.map(({ q, a }) => (
|
||||
<details
|
||||
key={q}
|
||||
className="group rounded-xl border border-gray-200 dark:border-slate-700 bg-white dark:bg-slate-900 px-5 py-4"
|
||||
>
|
||||
<summary className="flex items-center justify-between gap-4 cursor-pointer select-none font-medium text-gray-900 dark:text-gray-50 list-none [&::-webkit-details-marker]:hidden">
|
||||
{q}
|
||||
<ChevronIcon />
|
||||
</summary>
|
||||
<p className="mt-3 text-sm text-gray-500 dark:text-gray-400">{a}</p>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
function LinkIcon() {
|
||||
return (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||
<path d="M10 13a5 5 0 007.07 0l2.83-2.83a5 5 0 00-7.07-7.07l-1.5 1.5"/>
|
||||
<path d="M14 11a5 5 0 00-7.07 0l-2.83 2.83a5 5 0 007.07 7.07l1.5-1.5"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function SlidersIcon() {
|
||||
return (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||
<line x1="4" y1="6" x2="20" y2="6"/>
|
||||
<line x1="4" y1="12" x2="20" y2="12"/>
|
||||
<line x1="4" y1="18" x2="20" y2="18"/>
|
||||
<circle cx="9" cy="6" r="2" fill="currentColor" stroke="none"/>
|
||||
<circle cx="16" cy="12" r="2" fill="currentColor" stroke="none"/>
|
||||
<circle cx="11" cy="18" r="2" fill="currentColor" stroke="none"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function DownloadIcon() {
|
||||
return (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||
<path d="M12 3v12"/>
|
||||
<path d="M7 10l5 5 5-5"/>
|
||||
<path d="M4 21h16"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export async function HowItWorksSection({ locale }: { locale: string }) {
|
||||
const t = await getTranslations({ locale, namespace: 'howItWorks' })
|
||||
|
||||
const steps = [
|
||||
{ title: t('step1Title'), desc: t('step1Desc'), Icon: LinkIcon },
|
||||
{ title: t('step2Title'), desc: t('step2Desc'), Icon: SlidersIcon },
|
||||
{ title: t('step3Title'), desc: t('step3Desc'), Icon: DownloadIcon },
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="py-16 px-6 bg-gray-50 dark:bg-slate-900/50" aria-labelledby="how-it-works-heading">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<h2 id="how-it-works-heading" className="text-2xl md:text-3xl font-bold tracking-tight text-gray-900 dark:text-gray-50">
|
||||
{t('heading')}
|
||||
</h2>
|
||||
<p className="mt-2 text-gray-500 dark:text-gray-400">{t('subheading')}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-10">
|
||||
{steps.map(({ title, desc, Icon }, i) => (
|
||||
<div key={title} className="text-center">
|
||||
<div className="mx-auto inline-flex items-center justify-center w-14 h-14 rounded-full bg-violet-100 dark:bg-violet-900/30 text-violet-600 dark:text-violet-400 mb-4">
|
||||
<Icon />
|
||||
</div>
|
||||
<p className="text-xs font-semibold text-violet-600 dark:text-violet-400 mb-1">0{i + 1}</p>
|
||||
<p className="font-semibold text-gray-900 dark:text-gray-50">{title}</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1 max-w-xs mx-auto">{desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
const PLATFORMS = [
|
||||
'YouTube',
|
||||
'TikTok',
|
||||
'Instagram',
|
||||
'Facebook',
|
||||
'X (Twitter)',
|
||||
'Vimeo',
|
||||
'Twitch',
|
||||
'SoundCloud',
|
||||
'Dailymotion',
|
||||
]
|
||||
|
||||
export async function SupportedPlatformsSection({ locale }: { locale: string }) {
|
||||
const t = await getTranslations({ locale, namespace: 'platforms' })
|
||||
|
||||
return (
|
||||
<section className="py-16 px-6" aria-labelledby="platforms-heading">
|
||||
<div className="max-w-5xl mx-auto text-center">
|
||||
<h2 id="platforms-heading" className="text-2xl md:text-3xl font-bold tracking-tight text-gray-900 dark:text-gray-50">
|
||||
{t('heading')}
|
||||
</h2>
|
||||
<p className="mt-2 text-gray-500 dark:text-gray-400">{t('subheading')}</p>
|
||||
|
||||
<div className="mt-8 flex flex-wrap justify-center gap-3">
|
||||
{PLATFORMS.map((name) => (
|
||||
<span
|
||||
key={name}
|
||||
className="px-4 py-2 rounded-full border border-gray-200 dark:border-slate-700 bg-white dark:bg-slate-900 text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-gray-400 dark:text-slate-500">{t('more')}</p>
|
||||
<p className="mt-6 text-xs text-gray-400 dark:text-slate-500 max-w-2xl mx-auto">{t('disclaimer')}</p>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user