diff --git a/admin/class-wpfaevent-admin.php b/admin/class-wpfaevent-admin.php
index 9572546..a62d1cf 100644
--- a/admin/class-wpfaevent-admin.php
+++ b/admin/class-wpfaevent-admin.php
@@ -239,8 +239,12 @@ public function render_event_meta_box( $post ) {
$start_date = get_post_meta( $post->ID, 'wpfa_event_start_date', true );
$end_date = get_post_meta( $post->ID, 'wpfa_event_end_date', true );
+ $time = get_post_meta( $post->ID, 'wpfa_event_time', true );
$location = get_post_meta( $post->ID, 'wpfa_event_location', true );
$url = get_post_meta( $post->ID, 'wpfa_event_url', true );
+ $lead_text = get_post_meta( $post->ID, 'wpfa_event_lead_text', true );
+ $reg_link = get_post_meta( $post->ID, 'wpfa_event_registration_link', true );
+ $cfs_link = get_post_meta( $post->ID, 'wpfa_event_cfs_link', true );
$speakers = get_post_meta( $post->ID, 'wpfa_event_speakers', true );
// Normalize to array
@@ -258,14 +262,30 @@ public function render_event_meta_box( $post ) {
|
@@ -369,22 +389,34 @@ public function save_event_meta( $post_id ) {
return;
}
- if ( isset( $_POST['wpfa_event_start_date'] ) ) {
- update_post_meta( $post_id, 'wpfa_event_start_date', sanitize_text_field( wp_unslash( $_POST['wpfa_event_start_date'] ) ) );
- }
+ // List of all meta fields to save
+ $meta_fields = array(
+ 'wpfa_event_start_date',
+ 'wpfa_event_end_date',
+ 'wpfa_event_time',
+ 'wpfa_event_location',
+ 'wpfa_event_lead_text',
+ 'wpfa_event_url',
+ 'wpfa_event_registration_link',
+ 'wpfa_event_cfs_link',
+ );
- if ( isset( $_POST['wpfa_event_end_date'] ) ) {
- update_post_meta( $post_id, 'wpfa_event_end_date', sanitize_text_field( wp_unslash( $_POST['wpfa_event_end_date'] ) ) );
- }
+ foreach ( $meta_fields as $field ) {
+ if ( isset( $_POST[ $field ] ) ) {
+ $value = wp_unslash( $_POST[ $field ] );
- if ( isset( $_POST['wpfa_event_location'] ) ) {
- update_post_meta( $post_id, 'wpfa_event_location', sanitize_text_field( wp_unslash( $_POST['wpfa_event_location'] ) ) );
- }
+ // Special handling for URL fields
+ if ( in_array( $field, array( 'wpfa_event_url', 'wpfa_event_registration_link', 'wpfa_event_cfs_link' ), true ) ) {
+ $value = esc_url_raw( $value );
+ } else {
+ $value = sanitize_text_field( $value );
+ }
- if ( isset( $_POST['wpfa_event_url'] ) ) {
- update_post_meta( $post_id, 'wpfa_event_url', esc_url_raw( wp_unslash( $_POST['wpfa_event_url'] ) ) );
+ update_post_meta( $post_id, $field, $value );
+ }
}
+ // Handle speakers array
if ( isset( $_POST['wpfa_event_speakers'] ) && is_array( $_POST['wpfa_event_speakers'] ) ) {
$speakers = array_map( 'absint', $_POST['wpfa_event_speakers'] );
update_post_meta( $post_id, 'wpfa_event_speakers', $speakers );
@@ -446,458 +478,4 @@ public function maybe_show_block_theme_notice() {
);
echo '';
}
-
- /**
- * Handle AJAX request to get speaker data.
- *
- * @since 1.0.0
- */
- public function ajax_get_speaker() {
- // Verify nonce
- if ( ! check_ajax_referer( 'wpfa_speakers_ajax', 'nonce', false ) ) {
- wp_send_json_error(
- array(
- 'message' => esc_html__( 'Invalid nonce', 'wpfaevent' ),
- ),
- 403
- );
- }
-
- // Check permissions
- if ( ! current_user_can( 'manage_options' ) ) {
- wp_send_json_error(
- array( 'message' => __( 'Unauthorized', 'wpfaevent' ) ),
- 403
- );
- }
-
- $speaker_id = isset( $_POST['speaker_id'] ) ? absint( $_POST['speaker_id'] ) : 0;
-
- if ( ! $speaker_id ) {
- wp_send_json_error( esc_html__( 'Invalid speaker ID', 'wpfaevent' ) );
- }
-
- $speaker = get_post( $speaker_id );
-
- if ( ! $speaker || $speaker->post_type !== 'wpfa_speaker' ) {
- wp_send_json_error( esc_html__( 'Speaker not found', 'wpfaevent' ) );
- }
-
- // Get category term
- $category = '';
- $category_slug = '';
- $terms = wp_get_object_terms( $speaker_id, 'wpfa_speaker_category' );
- if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
- $category = $terms[0]->name;
- $category_slug = $terms[0]->slug;
- }
-
- $data = array(
- 'id' => $speaker_id,
- 'name' => $speaker->post_title,
- 'position' => get_post_meta( $speaker_id, 'wpfa_speaker_position', true ),
- 'organization' => get_post_meta( $speaker_id, 'wpfa_speaker_organization', true ),
- 'bio' => get_post_meta( $speaker_id, 'wpfa_speaker_bio', true ),
- 'headshot_url' => get_post_meta( $speaker_id, 'wpfa_speaker_headshot_url', true ),
- 'linkedin' => get_post_meta( $speaker_id, 'wpfa_speaker_linkedin', true ),
- 'twitter' => get_post_meta( $speaker_id, 'wpfa_speaker_twitter', true ),
- 'github' => get_post_meta( $speaker_id, 'wpfa_speaker_github', true ),
- 'website' => get_post_meta( $speaker_id, 'wpfa_speaker_website', true ),
- 'category' => $category,
- 'category_slug' => $category_slug,
- 'talk_title' => get_post_meta( $speaker_id, 'wpfa_speaker_talk_title', true ),
- 'talk_date' => get_post_meta( $speaker_id, 'wpfa_speaker_talk_date', true ),
- 'talk_time' => get_post_meta( $speaker_id, 'wpfa_speaker_talk_time', true ),
- 'talk_end_time' => get_post_meta( $speaker_id, 'wpfa_speaker_talk_end_time', true ),
- 'talk_abstract' => get_post_meta( $speaker_id, 'wpfa_speaker_talk_abstract', true ),
- );
-
- wp_send_json_success( $data );
- }
-
- /**
- * Handle AJAX request to add a new speaker.
- *
- * @since 1.0.0
- */
- public function ajax_add_speaker() {
- // Verify nonce
- if ( ! check_ajax_referer( 'wpfa_speakers_ajax', 'nonce', false ) ) {
- wp_send_json_error(
- array(
- 'message' => esc_html__( 'Invalid nonce', 'wpfaevent' ),
- ),
- 403
- );
- }
-
- // Check permissions
- if ( ! current_user_can( 'manage_options' ) ) {
- wp_send_json_error(
- array(
- 'message' => __( 'Unauthorized', 'wpfaevent' ),
- ),
- 403
- );
- }
-
- // Validate required fields
- $required_fields = array( 'name', 'position', 'bio', 'talk_title', 'talk_date', 'talk_time', 'talk_end_time' );
- foreach ( $required_fields as $field ) {
- if ( empty( $_POST[ $field ] ) ) {
- wp_send_json_error( sprintf( esc_html__( 'Missing required field: %s', 'wpfaevent' ), $field ) );
- }
- }
-
- // Create speaker post
- $speaker_data = array(
- 'post_title' => sanitize_text_field( wp_unslash( $_POST['name'] ) ),
- 'post_type' => 'wpfa_speaker',
- 'post_status' => 'publish',
- 'post_content' => '',
- );
-
- $speaker_id = wp_insert_post( $speaker_data );
-
- if ( is_wp_error( $speaker_id ) ) {
- wp_send_json_error( $speaker_id->get_error_message() );
- }
-
- // Handle image upload
- $image_url = '';
- if ( ! empty( $_FILES['image_upload']['name'] ) ) {
- // Validate file type
- $allowed_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp' );
- $file_type = $_FILES['image_upload']['type'];
-
- if ( ! in_array( $file_type, $allowed_types, true ) ) {
- wp_send_json_error( esc_html__( 'Invalid file type. Only JPG, PNG, GIF, and WebP are allowed.', 'wpfaevent' ) );
- }
-
- // Validate file size (2MB max)
- $max_size = 2 * 1024 * 1024; // 2MB in bytes
- if ( $_FILES['image_upload']['size'] > $max_size ) {
- wp_send_json_error( esc_html__( 'File size exceeds 2MB limit.', 'wpfaevent' ) );
- }
-
- require_once ABSPATH . 'wp-admin/includes/file.php';
- require_once ABSPATH . 'wp-admin/includes/image.php';
- require_once ABSPATH . 'wp-admin/includes/media.php';
-
- // Upload and create attachment
- $attachment_id = media_handle_upload( 'image_upload', 0 );
-
- if ( is_wp_error( $attachment_id ) ) {
- wp_send_json_error( sprintf( esc_html__( 'Image upload failed: %s', 'wpfaevent' ), $attachment_id->get_error_message() ) );
- }
-
- $image_url = wp_get_attachment_url( $attachment_id );
- } elseif ( ! empty( $_POST['image_url'] ) ) {
- $image_url = esc_url_raw( wp_unslash( $_POST['image_url'] ) );
- }
-
- // Save meta fields
- $meta_fields = array(
- 'wpfa_speaker_position' => 'position',
- 'wpfa_speaker_organization' => 'organization',
- 'wpfa_speaker_bio' => 'bio',
- 'wpfa_speaker_headshot_url' => 'image_url',
- 'wpfa_speaker_linkedin' => 'linkedin',
- 'wpfa_speaker_twitter' => 'twitter',
- 'wpfa_speaker_github' => 'github',
- 'wpfa_speaker_website' => 'website',
- );
-
- foreach ( $meta_fields as $meta_key => $post_key ) {
- if ( $post_key === 'image_url' && ! empty( $image_url ) ) {
- // Use uploaded image URL or provided URL
- update_post_meta( $speaker_id, $meta_key, $image_url );
- } elseif ( isset( $_POST[ $post_key ] ) ) {
- $value = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) );
-
- if ( $post_key === 'bio' ) {
- $value = wp_kses_post( wp_unslash( $_POST[ $post_key ] ) );
- } elseif ( in_array( $post_key, array( 'linkedin', 'twitter', 'github', 'website' ), true ) ) {
- $value = esc_url_raw( wp_unslash( $_POST[ $post_key ] ) );
- } else {
- $value = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) );
- }
-
- if ( strlen( $value ) === 0 ) {
- delete_post_meta( $speaker_id, $meta_key );
- } else {
- update_post_meta( $speaker_id, $meta_key, $value );
- }
- }
- }
-
- $session_fields = array(
- 'wpfa_speaker_talk_title' => 'talk_title',
- 'wpfa_speaker_talk_date' => 'talk_date',
- 'wpfa_speaker_talk_time' => 'talk_time',
- 'wpfa_speaker_talk_end_time' => 'talk_end_time',
- 'wpfa_speaker_talk_abstract' => 'talk_abstract',
- );
-
- foreach ( $session_fields as $meta_key => $post_key ) {
- if ( isset( $_POST[ $post_key ] ) ) {
-
- if ( $post_key === 'talk_abstract' ) {
- $value = wp_kses_post( wp_unslash( $_POST[ $post_key ] ) );
- } else {
- $value = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) );
- }
-
- if ( strlen( $value ) === 0 ) {
- delete_post_meta( $speaker_id, $meta_key );
- } else {
- update_post_meta( $speaker_id, $meta_key, $value );
- }
- }
- }
-
- if ( isset( $_POST['category'] ) ) {
- $category = sanitize_text_field( wp_unslash( $_POST['category'] ) );
-
- // If it's numeric, it's a term ID
- if ( is_numeric( $category ) ) {
- $term_id = (int) $category;
- wp_set_object_terms( $speaker_id, $term_id, 'wpfa_speaker_category' );
- }
- // If it's "_custom" with custom value
- elseif ( $category === '_custom' && isset( $_POST['category_custom'] ) && ! empty( $_POST['category_custom'] ) ) {
- $category_name = sanitize_text_field( wp_unslash( $_POST['category_custom'] ) );
- wp_set_object_terms( $speaker_id, $category_name, 'wpfa_speaker_category' );
- }
- // If it's a slug/name
- elseif ( ! empty( $category ) && $category !== '_custom' ) {
- wp_set_object_terms( $speaker_id, $category, 'wpfa_speaker_category' );
- }
- // Empty
- else {
- wp_set_object_terms( $speaker_id, array(), 'wpfa_speaker_category' );
- }
- }
-
- wp_send_json_success( array( 'speaker_id' => $speaker_id ) );
- }
-
- /**
- * Handle AJAX request to update a speaker.
- *
- * @since 1.0.0
- */
- public function ajax_update_speaker() {
- // Verify nonce
- if ( ! check_ajax_referer( 'wpfa_speakers_ajax', 'nonce', false ) ) {
- wp_send_json_error(
- array(
- 'message' => esc_html__( 'Invalid nonce', 'wpfaevent' ),
- ),
- 403
- );
- }
-
- // Check permissions
- if ( ! current_user_can( 'manage_options' ) ) {
- wp_send_json_error(
- array( 'message' => __( 'Unauthorized', 'wpfaevent' ) ),
- 403
- );
- }
-
- $speaker_id = isset( $_POST['speaker_id'] ) ? absint( $_POST['speaker_id'] ) : 0;
-
- if ( ! $speaker_id ) {
- wp_send_json_error( __( 'Invalid speaker ID', 'wpfaevent' ) );
- }
-
- // Verify speaker exists and user can edit it
- $speaker = get_post( $speaker_id );
- if ( ! $speaker || $speaker->post_type !== 'wpfa_speaker' || ! current_user_can( 'edit_post', $speaker_id ) ) {
- wp_send_json_error( __( 'Cannot edit this speaker', 'wpfaevent' ) );
- }
-
- // Validate required fields
- $required_fields = array( 'name', 'position', 'bio', 'talk_title', 'talk_date', 'talk_time', 'talk_end_time' );
- foreach ( $required_fields as $field ) {
- if ( empty( $_POST[ $field ] ) ) {
- wp_send_json_error( sprintf( esc_html__( 'Missing required field: %s', 'wpfaevent' ), $field ) );
- }
- }
-
- // Update post title if name changed
- if ( ! empty( $_POST['name'] ) ) {
- wp_update_post(
- array(
- 'ID' => $speaker_id,
- 'post_title' => sanitize_text_field( wp_unslash( $_POST['name'] ) ),
- )
- );
- }
-
- // Handle image upload
- $image_url = '';
- if ( ! empty( $_FILES['image_upload']['name'] ) ) {
- // Validate file type
- $allowed_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp' );
- $file_type = $_FILES['image_upload']['type'];
-
- if ( ! in_array( $file_type, $allowed_types, true ) ) {
- wp_send_json_error( esc_html__( 'Invalid file type. Only JPG, PNG, GIF, and WebP are allowed.', 'wpfaevent' ) );
- }
-
- // Validate file size (2MB max)
- $max_size = 2 * 1024 * 1024; // 2MB in bytes
- if ( $_FILES['image_upload']['size'] > $max_size ) {
- wp_send_json_error( esc_html__( 'File size exceeds 2MB limit.', 'wpfaevent' ) );
- }
-
- require_once ABSPATH . 'wp-admin/includes/file.php';
- require_once ABSPATH . 'wp-admin/includes/image.php';
- require_once ABSPATH . 'wp-admin/includes/media.php';
-
- // Upload and create attachment
- $attachment_id = media_handle_upload( 'image_upload', $speaker_id );
-
- if ( is_wp_error( $attachment_id ) ) {
- wp_send_json_error( sprintf( esc_html__( 'Image upload failed: %s', 'wpfaevent' ), $attachment_id->get_error_message() ) );
- }
-
- $image_url = wp_get_attachment_url( $attachment_id );
- } elseif ( ! empty( $_POST['image_url'] ) ) {
- $image_url = esc_url_raw( wp_unslash( $_POST['image_url'] ) );
- }
-
- // Save meta fields
- $meta_fields = array(
- 'wpfa_speaker_position' => 'position',
- 'wpfa_speaker_organization' => 'organization',
- 'wpfa_speaker_bio' => 'bio',
- 'wpfa_speaker_headshot_url' => 'image_url',
- 'wpfa_speaker_linkedin' => 'linkedin',
- 'wpfa_speaker_twitter' => 'twitter',
- 'wpfa_speaker_github' => 'github',
- 'wpfa_speaker_website' => 'website',
- );
-
- foreach ( $meta_fields as $meta_key => $post_key ) {
- if ( $post_key === 'image_url' && ! empty( $image_url ) ) {
- // Use uploaded image URL or provided URL
- update_post_meta( $speaker_id, $meta_key, $image_url );
- } elseif ( isset( $_POST[ $post_key ] ) ) {
-
- if ( $post_key === 'bio' ) {
- $value = wp_kses_post( wp_unslash( $_POST[ $post_key ] ) );
- } elseif ( in_array( $post_key, array( 'linkedin', 'twitter', 'github', 'website' ), true ) ) {
- $value = esc_url_raw( wp_unslash( $_POST[ $post_key ] ) );
- } else {
- $value = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) );
- }
-
- // Delete meta when field is intentionally cleared to avoid storing empty values
- if ( strlen( $value ) === 0 ) {
- delete_post_meta( $speaker_id, $meta_key );
- } else {
- update_post_meta( $speaker_id, $meta_key, $value );
- }
- }
- }
-
- $session_fields = array(
- 'wpfa_speaker_talk_title' => 'talk_title',
- 'wpfa_speaker_talk_date' => 'talk_date',
- 'wpfa_speaker_talk_time' => 'talk_time',
- 'wpfa_speaker_talk_end_time' => 'talk_end_time',
- 'wpfa_speaker_talk_abstract' => 'talk_abstract',
- );
-
- foreach ( $session_fields as $meta_key => $post_key ) {
- if ( isset( $_POST[ $post_key ] ) ) {
-
- if ( $post_key === 'talk_abstract' ) {
- $value = wp_kses_post( wp_unslash( $_POST[ $post_key ] ) );
- } else {
- $value = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) );
- }
-
- if ( strlen( $value ) === 0 ) {
- delete_post_meta( $speaker_id, $meta_key );
- } else {
- update_post_meta( $speaker_id, $meta_key, $value );
- }
- }
- }
-
- if ( isset( $_POST['category'] ) ) {
- $category = sanitize_text_field( wp_unslash( $_POST['category'] ) );
-
- // If it's numeric, it's a term ID
- if ( is_numeric( $category ) ) {
- $term_id = (int) $category;
- wp_set_object_terms( $speaker_id, $term_id, 'wpfa_speaker_category' );
- }
- // If it's "_custom" with custom value
- elseif ( $category === '_custom' && isset( $_POST['category_custom'] ) && ! empty( $_POST['category_custom'] ) ) {
- $category_name = sanitize_text_field( wp_unslash( $_POST['category_custom'] ) );
- wp_set_object_terms( $speaker_id, $category_name, 'wpfa_speaker_category' );
- }
- // If it's a slug/name
- elseif ( ! empty( $category ) && $category !== '_custom' ) {
- wp_set_object_terms( $speaker_id, $category, 'wpfa_speaker_category' );
- }
- // Empty
- else {
- wp_set_object_terms( $speaker_id, array(), 'wpfa_speaker_category' );
- }
- }
-
- wp_send_json_success();
- }
-
- /**
- * Handle AJAX request to delete a speaker.
- *
- * @since 1.0.0
- */
- public function ajax_delete_speaker() {
- // Verify nonce
- if ( ! check_ajax_referer( 'wpfa_speakers_ajax', 'nonce', false ) ) {
- wp_send_json_error(
- array(
- 'message' => esc_html__( 'Invalid nonce', 'wpfaevent' ),
- ),
- 403
- );
- }
-
- // Check permissions
- if ( ! current_user_can( 'manage_options' ) ) {
- wp_send_json_error(
- array( 'message' => __( 'Unauthorized', 'wpfaevent' ) ),
- 403
- );
- }
-
- $speaker_id = isset( $_POST['speaker_id'] ) ? absint( $_POST['speaker_id'] ) : 0;
-
- if ( ! $speaker_id ) {
- wp_send_json_error( __( 'Invalid speaker ID', 'wpfaevent' ) );
- }
-
- // Verify speaker exists and user can delete it
- $speaker = get_post( $speaker_id );
- if ( ! $speaker || $speaker->post_type !== 'wpfa_speaker' || ! current_user_can( 'delete_post', $speaker_id ) ) {
- wp_send_json_error( __( 'Cannot delete this speaker', 'wpfaevent' ) );
- }
-
- // Delete the speaker
- $result = wp_delete_post( $speaker_id, true );
-
- if ( ! $result ) {
- wp_send_json_error( __( 'Failed to delete speaker', 'wpfaevent' ) );
- }
-
- wp_send_json_success();
- }
}
\ No newline at end of file
diff --git a/admin/partials/ajax-handlers/class-wpfaevent-event-handler.php b/admin/partials/ajax-handlers/class-wpfaevent-event-handler.php
new file mode 100644
index 0000000..0ac6ceb
--- /dev/null
+++ b/admin/partials/ajax-handlers/class-wpfaevent-event-handler.php
@@ -0,0 +1,398 @@
+
+ * @since 1.0.0
+ */
+
+class Wpfaevent_Event_Handler {
+
+ /**
+ * The plugin name.
+ *
+ * @since 1.0.0
+ * @access private
+ * @var string $plugin_name The plugin name.
+ */
+ private $plugin_name;
+
+ /**
+ * The version of this plugin.
+ *
+ * @since 1.0.0
+ * @access private
+ * @var string $version The current version of this plugin.
+ */
+ private $version;
+
+ /**
+ * Initialize the class.
+ *
+ * @since 1.0.0
+ * @param string $plugin_name The name of this plugin.
+ * @param string $version The version of this plugin.
+ */
+ public function __construct( $plugin_name, $version ) {
+ $this->plugin_name = $plugin_name;
+ $this->version = $version;
+ }
+
+ /**
+ * Handle AJAX request to get event data.
+ *
+ * @since 1.0.0
+ */
+ public function ajax_get_event() {
+ // Verify nonce. Third param 'false' ensures we can handle the error response manually via JSON.
+ if ( ! check_ajax_referer( 'wpfa_events_ajax', 'nonce', false ) ) {
+ wp_send_json_error(
+ array(
+ 'message' => esc_html__( 'Invalid nonce', 'wpfaevent' ),
+ ),
+ 403
+ );
+ }
+
+ // Check permissions
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error(
+ array( 'message' => __( 'Unauthorized', 'wpfaevent' ) ),
+ 403
+ );
+ }
+
+ $event_id = isset( $_POST['event_id'] ) ? absint( $_POST['event_id'] ) : 0;
+
+ if ( ! $event_id ) {
+ wp_send_json_error( esc_html__( 'Invalid event ID', 'wpfaevent' ) );
+ }
+
+ $event = get_post( $event_id );
+
+ if ( ! $event || $event->post_type !== 'wpfa_event' ) {
+ wp_send_json_error( esc_html__( 'Event not found', 'wpfaevent' ) );
+ }
+
+ $data = array(
+ 'id' => $event_id,
+ 'title' => $event->post_title,
+ 'content' => $event->post_content,
+ 'excerpt' => $event->post_excerpt,
+ 'start_date' => get_post_meta( $event_id, 'wpfa_event_start_date', true ),
+ 'end_date' => get_post_meta( $event_id, 'wpfa_event_end_date', true ),
+ 'location' => get_post_meta( $event_id, 'wpfa_event_location', true ),
+ 'event_url' => get_post_meta( $event_id, 'wpfa_event_url', true ),
+ 'registration_link' => get_post_meta( $event_id, 'wpfa_event_registration_link', true ),
+ 'cfs_link' => get_post_meta( $event_id, 'wpfa_event_cfs_link', true ),
+ 'featured_image' => get_post_thumbnail_id( $event_id ),
+ );
+
+ wp_send_json_success( $data );
+ }
+
+ /**
+ * Handle AJAX request to add a new event.
+ *
+ * @since 1.0.0
+ */
+ public function ajax_add_event() {
+ // Verify nonce. Third param 'false' ensures we can handle the error response manually via JSON.
+ if ( ! check_ajax_referer( 'wpfa_events_ajax', 'nonce', false ) ) {
+ wp_send_json_error(
+ array(
+ 'message' => esc_html__( 'Invalid nonce', 'wpfaevent' ),
+ ),
+ 403
+ );
+ }
+
+ // Check permissions
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error(
+ array(
+ 'message' => __( 'Unauthorized', 'wpfaevent' ),
+ ),
+ 403
+ );
+ }
+
+ $title = isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : '';
+ $excerpt = isset( $_POST['excerpt'] ) ? sanitize_text_field( wp_unslash( $_POST['excerpt'] ) ) : '';
+ $start_date = isset( $_POST['start_date'] ) ? sanitize_text_field( wp_unslash( $_POST['start_date'] ) ) : '';
+ $location = isset( $_POST['location'] ) ? sanitize_text_field( wp_unslash( $_POST['location'] ) ) : '';
+ $registration_link = isset( $_POST['registration_link'] ) ? esc_url_raw( wp_unslash( $_POST['registration_link'] ) ) : '';
+
+ // Validate required fields
+ $required_fields = array(
+ 'title' => $title,
+ 'excerpt' => $excerpt,
+ 'start_date' => $start_date,
+ 'location' => $location,
+ 'registration_link' => $registration_link,
+ );
+
+ foreach ( $required_fields as $field_name => $field_value ) {
+ if ( empty( $field_value ) ) {
+ wp_send_json_error( sprintf( esc_html__( 'Missing required field: %s', 'wpfaevent' ), $field_name ) );
+ }
+ }
+
+ // Create event post
+ $event_data = array(
+ 'post_title' => $title,
+ 'post_content' => isset( $_POST['content'] ) ? wp_kses_post( wp_unslash( $_POST['content'] ) ) : '',
+ 'post_excerpt' => $excerpt,
+ 'post_type' => 'wpfa_event',
+ 'post_status' => 'publish',
+ );
+
+ $event_id = wp_insert_post( $event_data );
+
+ if ( is_wp_error( $event_id ) || 0 === $event_id ) {
+ $error_message = is_wp_error( $event_id ) ? $event_id->get_error_message() : esc_html__( 'Failed to create event.', 'wpfaevent' );
+ wp_send_json_error( $error_message );
+ }
+
+ // Save meta fields - using CORRECT form field names
+ $meta_fields = array(
+ 'wpfa_event_start_date' => 'start_date',
+ 'wpfa_event_end_date' => 'end_date',
+ 'wpfa_event_time' => 'time',
+ 'wpfa_event_location' => 'location',
+ 'wpfa_event_lead_text' => 'lead_text',
+ 'wpfa_event_registration_link' => 'registration_link',
+ 'wpfa_event_cfs_link' => 'cfs_link',
+ );
+
+ foreach ( $meta_fields as $meta_key => $post_key ) {
+ if ( isset( $_POST[ $post_key ] ) ) {
+ $value = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) );
+
+ // Special handling for URL fields
+ if ( in_array( $post_key, array( 'registration_link', 'cfs_link' ), true ) ) {
+ $value = esc_url_raw( wp_unslash( $_POST[ $post_key ] ) );
+ }
+
+ if ( strlen( $value ) > 0 ) {
+ update_post_meta( $event_id, $meta_key, $value );
+ }
+ }
+ }
+
+ // Handle featured image upload - use CORRECT file field name
+ if ( ! empty( $_FILES['featured_image']['name'] ) ) {
+
+ // Validate file type
+ $allowed_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp' );
+ $file_type = $_FILES['featured_image']['type'];
+
+ if ( ! in_array( $file_type, $allowed_types, true ) ) {
+ wp_send_json_error( esc_html__( 'Invalid file type. Only JPG, PNG, GIF, and WebP are allowed.', 'wpfaevent' ) );
+ }
+
+ // Validate file size (2MB max)
+ $max_size = 2 * 1024 * 1024; // 2MB in bytes
+ if ( $_FILES['featured_image']['size'] > $max_size ) {
+ wp_send_json_error( esc_html__( 'File size exceeds 2MB limit.', 'wpfaevent' ) );
+ }
+
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ require_once ABSPATH . 'wp-admin/includes/image.php';
+ require_once ABSPATH . 'wp-admin/includes/media.php';
+
+ // Upload and create attachment
+ $attachment_id = media_handle_upload( 'featured_image', $event_id );
+
+ if ( is_wp_error( $attachment_id ) ) {
+ wp_send_json_error( sprintf( esc_html__( 'Image upload failed: %s', 'wpfaevent' ), $attachment_id->get_error_message() ) );
+ }
+
+ // Set as featured image
+ set_post_thumbnail( $event_id, $attachment_id );
+ }
+
+ wp_send_json_success(
+ array(
+ 'event_id' => $event_id,
+ 'message' => esc_html__( 'Event created successfully!', 'wpfaevent' ),
+ )
+ );
+ }
+
+ /**
+ * Handle AJAX request to update an event.
+ *
+ * @since 1.0.0
+ */
+ public function ajax_update_event() {
+ // Verify nonce. Third param 'false' ensures we can handle the error response manually via JSON.
+ if ( ! check_ajax_referer( 'wpfa_events_ajax', 'nonce', false ) ) {
+ wp_send_json_error(
+ array(
+ 'message' => esc_html__( 'Invalid nonce', 'wpfaevent' ),
+ ),
+ 403
+ );
+ }
+
+ // Check permissions
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error(
+ array( 'message' => __( 'Unauthorized', 'wpfaevent' ) ),
+ 403
+ );
+ }
+
+ $event_id = isset( $_POST['event_id'] ) ? absint( $_POST['event_id'] ) : 0;
+
+ if ( ! $event_id ) {
+ wp_send_json_error( __( 'Invalid event ID', 'wpfaevent' ) );
+ }
+
+ // Verify event exists and user can edit it
+ $event = get_post( $event_id );
+ if ( ! $event || $event->post_type !== 'wpfa_event' || ! current_user_can( 'edit_post', $event_id ) ) {
+ wp_send_json_error( __( 'Cannot edit this event', 'wpfaevent' ) );
+ }
+
+ // Validate required fields
+ $required_fields = array( 'title', 'excerpt', 'start_date', 'location', 'registration_link' );
+ foreach ( $required_fields as $field ) {
+ if ( empty( $_POST[ $field ] ) ) {
+ wp_send_json_error( sprintf( esc_html__( 'Missing required field: %s', 'wpfaevent' ), $field ) );
+ }
+ }
+
+ // Update post
+ $event_data = array(
+ 'ID' => $event_id,
+ 'post_title' => sanitize_text_field( wp_unslash( $_POST['title'] ) ),
+ 'post_content' => wp_kses_post( wp_unslash( $_POST['content'] ?? '' ) ),
+ 'post_excerpt' => sanitize_text_field( wp_unslash( $_POST['excerpt'] ) ),
+ );
+
+ $update_result = wp_update_post( $event_data, true );
+
+ if ( is_wp_error( $update_result ) || 0 === $update_result ) {
+ wp_send_json_error(
+ array(
+ 'message' => esc_html__( 'Failed to update event.', 'wpfaevent' ),
+ ),
+ 500
+ );
+ }
+
+ // Save meta fields
+ $meta_fields = array(
+ 'wpfa_event_start_date' => 'start_date',
+ 'wpfa_event_end_date' => 'end_date',
+ 'wpfa_event_time' => 'time',
+ 'wpfa_event_location' => 'location',
+ 'wpfa_event_lead_text' => 'lead_text',
+ 'wpfa_event_url' => 'event_url',
+ 'wpfa_event_registration_link' => 'registration_link',
+ 'wpfa_event_cfs_link' => 'cfs_link',
+ );
+
+ foreach ( $meta_fields as $meta_key => $post_key ) {
+ if ( isset( $_POST[ $post_key ] ) ) {
+ $value = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) );
+
+ if ( in_array( $post_key, array( 'event_url', 'registration_link', 'cfs_link' ), true ) ) {
+ $value = esc_url_raw( wp_unslash( $_POST[ $post_key ] ) );
+ }
+
+ if ( strlen( $value ) === 0 ) {
+ delete_post_meta( $event_id, $meta_key );
+ } else {
+ update_post_meta( $event_id, $meta_key, $value );
+ }
+ }
+ }
+
+ // Handle featured image upload
+ if ( ! empty( $_FILES['featured_image']['name'] ) ) {
+ // Validate file type
+ $allowed_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp' );
+ $file_type = $_FILES['featured_image']['type'];
+
+ if ( ! in_array( $file_type, $allowed_types, true ) ) {
+ wp_send_json_error( esc_html__( 'Invalid file type. Only JPG, PNG, GIF, and WebP are allowed.', 'wpfaevent' ) );
+ }
+
+ // Validate file size (2MB max)
+ $max_size = 2 * 1024 * 1024; // 2MB in bytes
+ if ( $_FILES['featured_image']['size'] > $max_size ) {
+ wp_send_json_error( esc_html__( 'File size exceeds 2MB limit.', 'wpfaevent' ) );
+ }
+
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ require_once ABSPATH . 'wp-admin/includes/image.php';
+ require_once ABSPATH . 'wp-admin/includes/media.php';
+
+ // Upload and create attachment
+ $attachment_id = media_handle_upload( 'featured_image', $event_id );
+
+ if ( is_wp_error( $attachment_id ) ) {
+ wp_send_json_error( sprintf( esc_html__( 'Image upload failed: %s', 'wpfaevent' ), $attachment_id->get_error_message() ) );
+ }
+
+ // Set as featured image
+ set_post_thumbnail( $event_id, $attachment_id );
+ } elseif ( isset( $_POST['remove_featured_image'] ) && $_POST['remove_featured_image'] === 'true' ) {
+ // Remove featured image
+ delete_post_thumbnail( $event_id );
+ }
+
+ wp_send_json_success();
+ }
+
+ /**
+ * Handle AJAX request to delete an event.
+ *
+ * @since 1.0.0
+ */
+ public function ajax_delete_event() {
+ // Verify nonce. Third param 'false' ensures we can handle the error response manually via JSON.
+ if ( ! check_ajax_referer( 'wpfa_events_ajax', 'nonce', false ) ) {
+ wp_send_json_error(
+ array(
+ 'message' => esc_html__( 'Invalid nonce', 'wpfaevent' ),
+ ),
+ 403
+ );
+ }
+
+ // Check permissions
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error(
+ array( 'message' => __( 'Unauthorized', 'wpfaevent' ) ),
+ 403
+ );
+ }
+
+ $event_id = isset( $_POST['event_id'] ) ? absint( $_POST['event_id'] ) : 0;
+
+ if ( ! $event_id ) {
+ wp_send_json_error( __( 'Invalid event ID', 'wpfaevent' ) );
+ }
+
+ // Verify event exists and user can delete it
+ $event = get_post( $event_id );
+ if ( ! $event || $event->post_type !== 'wpfa_event' || ! current_user_can( 'delete_post', $event_id ) ) {
+ wp_send_json_error( __( 'Cannot delete this event', 'wpfaevent' ) );
+ }
+
+ // Delete the event
+ $result = wp_delete_post( $event_id, true );
+
+ if ( ! $result ) {
+ wp_send_json_error( __( 'Failed to delete event', 'wpfaevent' ) );
+ }
+
+ wp_send_json_success();
+ }
+}
diff --git a/admin/partials/ajax-handlers/class-wpfaevent-footer-handler.php b/admin/partials/ajax-handlers/class-wpfaevent-footer-handler.php
new file mode 100644
index 0000000..5890385
--- /dev/null
+++ b/admin/partials/ajax-handlers/class-wpfaevent-footer-handler.php
@@ -0,0 +1,78 @@
+
+ * @since 1.0.0
+ */
+
+class Wpfaevent_Footer_Handler {
+
+ /**
+ * The plugin name.
+ *
+ * @since 1.0.0
+ * @access private
+ * @var string $plugin_name The plugin name.
+ */
+ private $plugin_name;
+
+ /**
+ * The version of this plugin.
+ *
+ * @since 1.0.0
+ * @access private
+ * @var string $version The current version of this plugin.
+ */
+ private $version;
+
+ /**
+ * Initialize the class.
+ *
+ * @since 1.0.0
+ * @param string $plugin_name The name of this plugin.
+ * @param string $version The version of this plugin.
+ */
+ public function __construct( $plugin_name, $version ) {
+ $this->plugin_name = $plugin_name;
+ $this->version = $version;
+ }
+
+ /**
+ * Handle AJAX request to update footer text.
+ *
+ * @since 1.0.0
+ * @return void
+ */
+ public function ajax_update_footer_text() {
+ // Verify nonce. Third param 'false' ensures we can handle the error response manually via JSON.
+ if ( ! check_ajax_referer( 'wpfa_events_ajax', 'nonce', false ) ) {
+ wp_send_json_error( esc_html__( 'Invalid nonce', 'wpfaevent' ), 403 );
+ }
+
+ // Check permissions
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error( esc_html__( 'Unauthorized', 'wpfaevent' ), 403 );
+ }
+
+ // Check for the correct parameter name
+ if ( ! isset( $_POST['footer_text'] ) ) {
+ wp_send_json_error(
+ array(
+ 'message' => esc_html__( 'Footer text is required', 'wpfaevent' ),
+ )
+ );
+ }
+
+ $footer_text = sanitize_textarea_field( wp_unslash( $_POST['footer_text'] ) );
+
+ // Save to options
+ if ( update_option( 'wpfa_footer_text', $footer_text ) ) {
+ wp_send_json_success( esc_html__( 'Footer text updated successfully', 'wpfaevent' ) );
+ } else {
+ wp_send_json_error( esc_html__( 'Failed to update footer text', 'wpfaevent' ) );
+ }
+ }
+}
diff --git a/admin/partials/ajax-handlers/class-wpfaevent-speakers-handler.php b/admin/partials/ajax-handlers/class-wpfaevent-speakers-handler.php
new file mode 100644
index 0000000..b052433
--- /dev/null
+++ b/admin/partials/ajax-handlers/class-wpfaevent-speakers-handler.php
@@ -0,0 +1,496 @@
+
+ * @since 1.0.0
+ */
+
+class Wpfaevent_Speakers_Handler {
+
+ /**
+ * The plugin name.
+ *
+ * @since 1.0.0
+ * @access private
+ * @var string $plugin_name The plugin name.
+ */
+ private $plugin_name;
+
+ /**
+ * The version of this plugin.
+ *
+ * @since 1.0.0
+ * @access private
+ * @var string $version The current version of this plugin.
+ */
+ private $version;
+
+ /**
+ * Initialize the class.
+ *
+ * @since 1.0.0
+ * @param string $plugin_name The name of this plugin.
+ * @param string $version The version of this plugin.
+ */
+ public function __construct( $plugin_name, $version ) {
+ $this->plugin_name = $plugin_name;
+ $this->version = $version;
+ }
+
+ /**
+ * Handle AJAX request to get speaker data.
+ *
+ * @since 1.0.0
+ */
+ public function ajax_get_speaker() {
+ // Verify nonce
+ if ( ! check_ajax_referer( 'wpfa_speakers_ajax', 'nonce', false ) ) {
+ wp_send_json_error(
+ array(
+ 'message' => esc_html__( 'Invalid nonce', 'wpfaevent' ),
+ ),
+ 403
+ );
+ }
+
+ // Check permissions
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error(
+ array( 'message' => __( 'Unauthorized', 'wpfaevent' ) ),
+ 403
+ );
+ }
+
+ $speaker_id = isset( $_POST['speaker_id'] ) ? absint( $_POST['speaker_id'] ) : 0;
+
+ if ( ! $speaker_id ) {
+ wp_send_json_error( esc_html__( 'Invalid speaker ID', 'wpfaevent' ) );
+ }
+
+ $speaker = get_post( $speaker_id );
+
+ if ( ! $speaker || $speaker->post_type !== 'wpfa_speaker' ) {
+ wp_send_json_error( esc_html__( 'Speaker not found', 'wpfaevent' ) );
+ }
+
+ // Get category term
+ $category = '';
+ $category_slug = '';
+ $terms = wp_get_object_terms( $speaker_id, 'wpfa_speaker_category' );
+ if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
+ $category = $terms[0]->name;
+ $category_slug = $terms[0]->slug;
+ }
+
+ $data = array(
+ 'id' => $speaker_id,
+ 'name' => $speaker->post_title,
+ 'position' => get_post_meta( $speaker_id, 'wpfa_speaker_position', true ),
+ 'organization' => get_post_meta( $speaker_id, 'wpfa_speaker_organization', true ),
+ 'bio' => get_post_meta( $speaker_id, 'wpfa_speaker_bio', true ),
+ 'headshot_url' => get_post_meta( $speaker_id, 'wpfa_speaker_headshot_url', true ),
+ 'linkedin' => get_post_meta( $speaker_id, 'wpfa_speaker_linkedin', true ),
+ 'twitter' => get_post_meta( $speaker_id, 'wpfa_speaker_twitter', true ),
+ 'github' => get_post_meta( $speaker_id, 'wpfa_speaker_github', true ),
+ 'website' => get_post_meta( $speaker_id, 'wpfa_speaker_website', true ),
+ 'category' => $category,
+ 'category_slug' => $category_slug,
+ 'talk_title' => get_post_meta( $speaker_id, 'wpfa_speaker_talk_title', true ),
+ 'talk_date' => get_post_meta( $speaker_id, 'wpfa_speaker_talk_date', true ),
+ 'talk_time' => get_post_meta( $speaker_id, 'wpfa_speaker_talk_time', true ),
+ 'talk_end_time' => get_post_meta( $speaker_id, 'wpfa_speaker_talk_end_time', true ),
+ 'talk_abstract' => get_post_meta( $speaker_id, 'wpfa_speaker_talk_abstract', true ),
+ );
+
+ wp_send_json_success( $data );
+ }
+
+ /**
+ * Handle AJAX request to add a new speaker.
+ *
+ * @since 1.0.0
+ */
+ public function ajax_add_speaker() {
+ // Verify nonce
+ if ( ! check_ajax_referer( 'wpfa_speakers_ajax', 'nonce', false ) ) {
+ wp_send_json_error(
+ array(
+ 'message' => esc_html__( 'Invalid nonce', 'wpfaevent' ),
+ ),
+ 403
+ );
+ }
+
+ // Check permissions
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error(
+ array(
+ 'message' => __( 'Unauthorized', 'wpfaevent' ),
+ ),
+ 403
+ );
+ }
+
+ // Validate required fields
+ $required_fields = array( 'name', 'position', 'bio', 'talk_title', 'talk_date', 'talk_time', 'talk_end_time' );
+ foreach ( $required_fields as $field ) {
+ if ( empty( $_POST[ $field ] ) ) {
+ wp_send_json_error( sprintf( esc_html__( 'Missing required field: %s', 'wpfaevent' ), $field ) );
+ }
+ }
+
+ // Create speaker post
+ $speaker_data = array(
+ 'post_title' => sanitize_text_field( wp_unslash( $_POST['name'] ) ),
+ 'post_type' => 'wpfa_speaker',
+ 'post_status' => 'publish',
+ 'post_content' => '',
+ );
+
+ $speaker_id = wp_insert_post( $speaker_data );
+
+ if ( is_wp_error( $speaker_id ) ) {
+ wp_send_json_error( $speaker_id->get_error_message() );
+ }
+
+ // Handle image upload
+ $image_url = '';
+ if ( ! empty( $_FILES['image_upload']['name'] ) ) {
+ // Validate file type
+ $allowed_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp' );
+ $file_type = $_FILES['image_upload']['type'];
+
+ if ( ! in_array( $file_type, $allowed_types, true ) ) {
+ wp_send_json_error( esc_html__( 'Invalid file type. Only JPG, PNG, GIF, and WebP are allowed.', 'wpfaevent' ) );
+ }
+
+ // Validate file size (2MB max)
+ $max_size = 2 * 1024 * 1024; // 2MB in bytes
+ if ( $_FILES['image_upload']['size'] > $max_size ) {
+ wp_send_json_error( esc_html__( 'File size exceeds 2MB limit.', 'wpfaevent' ) );
+ }
+
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ require_once ABSPATH . 'wp-admin/includes/image.php';
+ require_once ABSPATH . 'wp-admin/includes/media.php';
+
+ // Upload and create attachment
+ $attachment_id = media_handle_upload( 'image_upload', 0 );
+
+ if ( is_wp_error( $attachment_id ) ) {
+ wp_send_json_error( sprintf( esc_html__( 'Image upload failed: %s', 'wpfaevent' ), $attachment_id->get_error_message() ) );
+ }
+
+ $image_url = wp_get_attachment_url( $attachment_id );
+ } elseif ( ! empty( $_POST['image_url'] ) ) {
+ $image_url = esc_url_raw( wp_unslash( $_POST['image_url'] ) );
+ }
+
+ // Save meta fields
+ $meta_fields = array(
+ 'wpfa_speaker_position' => 'position',
+ 'wpfa_speaker_organization' => 'organization',
+ 'wpfa_speaker_bio' => 'bio',
+ 'wpfa_speaker_headshot_url' => 'image_url',
+ 'wpfa_speaker_linkedin' => 'linkedin',
+ 'wpfa_speaker_twitter' => 'twitter',
+ 'wpfa_speaker_github' => 'github',
+ 'wpfa_speaker_website' => 'website',
+ );
+
+ foreach ( $meta_fields as $meta_key => $post_key ) {
+ if ( $post_key === 'image_url' && ! empty( $image_url ) ) {
+ // Use uploaded image URL or provided URL
+ update_post_meta( $speaker_id, $meta_key, $image_url );
+ } elseif ( isset( $_POST[ $post_key ] ) ) {
+ $value = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) );
+
+ if ( $post_key === 'bio' ) {
+ $value = wp_kses_post( wp_unslash( $_POST[ $post_key ] ) );
+ } elseif ( in_array( $post_key, array( 'linkedin', 'twitter', 'github', 'website' ), true ) ) {
+ $value = esc_url_raw( wp_unslash( $_POST[ $post_key ] ) );
+ } else {
+ $value = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) );
+ }
+
+ if ( strlen( $value ) === 0 ) {
+ delete_post_meta( $speaker_id, $meta_key );
+ } else {
+ update_post_meta( $speaker_id, $meta_key, $value );
+ }
+ }
+ }
+
+ $session_fields = array(
+ 'wpfa_speaker_talk_title' => 'talk_title',
+ 'wpfa_speaker_talk_date' => 'talk_date',
+ 'wpfa_speaker_talk_time' => 'talk_time',
+ 'wpfa_speaker_talk_end_time' => 'talk_end_time',
+ 'wpfa_speaker_talk_abstract' => 'talk_abstract',
+ );
+
+ foreach ( $session_fields as $meta_key => $post_key ) {
+ if ( isset( $_POST[ $post_key ] ) ) {
+
+ if ( $post_key === 'talk_abstract' ) {
+ $value = wp_kses_post( wp_unslash( $_POST[ $post_key ] ) );
+ } else {
+ $value = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) );
+ }
+
+ if ( strlen( $value ) === 0 ) {
+ delete_post_meta( $speaker_id, $meta_key );
+ } else {
+ update_post_meta( $speaker_id, $meta_key, $value );
+ }
+ }
+ }
+
+ if ( isset( $_POST['category'] ) ) {
+ $category = sanitize_text_field( wp_unslash( $_POST['category'] ) );
+
+ // If it's numeric, it's a term ID
+ if ( is_numeric( $category ) ) {
+ $term_id = (int) $category;
+ wp_set_object_terms( $speaker_id, $term_id, 'wpfa_speaker_category' );
+ }
+ // If it's "_custom" with custom value
+ elseif ( $category === '_custom' && isset( $_POST['category_custom'] ) && ! empty( $_POST['category_custom'] ) ) {
+ $category_name = sanitize_text_field( wp_unslash( $_POST['category_custom'] ) );
+ wp_set_object_terms( $speaker_id, $category_name, 'wpfa_speaker_category' );
+ }
+ // If it's a slug/name
+ elseif ( ! empty( $category ) && $category !== '_custom' ) {
+ wp_set_object_terms( $speaker_id, $category, 'wpfa_speaker_category' );
+ }
+ // Empty
+ else {
+ wp_set_object_terms( $speaker_id, array(), 'wpfa_speaker_category' );
+ }
+ }
+
+ wp_send_json_success( array( 'speaker_id' => $speaker_id ) );
+ }
+
+ /**
+ * Handle AJAX request to update a speaker.
+ *
+ * @since 1.0.0
+ */
+ public function ajax_update_speaker() {
+ // Verify nonce
+ if ( ! check_ajax_referer( 'wpfa_speakers_ajax', 'nonce', false ) ) {
+ wp_send_json_error(
+ array(
+ 'message' => esc_html__( 'Invalid nonce', 'wpfaevent' ),
+ ),
+ 403
+ );
+ }
+
+ // Check permissions
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error(
+ array( 'message' => __( 'Unauthorized', 'wpfaevent' ) ),
+ 403
+ );
+ }
+
+ $speaker_id = isset( $_POST['speaker_id'] ) ? absint( $_POST['speaker_id'] ) : 0;
+
+ if ( ! $speaker_id ) {
+ wp_send_json_error( __( 'Invalid speaker ID', 'wpfaevent' ) );
+ }
+
+ // Verify speaker exists and user can edit it
+ $speaker = get_post( $speaker_id );
+ if ( ! $speaker || $speaker->post_type !== 'wpfa_speaker' || ! current_user_can( 'edit_post', $speaker_id ) ) {
+ wp_send_json_error( __( 'Cannot edit this speaker', 'wpfaevent' ) );
+ }
+
+ // Validate required fields
+ $required_fields = array( 'name', 'position', 'bio', 'talk_title', 'talk_date', 'talk_time', 'talk_end_time' );
+ foreach ( $required_fields as $field ) {
+ if ( empty( $_POST[ $field ] ) ) {
+ wp_send_json_error( sprintf( esc_html__( 'Missing required field: %s', 'wpfaevent' ), $field ) );
+ }
+ }
+
+ // Update post title if name changed
+ if ( ! empty( $_POST['name'] ) ) {
+ wp_update_post(
+ array(
+ 'ID' => $speaker_id,
+ 'post_title' => sanitize_text_field( wp_unslash( $_POST['name'] ) ),
+ )
+ );
+ }
+
+ // Handle image upload
+ $image_url = '';
+ if ( ! empty( $_FILES['image_upload']['name'] ) ) {
+ // Validate file type
+ $allowed_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp' );
+ $file_type = $_FILES['image_upload']['type'];
+
+ if ( ! in_array( $file_type, $allowed_types, true ) ) {
+ wp_send_json_error( esc_html__( 'Invalid file type. Only JPG, PNG, GIF, and WebP are allowed.', 'wpfaevent' ) );
+ }
+
+ // Validate file size (2MB max)
+ $max_size = 2 * 1024 * 1024; // 2MB in bytes
+ if ( $_FILES['image_upload']['size'] > $max_size ) {
+ wp_send_json_error( esc_html__( 'File size exceeds 2MB limit.', 'wpfaevent' ) );
+ }
+
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ require_once ABSPATH . 'wp-admin/includes/image.php';
+ require_once ABSPATH . 'wp-admin/includes/media.php';
+
+ // Upload and create attachment
+ $attachment_id = media_handle_upload( 'image_upload', $speaker_id );
+
+ if ( is_wp_error( $attachment_id ) ) {
+ wp_send_json_error( sprintf( esc_html__( 'Image upload failed: %s', 'wpfaevent' ), $attachment_id->get_error_message() ) );
+ }
+
+ $image_url = wp_get_attachment_url( $attachment_id );
+ } elseif ( ! empty( $_POST['image_url'] ) ) {
+ $image_url = esc_url_raw( wp_unslash( $_POST['image_url'] ) );
+ }
+
+ // Save meta fields
+ $meta_fields = array(
+ 'wpfa_speaker_position' => 'position',
+ 'wpfa_speaker_organization' => 'organization',
+ 'wpfa_speaker_bio' => 'bio',
+ 'wpfa_speaker_headshot_url' => 'image_url',
+ 'wpfa_speaker_linkedin' => 'linkedin',
+ 'wpfa_speaker_twitter' => 'twitter',
+ 'wpfa_speaker_github' => 'github',
+ 'wpfa_speaker_website' => 'website',
+ );
+
+ foreach ( $meta_fields as $meta_key => $post_key ) {
+ if ( $post_key === 'image_url' && ! empty( $image_url ) ) {
+ // Use uploaded image URL or provided URL
+ update_post_meta( $speaker_id, $meta_key, $image_url );
+ } elseif ( isset( $_POST[ $post_key ] ) ) {
+
+ if ( $post_key === 'bio' ) {
+ $value = wp_kses_post( wp_unslash( $_POST[ $post_key ] ) );
+ } elseif ( in_array( $post_key, array( 'linkedin', 'twitter', 'github', 'website' ), true ) ) {
+ $value = esc_url_raw( wp_unslash( $_POST[ $post_key ] ) );
+ } else {
+ $value = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) );
+ }
+
+ // Delete meta when field is intentionally cleared to avoid storing empty values
+ if ( strlen( $value ) === 0 ) {
+ delete_post_meta( $speaker_id, $meta_key );
+ } else {
+ update_post_meta( $speaker_id, $meta_key, $value );
+ }
+ }
+ }
+
+ $session_fields = array(
+ 'wpfa_speaker_talk_title' => 'talk_title',
+ 'wpfa_speaker_talk_date' => 'talk_date',
+ 'wpfa_speaker_talk_time' => 'talk_time',
+ 'wpfa_speaker_talk_end_time' => 'talk_end_time',
+ 'wpfa_speaker_talk_abstract' => 'talk_abstract',
+ );
+
+ foreach ( $session_fields as $meta_key => $post_key ) {
+ if ( isset( $_POST[ $post_key ] ) ) {
+
+ if ( $post_key === 'talk_abstract' ) {
+ $value = wp_kses_post( wp_unslash( $_POST[ $post_key ] ) );
+ } else {
+ $value = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) );
+ }
+
+ if ( strlen( $value ) === 0 ) {
+ delete_post_meta( $speaker_id, $meta_key );
+ } else {
+ update_post_meta( $speaker_id, $meta_key, $value );
+ }
+ }
+ }
+
+ if ( isset( $_POST['category'] ) ) {
+ $category = sanitize_text_field( wp_unslash( $_POST['category'] ) );
+
+ // If it's numeric, it's a term ID
+ if ( is_numeric( $category ) ) {
+ $term_id = (int) $category;
+ wp_set_object_terms( $speaker_id, $term_id, 'wpfa_speaker_category' );
+ }
+ // If it's "_custom" with custom value
+ elseif ( $category === '_custom' && isset( $_POST['category_custom'] ) && ! empty( $_POST['category_custom'] ) ) {
+ $category_name = sanitize_text_field( wp_unslash( $_POST['category_custom'] ) );
+ wp_set_object_terms( $speaker_id, $category_name, 'wpfa_speaker_category' );
+ }
+ // If it's a slug/name
+ elseif ( ! empty( $category ) && $category !== '_custom' ) {
+ wp_set_object_terms( $speaker_id, $category, 'wpfa_speaker_category' );
+ }
+ // Empty
+ else {
+ wp_set_object_terms( $speaker_id, array(), 'wpfa_speaker_category' );
+ }
+ }
+
+ wp_send_json_success();
+ }
+
+ /**
+ * Handle AJAX request to delete a speaker.
+ *
+ * @since 1.0.0
+ */
+ public function ajax_delete_speaker() {
+ // Verify nonce
+ if ( ! check_ajax_referer( 'wpfa_speakers_ajax', 'nonce', false ) ) {
+ wp_send_json_error(
+ array(
+ 'message' => esc_html__( 'Invalid nonce', 'wpfaevent' ),
+ ),
+ 403
+ );
+ }
+
+ // Check permissions
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_send_json_error(
+ array( 'message' => __( 'Unauthorized', 'wpfaevent' ) ),
+ 403
+ );
+ }
+
+ $speaker_id = isset( $_POST['speaker_id'] ) ? absint( $_POST['speaker_id'] ) : 0;
+
+ if ( ! $speaker_id ) {
+ wp_send_json_error( __( 'Invalid speaker ID', 'wpfaevent' ) );
+ }
+
+ // Verify speaker exists and user can delete it
+ $speaker = get_post( $speaker_id );
+ if ( ! $speaker || $speaker->post_type !== 'wpfa_speaker' || ! current_user_can( 'delete_post', $speaker_id ) ) {
+ wp_send_json_error( __( 'Cannot delete this speaker', 'wpfaevent' ) );
+ }
+
+ // Delete the speaker
+ $result = wp_delete_post( $speaker_id, true );
+
+ if ( ! $result ) {
+ wp_send_json_error( __( 'Failed to delete speaker', 'wpfaevent' ) );
+ }
+
+ wp_send_json_success();
+ }
+}
diff --git a/includes/class-wpfaevent.php b/includes/class-wpfaevent.php
index 6f65867..8b37409 100644
--- a/includes/class-wpfaevent.php
+++ b/includes/class-wpfaevent.php
@@ -114,6 +114,11 @@ private function load_dependencies() {
require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/class-wpfaevent-admin.php';
require_once plugin_dir_path( dirname( __FILE__ ) ) . 'public/class-wpfaevent-public.php';
+ // AJAX handler classes
+ require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/partials/ajax-handlers/class-wpfaevent-footer-handler.php';
+ require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/partials/ajax-handlers/class-wpfaevent-event-handler.php';
+ require_once plugin_dir_path( dirname( __FILE__ ) ) . 'admin/partials/ajax-handlers/class-wpfaevent-speakers-handler.php';
+
// Optional utilities if present
if ( file_exists( plugin_dir_path( __FILE__ ) . 'class-wpfa-cli.php' ) ) {
require_once plugin_dir_path( __FILE__ ) . 'class-wpfa-cli.php';
@@ -197,10 +202,22 @@ private function define_admin_hooks() {
$this->loader->add_action( 'admin_notices', $this->plugin_admin, 'maybe_show_block_theme_notice' );
// Register AJAX handlers for speakers page
- $this->loader->add_action( 'wp_ajax_wpfa_get_speaker', $this->plugin_admin, 'ajax_get_speaker' );
- $this->loader->add_action( 'wp_ajax_wpfa_add_speaker', $this->plugin_admin, 'ajax_add_speaker' );
- $this->loader->add_action( 'wp_ajax_wpfa_update_speaker', $this->plugin_admin, 'ajax_update_speaker' );
- $this->loader->add_action( 'wp_ajax_wpfa_delete_speaker', $this->plugin_admin, 'ajax_delete_speaker' );
+ $plugin_speakers_handler = new Wpfaevent_Speakers_Handler( $this->plugin_name, $this->version );
+ $this->loader->add_action( 'wp_ajax_wpfa_get_speaker', $plugin_speakers_handler, 'ajax_get_speaker' );
+ $this->loader->add_action( 'wp_ajax_wpfa_add_speaker', $plugin_speakers_handler, 'ajax_add_speaker' );
+ $this->loader->add_action( 'wp_ajax_wpfa_update_speaker', $plugin_speakers_handler, 'ajax_update_speaker' );
+ $this->loader->add_action( 'wp_ajax_wpfa_delete_speaker', $plugin_speakers_handler, 'ajax_delete_speaker' );
+
+ // Register AJAX handlers for events page
+ $plugin_event_handler = new Wpfaevent_Event_Handler( $this->plugin_name, $this->version );
+ $this->loader->add_action( 'wp_ajax_wpfa_get_event', $plugin_event_handler, 'ajax_get_event' );
+ $this->loader->add_action( 'wp_ajax_wpfa_add_event', $plugin_event_handler, 'ajax_add_event' );
+ $this->loader->add_action( 'wp_ajax_wpfa_update_event', $plugin_event_handler, 'ajax_update_event' );
+ $this->loader->add_action( 'wp_ajax_wpfa_delete_event', $plugin_event_handler, 'ajax_delete_event' );
+
+ // Register AJAX handler for footer text update
+ $plugin_footer_handler = new Wpfaevent_Footer_Handler( $this->plugin_name, $this->version );
+ $this->loader->add_action( 'wp_ajax_wpfa_update_footer_text', $plugin_footer_handler, 'ajax_update_footer_text' );
// Instantiate the legacy plugin and keep a reference so we can reuse its methods
// if ( class_exists( 'Wpfaevent_Landing' ) ) {
diff --git a/includes/helpers/class-wpfaevent-news-functions.php b/includes/helpers/class-wpfaevent-news-functions.php
new file mode 100644
index 0000000..2513dc0
--- /dev/null
+++ b/includes/helpers/class-wpfaevent-news-functions.php
@@ -0,0 +1,223 @@
+get_item_quantity( 5 );
+
+ // Build an array of all the items, starting with element 0 (first element).
+ $rss_items = $rss->get_items( 0, $maxitems );
+
+ ob_start();
+
+ if ( $maxitems == 0 ) {
+ echo ' ' . esc_html__( 'No news items found.', 'wpfaevent' ) . ' ';
+ } else {
+ // Loop through each feed item and display each item as a hyperlink.
+ echo '';
+ foreach ( $rss_items as $item ) :
+ $permalink = esc_url( $item->get_permalink() );
+ $title = esc_html( $item->get_title() );
+ $date = esc_html( $item->get_date( 'F j, Y' ) );
+ $full_date = esc_attr( sprintf( __( 'Posted %s', 'wpfaevent' ), $item->get_date( 'F j, Y' ) ) );
+ ?>
+ -
+
+
+
+
+
+ ';
+
+ // Add "Visit Blog" CTA
+ echo '';
+ }
+
+ $news_html = ob_get_clean();
+
+ // Cache for 1 hour to reduce load
+ set_transient( 'wpfa_latest_news', $news_html, HOUR_IN_SECONDS );
+
+ echo $news_html;
+}
+
+/**
+ * Render fallback news content when RSS feed is not available.
+ *
+ * @since 1.0.0
+ * @return void Outputs HTML directly
+ */
+function wpfa_render_fallback_news() {
+ ob_start();
+ ?>
+
+
+ false,
+ 'message' => $rss->get_error_message(),
+ );
+ }
+
+ $item_count = $rss->get_item_quantity( 1 );
+ $items = $rss->get_items( 0, 1 );
+
+ return array(
+ 'success' => true,
+ 'message' => sprintf(
+ /* translators: %d: Number of news items fetched from the RSS feed */
+ __( 'Feed loaded successfully. Found %d items.', 'wpfaevent' ),
+ $item_count
+ ),
+ );
+}
\ No newline at end of file
diff --git a/includes/meta/class-wpfaevent-meta-event.php b/includes/meta/class-wpfaevent-meta-event.php
index 149fa18..34296d4 100644
--- a/includes/meta/class-wpfaevent-meta-event.php
+++ b/includes/meta/class-wpfaevent-meta-event.php
@@ -82,6 +82,45 @@ public static function register() {
)
);
+ // Event hero section lead text
+ register_post_meta(
+ self::$post_type,
+ 'wpfa_event_lead_text',
+ array(
+ 'type' => 'string',
+ 'single' => true,
+ 'show_in_rest' => true,
+ 'sanitize_callback' => 'sanitize_text_field',
+ 'description' => __( 'Event hero lead text', 'wpfaevent' ),
+ )
+ );
+
+ // Event registration link
+ register_post_meta(
+ self::$post_type,
+ 'wpfa_event_registration_link',
+ array(
+ 'type' => 'string',
+ 'single' => true,
+ 'show_in_rest' => true,
+ 'sanitize_callback' => 'esc_url_raw',
+ 'description' => __( 'Event registration link', 'wpfaevent' ),
+ )
+ );
+
+ // Call for speakers link
+ register_post_meta(
+ self::$post_type,
+ 'wpfa_event_cfs_link',
+ array(
+ 'type' => 'string',
+ 'single' => true,
+ 'show_in_rest' => true,
+ 'sanitize_callback' => 'esc_url_raw',
+ 'description' => __( 'Call for speakers link', 'wpfaevent' ),
+ )
+ );
+
// Event speakers (array of speaker IDs)
register_post_meta(
self::$post_type,
diff --git a/public/class-wpfaevent-public.php b/public/class-wpfaevent-public.php
index b700d19..2b29cde 100644
--- a/public/class-wpfaevent-public.php
+++ b/public/class-wpfaevent-public.php
@@ -3,22 +3,14 @@
/**
* The public-facing functionality of the plugin.
*
- * @link https://fossasia.org
- * @since 1.0.0
- *
- * @package Wpfaevent
- * @subpackage Wpfaevent/public
- */
-
-/**
- * The public-facing functionality of the plugin.
- *
- * Defines the plugin name, version, and two examples hooks for how to
- * enqueue the public-facing stylesheet and JavaScript.
+ * Defines asset loading, template-specific styles/scripts,
+ * and frontend JS configuration for WPFA templates.
*
* @package Wpfaevent
* @subpackage Wpfaevent/public
* @author FOSSASIA
+ * @link https://fossasia.org
+ * @since 1.0.0
*/
class Wpfaevent_Public {
@@ -181,6 +173,33 @@ public function enqueue_styles() {
'all'
);
+ // Footer script (handles footer text updates, shared with events config)
+ wp_enqueue_script(
+ $this->plugin_name . '-footer',
+ plugin_dir_url( __FILE__ ) . 'js/wpfaevent-footer.js',
+ array( 'jquery' ),
+ $this->version,
+ true
+ );
+
+ // Localize footer script data (shared with events config)
+ wp_localize_script(
+ $this->plugin_name . '-footer',
+ 'wpfaeventFooterConfig',
+ array(
+ 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
+ 'adminNonce' => wp_create_nonce( 'wpfa_events_ajax' ), // Same nonce as events
+ 'isAdmin' => current_user_can( 'manage_options' ),
+ 'i18n' => array(
+ 'saving' => __( 'Saving...', 'wpfaevent' ),
+ 'saveFooter' => __( 'Save Footer', 'wpfaevent' ),
+ 'footerSaveSuccess' => __( 'Footer text updated successfully.', 'wpfaevent' ),
+ 'footerSaveError' => __( 'Error updating footer text.', 'wpfaevent' ),
+ 'noPermission' => __( 'You do not have permission to perform this action.', 'wpfaevent' ),
+ ),
+ )
+ );
+
// Pagination component (only templates with pagination)
if ( $this->is_paginated_template() ) {
wp_enqueue_style(
@@ -277,23 +296,20 @@ public function enqueue_styles() {
);
}
- if ( is_page_template( 'page-speakers.php' ) ) {
+ // Events template
+ if ( is_page_template( 'page-events.php' ) ) {
wp_enqueue_style(
- $this->plugin_name . '-speakers',
- plugin_dir_url( dirname( __FILE__ ) ) . 'public/css/templates/speakers.css',
- array(
- $this->plugin_name,
- $this->plugin_name . '-navigation',
- $this->plugin_name . '-pagination',
- ),
+ $this->plugin_name . '-events',
+ plugin_dir_url( dirname( __FILE__ ) ) . 'public/css/templates/events.css',
+ array( $this->plugin_name ),
$this->version,
'all'
);
- // Enqueue speakers JavaScript
+ // Enqueue events JavaScript
wp_enqueue_script(
- $this->plugin_name . '-speakers',
- plugin_dir_url( __FILE__ ) . 'js/wpfaevent-speakers.js',
+ $this->plugin_name . '-events',
+ plugin_dir_url( __FILE__ ) . 'js/wpfaevent-events.js',
array( 'jquery' ),
$this->version,
true
@@ -301,30 +317,34 @@ public function enqueue_styles() {
// Pass data from PHP to JavaScript
wp_localize_script(
- $this->plugin_name . '-speakers',
- 'wpfaeventSpeakersConfig', // JavaScript object name
+ $this->plugin_name . '-events',
+ 'wpfaeventEventsConfig', // JavaScript object name
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
- 'adminNonce' => wp_create_nonce( 'wpfa_speakers_ajax' ),
+ 'adminNonce' => wp_create_nonce( 'wpfa_events_ajax' ),
'isAdmin' => current_user_can( 'manage_options' ),
// All translatable strings
'i18n' => array(
+ 'addEventTitle' => __( 'Create a New Event', 'wpfaevent' ),
+ 'editEventTitle' => __( 'Edit Event', 'wpfaevent' ),
+ 'addEventButton' => __( 'Create Card', 'wpfaevent' ),
+ 'editEventButton' => __( 'Save Changes', 'wpfaevent' ),
+ 'creating' => __( 'Creating...', 'wpfaevent' ),
+ 'saving' => __( 'Saving...', 'wpfaevent' ),
+ 'loading' => __( 'Loading...', 'wpfaevent' ),
'confirmDelete' => __( 'Are you sure you want to delete "%s"? This action cannot be undone.', 'wpfaevent' ),
- 'deleteSuccess' => __( 'Speaker deleted successfully. The page will now reload.', 'wpfaevent' ),
- 'deleteError' => __( 'Error deleting speaker', 'wpfaevent' ),
- 'deleteErrorGeneric' => __( 'Error deleting speaker. Please try again.', 'wpfaevent' ),
- 'addSuccess' => __( 'Speaker added successfully. The page will now reload.', 'wpfaevent' ),
- 'addError' => __( 'Error adding speaker', 'wpfaevent' ),
- 'addErrorGeneric' => __( 'Error adding speaker. Please try again.', 'wpfaevent' ),
- 'updateSuccess' => __( 'Speaker updated successfully. The page will now reload.', 'wpfaevent' ),
- 'updateError' => __( 'Error updating speaker', 'wpfaevent' ),
- 'updateErrorGeneric' => __( 'Error updating speaker. Please try again.', 'wpfaevent' ),
- 'loadError' => __( 'Error loading speaker data', 'wpfaevent' ),
- 'fetchError' => __( 'Error fetching speaker data', 'wpfaevent' ),
- 'fetchErrorGeneric' => __( 'Error fetching speaker data. Please try again.', 'wpfaevent' ),
+ 'deleteSuccess' => __( 'Event deleted successfully. The page will now reload.', 'wpfaevent' ),
+ 'deleteError' => __( 'Error deleting event', 'wpfaevent' ),
+ 'deleteErrorGeneric' => __( 'Error deleting event. Please try again.', 'wpfaevent' ),
+ 'addSuccess' => __( 'Event created successfully. The page will now reload.', 'wpfaevent' ),
+ 'addError' => __( 'Error creating event', 'wpfaevent' ),
+ 'addErrorGeneric' => __( 'Error creating event. Please try again.', 'wpfaevent' ),
+ 'updateSuccess' => __( 'Event updated successfully. The page will now reload.', 'wpfaevent' ),
+ 'updateError' => __( 'Error updating event', 'wpfaevent' ),
+ 'updateErrorGeneric' => __( 'Error updating event. Please try again.', 'wpfaevent' ),
'noPermission' => __( 'You do not have permission to perform this action.', 'wpfaevent' ),
- 'resultsCount' => __( 'Showing %d speakers', 'wpfaevent' ),
+ 'loadError' => __( 'Error loading event data', 'wpfaevent' ),
),
)
);
diff --git a/public/css/templates/events.css b/public/css/templates/events.css
new file mode 100644
index 0000000..13ebc13
--- /dev/null
+++ b/public/css/templates/events.css
@@ -0,0 +1,525 @@
+/* Events Listing Template - MVP Styling */
+
+/* Events-specific layout variables */
+.wpfaevent {
+ --card-radius: 16px;
+ --shadow: 0 10px 30px rgba(11, 11, 11, .08);
+ --container: 1200px;
+ --muted: #5b636a;
+}
+
+/* Page Layout */
+.page-layout {
+ display: grid;
+ grid-template-columns: 1fr 320px;
+ align-items: start;
+ gap: 30px;
+ margin-top: 40px;
+}
+
+@media (max-width: 980px) {
+ .page-layout {
+ grid-template-columns: 1fr;
+ }
+}
+
+.main-content {
+ background: #fff;
+ padding: 30px;
+ border-radius: var(--card-radius);
+ box-shadow: var(--shadow);
+}
+
+.sidebar {
+ background: #fff;
+ padding: 20px;
+ border-radius: var(--card-radius);
+ box-shadow: var(--shadow);
+}
+
+.sidebar h2 {
+ margin-top: 0;
+ font-size: 1.5rem;
+ color: var(--brand);
+}
+
+/* Hero Section (matches MVP) */
+.page-hero {
+ text-align: center;
+ padding: 60px 20px;
+ background: #fff;
+ margin-bottom: 10px;
+ position: relative;
+ overflow: hidden;
+}
+
+.hero-bg {
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ background: radial-gradient(40vw 40vw at 10% 50%, #feeceb, transparent),
+ radial-gradient(30vw 30vw at 90% 50%, #fddedc, transparent);
+ mix-blend-mode: normal;
+ opacity: .9;
+ pointer-events: none;
+}
+
+.page-hero > * {
+ position: relative;
+ z-index: 1;
+}
+
+.page-hero h1 {
+ margin: 0 0 10px;
+ font-size: 2.5rem;
+ color: var(--brand);
+}
+
+.page-hero p {
+ color: var(--muted);
+ font-size: 1.1rem;
+ max-width: 70ch;
+ margin: 0 auto;
+}
+
+/* Event Cards (matches MVP) */
+#events-container {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 20px;
+}
+
+.event-card {
+ background: #fff;
+ border-radius: var(--card-radius);
+ box-shadow: var(--shadow);
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ transition: transform .2s ease, box-shadow .2s ease;
+}
+
+.event-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 15px 35px rgba(11,11,11,.1);
+}
+
+.event-card-image {
+ height: 180px;
+ background-color: #f0f0f0;
+}
+
+.event-card-image img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.event-card-content {
+ padding: 15px;
+}
+
+.event-card-content h3 {
+ margin: 0 0 10px;
+ font-size: 1.25rem;
+}
+
+.event-card-content p {
+ margin: 5px 0 0;
+ color: var(--muted);
+ font-size: 0.95rem;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.event-card-description {
+ font-size: 0.9rem;
+ color: var(--muted);
+ line-height: 1.5;
+ margin-top: 10px;
+}
+
+.event-card-content p svg {
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+}
+
+.event-card-link {
+ text-decoration: none;
+ color: inherit;
+}
+
+/* Event Card Actions (Admin) */
+.event-card-actions {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ z-index: 5;
+ display: flex;
+ gap: 5px;
+}
+
+.event-card-actions button {
+ background: rgba(0,0,0,0.6);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 5px 8px;
+ font-size: 12px;
+ cursor: pointer;
+}
+
+.event-card-actions button:hover {
+ background: rgba(0,0,0,0.8);
+}
+
+.btn-edit-content {
+ background: #ffc107;
+ color: #212529 !important;
+ border: none;
+ border-radius: 4px;
+ padding: 5px 8px;
+ font-size: 12px;
+ cursor: pointer;
+ text-decoration: none;
+ font-weight: bold;
+ display: inline-block;
+}
+
+.btn-edit-content:hover {
+ background: #e0a800;
+}
+
+.event-card-actions .btn-edit-event {
+ background: #17a2b8;
+}
+
+.event-card-actions .btn-edit-event:hover {
+ background: #138496;
+}
+
+.event-card-actions .btn-delete-event {
+ background: #dc3545;
+}
+
+.event-card-actions .btn-delete-event:hover {
+ background: #c82333;
+}
+
+/* Main Content Header */
+.main-content-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.main-content-header h1 {
+ margin: 0;
+}
+
+/* Search and Filter */
+.wpfaevent-search-section {
+ background: transparent;
+ padding: 0px;
+ margin-bottom: 30px;
+}
+
+.wpfaevent .wpfaevent-search-input {
+ width: 100%;
+ padding: 12px 40px 12px 20px;
+ border: 1px solid #ddd;
+ border-radius: 30px;
+ font-size: 16px;
+}
+
+.results-info {
+ text-align: center;
+ margin: 20px 0;
+ font-size: 1.1rem;
+ color: var(--muted);
+ display: none;
+}
+
+/**
+ * Calendar/Archive Link Section
+ */
+.wpfaevent .wpfaevent-calendar-link-section {
+ margin-top: 50px;
+ grid-column: 1 / -1;
+ text-align: center;
+ padding: 40px 0;
+}
+
+.wpfaevent .wpfaevent-archive-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ text-decoration: none;
+ font-size: 1.2rem;
+ font-weight: 600;
+ color: var(--brand);
+ transition: opacity 0.2s ease;
+}
+
+.wpfaevent .wpfaevent-archive-link:hover {
+ color: #c00;
+ text-decoration: underline;
+}
+
+.wpfaevent .wpfaevent-icon-archive {
+ width: 28px;
+ height: 28px;
+ flex-shrink: 0;
+}
+
+/* News List (Sidebar) */
+.news-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.news-item {
+ border-bottom: 1px solid #eee;
+ padding: 15px 0;
+}
+
+.news-item:last-child {
+ border-bottom: none;
+}
+
+.news-item a {
+ font-weight: 600;
+ color: var(--text);
+ display: block;
+ margin-bottom: 5px;
+}
+
+.news-item a:hover {
+ color: var(--brand);
+}
+
+.news-date {
+ font-size: 0.85rem;
+ color: var(--muted);
+}
+
+.news-cta {
+ margin-top: 20px;
+ text-align: center;
+}
+
+/* Modals */
+.modal {
+ display: none;
+ position: fixed;
+ z-index: 100;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ background-color: rgba(0,0,0,0.6);
+ align-items: center;
+ justify-content: center;
+}
+
+.modal-content {
+ background-color: #fefefe;
+ margin: auto;
+ padding: 20px 30px 30px;
+ border: 1px solid #888;
+ width: 90%;
+ max-width: 500px;
+ border-radius: var(--card-radius);
+ position: relative;
+ box-shadow: 0 15px 40px rgba(0,0,0,0.2);
+}
+
+.close-btn {
+ color: #aaa;
+ position: absolute;
+ top: 10px;
+ right: 20px;
+ font-size: 28px;
+ font-weight: bold;
+ cursor: pointer;
+}
+
+#createEventForm,
+#editEventForm,
+#edit-footer-form {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+}
+
+#createEventForm h2,
+#editEventForm h2,
+#edit-footer-form h2 {
+ margin-top: 0;
+ color: var(--brand);
+}
+
+#createEventForm label,
+#editEventForm label,
+#edit-footer-form label {
+ font-weight: 600;
+ margin-bottom: -10px;
+}
+
+#createEventForm input,
+#editEventForm input,
+#edit-footer-form input {
+ width: 100%;
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ font-family: inherit;
+ font-size: 1rem;
+}
+
+.char-counter {
+ font-size: 0.85rem;
+ color: var(--muted);
+ text-align: right;
+ margin-top: -10px;
+}
+
+#createEventForm button,
+#editEventForm button,
+#edit-footer-form button {
+ margin-top: 15px;
+ align-self: flex-start;
+}
+
+/* Footer Styling (matches MVP) */
+.footer {
+ padding: 28px 0;
+ color: var(--muted);
+ border-top: 1px solid #f3f4f6;
+ text-align: center;
+ background: #fff;
+ margin-top: 40px;
+}
+
+.footer-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 15px;
+}
+
+.social-icons {
+ display: flex;
+ gap: 1rem;
+ margin-top: 1rem;
+ justify-content: center;
+}
+
+.social-icons a {
+ color: var(--muted);
+}
+
+.social-icons svg {
+ width: 24px;
+ height: 24px;
+}
+
+.social-icons a:hover {
+ color: var(--brand);
+}
+
+/* Placeholder Text */
+#events-container .placeholder-text {
+ grid-column: 1 / -1;
+ text-align: center;
+ color: var(--muted);
+ padding: 40px 0;
+}
+
+/**
+ * Admin Warning Styling
+ */
+.wpfaevent-admin-warning {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ background: #fff3cd;
+ border: 1px solid #ffeaa7;
+ border-radius: 8px;
+ padding: 10px 15px;
+ margin: 10px 0;
+ color: #856404;
+ font-size: 0.9rem;
+}
+
+.wpfaevent-warning-icon {
+ width: 18px;
+ height: 18px;
+ flex-shrink: 0;
+}
+
+/* ==========================================================================
+ Past Events Section on Main Events Page
+ ========================================================================== */
+
+/* Past Events Section */
+.past-events-section {
+ margin-top: 60px;
+ padding-top: 40px;
+ border-top: 2px solid #eee;
+}
+
+.past-events-section .main-content-header {
+ margin-bottom: 20px;
+}
+
+.past-events-section .main-content-header h2 {
+ font-size: 1.8rem;
+ color: var(--text);
+ margin: 0;
+}
+
+/* Past event cards - same styling as regular events but without admin features */
+.past-event-card {
+ opacity: 0.95;
+ transition: opacity 0.2s ease;
+}
+
+.past-event-card:hover {
+ opacity: 1;
+}
+
+/* Search should also filter past events */
+#past-events-container {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 20px;
+}
+
+/* When searching, hide/show past events too */
+#past-events-container .event-card {
+ display: block; /* Default: show */
+}
+
+#past-events-container .event-card.search-hidden {
+ display: none; /* Hidden by search */
+}
+
+/* Update results info to include past events */
+.results-info {
+ margin-bottom: 20px;
+}
+
+/* Responsive adjustments */
+@media (max-width: 768px) {
+ .past-events-section {
+ margin-top: 40px;
+ padding-top: 30px;
+ }
+}
\ No newline at end of file
diff --git a/public/css/wpfaevent-public.css b/public/css/wpfaevent-public.css
index 86e9fd3..553c741 100644
--- a/public/css/wpfaevent-public.css
+++ b/public/css/wpfaevent-public.css
@@ -50,3 +50,227 @@
margin: 0 auto;
padding: 24px;
}
+
+/* ==========================================================================
+ Footer Styles (MVP Design)
+ ========================================================================== */
+
+/* Footer Container */
+.wpfaevent .footer {
+ padding: 28px 0;
+ color: var(--muted);
+ border-top: 1px solid #f3f4f6;
+ text-align: center;
+ background: #fff;
+ margin-top: 40px;
+}
+
+.wpfaevent .footer .container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 20px;
+}
+
+/* Footer Text - Bigger font as in MVP */
+.wpfaevent .footer-text {
+ font-size: 1rem;
+ color: var(--brand); /* Using brand color as in MVP */
+ margin: 0;
+ line-height: 1.5;
+}
+
+/* Edit Button Container - Positioned inline with text */
+.wpfaevent .footer .container {
+ position: relative;
+}
+
+/* Edit Button - Positioned to the right of text as in MVP */
+.wpfaevent #edit-footer-btn {
+ margin-left: 15px;
+ padding: 4px 10px;
+ font-size: 12px;
+ border-radius: 8px;
+ position: absolute;
+ right: 24px;
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+/* Social Icons */
+.wpfaevent .social-icons {
+ display: flex;
+ gap: 1rem;
+ margin-top: 1rem;
+ justify-content: center;
+}
+
+.wpfaevent .social-icons a {
+ color: var(--muted);
+ transition: color 0.2s ease;
+}
+
+.wpfaevent .social-icons svg {
+ width: 24px;
+ height: 24px;
+}
+
+.wpfaevent .social-icons a:hover {
+ color: var(--brand);
+}
+
+/* Edit Footer Modal Styles */
+.wpfaevent .modal {
+ display: none;
+ position: fixed;
+ z-index: 100;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ background-color: rgba(0,0,0,0.6);
+ align-items: center;
+ justify-content: center;
+}
+
+.wpfaevent .modal.show {
+ display: flex;
+}
+
+.wpfaevent .modal-content {
+ background-color: #fefefe;
+ margin: auto;
+ padding: 20px 30px 30px;
+ border: 1px solid #888;
+ width: 90%;
+ max-width: 500px;
+ border-radius: var(--card-radius);
+ position: relative;
+ box-shadow: 0 15px 40px rgba(0,0,0,0.2);
+}
+
+.wpfaevent .close-btn {
+ color: #aaa;
+ position: absolute;
+ top: 10px;
+ right: 20px;
+ font-size: 28px;
+ font-weight: bold;
+ cursor: pointer;
+ transition: color 0.2s ease;
+}
+
+.wpfaevent .close-btn:hover {
+ color: var(--brand);
+}
+
+.wpfaevent #edit-footer-form {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+}
+
+.wpfaevent #edit-footer-form h2 {
+ margin-top: 0;
+ color: var(--brand);
+}
+
+.wpfaevent #edit-footer-form label {
+ font-weight: 600;
+ margin-bottom: -10px;
+}
+
+.wpfaevent #edit-footer-form input,
+.wpfaevent #edit-footer-form textarea {
+ width: 100%;
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ font-family: inherit;
+ font-size: 1rem;
+}
+
+.wpfaevent #edit-footer-form textarea {
+ min-height: 80px;
+ resize: vertical; /* Prevents breaking modal width */
+}
+
+.wpfaevent #edit-footer-form button {
+ margin-top: 15px;
+ align-self: flex-start;
+}
+
+/* Button Styles */
+.wpfaevent .btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 8px 16px;
+ border-radius: 999px;
+ font-weight: 700;
+ border: 2px solid transparent;
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ text-decoration: none;
+}
+
+.wpfaevent .btn-secondary {
+ background: #6c757d;
+ color: #fff;
+}
+
+.wpfaevent .btn-secondary:hover {
+ background: #5a6268;
+}
+
+.wpfaevent .btn-primary {
+ background: var(--brand);
+ color: #fff;
+ box-shadow: 0 8px 20px rgba(213, 16, 7, 0.14);
+}
+
+.wpfaevent .btn-primary:hover {
+ background: #b80c04;
+ box-shadow: 0 8px 25px rgba(213, 16, 7, 0.2);
+}
+
+/* Responsive Footer */
+@media (max-width: 768px) {
+ .wpfaevent .footer .container {
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ }
+
+ .wpfaevent #edit-footer-btn {
+ position: static;
+ margin: 10px 0 0 0;
+ transform: none;
+ align-self: center;
+ }
+
+ .wpfaevent .footer-text {
+ text-align: center;
+ }
+}
+
+/* ==========================================================================
+ Character Counter Styles
+ ========================================================================== */
+
+.wpfaevent-char-counter {
+ display: block;
+ text-align: right;
+ font-size: 12px;
+ color: var(--muted);
+ font-weight: 500;
+ transition: color 0.2s ease;
+}
+
+/* Visual warning when the user reaches the limit */
+.wpfaevent-char-counter.limit-reached {
+ color: var(--brand);
+ font-weight: bold;
+}
\ No newline at end of file
diff --git a/public/js/wpfaevent-events.js b/public/js/wpfaevent-events.js
new file mode 100644
index 0000000..7b9a60d
--- /dev/null
+++ b/public/js/wpfaevent-events.js
@@ -0,0 +1,555 @@
+/**
+ * WPFA Events JavaScript Module
+ * Handles search, admin functionality for events page
+ *
+ * @package Wpfaevent
+ * @subpackage Wpfaevent/public/js
+ */
+
+const WPFA_Events = (function() {
+ // Private variables
+ let config = {};
+ let elements = {};
+
+ /**
+ * Helper to extract error message from AJAX response
+ */
+ function getErrorMessage(data, fallback) {
+ if (data && typeof data.data === 'object' && data.data?.message) {
+ return `${fallback}: ${data.data.message}`;
+ }
+ if (data && typeof data.data === 'string') {
+ return `${fallback}: ${data.data}`;
+ }
+ return fallback;
+ }
+
+ /**
+ * Initialize the events module
+ */
+ function init(options) {
+ config = options || {};
+
+ // Ensure i18n object exists even if PHP fails to provide it
+ config.i18n = config.i18n || {};
+
+ // Cache DOM elements
+ cacheElements();
+
+ // Setup event listeners
+ setupEventListeners();
+
+ // Setup character counters
+ setupCharacterCounters();
+
+ // Setup admin functionality if user is admin
+ if (config.isAdmin) {
+ setupAdminFunctionality();
+ }
+
+ // Setup search functionality
+ setupSearch();
+ }
+
+ /**
+ * Cache DOM elements
+ */
+ function cacheElements() {
+ elements = {
+ // Search
+ searchForm: document.querySelector('.wpfa-search-form'),
+ searchInput: document.getElementById('eventSearchInput'),
+
+ // Admin buttons - Using MVP IDs
+ createEventBtn: document.getElementById('createEventBtn'),
+
+ // Modals - Using MVP IDs
+ createEventModal: document.getElementById('createEventModal'),
+ editEventModal: document.getElementById('editEventModal'),
+
+ // Modal close buttons
+ closeCreateEventModal: document.querySelector('#createEventModal .close-btn'),
+ closeEditEventModal: document.querySelector('#editEventModal .close-btn'),
+
+ // Forms - Using MVP IDs
+ createEventForm: document.getElementById('createEventForm'),
+ editEventForm: document.getElementById('editEventForm'),
+
+ // Events container
+ eventsContainer: document.getElementById('events-container'),
+ resultsInfo: document.querySelector('.results-info'),
+ resultsCount: document.getElementById('resultsCount'),
+ };
+ }
+
+ /**
+ * Setup event listeners
+ */
+ function setupEventListeners() {
+ // Create event button
+ if (elements.createEventBtn) {
+ elements.createEventBtn.addEventListener('click', openCreateEventModal);
+ }
+
+ // Modal close buttons
+ if (elements.closeCreateEventModal) {
+ elements.closeCreateEventModal.addEventListener('click', closeCreateEventModal);
+ }
+
+ if (elements.closeEditEventModal) {
+ elements.closeEditEventModal.addEventListener('click', closeEditEventModal);
+ }
+
+ // Close modals on background click
+ if (elements.createEventModal) {
+ elements.createEventModal.addEventListener('click', function(e) {
+ if (e.target === this) closeCreateEventModal();
+ });
+ }
+
+ if (elements.editEventModal) {
+ elements.editEventModal.addEventListener('click', function(e) {
+ if (e.target === this) closeEditEventModal();
+ });
+ }
+
+ // Form submissions
+ if (elements.createEventForm) {
+ elements.createEventForm.addEventListener('submit', handleCreateEventFormSubmit);
+ }
+
+ if (elements.editEventForm) {
+ elements.editEventForm.addEventListener('submit', handleEditEventFormSubmit);
+ }
+
+ // Event card actions delegation
+ if (elements.eventsContainer) {
+ elements.eventsContainer.addEventListener('click', handleCardActions);
+ }
+ }
+
+ /**
+ * Setup character counters
+ */
+ function setupCharacterCounters() {
+ document.querySelectorAll('textarea[maxlength]').forEach(textarea => {
+ const counter = textarea.nextElementSibling;
+
+ if (counter?.classList.contains('wpfaevent-char-counter')) {
+ const update = () => {
+ const currentLength = textarea.value.length;
+ const maxLength = textarea.maxLength;
+ counter.textContent = `${currentLength} / ${maxLength}`;
+
+ if (currentLength >= maxLength) {
+ counter.classList.add('limit-reached');
+ } else {
+ counter.classList.remove('limit-reached');
+ }
+ };
+
+ textarea.addEventListener('input', update);
+
+ textarea.form?.addEventListener('reset', () => {
+ setTimeout(update, 0);
+ });
+
+ // Call update immediately
+ update();
+
+ /** * If data is loaded dynamically (e.g. via AJAX or WP Modal),
+ * we wait a tiny bit to catch the filled value.
+ */
+ if (textarea.value.length === 0) {
+ setTimeout(update, 100);
+ }
+ }
+ });
+ }
+
+ /**
+ * Setup search functionality
+ */
+ function setupSearch() {
+ if (elements.searchInput) {
+ elements.searchInput.addEventListener('keyup', filterEvents);
+ }
+ }
+
+ /**
+ * Filter events based on search input
+ */
+ function filterEvents() {
+ const searchTerm = elements.searchInput.value.toLowerCase().trim();
+ const upcomingCards = elements.eventsContainer.querySelectorAll('.event-card');
+ const pastEventsContainer = document.getElementById('past-events-container');
+ const pastCards = pastEventsContainer ? pastEventsContainer.querySelectorAll('.event-card') : [];
+
+ let upcomingVisibleCount = 0;
+ let pastVisibleCount = 0;
+
+ // Filter upcoming events
+ upcomingCards.forEach(card => {
+ const name = card.dataset.name.toLowerCase();
+ const place = card.dataset.place.toLowerCase();
+ const description = card.dataset.description.toLowerCase();
+ const isVisible = name.includes(searchTerm) || place.includes(searchTerm) || description.includes(searchTerm);
+
+ card.style.display = isVisible ? '' : 'none';
+ if (isVisible) {
+ upcomingVisibleCount++;
+ }
+ });
+
+ // Filter past events if they exist
+ pastCards.forEach(card => {
+ const name = card.dataset.name.toLowerCase();
+ const place = card.dataset.place.toLowerCase();
+ const description = card.dataset.description.toLowerCase();
+ const isVisible = name.includes(searchTerm) || place.includes(searchTerm) || description.includes(searchTerm);
+
+ card.style.display = isVisible ? '' : 'none';
+ if (isVisible) {
+ pastVisibleCount++;
+ }
+ });
+
+ if (searchTerm) {
+ elements.resultsInfo.style.display = 'block';
+ elements.resultsCount.textContent = upcomingVisibleCount + pastVisibleCount;
+ } else {
+ elements.resultsInfo.style.display = 'none';
+ elements.resultsCount.textContent = upcomingCards.length;
+ }
+ }
+
+ /**
+ * Setup admin functionality
+ */
+ function setupAdminFunctionality() {
+ // Add admin class to body for styling
+ document.body.classList.add('wpfa-is-admin');
+ }
+
+ /**
+ * Handle event card actions
+ */
+ function handleCardActions(e) {
+ const target = e.target;
+
+ // Edit event button
+ if (target.matches('.btn-edit-event') || target.closest('.btn-edit-event')) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const button = target.matches('.btn-edit-event') ? target : target.closest('.btn-edit-event');
+ const card = button.closest('.event-card');
+
+ openEditEventModal(card);
+ }
+
+ // Delete event button
+ else if (target.matches('.btn-delete-event') || target.closest('.btn-delete-event')) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const button = target.matches('.btn-delete-event') ? target : target.closest('.btn-delete-event');
+ const eventId = button.closest('.event-card').dataset.postId;
+ const eventName = button.closest('.event-card').dataset.name;
+
+ deleteEvent(eventId, eventName);
+ }
+ }
+
+ /**
+ * Open modal for creating new event
+ */
+ function openCreateEventModal() {
+ // Reset form
+ if (elements.createEventForm) {
+ elements.createEventForm.reset();
+
+ // Set today's date as default
+ const today = new Date().toISOString().split('T')[0];
+ const eventDateInput = document.getElementById('eventDate');
+ if (eventDateInput) {
+ eventDateInput.value = today;
+ eventDateInput.min = today;
+ }
+
+ // Use the smart function to reset all counters to "0 / max"
+ setupCharacterCounters();
+ }
+
+ // Show modal
+ if (elements.createEventModal) {
+ elements.createEventModal.style.display = 'flex';
+ }
+ }
+
+ /**
+ * Open modal for editing event
+ */
+ function openEditEventModal(card) {
+ // Get data from card
+ const eventId = card.dataset.postId;
+
+ // Fill form with data from card dataset
+ document.getElementById('editEventId').value = eventId;
+ document.getElementById('editEventName').value = card.dataset.name || '';
+ document.getElementById('editEventDate').value = card.dataset.date || '';
+ document.getElementById('editEventEndDate').value = card.dataset.endDate || '';
+ document.getElementById('editEventPlace').value = card.dataset.place || '';
+ document.getElementById('editEventDescription').value = card.dataset.description || '';
+ document.getElementById('editEventLeadText').value = card.dataset.leadText || '';
+ document.getElementById('editRegistrationLink').value = card.dataset.registrationLink || '';
+ document.getElementById('editCfsLink').value = card.dataset.cfsLink || '';
+ document.getElementById('editEventTime').value = card.dataset.time || '';
+
+ // Sync all counters at once
+ // This looks for all textareas and updates their specific counters
+ setupCharacterCounters();
+
+ // Show modal
+ if (elements.editEventModal) {
+ elements.editEventModal.style.display = 'flex';
+ }
+ }
+
+ /**
+ * Handle create event form submission
+ */
+ function handleCreateEventFormSubmit(e) {
+ e.preventDefault();
+
+ if (!config.isAdmin) {
+ alert(config.i18n.noPermission || 'You do not have permission to perform this action.');
+ return;
+ }
+
+ const form = e.target;
+ const formData = new FormData(form);
+ const submitBtn = form.querySelector('button[type="submit"]');
+
+ // Validate required fields - using ACTUAL form field names
+ const requiredFields = ['title', 'excerpt', 'start_date', 'location', 'registration_link'];
+ let missingFields = [];
+
+ requiredFields.forEach(field => {
+ if (!formData.get(field) || formData.get(field).trim() === '') {
+ missingFields.push(field);
+ }
+ });
+
+ if (missingFields.length > 0) {
+ alert(config.i18n.missingFields || 'Missing required fields: ' + missingFields.join(', '));
+
+ // Re-enable button
+ if (submitBtn) {
+ submitBtn.disabled = false;
+ submitBtn.textContent = config.i18n.addEventButton || 'Create Card';
+ }
+ return;
+ }
+
+ // Disable button during submission
+ if (submitBtn) {
+ submitBtn.disabled = true;
+ submitBtn.textContent = config.i18n.creating || 'Creating...';
+ }
+
+ // Add nonce and AJAX action
+ formData.append('action', 'wpfa_add_event');
+ formData.append('nonce', config.adminNonce);
+
+ // Send form data
+ fetch(config.ajaxUrl, {
+ method: 'POST',
+ body: formData
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ alert(config.i18n.addSuccess || 'Event created successfully. The page will now reload.');
+ window.location.reload();
+ } else {
+ const baseMsg = config.i18n.addError || 'Error creating event';
+ alert(getErrorMessage(data, baseMsg));
+
+ // Re-enable button
+ if (submitBtn) {
+ submitBtn.disabled = false;
+ submitBtn.textContent = config.i18n.addEventButton || 'Create Card';
+ }
+ }
+ })
+ .catch(error => {
+ alert(config.i18n.addErrorGeneric || 'Error creating event. Please try again.');
+
+ // Re-enable button
+ if (submitBtn) {
+ submitBtn.disabled = false;
+ submitBtn.textContent = config.i18n.addEventButton || 'Create Card';
+ }
+ });
+ }
+
+ /**
+ * Handle edit event form submission
+ */
+ function handleEditEventFormSubmit(e) {
+ e.preventDefault();
+
+ if (!config.isAdmin) {
+ alert(config.i18n.noPermission || 'You do not have permission to perform this action.');
+ return;
+ }
+
+ const form = e.target;
+ const formData = new FormData(form);
+ const submitBtn = form.querySelector('button[type="submit"]');
+
+ // Validate required fields - using ACTUAL form field names
+ const requiredFields = ['title', 'excerpt', 'start_date', 'location', 'registration_link'];
+ let missingFields = [];
+
+ requiredFields.forEach(field => {
+ if (!formData.get(field) || formData.get(field).trim() === '') {
+ missingFields.push(field);
+ }
+ });
+
+ if (missingFields.length > 0) {
+ alert(config.i18n.missingFields || 'Missing required fields: ' + missingFields.join(', '));
+
+ // Re-enable button
+ if (submitBtn) {
+ submitBtn.disabled = false;
+ submitBtn.textContent = config.i18n.editEventButton || 'Save Changes';
+ }
+ return;
+ }
+
+ // Disable button during submission
+ if (submitBtn) {
+ submitBtn.disabled = true;
+ submitBtn.textContent = config.i18n.saving || 'Saving...';
+ }
+
+ // Add nonce and AJAX action
+ formData.append('action', 'wpfa_update_event');
+ formData.append('nonce', config.adminNonce);
+
+ // Send form data
+ fetch(config.ajaxUrl, {
+ method: 'POST',
+ body: formData
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ alert(config.i18n.updateSuccess || 'Event updated successfully. The page will now reload.');
+ window.location.reload();
+ } else {
+ const baseMsg = config.i18n.updateError || 'Error updating event';
+ alert(getErrorMessage(data, baseMsg));
+
+ // Re-enable button
+ if (submitBtn) {
+ submitBtn.disabled = false;
+ submitBtn.textContent = config.i18n.editEventButton || 'Save Changes';
+ }
+ }
+ })
+ .catch(error => {
+ alert(config.i18n.updateErrorGeneric || 'Error updating event. Please try again.');
+
+ // Re-enable button
+ if (submitBtn) {
+ submitBtn.disabled = false;
+ submitBtn.textContent = config.i18n.editEventButton || 'Save Changes';
+ }
+ });
+ }
+
+ /**
+ * Delete event confirmation and AJAX call
+ */
+ function deleteEvent(eventId, eventName) {
+ const confirmMsg = config.i18n.confirmDelete
+ ? config.i18n.confirmDelete.replace('%s', eventName)
+ : `Are you sure you want to delete "${eventName}"? This action cannot be undone.`;
+
+ if (!confirm(confirmMsg)) {
+ return;
+ }
+
+ fetch(config.ajaxUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: new URLSearchParams({
+ action: 'wpfa_delete_event',
+ nonce: config.adminNonce,
+ event_id: eventId
+ })
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ alert(config.i18n.deleteSuccess || 'Event deleted successfully. The page will now reload.');
+ window.location.reload();
+ } else {
+ const baseMsg = config.i18n.deleteError || 'Error deleting event';
+ alert(getErrorMessage(data, baseMsg));
+ }
+ })
+ .catch(error => {
+ alert(config.i18n.deleteErrorGeneric || 'Error deleting event. Please try again.');
+ });
+ }
+
+ /**
+ * Close create event modal
+ */
+ function closeCreateEventModal() {
+ if (elements.createEventModal) {
+ elements.createEventModal.style.display = 'none';
+ }
+ }
+
+ /**
+ * Close edit event modal
+ */
+ function closeEditEventModal() {
+ if (elements.editEventModal) {
+ elements.editEventModal.style.display = 'none';
+ }
+ }
+
+ // Public API
+ return {
+ init: init,
+ openCreateEventModal: openCreateEventModal,
+ openEditEventModal: openEditEventModal,
+ closeCreateEventModal: closeCreateEventModal,
+ closeEditEventModal: closeEditEventModal,
+ filterEvents: filterEvents
+ };
+})();
+
+// Export to global
+if (typeof window !== 'undefined') {
+ window.WPFA_Events = WPFA_Events;
+}
+
+// Initialize when page loads
+if (typeof document !== 'undefined') {
+ document.addEventListener('DOMContentLoaded', function() {
+ // Check if config exists (only on events page)
+ if (typeof wpfaeventEventsConfig !== 'undefined') {
+ WPFA_Events.init(wpfaeventEventsConfig);
+ }
+ });
+}
\ No newline at end of file
diff --git a/public/js/wpfaevent-footer.js b/public/js/wpfaevent-footer.js
new file mode 100644
index 0000000..febea45
--- /dev/null
+++ b/public/js/wpfaevent-footer.js
@@ -0,0 +1,184 @@
+/**
+ * WPFA Footer JavaScript Module
+ * Handles footer functionality across all pages
+ *
+ * @package Wpfaevent
+ * @subpackage Wpfaevent/public/js
+ */
+
+const WPFA_Footer = (function() {
+ // Private variables
+ let config = {};
+ let elements = {};
+
+ // Private Helper for consistency with events module
+ function getErrorMessage(data, fallback) {
+ if (data && typeof data.data === 'object' && data.data !== null && data.data.message) {
+ return `${fallback}: ${data.data.message}`;
+ }
+ if (data && typeof data.data === 'string') {
+ return `${fallback}: ${data.data}`;
+ }
+ return fallback;
+ }
+
+ /**
+ * Initialize the footer module
+ */
+ function init(options) {
+ config = options || {};
+
+ // Ensure i18n object exists
+ config.i18n = config.i18n || {};
+
+ // Cache DOM elements
+ cacheElements();
+
+ // Setup event listeners
+ setupEventListeners();
+ }
+
+ /**
+ * Cache DOM elements
+ */
+ function cacheElements() {
+ elements = {
+ editFooterBtn: document.getElementById('edit-footer-btn'),
+ footerModal: document.getElementById('edit-footer-modal'),
+ closeFooterModal: document.querySelector('#edit-footer-modal .close-btn'),
+ footerForm: document.getElementById('edit-footer-form'),
+ };
+ }
+
+ /**
+ * Setup event listeners
+ */
+ function setupEventListeners() {
+ // Edit footer button
+ if (elements.editFooterBtn) {
+ elements.editFooterBtn.addEventListener('click', openFooterModal);
+ }
+
+ // Modal close button
+ if (elements.closeFooterModal) {
+ elements.closeFooterModal.addEventListener('click', closeFooterModal);
+ }
+
+ // Close modal on background click
+ if (elements.footerModal) {
+ elements.footerModal.addEventListener('click', function(e) {
+ if (e.target === this) closeFooterModal();
+ });
+ }
+
+ // Form submission
+ if (elements.footerForm) {
+ elements.footerForm.addEventListener('submit', handleFooterFormSubmit);
+ }
+ }
+
+ /**
+ * Open footer modal
+ */
+ function openFooterModal() {
+ // Get current footer text
+ const footerTextElement = document.getElementById('footer-text-display');
+ const footerTextInput = document.getElementById('footer-text');
+
+ if (footerTextElement && footerTextInput) {
+ footerTextInput.value = footerTextElement.textContent.trim();
+ }
+
+ if (elements.footerModal) {
+ elements.footerModal.style.display = 'flex';
+ }
+ }
+
+ /**
+ * Handle footer form submission
+ */
+ function handleFooterFormSubmit(e) {
+ e.preventDefault();
+
+ const form = e.target;
+ const formData = new FormData(form);
+ const submitBtn = form.querySelector('button[type="submit"]');
+
+ // Disable button during submission
+ if (submitBtn) {
+ submitBtn.disabled = true;
+ submitBtn.textContent = config.i18n.saving || 'Saving...';
+ }
+
+ // Add AJAX action and nonce
+ formData.append('action', 'wpfa_update_footer_text');
+ formData.append('nonce', config.adminNonce);
+
+ fetch(config.ajaxUrl, {
+ method: 'POST',
+ body: formData
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ // Update footer text on page
+ const footerTextElement = document.getElementById('footer-text-display');
+ if (footerTextElement) {
+ footerTextElement.textContent = formData.get('footer_text');
+ }
+
+ alert(config.i18n.footerSaveSuccess || 'Footer text updated successfully.');
+ closeFooterModal();
+ } else {
+ const baseMsg = config.i18n.footerSaveError || 'Error updating footer text';
+ alert(getErrorMessage(data, baseMsg));
+ }
+
+ // Re-enable button
+ if (submitBtn) {
+ submitBtn.disabled = false;
+ submitBtn.textContent = config.i18n.saveFooter || 'Save Footer';
+ }
+ })
+ .catch(error => {
+ alert(config.i18n.footerSaveError || 'Error updating footer text.');
+
+ // Re-enable button
+ if (submitBtn) {
+ submitBtn.disabled = false;
+ submitBtn.textContent = config.i18n.saveFooter || 'Save Footer';
+ }
+ });
+ }
+
+ /**
+ * Close footer modal
+ */
+ function closeFooterModal() {
+ if (elements.footerModal) {
+ elements.footerModal.style.display = 'none';
+ }
+ }
+
+ // Public API
+ return {
+ init: init,
+ openFooterModal: openFooterModal,
+ closeFooterModal: closeFooterModal
+ };
+})();
+
+// Export to global
+if (typeof window !== 'undefined') {
+ window.WPFA_Footer = WPFA_Footer;
+}
+
+// Initialize when page loads
+if (typeof document !== 'undefined') {
+ document.addEventListener('DOMContentLoaded', function() {
+ // Check if config exists (footer exists on all template pages)
+ if ( typeof wpfaeventFooterConfig !== 'undefined') {
+ WPFA_Footer.init(wpfaeventFooterConfig);
+ }
+ });
+}
\ No newline at end of file
diff --git a/public/partials/events-listing-page.php b/public/partials/events-listing-page.php
deleted file mode 100644
index 1a66a62..0000000
--- a/public/partials/events-listing-page.php
+++ /dev/null
@@ -1,9 +0,0 @@
-';
-}
diff --git a/public/partials/events/event-card.php b/public/partials/events/event-card.php
new file mode 100644
index 0000000..4f439c2
--- /dev/null
+++ b/public/partials/events/event-card.php
@@ -0,0 +1,129 @@
+
+ */
+
+/**
+ * Prevent direct access to this file.
+ */
+if ( ! defined( 'ABSPATH' ) ) {
+ exit; // Exit if accessed directly.
+}
+
+// Use the passed $event_id if it exists; otherwise, fall back to the loop ID.
+$event_id = $event_id ?? get_the_ID();
+
+// Exit if we still don't have a valid ID (e.g., if called outside the loop)
+if ( ! $event_id ) {
+ return;
+}
+
+$today = current_time( 'Y-m-d' );
+
+// Get meta data exactly as the main template does
+$event_date = get_post_meta( $event_id, 'wpfa_event_start_date', true );
+$event_end_date = get_post_meta( $event_id, 'wpfa_event_end_date', true );
+$event_place = get_post_meta( $event_id, 'wpfa_event_location', true );
+$event_description = get_the_excerpt( $event_id );
+$featured_img_url = get_the_post_thumbnail_url( $event_id, 'large' ) ?: '';
+
+// Check if date is valid (Admin Warning Logic)
+$is_valid_date = ! empty( $event_date ) && strtotime( $event_date ) !== false;
+$is_past_event = $is_valid_date && strtotime( $event_date ) < strtotime( $today );
+
+// Format the date string
+$formatted_date = __( 'Date not set', 'wpfaevent' );
+if ( $is_valid_date ) {
+ if ( ! empty( $event_end_date ) && $event_end_date !== $event_date && strtotime( $event_end_date ) !== false ) {
+ $formatted_date = date_i18n( 'M j', strtotime( $event_date ) ) . ' - ' . date_i18n( 'M j, Y', strtotime( $event_end_date ) );
+ } else {
+ $formatted_date = date_i18n( 'F j, Y', strtotime( $event_date ) );
+ }
+}
+
+$is_admin = current_user_can( 'manage_options' );
+?>
+
+
\ No newline at end of file
diff --git a/public/partials/events/event-modal.php b/public/partials/events/event-modal.php
new file mode 100644
index 0000000..a93a026
--- /dev/null
+++ b/public/partials/events/event-modal.php
@@ -0,0 +1,108 @@
+
+ */
+
+/**
+ * Prevent direct access to this file.
+ */
+if ( ! defined( 'ABSPATH' ) ) {
+ exit; // Exit if accessed directly.
+}
+?>
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/partials/footer.php b/public/partials/footer.php
new file mode 100644
index 0000000..bc5fb87
--- /dev/null
+++ b/public/partials/footer.php
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/templates/page-events.php b/public/templates/page-events.php
index f139c12..f82d363 100644
--- a/public/templates/page-events.php
+++ b/public/templates/page-events.php
@@ -1,85 +1,347 @@
*/
-if ( ! defined( 'ABSPATH' ) ) {
- exit; }
-get_header();
-$today = current_time( 'Y-m-d' );
+/**
+ * Prevent direct access to this file.
+ */
+if ( ! defined( 'ABSPATH' ) ) {
+ exit; // Exit if accessed directly.
+}
-$per_page = max( 1, (int) apply_filters( 'wpfa_events_per_page', 10 ) );
-$paged = max( 1, (int) get_query_var( 'paged', 1 ) );
+// Set up query for upcoming events
+$today = current_time( 'Y-m-d' );
+$per_page = -1; // @TODO: pagination required for future enhancement, for now show all events
-$args = [
- 'post_type' => 'wpfa_event',
- 'post_status' => 'publish',
- 'meta_query' => [
- [
+$future_date_query = array(
+ 'relation' => 'AND',
+ array(
+ 'key' => 'wpfa_event_start_date',
+ 'compare' => 'EXISTS',
+ ),
+ array(
+ 'key' => 'wpfa_event_start_date',
+ 'value' => '',
+ 'compare' => '!=',
+ ),
+ array(
+ 'relation' => 'OR',
+ array(
+ 'key' => 'wpfa_event_end_date',
+ 'value' => $today,
+ 'compare' => '>=',
+ 'type' => 'DATE',
+ ),
+ array(
'key' => 'wpfa_event_start_date',
'value' => $today,
'compare' => '>=',
'type' => 'DATE',
- ],
- ],
+ ),
+ ),
+);
+
+/**
+ * Define Meta Queries separately to keep main $args readable.
+ */
+$missing_date_query = array(
+ 'relation' => 'OR',
+ array(
+ 'key' => 'wpfa_event_start_date',
+ 'compare' => 'NOT EXISTS',
+ ),
+ array(
+ 'key' => 'wpfa_event_start_date',
+ 'value' => '',
+ 'compare' => '=',
+ ),
+);
+
+// Determine final meta query based on user role
+$final_meta_query = current_user_can( 'manage_options' )
+ ? array(
+ 'relation' => 'OR',
+ $future_date_query,
+ $missing_date_query,
+ )
+ : $future_date_query;
+
+// Query for UPCOMING events
+$upcoming_args = array(
+ 'post_type' => 'wpfa_event',
+ 'post_status' => 'publish',
+ 'posts_per_page' => $per_page,
'orderby' => 'meta_value',
'meta_key' => 'wpfa_event_start_date',
'meta_type' => 'DATE',
'order' => 'ASC',
- 'posts_per_page' => $per_page,
- 'paged' => $paged,
- 'fields' => 'ids',
-];
+ 'meta_query' => $final_meta_query,
+);
+
+$upcoming_query = new WP_Query( $upcoming_args );
+
+// Query for PAST events (for display on same page, without admin features)
+$past_args = array(
+ 'post_type' => 'wpfa_event',
+ 'post_status' => 'publish',
+ 'posts_per_page' => 6, // Limit past events to 6 most recent
+ 'meta_query' => array(
+ array(
+ 'key' => 'wpfa_event_end_date',
+ 'value' => $today,
+ 'compare' => '<',
+ 'type' => 'DATE',
+ ),
+ ),
+ 'orderby' => 'meta_value',
+ 'meta_key' => 'wpfa_event_end_date',
+ 'meta_type' => 'DATE',
+ 'order' => 'DESC', // Most recent first
+);
+
+$past_query = new WP_Query( $past_args );
-$q = new WP_Query( $args );
+// Check if user is admin for admin functionality
+$is_admin = current_user_can( 'manage_options' );
+
+// Set up header configuration
+$header_vars = array(
+ 'site_logo_url' => apply_filters( 'wpfa_site_logo_url', get_option( 'wpfa_site_logo_url', WPFAEVENT_URL . 'assets/images/logo.png' ) ),
+ 'event_page_url' => home_url( '/events/' ),
+ 'show_back_button' => false, // Events page is usually the root
+ 'show_register_button' => false, // Or true if you want a global reg button
+ 'back_button_text' => __( 'Back to Hub', 'wpfaevent' ),
+ 'register_button_url' => '#',
+ 'register_button_text' => __( 'Register', 'wpfaevent' ),
+);
?>
-
-
- have_posts() ) : ?>
-
- posts as $eid ) :
- $title = get_the_title( $eid );
- $start = sanitize_text_field( get_post_meta( $eid, 'wpfa_event_start_date', true ) );
- $end = sanitize_text_field( get_post_meta( $eid, 'wpfa_event_end_date', true ) );
- $loc = sanitize_text_field( get_post_meta( $eid, 'wpfa_event_location', true ) );
- $url = get_post_meta( $eid, 'wpfa_event_url', true ) ?: get_permalink( $eid );
- ?>
- -
-
-
-
-
+>
+
+
+
+
+
+>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ found_posts;
+ $total_past = $past_query->found_posts;
+ $total_events = $total_upcoming + $total_past;
?>
- ·
-
-
-
-
-
- found_posts / $per_page ) );
- wpfa_render_pagination( $total, $paged, __( 'Events pagination', 'wpfaevent' ) );
- ?>
-
-
-
-
-
-
+
+
+
+
+
+ have_posts() ) : ?>
+ have_posts() ) :
+ $upcoming_query->the_post();
+ ?>
+
+
+
+
+
+
+
+
+
+ have_posts() ) : ?>
+
+
+
+
+
+
+
+ have_posts() ) :
+ $past_query->the_post();
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wpfaevent.php b/wpfaevent.php
index 52df175..a6043ae 100644
--- a/wpfaevent.php
+++ b/wpfaevent.php
@@ -46,6 +46,7 @@
require_once WPFAEVENT_PATH . 'includes/class-wpfaevent-deactivator.php';
require_once WPFAEVENT_PATH . 'includes/class-wpfaevent-templates.php';
require_once WPFAEVENT_PATH . 'includes/helpers/wpfaevent-pagination-helper.php';
+require_once WPFAEVENT_PATH . 'includes/helpers/class-wpfaevent-news-functions.php';
// Activation / Deactivation hooks
register_activation_hook( __FILE__, [ 'Wpfaevent_Activator', 'activate' ] );
|