forked from hazem/yslootahrobotics
183 lines
6.3 KiB
TypeScript
183 lines
6.3 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useCallback } from 'react';
|
|
import { orderStore, type PaymentInfo } from '@/store/useOrderStore';
|
|
|
|
const inputStyle: React.CSSProperties = {
|
|
width: '100%',
|
|
padding: '0.6rem 0.75rem',
|
|
borderRadius: '0.375rem',
|
|
border: '1px solid rgba(0, 0, 0, 0.08)',
|
|
background: 'rgba(255, 255, 255, 0.8)',
|
|
color: '#1a1a2e',
|
|
fontSize: '0.8rem',
|
|
outline: 'none',
|
|
transition: 'border-color 0.2s ease',
|
|
};
|
|
|
|
const labelStyle: React.CSSProperties = {
|
|
fontSize: '0.7rem',
|
|
fontWeight: 500,
|
|
color: '#94a3b8',
|
|
marginBottom: '0.3rem',
|
|
display: 'block',
|
|
};
|
|
|
|
const errorStyle: React.CSSProperties = {
|
|
fontSize: '0.65rem',
|
|
color: '#ef4444',
|
|
marginTop: '0.2rem',
|
|
};
|
|
|
|
interface FormErrors {
|
|
[key: string]: string;
|
|
}
|
|
|
|
function formatCardNumber(value: string): string {
|
|
const digits = value.replace(/\D/g, '').slice(0, 16);
|
|
return digits.replace(/(\d{4})(?=\d)/g, '$1 ');
|
|
}
|
|
|
|
function formatExpiry(value: string): string {
|
|
const digits = value.replace(/\D/g, '').slice(0, 4);
|
|
if (digits.length > 2) {
|
|
return `${digits.slice(0, 2)}/${digits.slice(2)}`;
|
|
}
|
|
return digits;
|
|
}
|
|
|
|
export function PaymentStep() {
|
|
const [form, setForm] = useState<PaymentInfo>({
|
|
cardNumber: '',
|
|
expiry: '',
|
|
cvv: '',
|
|
nameOnCard: '',
|
|
});
|
|
const [errors, setErrors] = useState<FormErrors>({});
|
|
|
|
const handleChange = useCallback((field: keyof PaymentInfo, value: string) => {
|
|
let processed = value;
|
|
if (field === 'cardNumber') processed = formatCardNumber(value);
|
|
if (field === 'expiry') processed = formatExpiry(value);
|
|
if (field === 'cvv') processed = value.replace(/\D/g, '').slice(0, 3);
|
|
|
|
setForm((prev) => ({ ...prev, [field]: processed }));
|
|
setErrors((prev) => {
|
|
const next = { ...prev };
|
|
delete next[field];
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
const validate = (): boolean => {
|
|
const errs: FormErrors = {};
|
|
const digits = form.cardNumber.replace(/\s/g, '');
|
|
if (digits.length < 16) errs.cardNumber = 'Enter a valid 16-digit card number';
|
|
if (form.expiry.length < 5) errs.expiry = 'Enter a valid expiry (MM/YY)';
|
|
if (form.cvv.length < 3) errs.cvv = 'Enter a valid 3-digit CVV';
|
|
if (!form.nameOnCard.trim()) errs.nameOnCard = 'Name on card is required';
|
|
setErrors(errs);
|
|
return Object.keys(errs).length === 0;
|
|
};
|
|
|
|
const handleSubmit = () => {
|
|
if (!validate()) return;
|
|
orderStore.getState().setPayment(form);
|
|
orderStore.getState().setStep('review');
|
|
};
|
|
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
|
<h3 style={{ fontSize: '1rem', fontWeight: 600, color: '#1a1a2e', margin: 0 }}>
|
|
Payment Details
|
|
</h3>
|
|
|
|
<div style={{
|
|
padding: '0.6rem 0.75rem',
|
|
borderRadius: '0.375rem',
|
|
background: 'rgba(245, 158, 11, 0.06)',
|
|
border: '1px solid rgba(245, 158, 11, 0.15)',
|
|
fontSize: '0.7rem',
|
|
color: '#d97706',
|
|
}}>
|
|
This is a demo checkout. No real payment will be processed.
|
|
</div>
|
|
|
|
<div>
|
|
<label style={labelStyle}>Card Number</label>
|
|
<input
|
|
type="text"
|
|
value={form.cardNumber}
|
|
onChange={(e) => handleChange('cardNumber', e.target.value)}
|
|
placeholder="4242 4242 4242 4242"
|
|
style={{ ...inputStyle, fontFamily: 'monospace', letterSpacing: '0.1em', borderColor: errors.cardNumber ? 'rgba(239, 68, 68, 0.4)' : 'rgba(0, 0, 0, 0.08)' }}
|
|
onFocus={(e) => { e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)'; }}
|
|
onBlur={(e) => { e.currentTarget.style.borderColor = errors.cardNumber ? 'rgba(239, 68, 68, 0.4)' : 'rgba(0, 0, 0, 0.08)'; }}
|
|
/>
|
|
{errors.cardNumber && <div style={errorStyle}>{errors.cardNumber}</div>}
|
|
</div>
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.75rem' }}>
|
|
<div>
|
|
<label style={labelStyle}>Expiry Date</label>
|
|
<input
|
|
type="text"
|
|
value={form.expiry}
|
|
onChange={(e) => handleChange('expiry', e.target.value)}
|
|
placeholder="MM/YY"
|
|
style={{ ...inputStyle, fontFamily: 'monospace', borderColor: errors.expiry ? 'rgba(239, 68, 68, 0.4)' : 'rgba(0, 0, 0, 0.08)' }}
|
|
onFocus={(e) => { e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)'; }}
|
|
onBlur={(e) => { e.currentTarget.style.borderColor = errors.expiry ? 'rgba(239, 68, 68, 0.4)' : 'rgba(0, 0, 0, 0.08)'; }}
|
|
/>
|
|
{errors.expiry && <div style={errorStyle}>{errors.expiry}</div>}
|
|
</div>
|
|
<div>
|
|
<label style={labelStyle}>CVV</label>
|
|
<input
|
|
type="text"
|
|
value={form.cvv}
|
|
onChange={(e) => handleChange('cvv', e.target.value)}
|
|
placeholder="123"
|
|
style={{ ...inputStyle, fontFamily: 'monospace', borderColor: errors.cvv ? 'rgba(239, 68, 68, 0.4)' : 'rgba(0, 0, 0, 0.08)' }}
|
|
onFocus={(e) => { e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)'; }}
|
|
onBlur={(e) => { e.currentTarget.style.borderColor = errors.cvv ? 'rgba(239, 68, 68, 0.4)' : 'rgba(0, 0, 0, 0.08)'; }}
|
|
/>
|
|
{errors.cvv && <div style={errorStyle}>{errors.cvv}</div>}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={labelStyle}>Name on Card</label>
|
|
<input
|
|
type="text"
|
|
value={form.nameOnCard}
|
|
onChange={(e) => handleChange('nameOnCard', e.target.value)}
|
|
placeholder="John Doe"
|
|
style={{ ...inputStyle, borderColor: errors.nameOnCard ? 'rgba(239, 68, 68, 0.4)' : 'rgba(0, 0, 0, 0.08)' }}
|
|
onFocus={(e) => { e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)'; }}
|
|
onBlur={(e) => { e.currentTarget.style.borderColor = errors.nameOnCard ? 'rgba(239, 68, 68, 0.4)' : 'rgba(0, 0, 0, 0.08)'; }}
|
|
/>
|
|
{errors.nameOnCard && <div style={errorStyle}>{errors.nameOnCard}</div>}
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleSubmit}
|
|
style={{
|
|
marginTop: '0.5rem',
|
|
padding: '0.75rem',
|
|
borderRadius: '0.375rem',
|
|
border: '1px solid rgba(59, 130, 246, 0.5)',
|
|
background: 'rgba(59, 130, 246, 0.08)',
|
|
color: '#2563eb',
|
|
cursor: 'pointer',
|
|
fontSize: '0.85rem',
|
|
fontWeight: 600,
|
|
transition: 'all 0.2s ease',
|
|
}}
|
|
>
|
|
Review Order
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|