203 lines
No EOL
5.7 KiB
JavaScript
203 lines
No EOL
5.7 KiB
JavaScript
import { useState, useEffect } from 'react';
|
||
import ModalErrorDisplay from '../common/ModalErrorDisplay';
|
||
import './PlannableForm.css';
|
||
|
||
const PlannableForm = ({ item, tripId, calendarSlots, onSubmit, onCancel }) => {
|
||
const [formData, setFormData] = useState({
|
||
name: '',
|
||
type: 'attraction',
|
||
address: '',
|
||
notes: '',
|
||
calendar_slot_id: null
|
||
});
|
||
|
||
const [errors, setErrors] = useState({});
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [submitError, setSubmitError] = useState(null);
|
||
|
||
useEffect(() => {
|
||
if (item) {
|
||
setFormData({
|
||
name: item.name || '',
|
||
type: item.type || 'attraction',
|
||
address: item.address || '',
|
||
notes: item.notes || '',
|
||
calendar_slot_id: item.calendar_slot_id || null
|
||
});
|
||
}
|
||
}, [item]);
|
||
|
||
const handleChange = (e) => {
|
||
const { name, value } = e.target;
|
||
setFormData(prev => ({
|
||
...prev,
|
||
[name]: value === '' ? null : value
|
||
}));
|
||
|
||
// Clear error for this field
|
||
if (errors[name]) {
|
||
setErrors(prev => ({
|
||
...prev,
|
||
[name]: null
|
||
}));
|
||
}
|
||
|
||
// Clear submit error when user starts typing
|
||
if (submitError) {
|
||
setSubmitError(null);
|
||
}
|
||
};
|
||
|
||
const validate = () => {
|
||
const newErrors = {};
|
||
|
||
if (!formData.name || formData.name.trim() === '') {
|
||
newErrors.name = 'Name is required';
|
||
}
|
||
|
||
if (!formData.type) {
|
||
newErrors.type = 'Type is required';
|
||
}
|
||
|
||
return newErrors;
|
||
};
|
||
|
||
const handleSubmit = async (e) => {
|
||
e.preventDefault();
|
||
|
||
const newErrors = validate();
|
||
if (Object.keys(newErrors).length > 0) {
|
||
setErrors(newErrors);
|
||
return;
|
||
}
|
||
|
||
setSubmitting(true);
|
||
setSubmitError(null);
|
||
try {
|
||
await onSubmit(formData);
|
||
} catch (err) {
|
||
console.error('Form submission error:', err);
|
||
setSubmitError(err.message || 'Failed to save item. Please try again.');
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="plannable-form-overlay">
|
||
<div className="plannable-form-modal">
|
||
<div className="form-header">
|
||
<h2>{item ? 'Edit Item' : 'Add New Item'}</h2>
|
||
<button className="btn-close" onClick={onCancel}>×</button>
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmit} className="plannable-form">
|
||
<ModalErrorDisplay
|
||
error={submitError}
|
||
onDismiss={() => setSubmitError(null)}
|
||
/>
|
||
|
||
<div className="form-group">
|
||
<label>Name *</label>
|
||
<input
|
||
type="text"
|
||
name="name"
|
||
value={formData.name}
|
||
onChange={handleChange}
|
||
className={`form-control ${errors.name ? 'error' : ''}`}
|
||
placeholder="Enter item name"
|
||
autoFocus
|
||
/>
|
||
{errors.name && <span className="error-message">{errors.name}</span>}
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>Type *</label>
|
||
<select
|
||
name="type"
|
||
value={formData.type}
|
||
onChange={handleChange}
|
||
className={`form-control ${errors.type ? 'error' : ''}`}
|
||
>
|
||
<option value="hotel">🏨 Hotel</option>
|
||
<option value="restaurant">🍽️ Restaurant</option>
|
||
<option value="attraction">🎯 Attraction</option>
|
||
<option value="transport">✈️ Transport</option>
|
||
<option value="activity">🎭 Activity</option>
|
||
</select>
|
||
{errors.type && <span className="error-message">{errors.type}</span>}
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>Address</label>
|
||
<input
|
||
type="text"
|
||
name="address"
|
||
value={formData.address}
|
||
onChange={handleChange}
|
||
className="form-control"
|
||
placeholder="Enter address (optional)"
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>Assign to Day</label>
|
||
<select
|
||
name="calendar_slot_id"
|
||
value={formData.calendar_slot_id || ''}
|
||
onChange={handleChange}
|
||
className="form-control"
|
||
>
|
||
<option value="">Unplanned</option>
|
||
{calendarSlots.map(slot => {
|
||
const slotDate = new Date(slot.slot_date);
|
||
const dateStr = slotDate.toLocaleDateString('en-US', {
|
||
weekday: 'short',
|
||
month: 'short',
|
||
day: 'numeric'
|
||
});
|
||
return (
|
||
<option key={slot.id} value={slot.id}>
|
||
{slot.name} - {dateStr}
|
||
</option>
|
||
);
|
||
})}
|
||
</select>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label>Notes</label>
|
||
<textarea
|
||
name="notes"
|
||
value={formData.notes}
|
||
onChange={handleChange}
|
||
className="form-control"
|
||
rows="3"
|
||
placeholder="Add any additional notes (optional)"
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-actions">
|
||
<button
|
||
type="button"
|
||
className="btn-secondary"
|
||
onClick={onCancel}
|
||
disabled={submitting}
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
className="btn-primary"
|
||
disabled={submitting}
|
||
>
|
||
{submitting ? 'Saving...' : (item ? 'Update' : 'Add')} Item
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default PlannableForm; |