Improve FAQ & video rendering, tighten homepage and CSS
This commit is contained in:
parent
05a1f316e1
commit
c69bb0d614
48 changed files with 4689 additions and 1077 deletions
|
|
@ -86,8 +86,13 @@ export default function PricingPage({
|
|||
if (currentTierIndex === 0) return;
|
||||
const container = scrollRef.current;
|
||||
const card = activeCardRef.current;
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const cardRect = card.getBoundingClientRect();
|
||||
const scrollLeft =
|
||||
card.offsetLeft - container.offsetLeft - (container.clientWidth - card.offsetWidth) / 2;
|
||||
container.scrollLeft +
|
||||
cardRect.left -
|
||||
containerRect.left -
|
||||
(container.clientWidth - card.offsetWidth) / 2;
|
||||
container.scrollLeft = Math.max(0, scrollLeft);
|
||||
setScrolledLeft(container.scrollLeft > 0);
|
||||
}, [pricing, currentTierIndex]);
|
||||
|
|
@ -191,7 +196,7 @@ export default function PricingPage({
|
|||
<p className="text-warm-200 font-semibold">{t('pricingPage.lessThanSurvey')}</p>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 max-w-5xl mx-auto px-6 pb-16">
|
||||
<div className="relative z-10 max-w-5xl mx-auto px-6 pb-8 md:pb-16">
|
||||
{/* Tier cards — full viewport width carousel */}
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-16">
|
||||
|
|
@ -199,7 +204,7 @@ export default function PricingPage({
|
|||
</div>
|
||||
) : pricing ? (
|
||||
<div
|
||||
className="relative mb-12"
|
||||
className="relative"
|
||||
style={{
|
||||
marginLeft: 'calc(-50vw + 50%)',
|
||||
marginRight: 'calc(-50vw + 50%)',
|
||||
|
|
@ -219,146 +224,151 @@ export default function PricingPage({
|
|||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={onScroll}
|
||||
className="flex justify-center gap-6 overflow-x-auto px-6 pb-4 scrollbar-hide"
|
||||
className="overflow-x-auto px-6 pb-4 scrollbar-hide"
|
||||
style={{ scrollbarWidth: 'none' }}
|
||||
>
|
||||
{pricing.tiers.map((tier, i) => {
|
||||
const isCurrent = i === currentTierIndex;
|
||||
const isFilled = tier.up_to !== null && pricing.licensed_count >= tier.up_to;
|
||||
const filledInTier = isCurrent
|
||||
? pricing.licensed_count - (i > 0 ? (pricing.tiers[i - 1].up_to ?? 0) : 0)
|
||||
: 0;
|
||||
const tierSlots = tier.slots;
|
||||
const fillPercent = isFilled
|
||||
? 100
|
||||
: isCurrent && tierSlots > 0
|
||||
? (filledInTier / tierSlots) * 100
|
||||
<div className="flex w-max gap-6 mx-auto">
|
||||
{pricing.tiers.map((tier, i) => {
|
||||
const isCurrent = i === currentTierIndex;
|
||||
const isFilled = tier.up_to !== null && pricing.licensed_count >= tier.up_to;
|
||||
const filledInTier = isCurrent
|
||||
? pricing.licensed_count - (i > 0 ? (pricing.tiers[i - 1].up_to ?? 0) : 0)
|
||||
: 0;
|
||||
const tierSlots = tier.slots;
|
||||
const fillPercent = isFilled
|
||||
? 100
|
||||
: isCurrent && tierSlots > 0
|
||||
? (filledInTier / tierSlots) * 100
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
ref={isCurrent ? activeCardRef : undefined}
|
||||
className={`relative flex flex-col rounded-2xl border overflow-hidden w-80 shrink-0 ${
|
||||
isCurrent
|
||||
? 'border-teal-400 ring-2 ring-teal-400 shadow-lg'
|
||||
: 'border-warm-700 shadow-md'
|
||||
} ${isFilled ? 'opacity-60' : ''}`}
|
||||
>
|
||||
{isCurrent && (
|
||||
<div className="bg-teal-600 text-white text-center text-xs font-semibold uppercase tracking-wide py-1.5">
|
||||
{t('pricingPage.currentTier')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`px-6 py-8 text-center ${
|
||||
key={i}
|
||||
ref={isCurrent ? activeCardRef : undefined}
|
||||
className={`relative flex flex-col rounded-2xl border overflow-hidden w-80 shrink-0 ${
|
||||
isCurrent
|
||||
? 'bg-gradient-to-br from-navy-950 to-teal-900'
|
||||
: 'bg-white dark:bg-warm-800'
|
||||
}`}
|
||||
? 'border-teal-400 ring-2 ring-teal-400 shadow-lg'
|
||||
: 'border-warm-700 shadow-md'
|
||||
} ${isFilled ? 'opacity-60' : ''}`}
|
||||
>
|
||||
<p
|
||||
className={`text-sm font-semibold uppercase tracking-wide mb-3 ${
|
||||
isCurrent ? 'text-teal-300' : 'text-warm-500 dark:text-warm-400'
|
||||
{isCurrent && (
|
||||
<div className="bg-teal-600 text-white text-center text-xs font-semibold uppercase tracking-wide py-1.5">
|
||||
{t('pricingPage.currentTier')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`px-6 py-8 text-center ${
|
||||
isCurrent
|
||||
? 'bg-gradient-to-br from-navy-950 to-teal-900'
|
||||
: 'bg-white dark:bg-warm-800'
|
||||
}`}
|
||||
>
|
||||
{i === 0
|
||||
? t('pricingPage.firstNUsers', { count: tier.slots })
|
||||
: tier.up_to === null
|
||||
? t('pricingPage.everyoneAfter')
|
||||
: t('pricingPage.nextNUsers', { count: tier.slots })}
|
||||
</p>
|
||||
<div className="flex items-baseline justify-center gap-1">
|
||||
<span
|
||||
className={`text-4xl font-extrabold ${
|
||||
isCurrent
|
||||
? 'text-white'
|
||||
: isFilled
|
||||
? 'text-warm-400 dark:text-warm-500 line-through'
|
||||
: 'text-navy-950 dark:text-warm-100'
|
||||
<p
|
||||
className={`text-sm font-semibold uppercase tracking-wide mb-3 ${
|
||||
isCurrent ? 'text-teal-300' : 'text-warm-500 dark:text-warm-400'
|
||||
}`}
|
||||
>
|
||||
{tier.price_pence === 0
|
||||
? t('upgrade.free')
|
||||
: formatPricePence(tier.price_pence)}
|
||||
</span>
|
||||
{tier.price_pence > 0 && (
|
||||
{i === 0
|
||||
? t('pricingPage.firstNUsers', { count: tier.slots })
|
||||
: tier.up_to === null
|
||||
? t('pricingPage.everyoneAfter')
|
||||
: t('pricingPage.nextNUsers', { count: tier.slots })}
|
||||
</p>
|
||||
<div className="flex items-baseline justify-center gap-1">
|
||||
<span
|
||||
className={`text-lg ${
|
||||
isCurrent ? 'text-warm-400' : 'text-warm-400 dark:text-warm-500'
|
||||
className={`text-4xl font-extrabold ${
|
||||
isCurrent
|
||||
? 'text-white'
|
||||
: isFilled
|
||||
? 'text-warm-400 dark:text-warm-500 line-through'
|
||||
: 'text-navy-950 dark:text-warm-100'
|
||||
}`}
|
||||
>
|
||||
{t('pricingPage.lifetime')}
|
||||
{tier.price_pence === 0
|
||||
? t('upgrade.free')
|
||||
: formatPricePence(tier.price_pence)}
|
||||
</span>
|
||||
{tier.price_pence > 0 && (
|
||||
<span
|
||||
className={`text-lg ${
|
||||
isCurrent ? 'text-warm-400' : 'text-warm-400 dark:text-warm-500'
|
||||
}`}
|
||||
>
|
||||
{t('pricingPage.lifetime')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isCurrent && spotsRemaining > 0 && (
|
||||
<p className="text-teal-300 text-sm mt-2 font-medium">
|
||||
{spotsRemaining === 1
|
||||
? t('pricingPage.spotsRemaining', { count: spotsRemaining })
|
||||
: t('pricingPage.spotsRemainingPlural', { count: spotsRemaining })}
|
||||
</p>
|
||||
)}
|
||||
{isFilled && (
|
||||
<p className="text-warm-400 dark:text-warm-500 text-sm mt-2 flex items-center justify-center gap-1">
|
||||
<CheckIcon className="w-4 h-4" /> {t('pricingPage.filled')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isCurrent && spotsRemaining > 0 && (
|
||||
<p className="text-teal-300 text-sm mt-2 font-medium">
|
||||
{spotsRemaining === 1
|
||||
? t('pricingPage.spotsRemaining', { count: spotsRemaining })
|
||||
: t('pricingPage.spotsRemainingPlural', { count: spotsRemaining })}
|
||||
</p>
|
||||
{/* Progress bar for current tier */}
|
||||
{isCurrent && tierSlots > 0 && (
|
||||
<div className="h-1.5 bg-warm-200 dark:bg-warm-700">
|
||||
<div
|
||||
className="h-full bg-teal-500"
|
||||
style={{ width: `${fillPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isFilled && (
|
||||
<p className="text-warm-400 dark:text-warm-500 text-sm mt-2 flex items-center justify-center gap-1">
|
||||
<CheckIcon className="w-4 h-4" /> {t('pricingPage.filled')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress bar for current tier */}
|
||||
{isCurrent && tierSlots > 0 && (
|
||||
<div className="h-1.5 bg-warm-200 dark:bg-warm-700">
|
||||
<div className="h-full bg-teal-500" style={{ width: `${fillPercent}%` }} />
|
||||
<div className="flex-1 flex flex-col px-6 py-6 bg-white dark:bg-warm-800">
|
||||
<ul className="space-y-3 mb-6 flex-1">
|
||||
{[
|
||||
t('pricingPage.feat1'),
|
||||
t('pricingPage.feat2'),
|
||||
t('pricingPage.feat3'),
|
||||
t('pricingPage.feat4'),
|
||||
t('pricingPage.feat5'),
|
||||
t('pricingPage.feat6'),
|
||||
].map((feat, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2.5 text-sm">
|
||||
<CheckIcon className="w-4 h-4 text-teal-600 dark:text-teal-400 shrink-0 mt-0.5" />
|
||||
<span className="text-warm-700 dark:text-warm-300">{feat}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{isCurrent ? (
|
||||
<>
|
||||
{ctaButton}
|
||||
{license.error && (
|
||||
<p className="mt-2 text-center text-sm text-red-600 dark:text-red-400">
|
||||
{license.error}
|
||||
</p>
|
||||
)}
|
||||
{isFree && (
|
||||
<p className="text-center text-xs text-warm-400 dark:text-warm-500 mt-2">
|
||||
{t('pricingPage.noCreditCard')}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : isFilled ? (
|
||||
<div className="mt-auto px-5 py-3 bg-warm-100 dark:bg-warm-700 text-warm-400 dark:text-warm-500 rounded-lg font-semibold text-center">
|
||||
{t('pricingPage.soldOut')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-auto px-5 py-3 bg-warm-100 dark:bg-warm-700 text-warm-500 dark:text-warm-400 rounded-lg font-semibold text-center">
|
||||
{t('pricingPage.upcoming')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 flex flex-col px-6 py-6 bg-white dark:bg-warm-800">
|
||||
<ul className="space-y-3 mb-6 flex-1">
|
||||
{[
|
||||
t('pricingPage.feat1'),
|
||||
t('pricingPage.feat2'),
|
||||
t('pricingPage.feat3'),
|
||||
t('pricingPage.feat4'),
|
||||
t('pricingPage.feat5'),
|
||||
t('pricingPage.feat6'),
|
||||
].map((feat, idx) => (
|
||||
<li key={idx} className="flex items-start gap-2.5 text-sm">
|
||||
<CheckIcon className="w-4 h-4 text-teal-600 dark:text-teal-400 shrink-0 mt-0.5" />
|
||||
<span className="text-warm-700 dark:text-warm-300">{feat}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{isCurrent ? (
|
||||
<>
|
||||
{ctaButton}
|
||||
{license.error && (
|
||||
<p className="mt-2 text-center text-sm text-red-600 dark:text-red-400">
|
||||
{license.error}
|
||||
</p>
|
||||
)}
|
||||
{isFree && (
|
||||
<p className="text-center text-xs text-warm-400 dark:text-warm-500 mt-2">
|
||||
{t('pricingPage.noCreditCard')}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : isFilled ? (
|
||||
<div className="mt-auto px-5 py-3 bg-warm-100 dark:bg-warm-700 text-warm-400 dark:text-warm-500 rounded-lg font-semibold text-center">
|
||||
{t('pricingPage.soldOut')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-auto px-5 py-3 bg-warm-100 dark:bg-warm-700 text-warm-500 dark:text-warm-400 rounded-lg font-semibold text-center">
|
||||
{t('pricingPage.upcoming')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue