feat: add tabbed FAQ, polish installation code blocks, refine integration styling

This commit is contained in:
Usman Baig
2026-03-21 19:52:32 +01:00
parent e789fb525b
commit a361649e60
6 changed files with 307 additions and 97 deletions

View File

@@ -1,37 +1,18 @@
'use client'
import { motion } from 'framer-motion'
import { useState } from 'react'
import { ChevronDownIcon } from '@ciphera-net/ui'
const faqs = [
{
question: "Is Pulse GDPR compliant?",
answer: "Yes, Pulse is GDPR compliant by design. We don't use cookies, don't collect personal data, and process all data anonymously."
},
{
question: "Do I need a cookie consent banner?",
answer: "No, you don't need a cookie consent banner. Pulse doesn't use cookies, so it's exempt from cookie consent requirements under GDPR."
},
{
question: "How does Pulse track visitors?",
answer: "We use a lightweight JavaScript snippet that sends anonymous pageview events. No cookies, no cross-session identifiers (we use sessionStorage only to group events within a single visit), and no cross-site tracking."
},
{
question: "What data does Pulse collect?",
answer: "We collect anonymous pageview data including page path, referrer, device type, browser, and country (derived from IP at request time; the IP itself is not stored). No personal information is collected."
},
{
question: "How accurate is the data?",
answer: "Our data is highly accurate. We exclude bot traffic and data center visits. Since we don't use cookies, we count unique sessions rather than unique users."
},
{
question: "Can I export my data?",
answer: "Yes, you can access all your analytics data through the dashboard. We're working on export functionality for bulk data downloads."
}
]
import PulseFAQ from '@/components/marketing/PulseFAQ'
// * JSON-LD FAQ Schema for rich snippets
const faqs = [
{ question: "Is Pulse GDPR compliant?", answer: "Yes, Pulse is GDPR compliant by design. We don't use cookies, don't collect personal data, and process all data anonymously." },
{ question: "Do I need a cookie consent banner?", answer: "No, you don't need a cookie consent banner. Pulse doesn't use cookies, so it's exempt from cookie consent requirements under GDPR." },
{ question: "How does Pulse track visitors?", answer: "We use a lightweight JavaScript snippet that sends anonymous pageview events. No cookies, no cross-session identifiers (we use sessionStorage only to group events within a single visit), and no cross-site tracking." },
{ question: "What data does Pulse collect?", answer: "We collect anonymous pageview data including page path, referrer, device type, browser, and country (derived from IP at request time; the IP itself is not stored). No personal information is collected." },
{ question: "How accurate is the data?", answer: "Our data is highly accurate. We exclude bot traffic and data center visits. Since we don't use cookies, we count unique sessions rather than unique users." },
{ question: "Can I export my data?", answer: "Yes, you can access all your analytics data through the dashboard. We're working on export functionality for bulk data downloads." },
]
const faqSchema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
@@ -45,47 +26,6 @@ const faqSchema = {
})),
}
function FAQItem({ faq, index }: { faq: typeof faqs[0]; index: number }) {
const [isOpen, setIsOpen] = useState(false)
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.05 }}
className="border-b border-neutral-200 dark:border-neutral-800"
>
<button
onClick={() => setIsOpen(!isOpen)}
className="w-full py-6 flex items-center justify-between text-left hover:text-brand-orange transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus-visible:ring-offset-2"
>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white pr-4">
{faq.question}
</h3>
<ChevronDownIcon
className={`w-5 h-5 text-neutral-500 shrink-0 transition-transform duration-300 ${
isOpen ? 'rotate-180' : ''
}`}
/>
</button>
{isOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
className="pb-6"
>
<p className="text-neutral-600 dark:text-neutral-400 leading-relaxed">
{faq.answer}
</p>
</motion.div>
)}
</motion.div>
)
}
export default function FAQPage() {
return (
<>
@@ -94,29 +34,9 @@ export default function FAQPage() {
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqSchema) }}
/>
<div className="container mx-auto px-4 py-16 max-w-4xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className="text-center mb-16"
>
<span className="badge-primary mb-4 inline-flex">FAQ</span>
<h1 className="text-4xl md:text-5xl font-bold text-neutral-900 dark:text-white mb-4">
Frequently asked questions
</h1>
<p className="text-lg text-neutral-600 dark:text-neutral-400 max-w-2xl mx-auto">
Learn more about how Pulse respects your privacy and handles your data.
</p>
</motion.div>
<div className="max-w-3xl mx-auto">
{faqs.map((faq, index) => (
<FAQItem key={faq.question} faq={faq} index={index} />
))}
</div>
<div className="pt-8 pb-16">
<PulseFAQ />
{/* * CTA */}
<motion.div
@@ -126,12 +46,12 @@ export default function FAQPage() {
transition={{ duration: 0.5, delay: 0.3 }}
className="text-center mt-12"
>
<p className="text-neutral-600 dark:text-neutral-400 mb-4">
<p className="text-neutral-400 mb-4">
Still have questions?
</p>
<a
href="mailto:support@ciphera.net"
className="inline-flex items-center justify-center gap-2 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 text-neutral-900 dark:text-white px-5 py-2.5 rounded-xl font-medium hover:bg-neutral-50 dark:hover:bg-neutral-800 shadow-sm hover:shadow-md dark:shadow-none transition-all duration-200"
className="inline-flex items-center justify-center gap-2 bg-neutral-900 border border-neutral-800 text-white px-5 py-2.5 rounded-xl font-medium hover:bg-neutral-800 transition-all duration-200"
>
Contact us
</a>

View File

@@ -31,7 +31,7 @@ export default function InstallationPage() {
<h2 className="text-2xl font-bold mb-8 text-white">Add the snippet</h2>
<p className="text-neutral-500 mb-8">Just add this snippet to your &lt;head&gt; tag in your layout or index file.</p>
<div className="max-w-2xl mx-auto bg-neutral-900 rounded-xl overflow-hidden shadow-2xl text-left border border-neutral-800">
<div className="max-w-2xl mx-auto bg-neutral-900/80 rounded-xl overflow-hidden shadow-2xl text-left border border-white/[0.08]">
<div className="flex items-center px-4 py-3 bg-neutral-800 border-b border-neutral-800">
<div className="flex gap-2">
<div className="w-3 h-3 rounded-full bg-red-500/20" />
@@ -53,6 +53,13 @@ export default function InstallationPage() {
<span className="text-blue-400">&gt;&lt;/script&gt;</span>
</code>
</div>
<div className="flex items-center gap-4 px-6 py-3 border-t border-neutral-800 text-xs text-neutral-500">
<span>1.6 KB gzipped</span>
<span className="flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-green-500" />
Non-blocking, async
</span>
</div>
</div>
</div>
@@ -61,7 +68,7 @@ export default function InstallationPage() {
<p className="text-neutral-500 mb-6 max-w-xl mx-auto">
Track custom events (e.g. signup, purchase) with <code className="px-1.5 py-0.5 rounded bg-neutral-700 text-sm font-mono">pulse.track(&apos;event_name&apos;)</code>. Use letters, numbers, and underscores only. Define goals in your site Settings Goals & Events to see counts in the dashboard.
</p>
<div className="max-w-2xl mx-auto bg-neutral-900 rounded-xl overflow-hidden shadow-2xl text-left border border-neutral-800">
<div className="max-w-2xl mx-auto bg-neutral-900/80 rounded-xl overflow-hidden shadow-2xl text-left border border-white/[0.08]">
<div className="flex items-center px-4 py-3 bg-neutral-800 border-b border-neutral-800">
<div className="flex gap-2">
<div className="w-3 h-3 rounded-full bg-red-500/20" />

View File

@@ -143,7 +143,7 @@ export default function IntegrationsPage() {
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search integrations..."
className="w-full pl-12 pr-16 py-3 bg-neutral-900/70 backdrop-blur-sm border border-neutral-800 rounded-xl text-white placeholder:text-neutral-400 focus:outline-none focus:ring-2 focus:ring-brand-orange/50 focus:border-brand-orange/50 transition-all"
className="w-full pl-12 pr-16 py-3 bg-neutral-900/70 backdrop-blur-sm border border-white/[0.08] rounded-xl text-white placeholder:text-neutral-400 focus:outline-none focus:ring-2 focus:ring-brand-orange/50 focus:border-brand-orange/50 transition-all"
/>
{query ? (
<button

View File

@@ -18,6 +18,7 @@ import { Cookie, ShieldCheck, Code, Lightning, ArrowRight, GithubLogo } from '@p
import FeatureSections from '@/components/marketing/FeatureSections'
import ComparisonCards from '@/components/marketing/ComparisonCards'
import CTASection from '@/components/marketing/CTASection'
import PulseFAQ from '@/components/marketing/PulseFAQ'
import { toast } from '@ciphera-net/ui'
import { getAuthErrorMessage } from '@ciphera-net/ui'
import { getSitesLimitForPlan } from '@/lib/plans'
@@ -221,6 +222,7 @@ export default function HomePage() {
<FeatureSections />
<ComparisonCards />
<PulseFAQ />
<CTASection />
</>
)