load_dependencies(); $this->set_locale(); $this->init_hooks(); } /** * Load dependencies */ private function load_dependencies() { // Core classes require_once THREECAL_PLUGIN_DIR . 'includes/class-activator.php'; require_once THREECAL_PLUGIN_DIR . 'includes/class-deactivator.php'; require_once THREECAL_PLUGIN_DIR . 'includes/class-i18n.php'; require_once THREECAL_PLUGIN_DIR . 'includes/class-event.php'; require_once THREECAL_PLUGIN_DIR . 'includes/class-location.php'; require_once THREECAL_PLUGIN_DIR . 'includes/class-category.php'; require_once THREECAL_PLUGIN_DIR . 'includes/class-shortcode.php'; require_once THREECAL_PLUGIN_DIR . 'includes/class-calendar-renderer.php'; require_once THREECAL_PLUGIN_DIR . 'includes/class-email.php'; // Admin classes if (is_admin()) { require_once THREECAL_PLUGIN_DIR . 'admin/class-admin.php'; require_once THREECAL_PLUGIN_DIR . 'admin/class-events-list.php'; } // Public classes require_once THREECAL_PLUGIN_DIR . 'public/class-public.php'; } /** * Set locale * * Note: WordPress 4.6+ automatically loads translations from the languages directory. * Manual load_plugin_textdomain() is discouraged by WordPress.org Plugin Guidelines. */ private function set_locale() { // WordPress 4.6+ automatically loads translations from: // wp-content/languages/plugins/3task-calendar-{locale}.mo // No manual loading needed per WordPress.org guidelines. } /** * Initialize hooks */ private function init_hooks() { // Admin if (is_admin()) { $this->admin = new ThreeCal_Admin('3task-calendar', THREECAL_VERSION); add_action('admin_enqueue_scripts', array($this->admin, 'enqueue_styles')); add_action('admin_enqueue_scripts', array($this->admin, 'enqueue_scripts')); add_action('admin_menu', array($this->admin, 'add_menu_pages')); add_action('admin_init', array($this->admin, 'register_settings')); add_action('admin_init', array($this, 'maybe_add_capabilities')); } // Public $this->public = new ThreeCal_Public('3task-calendar', THREECAL_VERSION); add_action('wp_enqueue_scripts', array($this->public, 'enqueue_styles')); add_action('wp_enqueue_scripts', array($this->public, 'enqueue_scripts')); // Shortcodes new ThreeCal_Shortcode(); // Register block add_action('init', array($this, 'register_block')); // REST API add_action('rest_api_init', array($this, 'register_rest_routes')); // Schema.org SEO add_action('wp_head', array($this, 'output_schema')); // AJAX handlers add_action('wp_ajax_threecal_get_events', array($this, 'ajax_get_events')); add_action('wp_ajax_nopriv_threecal_get_events', array($this, 'ajax_get_events')); add_action('wp_ajax_threecal_get_event_details', array($this, 'ajax_get_event_details')); add_action('wp_ajax_nopriv_threecal_get_event_details', array($this, 'ajax_get_event_details')); } /** * Register Gutenberg block */ public function register_block() { if (!function_exists('register_block_type')) { return; } wp_register_script( 'threecal-block-editor', THREECAL_PLUGIN_URL . 'blocks/calendar-block/index.js', array('wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-i18n', 'wp-server-side-render'), THREECAL_VERSION, true ); wp_register_style( 'threecal-block-editor', THREECAL_PLUGIN_URL . 'blocks/calendar-block/editor.css', array(), THREECAL_VERSION ); register_block_type('3task-calendar/calendar', array( 'api_version' => 3, 'editor_script' => 'threecal-block-editor', 'editor_style' => 'threecal-block-editor', 'render_callback' => array($this, 'render_block'), 'attributes' => array( 'view' => array( 'type' => 'string', 'default' => 'month' ), 'category' => array( 'type' => 'number', 'default' => 0 ), 'theme' => array( 'type' => 'string', 'default' => 'default' ) ) )); } /** * Render block callback */ public function render_block($attributes) { $view = isset($attributes['view']) ? $attributes['view'] : 'month'; $category = isset($attributes['category']) ? absint($attributes['category']) : 0; $theme = isset($attributes['theme']) ? $attributes['theme'] : 'default'; $shortcode = '[threecal view="' . esc_attr($view) . '"'; if ($category > 0) { $shortcode .= ' category="' . $category . '"'; } $shortcode .= ' theme="' . esc_attr($theme) . '"]'; return do_shortcode($shortcode); } /** * Output Schema.org markup for events */ public function output_schema() { if (!is_singular()) { return; } global $post; if (!$post) { return; } // Find calendar shortcodes in content if (has_shortcode($post->post_content, 'threecal') || has_shortcode($post->post_content, 'threecal_event')) { // Get upcoming events for schema $events = ThreeCal_Event::get_upcoming(5); foreach ($events as $event) { $this->output_event_schema($event); } } } /** * Output individual event schema */ private function output_event_schema($event) { $schema = array( '@context' => 'https://schema.org', '@type' => 'Event', 'name' => $event->title, 'description' => wp_strip_all_tags($event->description), 'startDate' => $event->start_date, 'endDate' => $event->end_date ?: $event->start_date, 'eventStatus' => 'https://schema.org/EventScheduled', 'eventAttendanceMode' => 'https://schema.org/OfflineEventAttendanceMode' ); // Add location if available if ($event->location_id) { $location = ThreeCal_Location::get($event->location_id); if ($location) { $schema['location'] = array( '@type' => 'Place', 'name' => $location->name, 'address' => array( '@type' => 'PostalAddress', 'streetAddress' => $location->address, 'addressLocality' => $location->city, 'postalCode' => $location->postal_code, 'addressCountry' => $location->country ) ); if ($location->latitude && $location->longitude) { $schema['location']['geo'] = array( '@type' => 'GeoCoordinates', 'latitude' => $location->latitude, 'longitude' => $location->longitude ); } } } // Add organizer $schema['organizer'] = array( '@type' => 'Organization', 'name' => get_bloginfo('name'), 'url' => home_url() ); echo '' . "\n"; } /** * Register REST API routes */ public function register_rest_routes() { register_rest_route('3task-calendar/v1', '/events', array( 'methods' => 'GET', 'callback' => array($this, 'rest_get_events'), 'permission_callback' => '__return_true', 'args' => array( 'start' => array( 'required' => false, 'type' => 'string' ), 'end' => array( 'required' => false, 'type' => 'string' ), 'category' => array( 'required' => false, 'type' => 'integer' ) ) )); register_rest_route('3task-calendar/v1', '/events/(?P\d+)', array( 'methods' => 'GET', 'callback' => array($this, 'rest_get_event'), 'permission_callback' => '__return_true' )); } /** * REST callback: Get events */ public function rest_get_events($request) { $start = $request->get_param('start'); $end = $request->get_param('end'); $category = $request->get_param('category'); $args = array( 'status' => 'published' ); if ($start) { $args['start_after'] = $start; } if ($end) { $args['start_before'] = $end; } if ($category) { $args['category_id'] = $category; } $events = ThreeCal_Event::get_all($args); $data = array(); foreach ($events as $event) { $data[] = $this->format_event_for_api($event); } return rest_ensure_response($data); } /** * REST callback: Get single event */ public function rest_get_event($request) { $id = $request->get_param('id'); $event = ThreeCal_Event::get($id); if (!$event) { return new WP_Error('not_found', __('Event not found', '3task-calendar'), array('status' => 404)); } return rest_ensure_response($this->format_event_for_api($event)); } /** * Format event for API response */ private function format_event_for_api($event) { $location = null; if ($event->location_id) { $loc = ThreeCal_Location::get($event->location_id); if ($loc) { $location = array( 'id' => $loc->id, 'name' => $loc->name, 'address' => $loc->address, 'city' => $loc->city, 'latitude' => $loc->latitude, 'longitude' => $loc->longitude ); } } $categories = ThreeCal_Event::get_categories($event->id); return array( 'id' => $event->id, 'title' => $event->title, 'description' => $event->description, 'start' => $event->start_date, 'end' => $event->end_date, 'allDay' => (bool) $event->all_day, 'location' => $location, 'categories' => $categories, 'color' => $event->color, 'url' => $event->url, 'featured_image' => $event->featured_image ? wp_get_attachment_url($event->featured_image) : null ); } /** * AJAX handler: Get events for calendar */ public function ajax_get_events() { check_ajax_referer('threecal_nonce', 'nonce'); // Support both start/end and month/year parameters. $start = isset( $_POST['start'] ) ? sanitize_text_field( wp_unslash( $_POST['start'] ) ) : ''; $end = isset( $_POST['end'] ) ? sanitize_text_field( wp_unslash( $_POST['end'] ) ) : ''; $month = isset( $_POST['month'] ) ? absint( $_POST['month'] ) : 0; $year = isset( $_POST['year'] ) ? absint( $_POST['year'] ) : 0; $category = isset( $_POST['category'] ) ? absint( $_POST['category'] ) : 0; $location = isset( $_POST['location'] ) ? absint( $_POST['location'] ) : 0; // If month/year provided, calculate start/end. if ( $month > 0 && $year > 0 ) { $start = gmdate( 'Y-m-d 00:00:00', mktime( 0, 0, 0, $month, 1, $year ) ); $end = gmdate( 'Y-m-t 23:59:59', mktime( 0, 0, 0, $month, 1, $year ) ); } $args = array( 'status' => 'published', 'start_after' => $start, 'start_before' => $end ); if ($category > 0) { $args['category_id'] = $category; } if ($location > 0) { $args['location_id'] = $location; } $events = ThreeCal_Event::get_all($args); $data = array(); $time_format = get_option( 'time_format', 'g:i a' ); foreach ($events as $event) { $event_data = $this->format_event_for_api($event); // Add day number and formatted time for calendar views. $event_data['day'] = (int) gmdate( 'j', strtotime( $event->start_date ) ); if ( $event->all_day ) { $event_data['time'] = __( 'All day', '3task-calendar' ); } else { $event_data['time'] = date_i18n( $time_format, strtotime( $event->start_date ) ); } $data[] = $event_data; } wp_send_json_success($data); } /** * AJAX handler: Get single event details for modal */ public function ajax_get_event_details() { check_ajax_referer('threecal_nonce', 'nonce'); $event_id = isset($_POST['event_id']) ? absint($_POST['event_id']) : 0; if (!$event_id) { wp_send_json_error(array('message' => __('Invalid event ID', '3task-calendar'))); } $event = ThreeCal_Event::get($event_id); if (!$event || $event->status !== 'published') { wp_send_json_error(array('message' => __('Event not found', '3task-calendar'))); } // Get location $location = null; if ($event->location_id) { $loc = ThreeCal_Location::get($event->location_id); if ($loc) { $location = array( 'id' => $loc->id, 'name' => $loc->name, 'address' => $loc->get_full_address() ); } } // Get categories $categories = ThreeCal_Event::get_categories($event->id); $cat_data = array(); foreach ($categories as $cat) { $cat_data[] = array( 'id' => $cat->id, 'name' => $cat->name, 'color' => $cat->color ); } // Format dates $date_format = get_option('date_format'); $time_format = get_option('time_format'); $data = array( 'id' => $event->id, 'title' => $event->title, 'description' => wpautop($event->description), 'start_date' => date_i18n($date_format, strtotime($event->start_date)), 'end_date' => $event->end_date ? date_i18n($date_format, strtotime($event->end_date)) : null, 'start_time' => !$event->all_day ? date_i18n($time_format, strtotime($event->start_date)) : null, 'end_time' => (!$event->all_day && $event->end_date) ? date_i18n($time_format, strtotime($event->end_date)) : null, 'all_day' => (bool) $event->all_day, 'location' => $location, 'categories' => $cat_data, 'color' => $event->color, 'url' => $event->url, 'featured_image' => $event->featured_image ? wp_get_attachment_url($event->featured_image) : null ); wp_send_json_success($data); } /** * Ensure capabilities are set (fallback if activation failed) */ public function maybe_add_capabilities() { $admin = get_role('administrator'); if ($admin && !$admin->has_cap('manage_threecal')) { ThreeCal_Activator::activate(); } } /** * Prevent cloning */ private function __clone() {} /** * Prevent unserialize */ public function __wakeup() { throw new Exception('Cannot unserialize singleton'); } } /** * Activation hook */ function threecal_activate() { require_once THREECAL_PLUGIN_DIR . 'includes/class-activator.php'; ThreeCal_Activator::activate(); } register_activation_hook(__FILE__, 'threecal_activate'); /** * Deactivation hook */ function threecal_deactivate() { require_once THREECAL_PLUGIN_DIR . 'includes/class-deactivator.php'; ThreeCal_Deactivator::deactivate(); } register_deactivation_hook(__FILE__, 'threecal_deactivate'); /** * Initialize plugin */ function threecal() { return ThreeCal::instance(); } // Start plugin add_action('plugins_loaded', 'threecal');