Skip to content

Commit 996852a

Browse files
committed
chore: Additional pages
1 parent 2fb5883 commit 996852a

11 files changed

Lines changed: 2510 additions & 2 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {motion} from 'framer-motion';
2+
import React from 'react';
3+
4+
interface CodeBlockProps {
5+
code: string;
6+
language?: string;
7+
title?: string;
8+
}
9+
10+
/**
11+
* Reusable code block component with syntax highlighting styling
12+
*/
13+
export function CodeBlock({code, language = 'javascript', title}: CodeBlockProps) {
14+
return (
15+
<motion.div
16+
initial={{opacity: 0, y: 20}}
17+
whileInView={{opacity: 1, y: 0}}
18+
viewport={{once: true}}
19+
transition={{duration: 0.7, ease: [0.22, 1, 0.36, 1]}}
20+
className={'overflow-hidden rounded-xl border border-neutral-200 bg-neutral-50'}
21+
>
22+
{title && (
23+
<div className={'border-b border-neutral-200 bg-white px-6 py-3'}>
24+
<span className={'text-sm font-medium text-neutral-900'}>{title}</span>
25+
</div>
26+
)}
27+
<pre className={'overflow-x-auto p-6'}>
28+
<code className={'font-mono text-sm text-neutral-900'}>{code}</code>
29+
</pre>
30+
</motion.div>
31+
);
32+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import {Check, X} from 'lucide-react';
2+
import {motion} from 'framer-motion';
3+
import React from 'react';
4+
5+
export interface ComparisonRow {
6+
feature: string;
7+
plunk: boolean | string;
8+
competitor: boolean | string;
9+
}
10+
11+
interface ComparisonTableProps {
12+
competitorName: string;
13+
rows: ComparisonRow[];
14+
}
15+
16+
/**
17+
* Reusable comparison table component for competitor pages
18+
*/
19+
export function ComparisonTable({competitorName, rows}: ComparisonTableProps) {
20+
return (
21+
<div className={'overflow-hidden rounded-xl border border-neutral-200'}>
22+
{/* Header */}
23+
<div className={'grid grid-cols-3 gap-px bg-neutral-200'}>
24+
<div className={'bg-white p-6'}>
25+
<span className={'text-sm font-semibold text-neutral-900'}>Feature</span>
26+
</div>
27+
<div className={'bg-white p-6 text-center'}>
28+
<span className={'text-sm font-semibold text-neutral-900'}>Plunk</span>
29+
</div>
30+
<div className={'bg-white p-6 text-center'}>
31+
<span className={'text-sm font-semibold text-neutral-900'}>{competitorName}</span>
32+
</div>
33+
</div>
34+
35+
{/* Rows */}
36+
<div className={'grid gap-px bg-neutral-200'}>
37+
{rows.map((row, index) => (
38+
<motion.div
39+
key={row.feature}
40+
initial={{opacity: 0, y: 10}}
41+
whileInView={{opacity: 1, y: 0}}
42+
viewport={{once: true}}
43+
transition={{duration: 0.5, delay: index * 0.05, ease: [0.22, 1, 0.36, 1]}}
44+
className={'grid grid-cols-3 gap-px bg-neutral-200'}
45+
>
46+
<div className={'bg-white p-6'}>
47+
<span className={'text-sm text-neutral-600'}>{row.feature}</span>
48+
</div>
49+
<div className={'bg-white p-6'}>
50+
<div className={'flex justify-center'}>
51+
{typeof row.plunk === 'boolean' ? (
52+
row.plunk ? (
53+
<Check className="h-5 w-5 text-neutral-900" strokeWidth={2} />
54+
) : (
55+
<X className="h-5 w-5 text-neutral-400" strokeWidth={2} />
56+
)
57+
) : (
58+
<span className={'text-sm text-neutral-900'}>{row.plunk}</span>
59+
)}
60+
</div>
61+
</div>
62+
<div className={'bg-white p-6'}>
63+
<div className={'flex justify-center'}>
64+
{typeof row.competitor === 'boolean' ? (
65+
row.competitor ? (
66+
<Check className="h-5 w-5 text-neutral-900" strokeWidth={2} />
67+
) : (
68+
<X className="h-5 w-5 text-neutral-400" strokeWidth={2} />
69+
)
70+
) : (
71+
<span className={'text-sm text-neutral-900'}>{row.competitor}</span>
72+
)}
73+
</div>
74+
</div>
75+
</motion.div>
76+
))}
77+
</div>
78+
</div>
79+
);
80+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {motion} from 'framer-motion';
2+
import Script from 'next/script';
3+
import React from 'react';
4+
5+
export interface FAQ {
6+
question: string;
7+
answer: string;
8+
}
9+
10+
interface FAQSectionProps {
11+
faqs: FAQ[];
12+
schemaId?: string;
13+
}
14+
15+
/**
16+
* Reusable FAQ section component with structured data support
17+
*/
18+
export function FAQSection({faqs, schemaId = 'faq-schema'}: FAQSectionProps) {
19+
return (
20+
<>
21+
<Script
22+
id={schemaId}
23+
type="application/ld+json"
24+
dangerouslySetInnerHTML={{
25+
__html: JSON.stringify({
26+
'@context': 'https://schema.org',
27+
'@type': 'FAQPage',
28+
'mainEntity': faqs.map((faq) => ({
29+
'@type': 'Question',
30+
'name': faq.question,
31+
'acceptedAnswer': {
32+
'@type': 'Answer',
33+
'text': faq.answer,
34+
},
35+
})),
36+
}),
37+
}}
38+
/>
39+
40+
<section className={'py-32'}>
41+
<motion.div
42+
initial={{opacity: 0, y: 20}}
43+
whileInView={{opacity: 1, y: 0}}
44+
viewport={{once: true}}
45+
transition={{duration: 0.7, ease: [0.22, 1, 0.36, 1]}}
46+
className={'mx-auto max-w-4xl'}
47+
>
48+
<h2 className={'mb-16 text-center text-5xl font-bold tracking-tight text-neutral-900'}>
49+
Frequently asked questions
50+
</h2>
51+
52+
<div className={'space-y-8'}>
53+
{faqs.map((faq, index) => (
54+
<motion.div
55+
key={index}
56+
initial={{opacity: 0, y: 20}}
57+
whileInView={{opacity: 1, y: 0}}
58+
viewport={{once: true}}
59+
transition={{duration: 0.5, delay: index * 0.1, ease: [0.22, 1, 0.36, 1]}}
60+
className={'border-b border-neutral-200 pb-8 last:border-b-0'}
61+
>
62+
<h3 className={'text-xl font-semibold text-neutral-900'}>{faq.question}</h3>
63+
<p className={'mt-4 leading-relaxed text-neutral-600'}>{faq.answer}</p>
64+
</motion.div>
65+
))}
66+
</div>
67+
</motion.div>
68+
</section>
69+
</>
70+
);
71+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import {motion} from 'framer-motion';
2+
import React, {useState} from 'react';
3+
4+
interface PricingData {
5+
name: string;
6+
calculatePrice: (emails: number) => number;
7+
color?: string;
8+
}
9+
10+
interface PricingCalculatorProps {
11+
competitors: PricingData[];
12+
defaultVolume?: number;
13+
}
14+
15+
const volumeOptions = [
16+
{label: '10K emails/month', value: 10000},
17+
{label: '100K emails/month', value: 100000},
18+
{label: '500K emails/month', value: 500000},
19+
{label: '1M emails/month', value: 1000000},
20+
];
21+
22+
/**
23+
* Interactive pricing calculator comparing Plunk with competitors
24+
*/
25+
export function PricingCalculator({competitors, defaultVolume = 100000}: PricingCalculatorProps) {
26+
const [volume, setVolume] = useState(defaultVolume);
27+
28+
const plunkPrice = volume * 0.001;
29+
30+
return (
31+
<motion.div
32+
initial={{opacity: 0, y: 20}}
33+
whileInView={{opacity: 1, y: 0}}
34+
viewport={{once: true}}
35+
transition={{duration: 0.7, ease: [0.22, 1, 0.36, 1]}}
36+
className={'overflow-hidden rounded-xl border border-neutral-200 bg-white'}
37+
>
38+
{/* Header */}
39+
<div className={'border-b border-neutral-200 bg-neutral-50 p-8'}>
40+
<h3 className={'text-2xl font-bold text-neutral-900'}>Pricing Calculator</h3>
41+
<p className={'mt-2 text-sm text-neutral-600'}>
42+
Compare costs across platforms at different email volumes
43+
</p>
44+
</div>
45+
46+
{/* Volume Selector */}
47+
<div className={'p-8'}>
48+
<label className={'block text-sm font-semibold text-neutral-900'}>Monthly Email Volume</label>
49+
<div className={'mt-4 grid grid-cols-2 gap-3 sm:grid-cols-4'}>
50+
{volumeOptions.map((option) => (
51+
<button
52+
key={option.value}
53+
onClick={() => setVolume(option.value)}
54+
className={`rounded-lg border px-4 py-3 text-sm font-medium transition ${
55+
volume === option.value
56+
? 'border-neutral-900 bg-neutral-900 text-white'
57+
: 'border-neutral-300 bg-white text-neutral-900 hover:border-neutral-400'
58+
}`}
59+
>
60+
{option.label}
61+
</button>
62+
))}
63+
</div>
64+
</div>
65+
66+
{/* Results */}
67+
<div className={'space-y-4 p-8 pt-0'}>
68+
{/* Plunk */}
69+
<div className={'rounded-lg border border-neutral-900 bg-neutral-50 p-6'}>
70+
<div className={'flex items-center justify-between'}>
71+
<div>
72+
<span className={'text-lg font-bold text-neutral-900'}>Plunk</span>
73+
<span className={'ml-3 rounded-full bg-neutral-900 px-3 py-1 text-xs font-medium text-white'}>
74+
Cheapest
75+
</span>
76+
</div>
77+
<div className={'text-right'}>
78+
<div className={'text-3xl font-bold text-neutral-900'}>${plunkPrice.toFixed(2)}</div>
79+
<div className={'text-sm text-neutral-600'}>per month</div>
80+
</div>
81+
</div>
82+
</div>
83+
84+
{/* Competitors */}
85+
{competitors.map((competitor, index) => {
86+
const price = competitor.calculatePrice(volume);
87+
const savings = ((price - plunkPrice) / price) * 100;
88+
89+
return (
90+
<div key={competitor.name} className={'rounded-lg border border-neutral-200 bg-white p-6'}>
91+
<div className={'flex items-center justify-between'}>
92+
<div>
93+
<span className={'text-lg font-semibold text-neutral-900'}>{competitor.name}</span>
94+
</div>
95+
<div className={'text-right'}>
96+
<div className={'text-3xl font-bold text-neutral-900'}>${price.toFixed(2)}</div>
97+
<div className={'text-sm text-neutral-600'}>per month</div>
98+
</div>
99+
</div>
100+
{savings > 0 && (
101+
<div className={'mt-3 text-sm text-neutral-600'}>
102+
Save <span className={'font-semibold text-neutral-900'}>{savings.toFixed(0)}%</span> with Plunk
103+
</div>
104+
)}
105+
</div>
106+
);
107+
})}
108+
</div>
109+
</motion.div>
110+
);
111+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
export * from './Navbar';
22
export * from './Footer';
3+
export * from './ComparisonTable';
4+
export * from './FAQSection';
5+
export * from './CodeBlock';
6+
export * from './PricingCalculator';

0 commit comments

Comments
 (0)