fix: hide mollie spinners, add placeholders, errors only on submit, sliding interval toggle
This commit is contained in:
@@ -17,8 +17,8 @@ interface PaymentFormProps {
|
|||||||
|
|
||||||
const inputClass =
|
const inputClass =
|
||||||
'w-full rounded-lg border border-neutral-700 bg-neutral-800/50 px-3 py-2.5 text-sm text-white placeholder:text-neutral-500 focus:outline-none focus:ring-1 focus:ring-brand-orange focus:border-brand-orange transition-colors'
|
'w-full rounded-lg border border-neutral-700 bg-neutral-800/50 px-3 py-2.5 text-sm text-white placeholder:text-neutral-500 focus:outline-none focus:ring-1 focus:ring-brand-orange focus:border-brand-orange transition-colors'
|
||||||
const mollieFieldClass =
|
const mollieFieldBase =
|
||||||
'w-full rounded-lg border border-neutral-700 bg-neutral-800/50 px-3 py-2.5 h-[42px] transition-colors focus-within:ring-1 focus-within:ring-brand-orange focus-within:border-brand-orange'
|
'w-full rounded-lg border border-neutral-700 bg-neutral-800/50 px-3 py-2.5 h-[42px] transition-all focus-within:ring-1 focus-within:ring-brand-orange focus-within:border-brand-orange'
|
||||||
|
|
||||||
export default function PaymentForm({ plan, interval, limit }: PaymentFormProps) {
|
export default function PaymentForm({ plan, interval, limit }: PaymentFormProps) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -31,7 +31,7 @@ export default function PaymentForm({ plan, interval, limit }: PaymentFormProps)
|
|||||||
const [mollieError, setMollieError] = useState(false)
|
const [mollieError, setMollieError] = useState(false)
|
||||||
const [formError, setFormError] = useState<string | null>(null)
|
const [formError, setFormError] = useState<string | null>(null)
|
||||||
const [cardErrors, setCardErrors] = useState<Record<string, string>>({})
|
const [cardErrors, setCardErrors] = useState<Record<string, string>>({})
|
||||||
const [touchedFields, setTouchedFields] = useState<Set<string>>(new Set())
|
const [submitted, setSubmitted] = useState(false)
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
|
||||||
const componentsRef = useRef<Record<string, MollieComponent | null>>({
|
const componentsRef = useRef<Record<string, MollieComponent | null>>({
|
||||||
@@ -56,26 +56,25 @@ export default function PaymentForm({ plan, interval, limit }: PaymentFormProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fields: Array<{ type: string; selector: string }> = [
|
const fields: Array<{ type: string; selector: string; placeholder?: string }> = [
|
||||||
{ type: 'cardHolder', selector: '#mollie-card-holder' },
|
{ type: 'cardHolder', selector: '#mollie-card-holder', placeholder: 'John Doe' },
|
||||||
{ type: 'cardNumber', selector: '#mollie-card-number' },
|
{ type: 'cardNumber', selector: '#mollie-card-number', placeholder: '1234 5678 9012 3456' },
|
||||||
{ type: 'expiryDate', selector: '#mollie-card-expiry' },
|
{ type: 'expiryDate', selector: '#mollie-card-expiry', placeholder: 'MM / YY' },
|
||||||
{ type: 'verificationCode', selector: '#mollie-card-cvc' },
|
{ type: 'verificationCode', selector: '#mollie-card-cvc', placeholder: 'CVC' },
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const { type, selector } of fields) {
|
for (const { type, selector, placeholder } of fields) {
|
||||||
const el = document.querySelector(selector) as HTMLElement | null
|
const el = document.querySelector(selector) as HTMLElement | null
|
||||||
if (!el) {
|
if (!el) {
|
||||||
setMollieError(true)
|
setMollieError(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const component = mollie.createComponent(type, { styles: MOLLIE_FIELD_STYLES })
|
const opts: Record<string, unknown> = { styles: MOLLIE_FIELD_STYLES }
|
||||||
|
if (placeholder) opts.placeholder = placeholder
|
||||||
|
const component = mollie.createComponent(type, opts)
|
||||||
component.mount(el)
|
component.mount(el)
|
||||||
component.addEventListener('change', (event: unknown) => {
|
component.addEventListener('change', (event: unknown) => {
|
||||||
const e = event as { error?: string; touched?: boolean }
|
const e = event as { error?: string }
|
||||||
if (e.touched) {
|
|
||||||
setTouchedFields((prev) => new Set(prev).add(type))
|
|
||||||
}
|
|
||||||
setCardErrors((prev) => {
|
setCardErrors((prev) => {
|
||||||
const next = { ...prev }
|
const next = { ...prev }
|
||||||
if (e.error) next[type] = e.error
|
if (e.error) next[type] = e.error
|
||||||
@@ -104,6 +103,7 @@ export default function PaymentForm({ plan, interval, limit }: PaymentFormProps)
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
setSubmitted(true)
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setFormError(null)
|
setFormError(null)
|
||||||
if (!country) {
|
if (!country) {
|
||||||
@@ -162,8 +162,8 @@ export default function PaymentForm({ plan, interval, limit }: PaymentFormProps)
|
|||||||
{/* Cardholder name */}
|
{/* Cardholder name */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="block text-sm font-medium text-neutral-300 mb-1.5">Cardholder name</label>
|
<label className="block text-sm font-medium text-neutral-300 mb-1.5">Cardholder name</label>
|
||||||
<div id="mollie-card-holder" className={mollieFieldClass} />
|
<div id="mollie-card-holder" className={`${mollieFieldBase} ${mollieReady ? 'opacity-100' : 'opacity-0'}`} />
|
||||||
{touchedFields.has('cardHolder') && cardErrors.cardHolder && (
|
{submitted && cardErrors.cardHolder && (
|
||||||
<p className="mt-1 text-xs text-red-400">{cardErrors.cardHolder}</p>
|
<p className="mt-1 text-xs text-red-400">{cardErrors.cardHolder}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -171,8 +171,8 @@ export default function PaymentForm({ plan, interval, limit }: PaymentFormProps)
|
|||||||
{/* Card number */}
|
{/* Card number */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label className="block text-sm font-medium text-neutral-300 mb-1.5">Card number</label>
|
<label className="block text-sm font-medium text-neutral-300 mb-1.5">Card number</label>
|
||||||
<div id="mollie-card-number" className={mollieFieldClass} />
|
<div id="mollie-card-number" className={`${mollieFieldBase} ${mollieReady ? 'opacity-100' : 'opacity-0'}`} />
|
||||||
{touchedFields.has('cardNumber') && cardErrors.cardNumber && (
|
{submitted && cardErrors.cardNumber && (
|
||||||
<p className="mt-1 text-xs text-red-400">{cardErrors.cardNumber}</p>
|
<p className="mt-1 text-xs text-red-400">{cardErrors.cardNumber}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -181,15 +181,15 @@ export default function PaymentForm({ plan, interval, limit }: PaymentFormProps)
|
|||||||
<div className="mb-4 grid grid-cols-2 gap-4">
|
<div className="mb-4 grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-neutral-300 mb-1.5">Expiry date</label>
|
<label className="block text-sm font-medium text-neutral-300 mb-1.5">Expiry date</label>
|
||||||
<div id="mollie-card-expiry" className={mollieFieldClass} />
|
<div id="mollie-card-expiry" className={`${mollieFieldBase} ${mollieReady ? 'opacity-100' : 'opacity-0'}`} />
|
||||||
{touchedFields.has('expiryDate') && cardErrors.expiryDate && (
|
{submitted && cardErrors.expiryDate && (
|
||||||
<p className="mt-1 text-xs text-red-400">{cardErrors.expiryDate}</p>
|
<p className="mt-1 text-xs text-red-400">{cardErrors.expiryDate}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-neutral-300 mb-1.5">CVC</label>
|
<label className="block text-sm font-medium text-neutral-300 mb-1.5">CVC</label>
|
||||||
<div id="mollie-card-cvc" className={mollieFieldClass} />
|
<div id="mollie-card-cvc" className={`${mollieFieldBase} ${mollieReady ? 'opacity-100' : 'opacity-0'}`} />
|
||||||
{touchedFields.has('verificationCode') && cardErrors.verificationCode && (
|
{submitted && cardErrors.verificationCode && (
|
||||||
<p className="mt-1 text-xs text-red-400">{cardErrors.verificationCode}</p>
|
<p className="mt-1 text-xs text-red-400">{cardErrors.verificationCode}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
import { Check } from '@phosphor-icons/react'
|
import { Check } from '@phosphor-icons/react'
|
||||||
import {
|
import {
|
||||||
TRAFFIC_TIERS,
|
TRAFFIC_TIERS,
|
||||||
@@ -83,29 +84,26 @@ export default function PlanSummary({ plan, interval, limit }: PlanSummaryProps)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Interval toggle */}
|
{/* Interval toggle */}
|
||||||
<div className="mb-6 flex rounded-lg bg-neutral-800/60 p-1">
|
<div className="mb-6 flex items-center gap-1 p-1 bg-neutral-800/50 rounded-xl">
|
||||||
<button
|
{(['month', 'year'] as const).map((iv) => (
|
||||||
type="button"
|
<button
|
||||||
onClick={() => handleIntervalToggle('month')}
|
key={iv}
|
||||||
className={`flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors ${
|
type="button"
|
||||||
currentInterval === 'month'
|
onClick={() => handleIntervalToggle(iv)}
|
||||||
? 'bg-neutral-700 text-white shadow-sm'
|
className={`relative flex-1 px-4 py-2 text-sm font-medium rounded-lg transition-colors duration-200 ${
|
||||||
: 'text-neutral-400 hover:text-neutral-200'
|
currentInterval === iv ? 'text-white' : 'text-neutral-400 hover:text-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Monthly
|
{currentInterval === iv && (
|
||||||
</button>
|
<motion.div
|
||||||
<button
|
layoutId="checkout-interval-bg"
|
||||||
type="button"
|
className="absolute inset-0 bg-neutral-700 rounded-lg shadow-sm"
|
||||||
onClick={() => handleIntervalToggle('year')}
|
transition={{ type: 'spring', bounce: 0.15, duration: 0.35 }}
|
||||||
className={`flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors ${
|
/>
|
||||||
currentInterval === 'year'
|
)}
|
||||||
? 'bg-neutral-700 text-white shadow-sm'
|
<span className="relative z-10">{iv === 'month' ? 'Monthly' : 'Yearly'}</span>
|
||||||
: 'text-neutral-400 hover:text-neutral-200'
|
</button>
|
||||||
}`}
|
))}
|
||||||
>
|
|
||||||
Yearly
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
|
|||||||
Reference in New Issue
Block a user