diff --git a/client/src/components/NameModal.css b/client/src/components/NameModal.css new file mode 100644 index 0000000..e4e7fe3 --- /dev/null +++ b/client/src/components/NameModal.css @@ -0,0 +1,100 @@ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + backdrop-filter: blur(4px); +} + +.modal { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 16px; + padding: 32px; + width: 360px; + max-width: 90vw; + text-align: center; + box-shadow: 0 20px 60px rgba(0,0,0,0.5); +} + +.modal h2 { font-size: 1.3rem; margin-bottom: 6px; color: var(--accent); } +.modal p { color: var(--text-muted); font-size: 0.85rem; margin-bottom: 20px; line-height: 1.5; } + +.modal form { display: flex; flex-direction: column; gap: 12px; } + +.modal input { + width: 100%; + padding: 10px 14px; + background: var(--surface2); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + font-size: 1rem; + outline: none; + transition: border-color 0.2s; +} +.modal input:focus { border-color: var(--accent); } + +.modal button { + width: 100%; + padding: 10px; + background: var(--accent); + color: var(--bg); + border: none; + border-radius: var(--radius-sm); + font-size: 0.9rem; + font-weight: 700; + transition: opacity 0.2s; +} +.modal button:hover:not(:disabled) { opacity: 0.85; } +.modal button:disabled { opacity: 0.4; cursor: not-allowed; } + +.name-error { + color: #f87171; + font-size: 0.8rem; + background: rgba(248, 113, 113, 0.1); + border: 1px solid rgba(248, 113, 113, 0.3); + border-radius: 6px; + padding: 6px 10px; + text-align: center; +} + +.pin-input { + letter-spacing: 0.3em !important; + font-size: 1.5rem !important; + text-align: center !important; + font-family: monospace !important; +} + +.back-btn { + background: transparent !important; + color: var(--text-muted) !important; + border: 1px solid var(--border) !important; + font-weight: 400 !important; + font-size: 0.8rem !important; +} + +@media (max-width: 640px) { + .modal-overlay { align-items: flex-end; justify-content: stretch; padding: 0; } + .modal { + width: 100%; + border-radius: 20px 20px 0 0; + padding: 12px 24px 32px; + max-height: 85vh; + overflow-y: auto; + box-shadow: 0 -8px 40px rgba(0,0,0,0.6); + animation: slideUp 0.3s ease-out; + } + @keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } } + .drag-handle { + width: 40px; height: 4px; + background: var(--border); + border-radius: 2px; + margin: 0 auto 14px; + } + .modal h2 { font-size: 1.15rem; } + .modal p { font-size: 0.8rem; margin-bottom: 14px; } +} diff --git a/client/src/components/NameModal.jsx b/client/src/components/NameModal.jsx new file mode 100644 index 0000000..040a8dc --- /dev/null +++ b/client/src/components/NameModal.jsx @@ -0,0 +1,107 @@ +import { useState, useEffect, useRef } from 'react' +import { GROOMSMEN, validateGroomsman } from '../groommen' +import './NameModal.css' + +const STORAGE_KEY_PIN = 'cabo_voter_pin' + +export default function NameModal({ onSubmit }) { + const [name, setName] = useState('') + const [pin, setPin] = useState('') + const [error, setError] = useState('') + const [step, setStep] = useState(1) // 1 = name, 2 = pin + const inputRef = useRef(null) + + useEffect(() => { + if (step === 1) inputRef.current?.focus() + }, [step]) + + const handleNameNext = (e) => { + e.preventDefault() + if (!name.trim()) return + + const key = name.trim().toLowerCase().replace(/\s+/g, '') + if (!GROOMSMEN[key]) { + setError(`"${name}" is not on the guest list. Check with the groom.`) + return + } + setError('') + setStep(2) + // Pre-fill pin from localStorage if saved + const saved = localStorage.getItem(STORAGE_KEY_PIN) + if (saved) setPin(saved) + } + + const handlePinSubmit = (e) => { + e.preventDefault() + if (pin.length !== 4) { + setError('Enter the last 4 digits of your phone number.') + return + } + + const key = name.trim().toLowerCase().replace(/\s+/g, '') + if (!validateGroomsman(name, pin)) { + setError('Wrong PIN. Make sure you\'re using the correct name + last 4 digits.') + return + } + + localStorage.setItem(STORAGE_KEY_PIN, pin) + onSubmit(name.trim()) + } + + const handleBack = () => { + setStep(1) + setPin('') + setError('') + } + + return ( +
Enter your full name as it appears on the guest list.
+ + > + ) : ( + <> +Enter the last 4 digits of your phone number, {name.split(' ')[0]}.
+ + > + )} +