Skip to content

Instantly share code, notes, and snippets.

@SeeThruHead
Created September 27, 2025 00:35
Show Gist options
  • Save SeeThruHead/af97d1588bb42918ef8f1286d62ec4c8 to your computer and use it in GitHub Desktop.
Save SeeThruHead/af97d1588bb42918ef8f1286d62ec4c8 to your computer and use it in GitHub Desktop.
clauded wizard
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">
Email
</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