Add equipment management to events

- Add equipmentProvided and requiredItems fields to Event model
- Update event creation form with equipment section
- Allow organizers to specify if equipment is provided or BYO
- Display required items list in event details
- Update database schema and run migration
This commit is contained in:
2025-11-18 19:43:22 +00:00
parent c1f8c00728
commit 4f06b1fe50
7 changed files with 202 additions and 3 deletions

View File

@@ -186,6 +186,54 @@ export function EventDetailModal({
</div>
</div>
{/* Equipment Information */}
<div className="flex items-start">
<svg className="h-6 w-6 text-gray-400 mr-3 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
/>
</svg>
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">Equipment</p>
{event.equipmentProvided ? (
<div className="mt-1">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
✓ Equipment Provided
</span>
<p className="text-sm text-gray-600 mt-2">
All necessary cleanup equipment will be provided by the organizer.
</p>
</div>
) : (
<div className="mt-1">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800">
Bring Your Own
</span>
{event.requiredItems && event.requiredItems.length > 0 ? (
<div className="mt-2">
<p className="text-sm text-gray-600 mb-2">Please bring the following items:</p>
<ul className="space-y-1">
{event.requiredItems.map((item, index) => (
<li key={index} className="text-sm text-gray-700 flex items-center">
<span className="text-blue-600 mr-2">•</span>
{item}
</li>
))}
</ul>
</div>
) : (
<p className="text-sm text-gray-600 mt-2">
Participants should bring their own cleanup equipment.
</p>
)}
</div>
)}
</div>
</div>
<div className="flex items-start">
<svg className="h-6 w-6 text-gray-400 mr-3 mt-0.5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path

View File

@@ -25,6 +25,8 @@ interface EventFormData {
scheduledDate: string;
scheduledTime: string;
duration: number;
equipmentProvided: boolean;
requiredItems: string[];
}
interface EventFormProps {
@@ -44,8 +46,12 @@ export function EventForm({ onSubmit, onCancel, initialData, isSubmitting = fals
scheduledDate: initialData?.scheduledDate || '',
scheduledTime: initialData?.scheduledTime || '',
duration: initialData?.duration || 120,
equipmentProvided: initialData?.equipmentProvided || false,
requiredItems: initialData?.requiredItems || [],
});
const [newItem, setNewItem] = useState('');
const [selectedLocation, setSelectedLocation] = useState<Coordinates | null>(
initialData?.latitude && initialData?.longitude
? { latitude: initialData.latitude, longitude: initialData.longitude }
@@ -328,6 +334,130 @@ export function EventForm({ onSubmit, onCancel, initialData, isSubmitting = fals
{errors.duration && <p className="mt-1 text-sm text-red-600">{errors.duration}</p>}
</div>
{/* Equipment Section */}
<div className="border border-gray-200 rounded-lg p-4 bg-gray-50">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Equipment</h3>
{/* Equipment Provided Toggle */}
<div className="mb-4">
<label className="flex items-center cursor-pointer">
<input
type="checkbox"
checked={formData.equipmentProvided}
onChange={(e) => {
setFormData(prev => ({ ...prev, equipmentProvided: e.target.checked }));
// Clear required items if equipment is provided
if (e.target.checked) {
setFormData(prev => ({ ...prev, requiredItems: [] }));
}
}}
className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-2 focus:ring-blue-500"
/>
<span className="ml-3 text-sm font-medium text-gray-700">
Equipment will be provided by organizer
</span>
</label>
<p className="mt-1 ml-8 text-sm text-gray-500">
Check this if you'll supply all necessary cleanup equipment
</p>
</div>
{/* Required Items List (only shown if equipment not provided) */}
{!formData.equipmentProvided && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Items Participants Need to Bring
</label>
<p className="text-sm text-gray-500 mb-3">
Add items that participants should bring (e.g., gloves, litter picker, bags, coat)
</p>
{/* Add Item Input */}
<div className="flex gap-2 mb-3">
<input
type="text"
value={newItem}
onChange={(e) => setNewItem(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (newItem.trim() && !formData.requiredItems.includes(newItem.trim())) {
setFormData(prev => ({
...prev,
requiredItems: [...prev.requiredItems, newItem.trim()]
}));
setNewItem('');
}
}
}}
placeholder="e.g., Gloves, Litter picker, Bags"
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
/>
<button
type="button"
onClick={() => {
if (newItem.trim() && !formData.requiredItems.includes(newItem.trim())) {
setFormData(prev => ({
...prev,
requiredItems: [...prev.requiredItems, newItem.trim()]
}));
setNewItem('');
}
}}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium"
>
Add
</button>
</div>
{/* Required Items List */}
{formData.requiredItems.length > 0 && (
<div className="space-y-2">
<p className="text-sm font-medium text-gray-700">Required Items:</p>
<ul className="space-y-1">
{formData.requiredItems.map((item, index) => (
<li
key={index}
className="flex items-center justify-between bg-white px-3 py-2 rounded-lg border border-gray-200"
>
<span className="text-sm text-gray-700">• {item}</span>
<button
type="button"
onClick={() => {
setFormData(prev => ({
...prev,
requiredItems: prev.requiredItems.filter((_, i) => i !== index)
}));
}}
className="text-red-600 hover:text-red-800 text-sm font-medium"
>
Remove
</button>
</li>
))}
</ul>
</div>
)}
{formData.requiredItems.length === 0 && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<p className="text-sm text-blue-700">
💡 Tip: Add common items like gloves, litter pickers, bags, or appropriate clothing
</p>
</div>
)}
</div>
)}
{formData.equipmentProvided && (
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
<p className="text-sm text-green-700">
✓ Participants will be informed that all equipment will be provided
</p>
</div>
)}
</div>
{/* Location Name */}
<div>
<label htmlFor="locationName" className="block text-sm font-medium text-gray-700 mb-1">

View File

@@ -62,6 +62,8 @@ export interface CleanupEvent {
participantCount: number;
litterCollected?: number; // in kilograms
photos?: string[];
equipmentProvided?: boolean; // Whether equipment is provided by organizer
requiredItems?: string[]; // List of items participants need to bring
createdAt: Date;
updatedAt: Date;
}