Created
September 27, 2025 00:35
-
-
Save SeeThruHead/af97d1588bb42918ef8f1286d62ec4c8 to your computer and use it in GitHub Desktop.
clauded wizard
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState, useEffect, useRef, createContext, useContext } from 'react'; | |
// Wizard Context | |
const WizardContext = createContext(); | |
// Wizard Root Component | |
const WizardRoot = ({ children, onPageChange }) => { | |
const [currentPage, setCurrentPage] = useState(0); | |
const [totalPages, setTotalPages] = useState(0); | |
const formRef = useRef(null); | |
const pageRefs = useRef(new Map()); // Store page refs by pageIndex | |
const registerPageRef = (pageIndex, ref) => { | |
pageRefs.current.set(pageIndex, ref); | |
}; | |
const unregisterPageRef = (pageIndex) => { | |
pageRefs.current.delete(pageIndex); | |
}; | |
// Track focus changes to determine current page | |
useEffect(() => { | |
const handleFocusChange = (e) => { | |
if (!formRef.current || !formRef.current.contains(e.target)) return; | |
// Check which page contains the focused element using refs | |
for (const [pageIndex, pageRef] of pageRefs.current.entries()) { | |
if (pageRef.current && pageRef.current.contains(e.target)) { | |
if (pageIndex !== currentPage) { | |
setCurrentPage(pageIndex); | |
if (onPageChange) { | |
onPageChange(pageIndex); | |
} | |
} | |
break; | |
} | |
} | |
}; | |
document.addEventListener('focusin', handleFocusChange); | |
return () => document.removeEventListener('focusin', handleFocusChange); | |
}, [currentPage, onPageChange]); | |
const goToPage = (pageIndex) => { | |
if (pageIndex >= 0 && pageIndex < totalPages) { | |
setCurrentPage(pageIndex); | |
if (onPageChange) { | |
onPageChange(pageIndex); | |
} | |
setTimeout(() => { | |
const pageRef = pageRefs.current.get(pageIndex); | |
if (pageRef && pageRef.current) { | |
const firstInput = pageRef.current.querySelector('input, textarea, select, button'); | |
if (firstInput) { | |
firstInput.focus(); | |
} | |
} | |
}, 50); | |
} | |
}; | |
const goToNextPage = () => { | |
goToPage(currentPage + 1); | |
}; | |
const goToPrevPage = () => { | |
if (currentPage > 0) { | |
const prevPage = currentPage - 1; | |
setCurrentPage(prevPage); | |
if (onPageChange) { | |
onPageChange(prevPage); | |
} | |
setTimeout(() => { | |
const pageRef = pageRefs.current.get(prevPage); | |
if (pageRef && pageRef.current) { | |
const inputs = pageRef.current.querySelectorAll('input, textarea, select'); | |
if (inputs.length > 0) { | |
const lastInput = inputs[inputs.length - 1]; | |
lastInput.focus(); | |
} | |
} | |
}, 50); | |
} | |
}; | |
return ( | |
<WizardContext.Provider value={{ | |
currentPage, | |
setCurrentPage, | |
totalPages, | |
setTotalPages, | |
goToNextPage, | |
goToPrevPage, | |
goToPage, | |
formRef, | |
registerPageRef, | |
unregisterPageRef | |
}}> | |
<div ref={formRef}> | |
{children} | |
</div> | |
</WizardContext.Provider> | |
); | |
}; | |
// Wizard Page Component | |
const WizardPage = ({ children, title, pageIndex }) => { | |
const { currentPage, setTotalPages, registerPageRef, unregisterPageRef } = useContext(WizardContext); | |
const isCurrent = currentPage === pageIndex; | |
const pageRef = useRef(null); | |
// Register this page with the wizard | |
useEffect(() => { | |
setTotalPages(prev => Math.max(prev, pageIndex + 1)); | |
registerPageRef(pageIndex, pageRef); | |
return () => { | |
unregisterPageRef(pageIndex); | |
}; | |
}, [pageIndex, setTotalPages, registerPageRef, unregisterPageRef]); | |
// Render all pages in DOM but only show current one | |
return ( | |
<div | |
ref={pageRef} | |
data-wizard-page={pageIndex} | |
style={{ | |
opacity: isCurrent ? 1 : 0, | |
pointerEvents: isCurrent ? 'auto' : 'none', | |
position: isCurrent ? 'static' : 'absolute', | |
top: isCurrent ? 'auto' : 0, | |
left: isCurrent ? 'auto' : 0, | |
width: isCurrent ? 'auto' : '100%', | |
zIndex: isCurrent ? 1 : -1 | |
}} | |
> | |
{title && ( | |
<h2 className="text-2xl font-bold text-gray-900 mb-6">{title}</h2> | |
)} | |
{children} | |
</div> | |
); | |
}; | |
// Wizard Navigation Component | |
const WizardNavigation = ({ className = "", onSubmit }) => { | |
const { currentPage, totalPages, goToNextPage, goToPrevPage } = useContext(WizardContext); | |
return ( | |
<div className={`flex justify-between ${className}`}> | |
<button | |
type="button" | |
onClick={goToPrevPage} | |
disabled={currentPage === 0} | |
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" | |
> | |
Previous | |
</button> | |
<span className="flex items-center text-sm text-gray-500"> | |
Page {currentPage + 1} of {totalPages} | |
</span> | |
{currentPage < totalPages - 1 ? ( | |
<button | |
type="button" | |
onClick={goToNextPage} | |
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700" | |
> | |
Next | |
</button> | |
) : ( | |
<button | |
type="button" | |
onClick={() => { | |
if (onSubmit) { | |
const submitEvent = new Event('submit', { bubbles: true, cancelable: true }); | |
onSubmit(submitEvent); | |
} | |
}} | |
className="px-4 py-2 text-sm font-medium text-white bg-green-600 border border-transparent rounded-md hover:bg-green-700" | |
> | |
Submit | |
</button> | |
)} | |
</div> | |
); | |
}; | |
// Wizard Progress Component | |
const WizardProgress = ({ steps, className = "" }) => { | |
const { currentPage, totalPages } = useContext(WizardContext); | |
return ( | |
<div className={className}> | |
<div className="flex items-center justify-between mb-4"> | |
{steps.map((step, index) => ( | |
<div key={index} className="flex items-center"> | |
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${ | |
index <= currentPage ? 'bg-blue-600 text-white' : 'bg-gray-300 text-gray-600' | |
}`}> | |
{index + 1} | |
</div> | |
<span className={`ml-2 text-sm ${ | |
index <= currentPage ? 'text-blue-600 font-medium' : 'text-gray-500' | |
}`}> | |
{step} | |
</span> | |
{index < steps.length - 1 && <div className="ml-4 w-8 h-px bg-gray-300" />} | |
</div> | |
))} | |
</div> | |
<div className="w-full bg-gray-200 rounded-full h-2"> | |
<div | |
className="bg-blue-600 h-2 rounded-full transition-all duration-300" | |
style={{ width: `${((currentPage + 1) / totalPages) * 100}%` }} | |
/> | |
</div> | |
</div> | |
); | |
}; | |
// Compose the Wizard object | |
const Wizard = { | |
Root: WizardRoot, | |
Page: WizardPage, | |
Navigation: WizardNavigation, | |
Progress: WizardProgress | |
}; | |
// Example Usage Component | |
const ExampleWizardForm = () => { | |
const [formData, setFormData] = useState({ | |
firstName: '', | |
lastName: '', | |
email: '', | |
phone: '', | |
address: '', | |
city: '', | |
country: '', | |
accountType: '', | |
contactMethod: '', | |
newsletter: false, | |
notifications: false, | |
preferences: '', | |
experience: '' | |
}); | |
const handleInputChange = (field, value) => { | |
setFormData(prev => ({ ...prev, [field]: value })); | |
}; | |
const handleSubmit = (e) => { | |
console.log('Form submitted:', formData); | |
alert('Form submitted successfully!'); | |
}; | |
const handlePageChange = (pageIndex) => { | |
console.log('Current page:', pageIndex); | |
}; | |
return ( | |
<div className="min-h-screen bg-gray-50 py-8"> | |
<div className="max-w-2xl mx-auto bg-white rounded-lg shadow-md"> | |
<div className="px-8 py-6 border-b border-gray-200"> | |
<Wizard.Root onPageChange={handlePageChange}> | |
<Wizard.Progress | |
steps={['Personal', 'Contact', 'Address', 'Account', 'Preferences']} | |
/> | |
<div className="p-8"> | |
<Wizard.Page pageIndex={0} title="Personal Information"> | |
<div className="space-y-4"> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-1"> | |
First Name | |
</label> | |
<input | |
type="text" | |
name="firstName" | |
value={formData.firstName} | |
onChange={(e) => handleInputChange('firstName', e.target.value)} | |
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" | |
placeholder="Enter your first name" | |
/> | |
</div> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-1"> | |
Last Name | |
</label> | |
<input | |
type="text" | |
name="lastName" | |
value={formData.lastName} | |
onChange={(e) => handleInputChange('lastName', e.target.value)} | |
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" | |
placeholder="Enter your last name" | |
/> | |
</div> | |
</div> | |
</Wizard.Page> | |
<Wizard.Page pageIndex={1} title="Contact Information"> | |
<div className="space-y-4"> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-1"> | |
Email Address | |
</label> | |
<input | |
type="email" | |
name="email" | |
value={formData.email} | |
onChange={(e) => handleInputChange('email', e.target.value)} | |
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" | |
placeholder="Enter your email" | |
/> | |
</div> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-1"> | |
Phone Number | |
</label> | |
<input | |
type="tel" | |
name="phone" | |
value={formData.phone} | |
onChange={(e) => handleInputChange('phone', e.target.value)} | |
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" | |
placeholder="Enter your phone number" | |
/> | |
</div> | |
</div> | |
</Wizard.Page> | |
<Wizard.Page pageIndex={2} title="Address Information"> | |
<div className="space-y-4"> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-1"> | |
Street Address | |
</label> | |
<input | |
type="text" | |
name="address" | |
value={formData.address} | |
onChange={(e) => handleInputChange('address', e.target.value)} | |
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" | |
placeholder="Enter your address" | |
/> | |
</div> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-1"> | |
City | |
</label> | |
<input | |
type="text" | |
name="city" | |
value={formData.city} | |
onChange={(e) => handleInputChange('city', e.target.value)} | |
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" | |
placeholder="Enter your city" | |
/> | |
</div> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-1"> | |
Country | |
</label> | |
<select | |
name="country" | |
value={formData.country} | |
onChange={(e) => handleInputChange('country', e.target.value)} | |
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" | |
> | |
<option value="">Select a country</option> | |
<option value="us">United States</option> | |
<option value="ca">Canada</option> | |
<option value="uk">United Kingdom</option> | |
<option value="au">Australia</option> | |
<option value="de">Germany</option> | |
<option value="fr">France</option> | |
<option value="jp">Japan</option> | |
<option value="other">Other</option> | |
</select> | |
</div> | |
</div> | |
</Wizard.Page> | |
<Wizard.Page pageIndex={3} title="Account Settings"> | |
<div className="space-y-6"> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-3"> | |
Account Type | |
</label> | |
<div className="space-y-2"> | |
<div className="flex items-center"> | |
<input | |
type="radio" | |
id="personal" | |
name="accountType" | |
value="personal" | |
checked={formData.accountType === 'personal'} | |
onChange={(e) => handleInputChange('accountType', e.target.value)} | |
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" | |
/> | |
<label htmlFor="personal" className="ml-2 text-sm text-gray-700"> | |
Personal Account | |
</label> | |
</div> | |
<div className="flex items-center"> | |
<input | |
type="radio" | |
id="business" | |
name="accountType" | |
value="business" | |
checked={formData.accountType === 'business'} | |
onChange={(e) => handleInputChange('accountType', e.target.value)} | |
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" | |
/> | |
<label htmlFor="business" className="ml-2 text-sm text-gray-700"> | |
Business Account | |
</label> | |
</div> | |
<div className="flex items-center"> | |
<input | |
type="radio" | |
id="enterprise" | |
name="accountType" | |
value="enterprise" | |
checked={formData.accountType === 'enterprise'} | |
onChange={(e) => handleInputChange('accountType', e.target.value)} | |
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" | |
/> | |
<label htmlFor="enterprise" className="ml-2 text-sm text-gray-700"> | |
Enterprise Account | |
</label> | |
</div> | |
</div> | |
</div> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-3"> | |
Preferred Contact Method | |
</label> | |
<div className="space-y-2"> | |
<div className="flex items-center"> | |
<input | |
type="radio" | |
id="contactEmail" | |
name="contactMethod" | |
value="email" | |
checked={formData.contactMethod === 'email'} | |
onChange={(e) => handleInputChange('contactMethod', e.target.value)} | |
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" | |
/> | |
<label htmlFor="contactEmail" className="ml-2 text-sm text-gray-700"> | |
</label> | |
</div> | |
<div className="flex items-center"> | |
<input | |
type="radio" | |
id="contactPhone" | |
name="contactMethod" | |
value="phone" | |
checked={formData.contactMethod === 'phone'} | |
onChange={(e) => handleInputChange('contactMethod', e.target.value)} | |
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" | |
/> | |
<label htmlFor="contactPhone" className="ml-2 text-sm text-gray-700"> | |
Phone | |
</label> | |
</div> | |
<div className="flex items-center"> | |
<input | |
type="radio" | |
id="contactSms" | |
name="contactMethod" | |
value="sms" | |
checked={formData.contactMethod === 'sms'} | |
onChange={(e) => handleInputChange('contactMethod', e.target.value)} | |
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" | |
/> | |
<label htmlFor="contactSms" className="ml-2 text-sm text-gray-700"> | |
SMS | |
</label> | |
</div> | |
</div> | |
</div> | |
</div> | |
</Wizard.Page> | |
<Wizard.Page pageIndex={4} title="Preferences"> | |
<div className="space-y-4"> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-1"> | |
Experience Level | |
</label> | |
<select | |
name="experience" | |
value={formData.experience} | |
onChange={(e) => handleInputChange('experience', e.target.value)} | |
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" | |
> | |
<option value="">Select your experience level</option> | |
<option value="beginner">Beginner</option> | |
<option value="intermediate">Intermediate</option> | |
<option value="advanced">Advanced</option> | |
<option value="expert">Expert</option> | |
</select> | |
</div> | |
<div> | |
<label className="block text-sm font-medium text-gray-700 mb-1"> | |
Special Preferences | |
</label> | |
<textarea | |
name="preferences" | |
value={formData.preferences} | |
onChange={(e) => handleInputChange('preferences', e.target.value)} | |
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" | |
rows="4" | |
placeholder="Any special preferences or notes..." | |
/> | |
</div> | |
<div className="space-y-3"> | |
<div className="flex items-center"> | |
<input | |
type="checkbox" | |
name="newsletter" | |
checked={formData.newsletter} | |
onChange={(e) => handleInputChange('newsletter', e.target.checked)} | |
className="mr-3 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" | |
/> | |
<label className="text-sm text-gray-700"> | |
Subscribe to our newsletter for updates and tips | |
</label> | |
</div> | |
<div className="flex items-center"> | |
<input | |
type="checkbox" | |
name="notifications" | |
checked={formData.notifications} | |
onChange={(e) => handleInputChange('notifications', e.target.checked)} | |
className="mr-3 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" | |
/> | |
<label className="text-sm text-gray-700"> | |
Receive push notifications for important updates | |
</label> | |
</div> | |
</div> | |
</div> | |
</Wizard.Page> | |
<div className="pt-6 border-t border-gray-200 mt-8"> | |
<Wizard.Navigation onSubmit={handleSubmit} /> | |
</div> | |
</div> | |
</Wizard.Root> | |
</div> | |
</div> | |
</div> | |
); | |
}; | |
export default ExampleWizardForm; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment