diff --git a/includes/class-hvac-event-form-builder.php b/includes/class-hvac-event-form-builder.php new file mode 100644 index 00000000..a060cf02 --- /dev/null +++ b/includes/class-hvac-event-form-builder.php @@ -0,0 +1,680 @@ + [ + 'type' => 'datetime-local', + 'class' => 'hvac-datetime-field', + 'validate' => ['datetime'], + 'sanitize' => 'datetime', + ], + 'venue-group' => [ + 'type' => 'venue-group', + 'class' => 'hvac-venue-group', + 'wrapper_class' => 'form-group venue-group', + ], + 'organizer-group' => [ + 'type' => 'organizer-group', + 'class' => 'hvac-organizer-group', + 'wrapper_class' => 'form-group organizer-group', + ], + ]; + + /** + * WordPress timezone list for event timezone selection + * + * @var array|null + */ + private ?array $timezone_list = null; + + /** + * Constructor with promoted property. + * + * @param string $nonce_action Nonce action for the form + */ + public function __construct(string $nonce_action = 'hvac_event_form') { + parent::__construct($nonce_action); + $this->init_event_specific_features(); + } + + /** + * Initialize event-specific features + */ + private function init_event_specific_features(): void { + // Set form enctype for file uploads (featured images) + $this->set_attributes(['enctype' => 'multipart/form-data']); + + // Load timezone list + $this->timezone_list = $this->get_wordpress_timezones(); + } + + /** + * Add event datetime field + * + * @param array $field_config Field configuration + * @return self + */ + public function add_datetime_field(array $field_config): self { + $defaults = array_merge($this->event_field_defaults['datetime-local'], [ + 'required' => true, + 'description' => 'Select date and time for the event', + ]); + + $field = wp_parse_args($field_config, $defaults); + + // Ensure proper ID and name + if (empty($field['id']) && !empty($field['name'])) { + $field['id'] = sanitize_html_class($field['name']); + } + + return $this->add_field($field); + } + + /** + * Add event timezone selection field + * + * @param array $field_config Field configuration + * @return self + */ + public function add_timezone_field(array $field_config = []): self { + $defaults = [ + 'type' => 'select', + 'name' => 'event_timezone', + 'label' => 'Event Timezone', + 'options' => $this->timezone_list ?? [], + 'value' => wp_timezone_string(), + 'required' => true, + 'class' => 'hvac-timezone-field', + 'description' => 'Select the timezone for this event', + ]; + + $field = wp_parse_args($field_config, $defaults); + return $this->add_field($field); + } + + /** + * Add all-day event toggle + * + * @param array $field_config Field configuration + * @return self + */ + public function add_all_day_field(array $field_config = []): self { + $defaults = [ + 'type' => 'checkbox', + 'name' => 'event_all_day', + 'label' => 'All Day Event', + 'value' => '1', + 'class' => 'hvac-all-day-field', + 'description' => 'Check if this is an all-day event', + ]; + + $field = wp_parse_args($field_config, $defaults); + return $this->add_field($field); + } + + /** + * Add venue field group + * + * @param array $field_config Field configuration + * @return self + */ + public function add_venue_group(array $field_config = []): self { + $venue_fields = [ + [ + 'type' => 'text', + 'name' => 'venue_name', + 'label' => 'Venue Name', + 'required' => true, + 'class' => 'hvac-venue-name', + 'placeholder' => 'Enter venue name', + ], + [ + 'type' => 'textarea', + 'name' => 'venue_address', + 'label' => 'Address', + 'required' => false, + 'class' => 'hvac-venue-address', + 'placeholder' => 'Street address', + 'validate' => ['max_length' => 500], + ], + [ + 'type' => 'text', + 'name' => 'venue_city', + 'label' => 'City', + 'required' => false, + 'class' => 'hvac-venue-city', + ], + [ + 'type' => 'text', + 'name' => 'venue_state', + 'label' => 'State/Province', + 'required' => false, + 'class' => 'hvac-venue-state', + ], + [ + 'type' => 'text', + 'name' => 'venue_zip', + 'label' => 'Postal Code', + 'required' => false, + 'class' => 'hvac-venue-zip', + 'validate' => ['pattern' => '/^[A-Za-z0-9\s\-]{3,10}$/'], + ], + [ + 'type' => 'number', + 'name' => 'venue_capacity', + 'label' => 'Venue Capacity', + 'required' => false, + 'class' => 'hvac-venue-capacity', + 'validate' => ['min' => 1, 'max' => 10000], + 'description' => 'Maximum number of attendees (optional)', + ], + [ + 'type' => 'hidden', + 'name' => 'venue_latitude', + 'class' => 'hvac-venue-lat', + ], + [ + 'type' => 'hidden', + 'name' => 'venue_longitude', + 'class' => 'hvac-venue-lng', + ], + ]; + + // Add all venue fields + foreach ($venue_fields as $field) { + $this->add_field($field); + } + + return $this; + } + + /** + * Add organizer field group + * + * @param array $field_config Field configuration + * @return self + */ + public function add_organizer_group(array $field_config = []): self { + $organizer_fields = [ + [ + 'type' => 'text', + 'name' => 'organizer_name', + 'label' => 'Organizer Name', + 'required' => true, + 'class' => 'hvac-organizer-name', + 'placeholder' => 'Event organizer name', + ], + [ + 'type' => 'email', + 'name' => 'organizer_email', + 'label' => 'Organizer Email', + 'required' => false, + 'class' => 'hvac-organizer-email', + 'validate' => ['email'], + 'sanitize' => 'email', + ], + [ + 'type' => 'tel', + 'name' => 'organizer_phone', + 'label' => 'Organizer Phone', + 'required' => false, + 'class' => 'hvac-organizer-phone', + 'validate' => ['pattern' => '/^[\+]?[1-9][\d]{0,15}$/'], + ], + [ + 'type' => 'url', + 'name' => 'organizer_website', + 'label' => 'Organizer Website', + 'required' => false, + 'class' => 'hvac-organizer-website', + 'validate' => ['url'], + 'sanitize' => 'url', + 'placeholder' => 'https://example.com', + ], + ]; + + // Add all organizer fields + foreach ($organizer_fields as $field) { + $this->add_field($field); + } + + return $this; + } + + /** + * Add HVAC-specific event fields + * + * @return self + */ + public function add_hvac_fields(): self { + $hvac_fields = [ + [ + 'type' => 'select', + 'name' => 'trainer_requirements', + 'label' => 'Trainer Requirements', + 'options' => $this->get_trainer_requirement_options(), + 'required' => false, + 'class' => 'hvac-trainer-requirements', + 'description' => 'What type of trainer is required for this event?', + ], + [ + 'type' => 'checkbox', + 'name' => 'certification_levels', + 'label' => 'Certification Levels', + 'options' => $this->get_certification_level_options(), + 'required' => false, + 'class' => 'hvac-certification-levels', + 'description' => 'Which certification levels does this event support?', + ], + [ + 'type' => 'textarea', + 'name' => 'equipment_needed', + 'label' => 'Equipment Needed', + 'required' => false, + 'class' => 'hvac-equipment-needed', + 'placeholder' => 'List any specific equipment attendees should bring', + 'validate' => ['max_length' => 1000], + ], + [ + 'type' => 'textarea', + 'name' => 'prerequisites', + 'label' => 'Prerequisites', + 'required' => false, + 'class' => 'hvac-prerequisites', + 'placeholder' => 'Any required knowledge or certifications for attendees', + 'validate' => ['max_length' => 1000], + ], + ]; + + // Add all HVAC-specific fields + foreach ($hvac_fields as $field) { + $this->add_field($field); + } + + return $this; + } + + /** + * Add featured image upload field + * + * @param array $field_config Field configuration + * @return self + */ + public function add_featured_image_field(array $field_config = []): self { + $defaults = [ + 'type' => 'file', + 'name' => 'event_featured_image', + 'label' => 'Featured Image', + 'required' => false, + 'class' => 'hvac-featured-image', + 'description' => 'Upload a featured image for this event (JPG, PNG, WebP)', + 'validate' => ['file_type' => ['jpg', 'jpeg', 'png', 'webp']], + ]; + + $field = wp_parse_args($field_config, $defaults); + return $this->add_field($field); + } + + /** + * Enhanced validation for event-specific fields + * + * @param array $data Form data to validate + * @return array Validation errors + */ + public function validate(array $data): array { + $errors = parent::validate($data); + + // Event datetime validation + $errors = array_merge($errors, $this->validate_event_datetime($data)); + + // Venue validation + $errors = array_merge($errors, $this->validate_venue_data($data)); + + // HVAC-specific validation + $errors = array_merge($errors, $this->validate_hvac_fields($data)); + + return $errors; + } + + /** + * Validate event datetime fields + * + * @param array $data Form data + * @return array Validation errors + */ + private function validate_event_datetime(array $data): array { + $errors = []; + + if (isset($data['event_start_date']) && isset($data['event_end_date'])) { + $start_time = strtotime($data['event_start_date']); + $end_time = strtotime($data['event_end_date']); + + if ($start_time === false) { + $errors['event_start_date'] = 'Invalid start date format.'; + } elseif ($start_time < time()) { + $errors['event_start_date'] = 'Start date cannot be in the past.'; + } + + if ($end_time === false) { + $errors['event_end_date'] = 'Invalid end date format.'; + } elseif ($start_time !== false && $end_time !== false && $end_time <= $start_time) { + $errors['event_end_date'] = 'End date must be after start date.'; + } + } + + return $errors; + } + + /** + * Validate venue data + * + * @param array $data Form data + * @return array Validation errors + */ + private function validate_venue_data(array $data): array { + $errors = []; + + // Venue capacity validation + if (!empty($data['venue_capacity'])) { + $capacity = intval($data['venue_capacity']); + if ($capacity < 1) { + $errors['venue_capacity'] = 'Venue capacity must be at least 1.'; + } elseif ($capacity > 10000) { + $errors['venue_capacity'] = 'Venue capacity cannot exceed 10,000.'; + } + } + + return $errors; + } + + /** + * Validate HVAC-specific fields + * + * @param array $data Form data + * @return array Validation errors + */ + private function validate_hvac_fields(array $data): array { + $errors = []; + + // Validate trainer requirements against available options + if (!empty($data['trainer_requirements'])) { + $valid_requirements = array_keys($this->get_trainer_requirement_options()); + if (!in_array($data['trainer_requirements'], $valid_requirements)) { + $errors['trainer_requirements'] = 'Invalid trainer requirement selected.'; + } + } + + return $errors; + } + + /** + * Enhanced sanitization for event fields + * + * @param array $data Raw form data + * @return array Sanitized data + */ + public function sanitize(array $data): array { + $sanitized = parent::sanitize($data); + + // Event-specific sanitization + $event_fields = [ + 'event_start_date' => 'datetime', + 'event_end_date' => 'datetime', + 'event_timezone' => 'text', + 'venue_name' => 'text', + 'venue_address' => 'textarea', + 'venue_city' => 'text', + 'venue_state' => 'text', + 'venue_zip' => 'text', + 'venue_capacity' => 'int', + 'organizer_name' => 'text', + 'organizer_email' => 'email', + 'organizer_phone' => 'text', + 'organizer_website' => 'url', + 'trainer_requirements' => 'text', + 'equipment_needed' => 'textarea', + 'prerequisites' => 'textarea', + ]; + + foreach ($event_fields as $field => $sanitize_type) { + if (isset($data[$field])) { + switch ($sanitize_type) { + case 'datetime': + $sanitized[$field] = $this->sanitize_datetime($data[$field]); + break; + case 'int': + $sanitized[$field] = absint($data[$field]); + break; + case 'email': + $sanitized[$field] = sanitize_email($data[$field]); + break; + case 'url': + $sanitized[$field] = esc_url_raw($data[$field]); + break; + case 'textarea': + $sanitized[$field] = sanitize_textarea_field($data[$field]); + break; + default: + $sanitized[$field] = sanitize_text_field($data[$field]); + } + } + } + + return $sanitized; + } + + /** + * Sanitize datetime input + * + * @param string $datetime Raw datetime input + * @return string Sanitized datetime + */ + private function sanitize_datetime(string $datetime): string { + $timestamp = strtotime($datetime); + return $timestamp !== false ? date('Y-m-d\TH:i', $timestamp) : ''; + } + + /** + * Get WordPress timezone options + * + * @return array Timezone options + */ + private function get_wordpress_timezones(): array { + $zones = wp_timezone_choice('UTC'); + $timezone_options = []; + + // Parse the HTML select options into key-value pairs + if (preg_match_all('/