commit 63fc9f034dd32db6c1bd69ec7ec5e56078fed9c4 Author: Keith Solomon Date: Sat Aug 30 14:22:44 2025 -0500 ✨feature: Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a79ebc0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +notes/ +phpcs-results.txt diff --git a/.phpcs.xml b/.phpcs.xml new file mode 100644 index 0000000..b7aa2c8 --- /dev/null +++ b/.phpcs.xml @@ -0,0 +1,45 @@ + + + Coding Style Checks. + + + + + + + + vendor/ + node_modules/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..36281d1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "workbench.colorCustomizations": { + "tree.indentGuidesStroke": "#3d92ec", + "activityBar.background": "#123509", + "titleBar.activeBackground": "#1A4A0D", + "titleBar.activeForeground": "#F5FDF3" + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..447b54f --- /dev/null +++ b/README.md @@ -0,0 +1,200 @@ +# AA Events + +An approachable, theme‑friendly events plugin for WordPress. It adds an `Event` post type, useful taxonomies, ACF‑powered fields, and accessible front‑end templates (archive, single, and a calendar page template). Everything can be overridden from your theme when you need custom markup. + +## What you get + +- Event post type with archive at `/events`. +- Two taxonomies: `Event Types` (hierarchical) and `Event Tags` (non‑hierarchical). +- Event details via ACF: date/time, cost, location (online URL vs in‑person address). +- Front‑end templates: archive list, single view, and a calendar page template. +- Theme overrides for templates and styles with simple folder conventions. +- Sensible defaults: admin column for date, sortable by event date; taxonomy views sort upcoming first; main events archive sorts newest first. + +--- + +## Requirements + +- WordPress 5.8+ (uses modern APIs like `wp_date()` and template filters). +- Advanced Custom Fields (free) is required for the event fields. The plugin will load without ACF, but the Event fields and related UI will not be available until ACF is active. + +--- + +## Installation + +1. Copy the `aa-events` folder into `wp-content/plugins/`. +2. (Required) Install and activate the “Advanced Custom Fields” plugin. +3. In WordPress Admin → Plugins, activate “AA Events”. +4. Visit Settings → Permalinks and click Save if your events archive doesn’t appear (activation also flushes rewrites). + +--- + +## Quick Start + +1. Create events: Admin → Events → Add New. +2. Fill in the “Event Details” fields: + - Event Date & Time (required) + - Event Cost (optional) + - Online/In‑Person toggle (required) + - Event URL (required when Online) + - Event Address (required when In‑Person) +3. View your list of events at `/events` and single event pages at their permalinks. +4. For a calendar view, create a new Page and choose the “Events Calendar” template, then publish. + +--- + +## Content Model + +- Post type: `event` + - Public, has archive (`/events`), supports title, editor, author, thumbnail. + - Default sort: + - Admin list: ascending by Event Date & Time. + - Taxonomy views: ascending (upcoming first). + - Main events archive: descending (newest first). +- Taxonomies + - `event_type` (hierarchical, slug `event-type`) + - `event_tag` (non‑hierarchical, slug `event-tag`) +- Admin UX + - Adds a “Date & Time” column on Events list, sortable by the event date. + +--- + +## Front‑End Views + +- Archive: `/events` uses `templates/archive-event.php`. +- Single: each event uses `templates/single-event.php`. +- Calendar page: Page → Template → “Events Calendar” renders `templates/template-calendar.php` which includes `templates/calendar.php`. +- Calendar navigation: query args `?month=MM&year=YYYY` change the visible month; “Today” jumps back to the current month. + +--- + +## Theme Overrides + +The plugin is designed to be theme‑override friendly. Copy any template into your theme under `your-theme/aa-events/` and it will be used instead of the plugin version. + +- Templates you can override: + - `archive-event.php` + - `single-event.php` + - `template-calendar.php` (page template) + - `calendar.php` + +Example: to override the archive, create `your-theme/aa-events/archive-event.php`. + +Page template in a theme: the “Events Calendar” page template also respects theme overrides at `your-theme/aa-events/template-calendar.php`. + +--- + +## Styling + +- Default CSS ships with the plugin and is enqueued automatically. +- To replace it, add `your-theme/aa-events/aa-events.css`. If present, the theme stylesheet is loaded instead of the plugin’s. + +Note: The bundled CSS is intentionally minimal. Use the override to match your theme. + +--- + +## Developer Notes + +- Template loader helpers: + - `events_get_template( $template_name, $args = [], $template_path = 'aa-events/', $default_path = AA_EVENTS_PLUGIN_DIR . 'templates/' )` + - `events_locate_template( $template_name, $template_path = 'aa-events/', $default_path = AA_EVENTS_PLUGIN_DIR . 'templates/' )` +- Filters/actions: + - `events_get_template` (filter: final located path) + - `events_locate_template` (filter: path resolution) + - `events_before_template_part` / `events_after_template_part` (actions around includes) +- Page template registration (no theme file required): + - Registered as “Events Calendar” and loads from the plugin when selected. + +--- + +## Screenshots + +A quick gallery of the key screens. The images below are placeholders you can replace. Drop your own PNG/JPG/GIF assets into `assets/screenshots/` with the same filenames to update the gallery. + +| Admin Events List | Event Edit | Events Archive | Calendar Page | +| --- | --- | --- | --- | +| ![Admin Events List](assets/screenshots/01-admin-events-list.png) | ![Event Edit](assets/screenshots/02-event-edit-fields.png) | ![Events Archive](assets/screenshots/03-events-archive.png) | ![Calendar Page](assets/screenshots/04-calendar-template.png) | + +Tip: GIFs are great for showing calendar navigation (Today/Prev/Next). If you add GIFs, keep the same names but `.gif` extension and update the links here. + +--- + +## Override Examples + +Below are minimal examples showing how to override templates and styles from a theme. Copy into your theme under `your-theme/aa-events/`. + +Example: override archive layout at `your-theme/aa-events/archive-event.php`: + +```php + + +
+
+ + + +
> +

+

+ Date: + +

+
+
+ + +

No events found.

+ +
+
+> +

+

+ Date: + +

+ +

View details

+ +``` + +Example: replace styles by adding `your-theme/aa-events/aa-events.css`: + +```css +/* Theme override for AA Events */ +.aa-events-calendar th, +.aa-events-calendar td { border-color: #ddd; } +.aa-events-calendar .today-cell { background: #e6f7ff; border-color: #1890ff; } +.aa-event-title { font-size: 1.25rem; margin: .25rem 0 .5rem; } +``` + +--- + +## FAQ + +- The “Events Calendar” template doesn’t show up? + - Ensure the plugin is active. The template is registered by the plugin and appears in the Page Template dropdown. If you still don’t see it, check the Page sidebar and expand the Template panel. +- Calendar shows no events? + - Make sure you created Event posts and set the “Event Date & Time”. The calendar shows events within the selected month using site timezone. +- Can I use only the archive without the calendar? + - Yes. The archive at `/events` works independently of the calendar page template. + +--- + +## License + +GPL-2.0-or-later. See `LICENSE` or the plugin header for details. diff --git a/aa-events.php b/aa-events.php new file mode 100644 index 0000000..5ddb46b --- /dev/null +++ b/aa-events.php @@ -0,0 +1,68 @@ + 'group_61b0c5f5a3e7e', + 'title' => 'Event Details', + 'fields' => array( + array( + 'key' => 'field_61b0c609a3e7f', + 'label' => 'Event Date & Time', + 'name' => 'event_datetime', + 'type' => 'date_time_picker', + 'instructions' => '', + 'required' => 1, + 'conditional_logic' => 0, + 'wrapper' => array( + 'width' => '', + 'class' => '', + 'id' => '', + ), + 'display_format' => 'F j, Y g:i a', + 'return_format' => 'Y-m-d H:i:s', + 'first_day' => 1, + ), + array( + 'key' => 'field_61b0c6a5a3e82', + 'label' => 'Event Cost', + 'name' => 'event_cost', + 'type' => 'text', + 'instructions' => '', + 'required' => 0, + 'conditional_logic' => 0, + 'wrapper' => array( + 'width' => '', + 'class' => '', + 'id' => '', + ), + 'default_value' => '', + 'placeholder' => '', + 'prepend' => '', + 'append' => '', + 'maxlength' => '', + ), + array( + 'key' => 'field_61b0c6cba3e83', + 'label' => 'Online/In-Person', + 'name' => 'event_location_type', + 'type' => 'true_false', + 'instructions' => '', + 'required' => 0, + 'conditional_logic' => 0, + 'wrapper' => array( + 'width' => '', + 'class' => '', + 'id' => '', + ), + 'message' => '', + 'default_value' => 0, + 'ui' => 1, + 'ui_on_text' => 'Online', + 'ui_off_text' => 'In-Person', + ), + array( + 'key' => 'field_61b0c702a3e84', + 'label' => 'Event URL', + 'name' => 'event_url', + 'type' => 'url', + 'instructions' => '', + 'required' => 1, + 'conditional_logic' => array( + array( + array( + 'field' => 'field_61b0c6cba3e83', + 'operator' => '==', + 'value' => '1', + ), + ), + ), + 'wrapper' => array( + 'width' => '', + 'class' => '', + 'id' => '', + ), + 'default_value' => '', + 'placeholder' => '', + ), + array( + 'key' => 'field_61b0c72fa3e85', + 'label' => 'Event Address', + 'name' => 'event_address', + 'type' => 'textarea', + 'instructions' => '', + 'required' => 1, + 'conditional_logic' => array( + array( + array( + 'field' => 'field_61b0c6cba3e83', + 'operator' => '!=', + 'value' => '1', + ), + ), + ), + 'wrapper' => array( + 'width' => '', + 'class' => '', + 'id' => '', + ), + 'default_value' => '', + 'placeholder' => '', + 'maxlength' => '', + 'rows' => '', + 'new_lines' => '', + ), + ), + 'location' => array( + array( + array( + 'param' => 'post_type', + 'operator' => '==', + 'value' => 'event', + ), + ), + ), + 'menu_order' => 0, + 'position' => 'normal', + 'style' => 'default', + 'label_placement' => 'top', + 'instruction_placement' => 'label', + 'hide_on_screen' => '', + 'active' => true, + 'description' => '', + ) + ); + + endif; +} +add_action( 'acf/init', 'events_register_acf_fields' ); diff --git a/includes/class-aa-events.php b/includes/class-aa-events.php new file mode 100644 index 0000000..6e90fcb --- /dev/null +++ b/includes/class-aa-events.php @@ -0,0 +1,128 @@ +includes(); + $this->init_hooks(); + } + + /** + * Include required core files. + */ + public function includes() { + include_once AA_EVENTS_PLUGIN_DIR . 'includes/post-types.php'; + include_once AA_EVENTS_PLUGIN_DIR . 'includes/acf-fields.php'; + include_once AA_EVENTS_PLUGIN_DIR . 'includes/template-loader.php'; + } + + /** + * Hook into actions and filters. + */ + private function init_hooks() { + add_action( 'init', array( $this, 'load_plugin_textdomain' ) ); + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_styles' ) ); + // Ensure the active theme supports thumbnails for the event post type. + add_action( 'after_setup_theme', array( $this, 'ensure_post_thumbnails_support' ) ); + } + + /** + * Enqueue styles. + */ + public function enqueue_styles() { + $theme_stylesheet = get_stylesheet_directory() . '/aa-events/aa-events.css'; + + if ( file_exists( $theme_stylesheet ) ) { + wp_enqueue_style( + 'aa-events-theme', + get_stylesheet_directory_uri() . '/aa-events/aa-events.css', + array(), + filemtime( $theme_stylesheet ) + ); + } else { + wp_enqueue_style( + 'aa-events', + AA_EVENTS_PLUGIN_URL . 'assets/css/aa-events.css', + array(), + AA_EVENTS_VERSION + ); + } + } + + /** + * Load Localisation files. + */ + public function load_plugin_textdomain() { + load_plugin_textdomain( + 'aa-events', + false, + dirname( plugin_basename( __FILE__ ) ) . '/languages/' + ); + } + + /** + * Ensure the theme supports post thumbnails for events. + * + * Some themes do not declare `add_theme_support( 'post-thumbnails' )`, + * which hides the Featured Image panel. This enables thumbnails at least + * for the `event` post type without impacting other types. + */ + public function ensure_post_thumbnails_support() { + $support = get_theme_support( 'post-thumbnails' ); + + // If theme has no support at all, enable it for events only. + if ( false === $support ) { + add_theme_support( 'post-thumbnails', array( 'event' ) ); + return; + } + + // If theme supports specific post types, ensure 'event' is included. + if ( is_array( $support ) && isset( $support[0] ) && is_array( $support[0] ) ) { + $post_types = $support[0]; + if ( ! in_array( 'event', $post_types, true ) ) { + $post_types[] = 'event'; + add_theme_support( 'post-thumbnails', $post_types ); + } + } + // If theme support is boolean true (all types), nothing to do. + } +} diff --git a/includes/post-types.php b/includes/post-types.php new file mode 100644 index 0000000..fd954b0 --- /dev/null +++ b/includes/post-types.php @@ -0,0 +1,224 @@ + _x( 'Events', 'post type general name', 'aa-events' ), + 'singular_name' => _x( 'Event', 'post type singular name', 'aa-events' ), + 'menu_name' => _x( 'Events', 'admin menu', 'aa-events' ), + 'name_admin_bar' => _x( 'Event', 'add new on admin bar', 'aa-events' ), + 'add_new' => _x( 'Add New', 'event', 'aa-events' ), + 'add_new_item' => __( 'Add New Event', 'aa-events' ), + 'new_item' => __( 'New Event', 'aa-events' ), + 'edit_item' => __( 'Edit Event', 'aa-events' ), + 'view_item' => __( 'View Event', 'aa-events' ), + 'all_items' => __( 'All Events', 'aa-events' ), + 'search_items' => __( 'Search Events', 'aa-events' ), + 'parent_item_colon' => __( 'Parent Events:', 'aa-events' ), + 'not_found' => __( 'No events found.', 'aa-events' ), + 'not_found_in_trash' => __( 'No events found in Trash.', 'aa-events' ), + ); + + $args = array( + 'labels' => $labels, + 'public' => true, + 'publicly_queryable' => true, + 'show_ui' => true, + 'show_in_menu' => true, + 'query_var' => true, + 'rewrite' => array( 'slug' => 'events' ), + 'capability_type' => 'post', + 'has_archive' => true, + 'hierarchical' => false, + 'menu_position' => 5, + 'supports' => array( 'title', 'editor', 'author', 'thumbnail' ), + 'menu_icon' => 'dashicons-calendar-alt', + ); + + register_post_type( 'event', $args ); +} +add_action( 'init', 'events_register_post_type' ); + +/** + * Register a custom taxonomy for event types. + */ +function events_register_event_type_taxonomy() { + $labels = array( + 'name' => 'Event Types', + 'singular_name' => 'Event Type', + 'search_items' => 'Search Event Types', + 'all_items' => 'All Event Types', + 'parent_item' => 'Parent Event Type', + 'parent_item_colon' => 'Parent Event Type:', + 'edit_item' => 'Edit Event Type', + 'update_item' => 'Update Event Type', + 'add_new_item' => 'Add New Event Type', + 'new_item_name' => 'New Event Type Name', + 'menu_name' => 'Event Types', + ); + + $args = array( + 'hierarchical' => true, + 'labels' => $labels, + 'show_ui' => true, + 'show_admin_column' => true, + 'query_var' => true, + 'rewrite' => array( 'slug' => 'event-type' ), + ); + + register_taxonomy( 'event_type', array( 'event' ), $args ); +} +add_action( 'init', 'events_register_event_type_taxonomy' ); + +/** + * Register a custom taxonomy for event tags. + */ +function events_register_event_tag_taxonomy() { + $labels = array( + 'name' => _x( 'Event Tags', 'taxonomy general name', 'aa-events' ), + 'singular_name' => _x( 'Event Tag', 'taxonomy singular name', 'aa-events' ), + 'search_items' => __( 'Search Event Tags', 'aa-events' ), + 'all_items' => __( 'All Event Tags', 'aa-events' ), + 'parent_item' => null, + 'parent_item_colon' => null, + 'edit_item' => __( 'Edit Event Tag', 'aa-events' ), + 'update_item' => __( 'Update Event Tag', 'aa-events' ), + 'add_new_item' => __( 'Add New Event Tag', 'aa-events' ), + 'new_item_name' => __( 'New Event Tag Name', 'aa-events' ), + 'separate_items_with_commas' => __( 'Separate event tags with commas', 'aa-events' ), + 'add_or_remove_items' => __( 'Add or remove event tags', 'aa-events' ), + 'choose_from_most_used' => __( 'Choose from the most used event tags', 'aa-events' ), + 'not_found' => __( 'No event tags found.', 'aa-events' ), + 'menu_name' => __( 'Event Tags', 'aa-events' ), + ); + + $args = array( + 'hierarchical' => false, + 'labels' => $labels, + 'show_ui' => true, + 'show_admin_column' => true, + 'query_var' => true, + 'rewrite' => array( 'slug' => 'event-tag' ), + ); + + register_taxonomy( 'event_tag', array( 'event' ), $args ); +} +add_action( 'init', 'events_register_event_tag_taxonomy' ); + +/** + * Add custom columns to the event list table. + * + * @param array $columns The existing columns. + * @return array The modified columns. + */ +function events_add_admin_columns( $columns ) { + $new_columns = array(); + foreach ( $columns as $key => $value ) { + $new_columns[ $key ] = $value; + if ( 'title' === $key ) { + $new_columns['event_datetime'] = __( 'Event Date & Time', 'aa-events' ); + } + } + return $new_columns; +} +add_filter( 'manage_edit-event_columns', 'events_add_admin_columns' ); + +/** + * Display the content for the custom columns. + * + * @param string $column The name of the column. + * @param int $post_id The ID of the post. + */ +function events_display_admin_column_content( $column, $post_id ) { + if ( 'event_datetime' === $column ) { + $datetime = get_field( 'event_datetime', $post_id ); + if ( $datetime ) { + echo esc_html( gmdate( 'F j, Y g:i a', strtotime( $datetime ) ) ); + } + } +} +add_action( 'manage_event_posts_custom_column', 'events_display_admin_column_content', 10, 2 ); + +/** + * Make the custom column sortable. + * + * @param array $columns The existing sortable columns. + * @return array The modified sortable columns. + */ +function events_make_admin_column_sortable( $columns ) { + $columns['event_datetime'] = 'event_datetime'; + return $columns; +} +add_filter( 'manage_edit-event_sortable_columns', 'events_make_admin_column_sortable' ); + +/** + * Handle sorting by the custom column. + * + * @param WP_Query $query The WordPress query object. + */ +function events_sort_by_custom_column( $query ) { + if ( ! is_admin() || ! $query->is_main_query() ) { + return; + } + + if ( 'event_datetime' === $query->get( 'orderby' ) ) { + $query->set( 'meta_key', 'event_datetime' ); + $query->set( 'orderby', 'meta_value' ); + } +} +add_action( 'pre_get_posts', 'events_sort_by_custom_column' ); + +/** + * Set default sort order for event post type by event_datetime ascending. + * Applies to both admin and frontend queries. + * + * @param WP_Query $query The WordPress query object. + */ +function events_set_default_sort_order( $query ) { + // Only modify main queries for event post type + if ( ! $query->is_main_query() ) { + return; + } + + // Admin list table + if ( is_admin() && $query->get( 'post_type' ) === 'event' ) { + if ( ! $query->get( 'orderby' ) ) { + $query->set( 'meta_key', 'event_datetime' ); + $query->set( 'orderby', 'meta_value' ); + $query->set( 'order', 'ASC' ); + } + } + + // Frontend: event archives and taxonomies + if ( ! is_admin() && $query->get( 'post_type' ) === 'event' ) { + // Main event archive: newest first (descending by date/time) + if ( $query->is_post_type_archive( 'event' ) ) { + if ( ! $query->get( 'orderby' ) ) { + $query->set( 'meta_key', 'event_datetime' ); + $query->set( 'meta_type', 'DATETIME' ); + $query->set( 'orderby', 'meta_value' ); + $query->set( 'order', 'DESC' ); + } + } + + // Taxonomy views: upcoming first (ascending) + if ( ( $query->is_tax( 'event_type' ) || $query->is_tax( 'event_tag' ) ) && ! $query->get( 'orderby' ) ) { + $query->set( 'meta_key', 'event_datetime' ); + $query->set( 'meta_type', 'DATETIME' ); + $query->set( 'orderby', 'meta_value' ); + $query->set( 'order', 'ASC' ); + } + } +} +add_action( 'pre_get_posts', 'events_set_default_sort_order' ); diff --git a/includes/template-loader.php b/includes/template-loader.php new file mode 100644 index 0000000..42c8a36 --- /dev/null +++ b/includes/template-loader.php @@ -0,0 +1,163 @@ +%s does not exist.', esc_html( $located ) ), '1.0' ); + return; + } + + // Allow 3rd party plugin filter template file from theme. + $located = apply_filters( 'events_get_template', $located, $template_name, $args, $template_path, $default_path ); + + do_action( 'events_before_template_part', $template_name, $template_path, $located, $args ); + + include $located; + + do_action( 'events_after_template_part', $template_name, $template_path, $located, $args ); +} + +/** + * Locate a template and return the path for inclusion. + * + * This is the load order: + * + * yourtheme/$template_path/$template_name + * yourtheme/$template_name + * $default_path/$template_name + * + * @param string $template_name Template name. + * @param string $template_path Template path. (default: ''). + * @param string $default_path Default path. (default: ''). + * @return string + */ +function events_locate_template( $template_name, $template_path = '', $default_path = '' ) { + if ( ! $template_path ) { + $template_path = 'aa-events/'; + } + + if ( ! $default_path ) { + $default_path = AA_EVENTS_PLUGIN_DIR . 'templates/'; + } + + // Look within passed path within the theme - this is priority. + $template = locate_template( + array( + trailingslashit( $template_path ) . $template_name, + $template_name, + ) + ); + + // Get default template/ + if ( ! $template ) { + $template = $default_path . $template_name; + } + + // Return what we found. + return apply_filters( 'events_locate_template', $template, $template_name, $template_path ); +} + +/** + * Filter the single template. + * + * @param string $template Template. + * @return string + */ +function events_single_template( $template ) { + if ( get_post_type() === 'event' ) { + $template = events_locate_template( 'single-event.php' ); + } + return $template; +} +add_filter( 'single_template', 'events_single_template' ); + +/** + * Filter the archive template. + * + * @param string $template Template. + * @return string + */ +function events_archive_template( $template ) { + if ( is_post_type_archive( 'event' ) ) { + $template = events_locate_template( 'archive-event.php' ); + } + return $template; +} +add_filter( 'archive_template', 'events_archive_template' ); + +/** + * Register plugin-provided page templates so they appear in the editor. + * + * WordPress only scans themes for page templates by default. This filter + * injects our plugin template into the list shown in the Page Attributes + * (or Template) dropdown, without requiring the file to live in the theme. + * + * @param array $page_templates Array of page templates. + * @param WP_Theme $theme WP_Theme object. + * @param WP_Post|null $post The post being edited (or null). + * @param string $post_type Post type slug. + * @return array + */ +function events_register_page_templates( $page_templates, $theme, $post, $post_type ) { + if ( 'page' !== $post_type ) { + return $page_templates; + } + + // Key can be any unique string. We'll look for this exact value when loading. + $page_templates['aa-events-template-calendar.php'] = __( 'Events Calendar', 'aa-events' ); + + return $page_templates; +} +add_filter( 'theme_page_templates', 'events_register_page_templates', 10, 4 ); + +/** + * Load the selected plugin template on the front end. + * + * When a page uses our registered template slug, point WordPress to the + * template file bundled inside the plugin. + * + * @param string $template Resolved template path. + * @return string + */ +function events_load_plugin_page_template( $template ) { + if ( is_singular( 'page' ) ) { + $post_id = get_queried_object_id(); + if ( $post_id ) { + $selected = get_post_meta( $post_id, '_wp_page_template', true ); + if ( 'aa-events-template-calendar.php' === $selected ) { + // Allow theme override via aa-events/template-calendar.php, then fall back to plugin file. + $resolved = events_locate_template( 'template-calendar.php' ); + if ( $resolved && file_exists( $resolved ) ) { + return $resolved; + } + } + } + } + + return $template; +} +add_filter( 'template_include', 'events_load_plugin_page_template', 20 ); diff --git a/templates/aa-events.css b/templates/aa-events.css new file mode 100644 index 0000000..d5bf8bc --- /dev/null +++ b/templates/aa-events.css @@ -0,0 +1,71 @@ +/** + * AA Events Frontend CSS. + */ + +.aa-events-calendar-wrap { margin-bottom: 1.25rem; } + +.aa-events-calendar-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.625rem; +} + +.aa-events-calendar-today-btn { + background: #aaa; + border-radius: 0.25rem; + color: #222; + display: inline-block; + font-weight: bold; + padding: 0.5rem 1rem; + text-decoration: none; + + &:hover { background: #eee; } +} + +.aa-events-calendar { + border-collapse: collapse; + table-layout: fixed; + width: 100%; + + th, td { + border: 1px solid #aaa; + padding: 0.625rem; + text-align: left; + vertical-align: top; + } + + th { + background-color: #f5f5f5; + text-align: center; + } + + td { height: 9.375rem; } + + .day-number { + font-weight: bold; + margin-bottom: 0.3125rem; + } + + .today-cell { + background: #ffe9b3; + border: 2px solid #f7b500; + font-weight: bold; + } + + .events-list { + list-style: none; + margin: 0; + padding: 0; + + li { margin-bottom: 0.3125rem; } + } +} + +.aa-event-item .aa-event-details { + border-top: 1px solid #ddd; + margin-top: 1.25rem; + padding-top: 1.25rem; + + div { margin-bottom: 0.625rem; } +} diff --git a/templates/archive-event.php b/templates/archive-event.php new file mode 100644 index 0000000..620f1eb --- /dev/null +++ b/templates/archive-event.php @@ -0,0 +1,104 @@ + + +
+
+ + + +
> +
+ ', '' ); ?> +
+ +
+ +
+ +
+
+
+ + getTimestamp(); + $human = wp_date( 'F j, Y, g:i A', $ts ); + $iso_attr = wp_date( 'c', $ts ); + ?> + + +
+ + +
+ + +
+ + +
+ + ' . esc_html( $location ) . ''; + } else { + // In-person event + $location = get_field( 'event_address' ); + echo '

' . esc_html( $location ) . '

'; + } + ?> +
+ + +
+ + +
+ + + +
+ + +
+ +
+
+
+ + +

+ + +
+
+ + array( + 'min_range' => 1, + 'max_range' => 12, + ), + ) +); + +$year_input = filter_input( + INPUT_GET, + 'year', + FILTER_VALIDATE_INT, + array( + 'options' => array( + // Choose a sane range for years. + 'min_range' => 1970, + 'max_range' => 2200, + ), + ) +); + +$curMonth = ( false !== $month_input && null !== $month_input ) ? (int) $month_input : (int) wp_date( 'n' ); +$curYear = ( false !== $year_input && null !== $year_input ) ? (int) $year_input : (int) wp_date( 'Y' ); + +// Get previous and next month and year. +$prev_month = $curMonth - 1; +$prev_year = $curYear; + +if ( $prev_month < 1 ) { + $prev_month = 12; + --$prev_year; +} + +$next_month = $curMonth + 1; +$next_year = $curYear; +if ( $next_month > 12 ) { + $next_month = 1; + ++$next_year; +} + +// Get the number of days in the month. +$days_in_month = cal_days_in_month( CAL_GREGORIAN, $curMonth, $curYear ); + +// Get the first day of the month in site timezone. +$tz = wp_timezone(); +$first_ts = ( new DateTimeImmutable( sprintf( '%04d-%02d-01 00:00:00', $curYear, $curMonth ), $tz ) )->getTimestamp(); +$first_day = (int) wp_date( 'N', $first_ts ); + +// Get events for the current month. +$events = new WP_Query( + array( + 'post_type' => 'event', + 'posts_per_page' => -1, + 'meta_query' => array( + array( + 'key' => 'event_datetime', + 'value' => array( "$curYear-$curMonth-01 00:00:00", "$curYear-$curMonth-$days_in_month 23:59:59" ), + 'compare' => 'BETWEEN', + 'type' => 'DATETIME', + ), + ), + ) +); + +$events_by_day = array(); + +if ( $events->have_posts() ) { + while ( $events->have_posts() ) { + $events->the_post(); + $event_date = get_field( 'event_datetime' ); + $event_ts = ( new DateTimeImmutable( $event_date, $tz ) )->getTimestamp(); + $day = (int) wp_date( 'j', $event_ts ); + $events_by_day[ $day ][] = '' . get_the_title() . ''; + } + + wp_reset_postdata(); +} + +// Get today's date. +$todayDay = intval( wp_date( 'j' ) ); +$todayMonth = intval( wp_date( 'n' ) ); +$todayYear = intval( wp_date( 'Y' ) ); + +// Helper to check if today is in current view. +$isTodayInCurrentMonth = ( $curMonth === $todayMonth && $curYear === $todayYear ); +?> + +'; + echo '
' . esc_html( $day ) . '
'; + if ( isset( $eventsByDay[ $day ] ) ) { + echo ''; + } + echo ''; +}; + +// Overwrite the default day cell output in the table loop. +ob_start(); +for ( $day = 1; $day <= $days_in_month; $day++ ) { + if ( ( $day + $first_day - 2 ) % 7 === 0 && $day > 1 ) { + echo ''; + } + $originalEchoDayCell( $day, $first_day, $events_by_day ); +} +$calendarRows = ob_get_clean(); + +// Replace the original loop output with the new one. +$calendarOutput = preg_replace( + '/for\s*\(\s*\$day\s*=\s*1;\s*\$day\s*<=\s*\$days_in_month;\s*\$day\+\+\s*\)\s*\{.*?\}/s', + $calendarRows, + ob_get_contents() +); +?> + +
+ + +
+ getTimestamp(); + $next_ts = ( new DateTimeImmutable( sprintf( '%04d-%02d-01 00:00:00', $next_year, $next_month ), $tz ) )->getTimestamp(); + $prevMonthName = wp_date( 'F', $prev_ts ); + $nextMonthName = wp_date( 'F', $next_ts ); + ?> + " + >« + +

+ + " + > » +
+ + + + + + + + + + + + + + + + + '; + } + + // Loop through the days of the month. + for ( $day = 1; $day <= $days_in_month; $day++ ) { + if ( ( $day + $first_day - 2 ) % 7 === 0 && $day > 1 ) { + echo ''; + } + + $cellClass = ''; + + if ( $isTodayInCurrentMonth && $day === $todayDay ) { + $cellClass = 'today-cell'; + } + + $cell_ts = ( new DateTimeImmutable( sprintf( '%04d-%02d-%02d 00:00:00', $curYear, $curMonth, $day ), $tz ) )->getTimestamp(); + echo ''; + } + + // Add empty cells for the last week. + $remaining_days = 7 - ( ( $days_in_month + $first_day - 1 ) % 7 ); + + if ( $remaining_days < 7 ) { + for ( $i = 0; $i < $remaining_days; $i++ ) { + echo ''; + } + } + ?> + + +
'; + + echo '
' . esc_html( $day ) . '
'; + + if ( isset( $events_by_day[ $day ] ) ) { + echo '
    '; + + foreach ( $events_by_day[ $day ] as $event ) { + echo '
  • ' . wp_kses_post( $event ) . '
  • '; + } + + echo '
'; + } + echo '
+
diff --git a/templates/single-event.php b/templates/single-event.php new file mode 100644 index 0000000..255999b --- /dev/null +++ b/templates/single-event.php @@ -0,0 +1,102 @@ + + +
+
+ + +
> +
+ ', '' ); ?> +
+ +
+
+ +
+ + +
+ the_title_attribute( 'echo=0' ) ) ); ?> +
+ +
+ +
+
+
+ + getTimestamp(); + $human = wp_date( 'F j, Y, g:i A', $ts ); + $iso_attr = wp_date( 'c', $ts ); + ?> + + +
+ + +
+ + +
+ + +
+ + ' . esc_html( $location ) . ''; + } else { + // In-person event + $location = get_field( 'event_address' ); + echo '

' . esc_html( $location ) . '

'; + } + ?> +
+ + +
+ + +
+ + + +
+ + +
+ +
+
+
+ + +
+
+ + + +
+
+ + + +
+
+ +