diff --git a/wp-content/plugins/wporg-learn/.prettierrc b/wp-content/plugins/wporg-learn/.prettierrc
new file mode 100644
index 000000000..cb7f2abe3
--- /dev/null
+++ b/wp-content/plugins/wporg-learn/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "useTabs": true,
+ "tabWidth": 4,
+ "printWidth": 120
+}
diff --git a/wp-content/plugins/wporg-learn/inc/activity-kit-import.php b/wp-content/plugins/wporg-learn/inc/activity-kit-import.php
new file mode 100644
index 000000000..d20548960
--- /dev/null
+++ b/wp-content/plugins/wporg-learn/inc/activity-kit-import.php
@@ -0,0 +1,138 @@
+ 'Debugging for Developers',
+ 'topics' => array( 'development' ),
+ 'levels' => array( 'Intermediate' ),
+ ),
+ array(
+ 'title' => 'Debugging for Site Owners',
+ 'topics' => array( 'site-management' ),
+ 'levels' => array( 'Beginner' ),
+ ),
+ array(
+ 'title' => 'eCommerce with WooCommerce',
+ 'topics' => array( 'woocommerce', 'ecommerce' ),
+ 'levels' => array( 'Beginner' ),
+ ),
+ array(
+ 'title' => 'SEO Foundations',
+ 'topics' => array( 'seo' ),
+ 'levels' => array( 'Beginner' ),
+ ),
+ array(
+ 'title' => 'WordPress Playground',
+ 'topics' => array( 'playground' ),
+ 'levels' => array( 'Beginner' ),
+ ),
+ array(
+ 'title' => 'Content Creation',
+ 'topics' => array( 'content-creation' ),
+ 'levels' => array( 'Beginner' ),
+ ),
+ array(
+ 'title' => 'Using AI in your WordPress Dashboard',
+ 'topics' => array( 'ai', 'site-management' ),
+ 'levels' => array( 'Beginner' ),
+ ),
+ array(
+ 'title' => 'Managing your WordPress site with AI',
+ 'topics' => array( 'ai', 'site-management' ),
+ 'levels' => array( 'Intermediate' ),
+ ),
+ array(
+ 'title' => 'Contributor Onboarding',
+ 'topics' => array( 'contributing' ),
+ 'levels' => array( 'Beginner' ),
+ ),
+ array(
+ 'title' => 'WordPress Security Essentials',
+ 'topics' => array( 'security' ),
+ 'levels' => array( 'Beginner' ),
+ ),
+ array(
+ 'title' => 'Accessibility Testing in WordPress',
+ 'topics' => array( 'accessibility' ),
+ 'levels' => array( 'Beginner' ),
+ ),
+ );
+
+ foreach ( $kits as $kit_data ) {
+ $existing_query = new \WP_Query(
+ array(
+ 'post_type' => 'activity_kit',
+ 'post_status' => 'any',
+ 'title' => $kit_data['title'],
+ 'posts_per_page' => 1,
+ 'fields' => 'ids',
+ )
+ );
+ $existing = $existing_query->have_posts() ? get_post( $existing_query->posts[0] ) : null;
+
+ if ( $existing && ! $force ) {
+ \WP_CLI::log( sprintf( 'Skipping "%s" — already exists (ID %d). Use --force to re-import.', $kit_data['title'], $existing->ID ) );
+ continue;
+ }
+
+ $post_id = wp_insert_post(
+ array(
+ 'post_title' => $kit_data['title'],
+ 'post_type' => 'activity_kit',
+ 'post_status' => 'draft',
+ 'post_author' => 0,
+ ),
+ true
+ );
+
+ if ( is_wp_error( $post_id ) ) {
+ \WP_CLI::warning( sprintf( 'Failed to create "%s": %s', $kit_data['title'], $post_id->get_error_message() ) );
+ continue;
+ }
+
+ if ( ! empty( $kit_data['topics'] ) ) {
+ wp_set_object_terms( $post_id, $kit_data['topics'], 'topic' );
+ }
+
+ if ( ! empty( $kit_data['levels'] ) ) {
+ wp_set_object_terms( $post_id, $kit_data['levels'], 'level' );
+ }
+
+ \WP_CLI::success( sprintf( 'Created "%s" (ID %d)', $kit_data['title'], $post_id ) );
+ }
+
+ \WP_CLI::log( 'Import complete.' );
+ }
+}
+
+\WP_CLI::add_command( 'activity-kit', __NAMESPACE__ . '\Activity_Kit_CLI' );
diff --git a/wp-content/plugins/wporg-learn/inc/activity-kit-rest.php b/wp-content/plugins/wporg-learn/inc/activity-kit-rest.php
new file mode 100644
index 000000000..a710fb0fc
--- /dev/null
+++ b/wp-content/plugins/wporg-learn/inc/activity-kit-rest.php
@@ -0,0 +1,273 @@
+ 'GET',
+ 'callback' => __NAMESPACE__ . '\handle_stats',
+ 'permission_callback' => function () {
+ return current_user_can( 'manage_options' );
+ },
+ 'args' => array(
+ 'metric' => array(
+ 'default' => 'both',
+ 'enum' => array( 'both', 'views', 'downloads' ),
+ ),
+ 'range' => array(
+ 'default' => 'all',
+ 'enum' => array( '7d', '30d', '90d', 'all' ),
+ ),
+ 'kit' => array(
+ 'default' => '',
+ 'type' => 'string',
+ 'sanitize_callback' => 'sanitize_text_field',
+ ),
+ ),
+ )
+ );
+}
+
+/**
+ * Shorten Jetpack's stats API cache so the activity kit dashboard tracks
+ * WordPress.com's near-real-time counts more closely. Registered only for the
+ * duration of the activity kit stats REST request (see handle_stats()), so it
+ * does not affect other Jetpack Stats consumers site-wide.
+ *
+ * Jetpack caches stats REST responses for 5 minutes by default. This caps the
+ * lifetime at 1 minute (never lengthening it) and floors it at 1 second, so a
+ * stray 0 from another filter can't turn into a never-expiring transient. Note
+ * this only affects the local cache layer — WordPress.com's own per-post
+ * aggregation has its own, separate processing delay that cannot be shortened
+ * from here.
+ *
+ * @param int $expiration Default cache expiration, in seconds.
+ * @return int Cache expiration, in seconds.
+ */
+function stats_cache_expiration( $expiration ) {
+ return (int) max( 1, min( $expiration, MINUTE_IN_SECONDS ) );
+}
+
+/**
+ * Handle GET /activity-kits/v1/stats
+ *
+ * @param \WP_REST_Request $request The REST request.
+ * @return \WP_REST_Response
+ */
+function handle_stats( $request ) {
+ $metric = $request->get_param( 'metric' );
+ $range = $request->get_param( 'range' );
+ $kit = $request->get_param( 'kit' );
+
+ $kits = get_posts(
+ array(
+ 'post_type' => 'activity_kit',
+ 'posts_per_page' => -1,
+ 'post_status' => 'publish',
+ 'orderby' => 'title',
+ 'order' => 'ASC',
+ )
+ );
+
+ $jetpack_unavailable = ! class_exists( '\Automattic\Jetpack\Stats\WPCOM_Stats' );
+
+ // Build ZIP URL => post IDs map for download click matching.
+ // A URL maps to an array of kit IDs so that if multiple kits share a zip,
+ // each kit is credited rather than silently dropped by a key collision.
+ $zip_url_map = array();
+ foreach ( $kits as $kit_post ) {
+ $zip_id = (int) get_post_meta( $kit_post->ID, '_activity_zip_id', true );
+ if ( $zip_id ) {
+ $zip_url = wp_get_attachment_url( $zip_id );
+ if ( $zip_url ) {
+ $zip_url_map[ $zip_url ][] = $kit_post->ID;
+ }
+ }
+ }
+
+ $views_map = array();
+ $downloads_map = array();
+
+ if ( ! $jetpack_unavailable ) {
+ // Shorten the Jetpack stats cache for this dashboard request only, so it
+ // doesn't affect other Jetpack Stats consumers site-wide.
+ add_filter( 'jetpack_fetch_stats_cache_expiration', __NAMESPACE__ . '\stats_cache_expiration' );
+
+ if ( 'both' === $metric || 'views' === $metric ) {
+ $views_map = get_jetpack_post_views( $range );
+ }
+ if ( 'both' === $metric || 'downloads' === $metric ) {
+ $downloads_map = get_jetpack_download_clicks( $range, $zip_url_map );
+ }
+ }
+
+ $results = array();
+
+ foreach ( $kits as $kit_post ) {
+ if ( $kit && $kit_post->post_name !== $kit ) {
+ continue;
+ }
+
+ $data = array(
+ 'id' => $kit_post->ID,
+ 'title' => $kit_post->post_title,
+ 'slug' => $kit_post->post_name,
+ 'updated' => get_the_modified_date( 'Y-m-d', $kit_post->ID ),
+ );
+
+ if ( $jetpack_unavailable ) {
+ $data['jetpack_unavailable'] = true;
+ $data['views'] = 0;
+ $data['downloads'] = 0;
+ } else {
+ if ( 'both' === $metric || 'views' === $metric ) {
+ $data['views'] = $views_map[ $kit_post->ID ] ?? 0;
+ }
+ if ( 'both' === $metric || 'downloads' === $metric ) {
+ $data['downloads'] = $downloads_map[ $kit_post->ID ] ?? 0;
+ }
+ }
+
+ $results[] = $data;
+ }
+
+ return rest_ensure_response( $results );
+}
+
+/**
+ * Get per-post view counts from Jetpack Stats for a given time range.
+ *
+ * @param string $range One of '7d', '30d', '90d', 'all'.
+ * @return array Map of post_id (int) => view_count (int). Empty on failure.
+ */
+function get_jetpack_post_views( $range ) {
+ if ( ! class_exists( '\Automattic\Jetpack\Stats\WPCOM_Stats' ) ) {
+ return array();
+ }
+
+ $stats = new \Automattic\Jetpack\Stats\WPCOM_Stats();
+
+ if ( 'all' === $range ) {
+ $period = 'month';
+ $num = 36;
+ } else {
+ $period = 'day';
+ $num = intval( str_replace( 'd', '', $range ) );
+ }
+
+ $result = $stats->get_top_posts(
+ array(
+ 'period' => $period,
+ 'num' => $num,
+ 'date' => gmdate( 'Y-m-d' ),
+ 'summarize' => true,
+ 'max' => 1000,
+ )
+ );
+
+ if ( is_wp_error( $result ) || ! is_array( $result ) ) {
+ return array();
+ }
+
+ $post_views = isset( $result['summary']['postviews'] ) ? $result['summary']['postviews'] : array();
+ if ( ! is_array( $post_views ) ) {
+ return array();
+ }
+
+ $map = array();
+ foreach ( $post_views as $post_data ) {
+ if ( isset( $post_data['id'], $post_data['views'] ) ) {
+ $map[ (int) $post_data['id'] ] = (int) $post_data['views'];
+ }
+ }
+
+ return $map;
+}
+
+/**
+ * Get per-kit download click counts from Jetpack Clicks report.
+ *
+ * @param string $range One of '7d', '30d', '90d', 'all'.
+ * @param array $zip_url_map Map of zip_url (string) => array of post_ids (int[]).
+ * @return array Map of post_id (int) => click_count (int). Empty on failure.
+ */
+function get_jetpack_download_clicks( $range, $zip_url_map ) {
+ if ( ! class_exists( '\Automattic\Jetpack\Stats\WPCOM_Stats' ) || empty( $zip_url_map ) ) {
+ return array();
+ }
+
+ $stats = new \Automattic\Jetpack\Stats\WPCOM_Stats();
+
+ if ( 'all' === $range ) {
+ $period = 'month';
+ $num = 36;
+ } else {
+ $period = 'day';
+ $num = intval( str_replace( 'd', '', $range ) );
+ }
+
+ $result = $stats->get_clicks(
+ array(
+ 'period' => $period,
+ 'num' => $num,
+ 'date' => gmdate( 'Y-m-d' ),
+ 'summarize' => true,
+ 'max' => 1000,
+ )
+ );
+
+ if ( is_wp_error( $result ) || ! is_array( $result ) ) {
+ return array();
+ }
+
+ $clicks = isset( $result['summary']['clicks'] ) ? $result['summary']['clicks'] : array();
+ if ( ! is_array( $clicks ) ) {
+ return array();
+ }
+
+ // Jetpack groups clicks into a tree: a domain node carries a NULL url and
+ // the real per-URL clicks under `children`, while a lone click can appear
+ // as a flat leaf. Walk the tree and keep only leaves with a url + views.
+ $leaves = array();
+ $stack = $clicks;
+ while ( $stack ) {
+ $node = array_pop( $stack );
+ if ( ! empty( $node['children'] ) && is_array( $node['children'] ) ) {
+ foreach ( $node['children'] as $child ) {
+ $stack[] = $child;
+ }
+ } elseif ( isset( $node['url'], $node['views'] ) ) {
+ $leaves[] = $node;
+ }
+ }
+
+ $map = array();
+ foreach ( $leaves as $leaf ) {
+ if ( ! isset( $zip_url_map[ $leaf['url'] ] ) ) {
+ continue;
+ }
+ foreach ( $zip_url_map[ $leaf['url'] ] as $post_id ) {
+ $map[ $post_id ] = ( isset( $map[ $post_id ] ) ? $map[ $post_id ] : 0 ) + (int) $leaf['views'];
+ }
+ }
+
+ return $map;
+}
diff --git a/wp-content/plugins/wporg-learn/inc/activity-kit-settings.php b/wp-content/plugins/wporg-learn/inc/activity-kit-settings.php
new file mode 100644
index 000000000..b1b1bbe93
--- /dev/null
+++ b/wp-content/plugins/wporg-learn/inc/activity-kit-settings.php
@@ -0,0 +1,88 @@
+ 'string',
+ 'sanitize_callback' => 'esc_url_raw',
+ 'default' => '',
+ )
+ );
+}
+
+/**
+ * Add a Settings submenu under the Activity Kits post-type menu.
+ */
+function add_settings_page(): void {
+ add_submenu_page(
+ 'edit.php?post_type=activity_kit',
+ __( 'Activity Kit Settings', 'wporg-learn' ),
+ __( 'Settings', 'wporg-learn' ),
+ 'manage_options',
+ 'activity-kit-settings',
+ __NAMESPACE__ . '\render_settings_page'
+ );
+}
+
+/**
+ * Render the settings page.
+ */
+function render_settings_page(): void {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ return;
+ }
+ ?>
+
+ rest_url( 'activity-kits/v1/stats' ),
+ 'nonce' => wp_create_nonce( 'wp_rest' ),
+ 'jetpackAvailable' => class_exists( '\Automattic\Jetpack\Stats\WPCOM_Stats' ),
+ )
+ );
+}
+
+/**
+ * Render the Activity Kit Stats admin page.
+ */
+function render_page() {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( esc_html__( 'You do not have permission to access this page.', 'wporg-learn' ) );
+ }
+
+ $kits = get_posts(
+ array(
+ 'post_type' => 'activity_kit',
+ 'posts_per_page' => -1,
+ 'post_status' => 'publish',
+ 'orderby' => 'title',
+ 'order' => 'ASC',
+ )
+ );
+
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ —
+
+
+
+ —
+
+
+
+ —
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ |
+
+ ↓
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+ $item ) {
+ if ( isset( $item[2] ) && (
+ false !== strpos( $item[2], 'taxonomy=level' ) ||
+ false !== strpos( $item[2], 'taxonomy=topic' )
+ ) ) {
+ unset( $submenu[ $parent ][ $key ] );
+ }
+ }
+}
+
+/**
+ * Add a Stats submenu page under the Activity Kits post type menu.
+ */
+function add_activity_kit_stats_submenu() {
+ add_submenu_page(
+ 'edit.php?post_type=activity_kit',
+ __( 'Activity Kit Stats', 'wporg-learn' ),
+ __( 'Stats', 'wporg-learn' ),
+ 'manage_options',
+ 'activity-kit-stats',
+ 'WPOrg_Learn\Activity_Kit_Stats\render_page'
+ );
+}
diff --git a/wp-content/plugins/wporg-learn/inc/blocks.php b/wp-content/plugins/wporg-learn/inc/blocks.php
index 3bd22205c..0ff3b9bf8 100644
--- a/wp-content/plugins/wporg-learn/inc/blocks.php
+++ b/wp-content/plugins/wporg-learn/inc/blocks.php
@@ -24,6 +24,8 @@
* Actions and filters.
*/
add_action( 'init', __NAMESPACE__ . '\register_types' );
+add_action( 'enqueue_block_editor_assets', __NAMESPACE__ . '\enqueue_activity_kit_editor_assets' );
+add_filter( 'query_loop_block_query_vars', __NAMESPACE__ . '\activity_kit_query_vars', 10, 2 );
/**
* Register block types.
@@ -39,6 +41,7 @@ function register_types() {
register_lesson_plan_details();
register_workshop_details();
register_workshop_application_form();
+ register_activity_kit_card();
}
/**
@@ -333,7 +336,7 @@ function workshop_details_render_callback( $attributes, $content ) {
'label' => __( 'Subtitles', 'wporg-learn' ),
'param' => $captions,
'value' => array_map(
- function( $caption_lang ) {
+ function ( $caption_lang ) {
return esc_html( get_locale_name_from_code( $caption_lang, 'native' ) );
},
$captions
@@ -342,7 +345,7 @@ function( $caption_lang ) {
);
// Remove fields with empty values.
- $fields = array_filter( $fields, function( $data ) {
+ $fields = array_filter( $fields, function ( $data ) {
return $data['value'];
} );
@@ -422,7 +425,7 @@ function register_learning_duration() {
register_block_type(
get_js_path() . 'learning-duration/',
array(
- 'render_callback' => function( $attributes, $content, $block ) {
+ 'render_callback' => function ( $attributes, $content, $block ) {
return \WPOrg_Learn\View\Blocks\Learning_Duration\render( $attributes, $content, $block );
},
)
@@ -436,7 +439,7 @@ function register_lesson_count() {
register_block_type(
get_js_path() . 'lesson-count/',
array(
- 'render_callback' => function( $attributes, $content, $block ) {
+ 'render_callback' => function ( $attributes, $content, $block ) {
return \WPOrg_Learn\View\Blocks\Lesson_Count\render( $attributes, $content, $block );
},
)
@@ -450,10 +453,79 @@ function register_course_status() {
register_block_type(
get_js_path() . 'course-status/',
array(
- 'render_callback' => function( $attributes, $content, $block ) {
+ 'render_callback' => function ( $attributes, $content, $block ) {
return \WPOrg_Learn\View\Blocks\Course_Status\render( $attributes, $content, $block );
},
)
);
}
+/**
+ * Register the Activity Kit Card block.
+ */
+function register_activity_kit_card() {
+ register_block_type(
+ get_js_path() . 'activity-kit-card/',
+ array(
+ 'render_callback' => function ( $attributes, $content, $block ) {
+ ob_start();
+ require get_views_path() . 'block-activity-kit-card.php';
+ return ob_get_clean();
+ },
+ )
+ );
+}
+
+/**
+ * Enqueue editor assets for the Activity Kit sidebar panel and query variation.
+ * The sidebar panel is enqueued only when editing activity_kit posts.
+ */
+function enqueue_activity_kit_editor_assets() {
+ global $typenow;
+
+ // Query variation: enqueue for all post types so it appears in the block inserter.
+ $variation_asset_path = get_build_path() . 'query-activity-kits.asset.php';
+ if ( is_readable( $variation_asset_path ) ) {
+ $variation_asset = require $variation_asset_path;
+ wp_enqueue_script(
+ 'query-activity-kits',
+ get_build_url() . 'query-activity-kits.js',
+ $variation_asset['dependencies'],
+ $variation_asset['version'],
+ true
+ );
+ }
+
+ // Sidebar panel: only on activity_kit post edit screens.
+ if ( 'activity_kit' !== $typenow ) {
+ return;
+ }
+
+ $sidebar_asset_path = get_build_path() . 'activity-kit-sidebar.asset.php';
+ if ( ! is_readable( $sidebar_asset_path ) ) {
+ return;
+ }
+
+ $sidebar_asset = require $sidebar_asset_path;
+ wp_enqueue_script(
+ 'activity-kit-sidebar',
+ get_build_url() . 'activity-kit-sidebar.js',
+ $sidebar_asset['dependencies'],
+ $sidebar_asset['version'],
+ true
+ );
+}
+
+/**
+ * Map the wporg/activity-kits query variation namespace to the activity_kit post type.
+ *
+ * @param array $query
+ * @param \WP_Block $block
+ * @return array
+ */
+function activity_kit_query_vars( $query, $block ) {
+ if ( isset( $block->context['query']['namespace'] ) && 'wporg/activity-kits' === $block->context['query']['namespace'] ) {
+ $query['post_type'] = 'activity_kit';
+ }
+ return $query;
+}
diff --git a/wp-content/plugins/wporg-learn/inc/capabilities.php b/wp-content/plugins/wporg-learn/inc/capabilities.php
index 939f1eb09..6769b4d58 100644
--- a/wp-content/plugins/wporg-learn/inc/capabilities.php
+++ b/wp-content/plugins/wporg-learn/inc/capabilities.php
@@ -27,6 +27,7 @@ function set_post_type_caps( $user_caps ) {
$capability_types = array(
array( 'lesson_plan', 'lesson_plans' ),
array( 'tutorial', 'tutorials' ),
+ array( 'activity_kit', 'activity_kits' ),
);
foreach ( $capability_types as $capability_type ) {
@@ -80,7 +81,7 @@ function map_meta_caps( $required_caps, $current_cap, $user_id, $args ) {
switch ( $current_cap ) {
case 'edit_any_learn_content':
$required_caps = array();
- $learn_content_types = array( 'lesson-plan', 'wporg_workshop', 'course', 'lesson' );
+ $learn_content_types = array( 'lesson-plan', 'wporg_workshop', 'course', 'lesson', 'activity_kit' );
// Grant `edit_any_learn_content` when the user has `edit_posts` for any of our custom post types.
foreach ( $learn_content_types as $post_type ) {
diff --git a/wp-content/plugins/wporg-learn/inc/post-meta.php b/wp-content/plugins/wporg-learn/inc/post-meta.php
index 8fa25df35..54377d3a3 100644
--- a/wp-content/plugins/wporg-learn/inc/post-meta.php
+++ b/wp-content/plugins/wporg-learn/inc/post-meta.php
@@ -35,6 +35,7 @@ function register() {
register_lesson_meta();
register_lesson_plan_meta();
register_workshop_meta();
+ register_activity_kit_meta();
}
/**
@@ -50,7 +51,7 @@ function register_course_meta() {
'single' => true,
'sanitize_callback' => 'sanitize_text_field',
'show_in_rest' => true,
- 'auth_callback' => function( $allowed, $meta_key, $post_id ) {
+ 'auth_callback' => function ( $allowed, $meta_key, $post_id ) {
return current_user_can( 'edit_post', $post_id );
},
)
@@ -66,7 +67,7 @@ function register_course_meta() {
'default' => '',
'sanitize_callback' => 'esc_url_raw',
'show_in_rest' => true,
- 'auth_callback' => function( $allowed, $meta_key, $post_id ) {
+ 'auth_callback' => function ( $allowed, $meta_key, $post_id ) {
return current_user_can( 'edit_post', $post_id );
},
)
@@ -86,7 +87,7 @@ function register_lesson_meta() {
'single' => true,
'sanitize_callback' => 'sanitize_text_field',
'show_in_rest' => true,
- 'auth_callback' => function( $allowed, $meta_key, $post_id ) {
+ 'auth_callback' => function ( $allowed, $meta_key, $post_id ) {
return current_user_can( 'edit_post', $post_id );
},
),
@@ -220,7 +221,7 @@ function register_common_meta() {
}
// Language field.
- $post_types = array( 'lesson-plan', 'wporg_workshop', 'meeting', 'course', 'lesson' );
+ $post_types = array( 'lesson-plan', 'wporg_workshop', 'meeting', 'course', 'lesson', 'activity_kit' );
foreach ( $post_types as $post_type ) {
register_post_meta(
$post_type,
@@ -247,11 +248,11 @@ function register_common_meta() {
'type' => 'number',
'single' => true,
'default' => 0,
- 'sanitize_callback' => function( $value ) {
+ 'sanitize_callback' => function ( $value ) {
return floatval( $value );
},
'show_in_rest' => true,
- 'auth_callback' => function() {
+ 'auth_callback' => function () {
return current_user_can( 'edit_courses' ) || current_user_can( 'edit_lessons' );
},
)
@@ -321,6 +322,11 @@ function register_common_meta() {
*/
function sanitize_locale( $meta_value, $meta_key, $object_type, $object_subtype ) {
$meta_value = trim( $meta_value );
+
+ if ( ! function_exists( 'WordPressdotorg\Locales\get_locales_with_english_names' ) ) {
+ return sanitize_text_field( $meta_value );
+ }
+
$locales = array_keys( get_locales_with_english_names() );
if ( ! in_array( $meta_value, $locales, true ) ) {
@@ -352,7 +358,7 @@ function get_workshop_duration( WP_Post $workshop, $format = 'raw' ) {
if ( $interval->d > 0 ) {
$return = human_time_diff( 0, $interval->d * DAY_IN_SECONDS );
} elseif ( $interval->h > 0 ) {
- $hours = human_time_diff( 0, $interval->h * HOUR_IN_SECONDS );
+ $hours = human_time_diff( 0, $interval->h * HOUR_IN_SECONDS );
$return = $hours;
if ( $interval->i > 0 ) {
@@ -406,17 +412,19 @@ function get_available_post_type_locales( $meta_key, $post_type, $post_status, $
}
}
- $results = $wpdb->get_col( $wpdb->prepare(
+ $results = $wpdb->get_col(
+ $wpdb->prepare(
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $and_post_status and $and_post_type only include $post_status and $post_type if they match an allowed string.
- "SELECT DISTINCT postmeta.meta_value
+ "SELECT DISTINCT postmeta.meta_value
FROM {$wpdb->postmeta} postmeta
JOIN {$wpdb->posts} posts ON posts.ID = postmeta.post_id
$and_post_type
$and_post_status
WHERE postmeta.meta_key = %s",
- $meta_key
+ $meta_key
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
- ) );
+ )
+ );
if ( empty( $results ) ) {
return array();
@@ -425,7 +433,11 @@ function get_available_post_type_locales( $meta_key, $post_type, $post_status, $
$available_locales = array_fill_keys( $results, '' );
$locale_fn = "\WordPressdotorg\Locales\get_locales_with_{$label_language}_names";
- $locales = $locale_fn();
+ if ( ! function_exists( $locale_fn ) ) {
+ // Fallback for environments without WordPressdotorg\Locales: use locale codes as labels.
+ return array_combine( $results, $results );
+ }
+ $locales = $locale_fn();
return array_intersect_key( $locales, $available_locales );
}
@@ -471,10 +483,12 @@ function save_lesson_plan_metabox_fields( $post_id ) {
return;
}
- $view_url = filter_input( INPUT_POST, 'slides-view-url', FILTER_VALIDATE_URL ) ?: '';
+ $filtered_view_url = filter_input( INPUT_POST, 'slides-view-url', FILTER_VALIDATE_URL );
+ $view_url = $filtered_view_url ? $filtered_view_url : '';
update_post_meta( $post_id, 'slides_view_url', $view_url );
- $download_url = filter_input( INPUT_POST, 'slides-download-url', FILTER_VALIDATE_URL ) ?: '';
+ $filtered_download_url = filter_input( INPUT_POST, 'slides-download-url', FILTER_VALIDATE_URL );
+ $download_url = $filtered_download_url ? $filtered_download_url : '';
update_post_meta( $post_id, 'slides_download_url', $download_url );
// This language meta field is rendered in the editor sidebar using a PluginDocumentSettingPanel block,
@@ -578,7 +592,8 @@ function add_meeting_metaboxes( $post_type = '' ) {
function render_metabox_workshop_details( WP_Post $post ) {
$duration_interval = get_workshop_duration( $post, 'interval' );
$locales = get_locales_with_english_names();
- $captions = get_post_meta( $post->ID, 'video_caption_language' ) ?: array();
+ $captions_meta = get_post_meta( $post->ID, 'video_caption_language' );
+ $captions = $captions_meta ? $captions_meta : array();
require get_views_path() . 'metabox-workshop-details.php';
}
@@ -598,7 +613,8 @@ function render_metabox_lesson_video( WP_Post $post ) {
* @param WP_Post $post
*/
function render_metabox_workshop_presenters( WP_Post $post ) {
- $presenters = get_post_meta( $post->ID, 'presenter_wporg_username' ) ?: array();
+ $presenters_meta = get_post_meta( $post->ID, 'presenter_wporg_username' );
+ $presenters = $presenters_meta ? $presenters_meta : array();
require get_views_path() . 'metabox-workshop-presenters.php';
}
@@ -609,7 +625,8 @@ function render_metabox_workshop_presenters( WP_Post $post ) {
* @param WP_Post $post
*/
function render_metabox_workshop_other_contributors( WP_Post $post ) {
- $other_contributors = get_post_meta( $post->ID, 'other_contributor_wporg_username' ) ?: array();
+ $other_contributors_meta = get_post_meta( $post->ID, 'other_contributor_wporg_username' );
+ $other_contributors = $other_contributors_meta ? $other_contributors_meta : array();
require get_views_path() . 'metabox-workshop-other-contributors.php';
}
@@ -620,9 +637,10 @@ function render_metabox_workshop_other_contributors( WP_Post $post ) {
* @param WP_Post $post
*/
function render_metabox_workshop_application( WP_Post $post ) {
- $schema = get_workshop_application_field_schema();
- $application = wp_parse_args(
- get_post_meta( $post->ID, 'original_application', true ) ?: array(),
+ $schema = get_workshop_application_field_schema();
+ $application_meta = get_post_meta( $post->ID, 'original_application', true );
+ $application = wp_parse_args(
+ $application_meta ? $application_meta : array(),
wp_list_pluck( $schema['properties'], 'default' )
);
@@ -635,8 +653,9 @@ function render_metabox_workshop_application( WP_Post $post ) {
* @param WP_Post $post
*/
function render_metabox_meeting_language( WP_Post $post ) {
- $locales = get_locales_with_english_names();
- $language = get_post_meta( $post->ID, 'language', true ) ?: '';
+ $locales = get_locales_with_english_names();
+ $language_meta = get_post_meta( $post->ID, 'language', true );
+ $language = $language_meta ? $language_meta : '';
require get_views_path() . 'metabox-meeting-language.php';
}
@@ -767,7 +786,6 @@ function save_meeting_metabox_fields( $post_id ) {
$language = filter_input( INPUT_POST, 'meeting-language' );
update_post_meta( $post_id, 'language', $language );
-
}
/**
@@ -776,12 +794,39 @@ function save_meeting_metabox_fields( $post_id ) {
function render_locales_list() {
global $typenow;
- $post_types_with_language = array( 'lesson-plan', 'wporg_workshop', 'meeting', 'course', 'lesson' );
- if ( in_array( $typenow, $post_types_with_language, true ) ) {
- $locales = get_locales_with_english_names();
+ $post_types_with_language = array( 'lesson-plan', 'wporg_workshop', 'meeting', 'course', 'lesson', 'activity_kit' );
+ if ( ! in_array( $typenow, $post_types_with_language, true ) ) {
+ return;
+ }
- require get_views_path() . 'locales-list.php';
+ if ( function_exists( 'WordPressdotorg\Locales\get_locales_with_english_names' ) ) {
+ $locales = get_locales_with_english_names();
+ } else {
+ $locales = array(
+ 'en_US' => 'English (United States)',
+ 'ar' => 'Arabic',
+ 'de_DE' => 'German',
+ 'es_ES' => 'Spanish (Spain)',
+ 'fr_FR' => 'French (France)',
+ 'he_IL' => 'Hebrew',
+ 'hi_IN' => 'Hindi',
+ 'id_ID' => 'Indonesian',
+ 'it_IT' => 'Italian',
+ 'ja' => 'Japanese',
+ 'ko_KR' => 'Korean',
+ 'nl_NL' => 'Dutch',
+ 'pt_BR' => 'Portuguese (Brazil)',
+ 'pt_PT' => 'Portuguese (Portugal)',
+ 'ro_RO' => 'Romanian',
+ 'ru_RU' => 'Russian',
+ 'sv_SE' => 'Swedish',
+ 'tr_TR' => 'Turkish',
+ 'zh_CN' => 'Chinese (China)',
+ 'zh_TW' => 'Chinese (Taiwan)',
+ );
}
+
+ require get_views_path() . 'locales-list.php';
}
/**
@@ -808,7 +853,7 @@ function enqueue_expiration_date_assets() {
wp_die( 'You need to run `yarn start` or `yarn build` to build the required assets.' );
}
- $script_asset = require( $script_asset_path );
+ $script_asset = require $script_asset_path;
wp_enqueue_script(
'wporg-learn-expiration-date',
get_build_url() . 'expiration-date.js',
@@ -828,14 +873,14 @@ function enqueue_expiration_date_assets() {
function enqueue_language_meta_assets() {
global $typenow;
- $post_types_with_language = array( 'lesson-plan', 'wporg_workshop', 'meeting', 'course', 'lesson' );
+ $post_types_with_language = array( 'lesson-plan', 'wporg_workshop', 'meeting', 'course', 'lesson', 'activity_kit' );
if ( in_array( $typenow, $post_types_with_language, true ) ) {
$script_asset_path = get_build_path() . 'language-meta.asset.php';
if ( ! file_exists( $script_asset_path ) ) {
wp_die( 'You need to run `yarn start` or `yarn build` to build the required assets.' );
}
- $script_asset = require( $script_asset_path );
+ $script_asset = require $script_asset_path;
wp_enqueue_script(
'wporg-learn-language-meta',
get_build_url() . 'language-meta.js',
@@ -860,7 +905,7 @@ function enqueue_lesson_featured_meta_assets() {
wp_die( 'You need to run `yarn start` or `yarn build` to build the required assets.' );
}
- $script_asset = require( $script_asset_path );
+ $script_asset = require $script_asset_path;
wp_enqueue_script(
'wporg-learn-lesson-featured-meta',
get_build_url() . 'lesson-featured-meta.js',
@@ -886,7 +931,7 @@ function enqueue_duration_meta_assets() {
wp_die( 'You need to run `yarn start` or `yarn build` to build the required assets.' );
}
- $script_asset = require( $script_asset_path );
+ $script_asset = require $script_asset_path;
wp_enqueue_script(
'wporg-learn-duration-meta',
get_build_url() . 'duration-meta.js',
@@ -911,7 +956,7 @@ function enqueue_course_completion_meta_assets() {
wp_die( 'You need to run `yarn start` or `yarn build` to build the required assets.' );
}
- $script_asset = require( $script_asset_path );
+ $script_asset = require $script_asset_path;
wp_enqueue_script(
'wporg-learn-course-completion-meta',
get_build_url() . 'course-completion-meta.js',
@@ -923,3 +968,82 @@ function enqueue_course_completion_meta_assets() {
wp_set_script_translations( 'wporg-learn-course-completion-meta', 'wporg-learn' );
}
}
+
+/**
+ * Register post meta keys for activity kits.
+ */
+function register_activity_kit_meta() {
+ $auth_callback = function ( $allowed, $meta_key, $post_id ) {
+ return current_user_can( 'edit_post', $post_id );
+ };
+
+ register_post_meta(
+ 'activity_kit',
+ '_activity_duration',
+ array(
+ 'description' => __( 'Duration of the activity, e.g. "60–90 minutes".', 'wporg-learn' ),
+ 'type' => 'string',
+ 'single' => true,
+ 'default' => '',
+ 'sanitize_callback' => 'sanitize_text_field',
+ 'show_in_rest' => true,
+ 'auth_callback' => $auth_callback,
+ )
+ );
+
+ register_post_meta(
+ 'activity_kit',
+ '_activity_guide_pdf_id',
+ array(
+ 'description' => __( 'Attachment ID of the Facilitator Guide PDF.', 'wporg-learn' ),
+ 'type' => 'integer',
+ 'single' => true,
+ 'default' => 0,
+ 'sanitize_callback' => 'absint',
+ 'show_in_rest' => true,
+ 'auth_callback' => $auth_callback,
+ )
+ );
+
+ register_post_meta(
+ 'activity_kit',
+ '_activity_slides_pdf_id',
+ array(
+ 'description' => __( 'Attachment ID of the Slide Deck PDF.', 'wporg-learn' ),
+ 'type' => 'integer',
+ 'single' => true,
+ 'default' => 0,
+ 'sanitize_callback' => 'absint',
+ 'show_in_rest' => true,
+ 'auth_callback' => $auth_callback,
+ )
+ );
+
+ register_post_meta(
+ 'activity_kit',
+ '_activity_zip_id',
+ array(
+ 'description' => __( 'Attachment ID of the downloadable ZIP file for this activity kit.', 'wporg-learn' ),
+ 'type' => 'integer',
+ 'single' => true,
+ 'default' => 0,
+ 'sanitize_callback' => 'absint',
+ 'show_in_rest' => true,
+ 'auth_callback' => $auth_callback,
+ )
+ );
+
+ register_post_meta(
+ 'activity_kit',
+ '_activity_feedback_url',
+ array(
+ 'description' => __( 'Optional per-kit feedback form URL. Overrides the global setting when set.', 'wporg-learn' ),
+ 'type' => 'string',
+ 'single' => true,
+ 'default' => '',
+ 'sanitize_callback' => 'esc_url_raw',
+ 'show_in_rest' => true,
+ 'auth_callback' => $auth_callback,
+ )
+ );
+}
diff --git a/wp-content/plugins/wporg-learn/inc/post-type.php b/wp-content/plugins/wporg-learn/inc/post-type.php
index 567c64a57..d4bc3b732 100644
--- a/wp-content/plugins/wporg-learn/inc/post-type.php
+++ b/wp-content/plugins/wporg-learn/inc/post-type.php
@@ -20,6 +20,7 @@
function register() {
register_lesson_plan();
register_workshop();
+ register_activity_kit();
}
/**
@@ -156,6 +157,69 @@ function register_workshop() {
register_post_type( 'wporg_workshop', $args );
}
+/**
+ * Register an Activity Kit post type.
+ */
+function register_activity_kit() {
+ $labels = array(
+ 'name' => _x( 'Activity Kits', 'Post Type General Name', 'wporg_learn' ),
+ 'singular_name' => _x( 'Activity Kit', 'Post Type Singular Name', 'wporg_learn' ),
+ 'menu_name' => __( 'Activity Kits', 'wporg_learn' ),
+ 'name_admin_bar' => __( 'Activity Kit', 'wporg_learn' ),
+ 'archives' => __( 'Activity Kit Archives', 'wporg_learn' ),
+ 'attributes' => __( 'Activity Kit Attributes', 'wporg_learn' ),
+ 'parent_item_colon' => __( 'Parent Activity Kit:', 'wporg_learn' ),
+ 'all_items' => __( 'All Activity Kits', 'wporg_learn' ),
+ 'add_new_item' => __( 'Add Activity Kit', 'wporg_learn' ),
+ 'add_new' => __( 'Add Activity Kit', 'wporg_learn' ),
+ 'new_item' => __( 'New Activity Kit', 'wporg_learn' ),
+ 'edit_item' => __( 'Edit Activity Kit', 'wporg_learn' ),
+ 'update_item' => __( 'Update Activity Kit', 'wporg_learn' ),
+ 'view_item' => __( 'View Activity Kit', 'wporg_learn' ),
+ 'view_items' => __( 'View Activity Kits', 'wporg_learn' ),
+ 'search_items' => __( 'Search Activity Kits', 'wporg_learn' ),
+ 'not_found' => __( 'No activity kits found.', 'wporg_learn' ),
+ 'not_found_in_trash' => __( 'No activity kits found in Trash.', 'wporg_learn' ),
+ 'featured_image' => __( 'Featured image', 'wporg_learn' ),
+ 'set_featured_image' => __( 'Set featured image', 'wporg_learn' ),
+ 'remove_featured_image' => __( 'Remove featured image', 'wporg_learn' ),
+ 'use_featured_image' => __( 'Use as featured image', 'wporg_learn' ),
+ 'insert_into_item' => __( 'Insert into activity kit', 'wporg_learn' ),
+ 'uploaded_to_this_item' => __( 'Uploaded to this activity kit', 'wporg_learn' ),
+ 'items_list' => __( 'Activity kits list', 'wporg_learn' ),
+ 'items_list_navigation' => __( 'Activity kits list navigation', 'wporg_learn' ),
+ 'filter_items_list' => __( 'Filter activity kits list', 'wporg_learn' ),
+ );
+
+ $args = array(
+ 'label' => __( 'Activity Kit', 'wporg_learn' ),
+ 'description' => __( 'WordPress.org Training Activity Kit', 'wporg_learn' ),
+ 'labels' => $labels,
+ 'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt', 'custom-fields', 'author' ),
+ 'hierarchical' => false,
+ 'public' => true,
+ 'show_ui' => true,
+ 'show_in_menu' => true,
+ 'menu_position' => 7,
+ 'menu_icon' => 'dashicons-clipboard',
+ 'show_in_admin_bar' => true,
+ 'show_in_nav_menus' => true,
+ 'can_export' => true,
+ 'has_archive' => 'activity-library',
+ 'exclude_from_search' => false,
+ 'publicly_queryable' => true,
+ 'capability_type' => array( 'activity_kit', 'activity_kits' ),
+ 'map_meta_cap' => true,
+ 'show_in_rest' => true,
+ 'rewrite' => array(
+ 'slug' => 'activity-library',
+ 'with_front' => false,
+ ),
+ );
+
+ register_post_type( 'activity_kit', $args );
+}
+
/**
* Create an array representation of a workshop's content template.
*
@@ -237,6 +301,7 @@ function generate_workshop_template_structure() {
function jetpack_copy_post_post_types( $post_types ) {
$post_types[] = 'lesson-plan';
$post_types[] = 'wporg_workshop';
+ $post_types[] = 'activity_kit';
return $post_types;
}
@@ -255,6 +320,7 @@ function jetpack_sitemap_post_types( $post_types ) {
// The "lesson" has been excluded in Sensei LMS, but it is needed in Learn.
// See https://github.com/Automattic/sensei/blob/trunk/includes/class-sensei-posttypes.php#L25.
$post_types[] = 'lesson';
+ $post_types[] = 'activity_kit';
return $post_types;
}
@@ -268,7 +334,7 @@ function jetpack_sitemap_post_types( $post_types ) {
* @return array
*/
function jetpack_page_sitemap_other_urls( $urls ) {
- foreach ( array( 'wporg_workshop', 'lesson-plan' ) as $post_type ) {
+ foreach ( array( 'wporg_workshop', 'lesson-plan', 'activity_kit' ) as $post_type ) {
$url = get_post_type_archive_link( $post_type );
if ( ! $url ) {
continue;
diff --git a/wp-content/plugins/wporg-learn/inc/sensei.php b/wp-content/plugins/wporg-learn/inc/sensei.php
index e7563e40e..37745ea34 100644
--- a/wp-content/plugins/wporg-learn/inc/sensei.php
+++ b/wp-content/plugins/wporg-learn/inc/sensei.php
@@ -261,6 +261,9 @@ class="sensei-date-picker"
* Redirect requests for the "My Courses" page to the login page and back, if logged out.
*/
function restrict_my_courses_page_access() {
+ if ( ! function_exists( 'Sensei' ) ) {
+ return;
+ }
if ( ! is_user_logged_in() && is_page( Sensei()->settings->get_my_courses_page_id() ) ) {
$redirect_to = wp_unslash( $_GET['redirect_to'] ?? '' ) ?: sensei_get_current_page_url();
diff --git a/wp-content/plugins/wporg-learn/inc/taxonomy.php b/wp-content/plugins/wporg-learn/inc/taxonomy.php
index 60ae22d8b..752f94f54 100644
--- a/wp-content/plugins/wporg-learn/inc/taxonomy.php
+++ b/wp-content/plugins/wporg-learn/inc/taxonomy.php
@@ -258,7 +258,7 @@ function register_lesson_level() {
),
);
- register_taxonomy( 'level', array( 'lesson-plan', 'lesson', 'course' ), $args );
+ register_taxonomy( 'level', array( 'lesson-plan', 'lesson', 'course', 'activity_kit' ), $args );
}
/**
@@ -445,7 +445,7 @@ function register_topic() {
),
);
- register_taxonomy( 'topic', array( 'lesson-plan', 'wporg_workshop', 'course', 'lesson', 'meeting' ), $args );
+ register_taxonomy( 'topic', array( 'lesson-plan', 'wporg_workshop', 'course', 'lesson', 'meeting', 'activity_kit' ), $args );
}
/**
@@ -682,6 +682,10 @@ function tax_edit_term_fields( $term, $taxonomy ) {
* @param int $term_id the term id to update.
*/
function tax_save_term_fields( $term_id ) {
+ if ( defined( 'WP_CLI' ) && WP_CLI ) {
+ return;
+ }
+
$wp_list_table = \_get_list_table( 'WP_Terms_List_Table' );
if ( 'add-tag' === $wp_list_table->current_action() ) {
diff --git a/wp-content/plugins/wporg-learn/js/activity-kit-card/block.json b/wp-content/plugins/wporg-learn/js/activity-kit-card/block.json
new file mode 100644
index 000000000..368c44855
--- /dev/null
+++ b/wp-content/plugins/wporg-learn/js/activity-kit-card/block.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "wporg/activity-kit-card",
+ "version": "0.1.0",
+ "title": "Activity Kit Card",
+ "category": "wporg-learn",
+ "description": "Displays a single activity kit as a card.",
+ "parent": [ "core/post-template" ],
+ "usesContext": [ "postId", "postType" ],
+ "attributes": {},
+ "supports": {
+ "html": false
+ },
+ "textdomain": "wporg-learn",
+ "editorScript": "file:../../build/activity-kit-card.js"
+}
diff --git a/wp-content/plugins/wporg-learn/js/activity-kit-card/src/index.js b/wp-content/plugins/wporg-learn/js/activity-kit-card/src/index.js
new file mode 100644
index 000000000..d24b4ec87
--- /dev/null
+++ b/wp-content/plugins/wporg-learn/js/activity-kit-card/src/index.js
@@ -0,0 +1,7 @@
+import { registerBlockType } from '@wordpress/blocks';
+import Edit from '../../shared/dynamic-edit';
+import metadata from '../block.json';
+
+registerBlockType( metadata.name, {
+ edit: Edit,
+} );
diff --git a/wp-content/plugins/wporg-learn/js/activity-kit-sidebar/src/index.js b/wp-content/plugins/wporg-learn/js/activity-kit-sidebar/src/index.js
new file mode 100644
index 000000000..c4be630cb
--- /dev/null
+++ b/wp-content/plugins/wporg-learn/js/activity-kit-sidebar/src/index.js
@@ -0,0 +1,189 @@
+import { registerPlugin } from '@wordpress/plugins';
+import { PluginDocumentSettingPanel } from '@wordpress/edit-post';
+import { useDispatch, useSelect } from '@wordpress/data';
+import { store as coreStore } from '@wordpress/core-data';
+import { BaseControl, Button, RadioControl, TextControl } from '@wordpress/components';
+import { MediaUpload, MediaUploadCheck } from '@wordpress/block-editor';
+import { __ } from '@wordpress/i18n';
+import { useEffect } from '@wordpress/element';
+
+function ActivityKitDetailsPanel() {
+ const postMeta = useSelect( ( select ) => select( 'core/editor' ).getEditedPostAttribute( 'meta' ) || {} );
+ const { editPost } = useDispatch( 'core/editor' );
+
+ const setMeta = ( key, value ) => editPost( { meta: { ...postMeta, [ key ]: value } } );
+
+ const duration = postMeta._activity_duration || '';
+ const guidePdfId = postMeta._activity_guide_pdf_id || 0;
+ const slidesPdfId = postMeta._activity_slides_pdf_id || 0;
+ const zipId = postMeta._activity_zip_id || 0;
+ const feedbackUrl = postMeta._activity_feedback_url || '';
+
+ const guideTitle = useSelect(
+ ( select ) => ( guidePdfId ? select( coreStore ).getMedia( guidePdfId )?.title?.rendered : null ),
+ [ guidePdfId ]
+ );
+ const slidesTitle = useSelect(
+ ( select ) => ( slidesPdfId ? select( coreStore ).getMedia( slidesPdfId )?.title?.rendered : null ),
+ [ slidesPdfId ]
+ );
+ const zipTitle = useSelect(
+ ( select ) => ( zipId ? select( coreStore ).getMedia( zipId )?.title?.rendered : null ),
+ [ zipId ]
+ );
+
+ return (
+
+ setMeta( '_activity_duration', value ) }
+ placeholder={ __( '60', 'wporg-learn' ) }
+ />
+
+
+
+ { guidePdfId ? (
+ <>
+ { guideTitle || `Attachment #${ guidePdfId }` }
+
+ >
+ ) : (
+
+ setMeta( '_activity_guide_pdf_id', media.id ) }
+ allowedTypes={ [ 'application/pdf' ] }
+ render={ ( { open } ) => (
+
+ ) }
+ />
+
+ ) }
+
+
+
+
+
+ { slidesPdfId ? (
+ <>
+ { slidesTitle || `Attachment #${ slidesPdfId }` }
+
+ >
+ ) : (
+
+ setMeta( '_activity_slides_pdf_id', media.id ) }
+ allowedTypes={ [ 'application/pdf' ] }
+ render={ ( { open } ) => (
+
+ ) }
+ />
+
+ ) }
+
+
+
+
+
+ { zipId ? (
+ <>
+ { zipTitle || `Attachment #${ zipId }` }
+
+ >
+ ) : (
+
+ setMeta( '_activity_zip_id', media.id ) }
+ allowedTypes={ [ 'application/zip', 'application/x-zip-compressed' ] }
+ render={ ( { open } ) => (
+
+ ) }
+ />
+
+ ) }
+
+
+
+ setMeta( '_activity_feedback_url', value ) }
+ placeholder="https://..."
+ />
+
+ );
+}
+
+function ActivityKitLevelPanel() {
+ const { removeEditorPanel } = useDispatch( 'core/editor' );
+
+ const levelTermIds = useSelect( ( select ) => select( 'core/editor' ).getEditedPostAttribute( 'level' ) || [] );
+ const { editPost } = useDispatch( 'core/editor' );
+
+ const levelTerms = useSelect(
+ ( select ) =>
+ select( coreStore ).getEntityRecords( 'taxonomy', 'level', {
+ per_page: 100,
+ orderby: 'id',
+ order: 'asc',
+ } ),
+ []
+ );
+
+ useEffect( () => {
+ removeEditorPanel( 'taxonomy-panel-level' );
+ }, [ removeEditorPanel ] );
+
+ const options = [
+ { label: __( '— None —', 'wporg-learn' ), value: '' },
+ ...( levelTerms || [] ).map( ( term ) => ( {
+ label: term.name,
+ value: String( term.id ),
+ } ) ),
+ ];
+
+ const selectedValue = levelTermIds && levelTermIds.length > 0 ? String( levelTermIds[ 0 ] ) : '';
+
+ return (
+
+
+ editPost( {
+ level: value ? [ parseInt( value, 10 ) ] : [],
+ } )
+ }
+ />
+
+ );
+}
+
+function ActivityKitSidebar() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+registerPlugin( 'wporg-activity-kit-details', {
+ render: ActivityKitSidebar,
+} );
diff --git a/wp-content/plugins/wporg-learn/js/activity-kit-stats/index.js b/wp-content/plugins/wporg-learn/js/activity-kit-stats/index.js
new file mode 100644
index 000000000..179c92b30
--- /dev/null
+++ b/wp-content/plugins/wporg-learn/js/activity-kit-stats/index.js
@@ -0,0 +1,529 @@
+/* global activityKitStats, Chart */
+
+( function () {
+ const { restUrl, nonce, jetpackAvailable } = activityKitStats;
+
+ // ── Constants ──
+ const CHART_PAGE = 8;
+
+ // ── State ──
+ let allData = [];
+ let chart = null;
+ let activeMetric = 'both';
+ let activeRange = 'all';
+ let activeKit = '';
+ let sortCol = 'views';
+ let sortDir = 'desc';
+ let chartOffset = 0;
+
+ // ── DOM refs ──
+ const filterKit = document.getElementById( 'ak-filter-kit' );
+ const tableBody = document.getElementById( 'ak-stats-table-body' );
+ const chartCanvas = document.getElementById( 'ak-stats-chart' );
+
+ if ( ! filterKit || ! tableBody || ! chartCanvas ) {
+ return;
+ }
+
+ const summaryViews = document.getElementById( 'ak-summary-views' );
+ const summaryDownloads = document.getElementById( 'ak-summary-downloads' );
+ const summaryRate = document.getElementById( 'ak-summary-rate' );
+ const boxKits = document.getElementById( 'ak-box-kits' );
+ const boxViews = document.getElementById( 'ak-box-views' );
+ const boxDownloads = document.getElementById( 'ak-box-downloads' );
+ const boxRate = document.getElementById( 'ak-box-rate' );
+ const totalKits = document.getElementById( 'ak-total-kits' );
+ const chartTitle = document.getElementById( 'ak-chart-title' );
+ const chartSubtitle = document.getElementById( 'ak-chart-subtitle' );
+ const legendViews = document.getElementById( 'ak-legend-views' );
+ const legendDownloads = document.getElementById( 'ak-legend-downloads' );
+ const tableSubtitle = document.getElementById( 'ak-table-subtitle' );
+ const backLinkBar = document.getElementById( 'ak-back-link-bar' );
+ const kitBanner = document.getElementById( 'ak-kit-banner' );
+ const kitBannerName = document.getElementById( 'ak-kit-banner-name' );
+ const thViews = document.getElementById( 'ak-th-views' );
+ const thDownloads = document.getElementById( 'ak-th-downloads' );
+ const exportBtn = document.getElementById( 'ak-export-csv' );
+ const chartSliderWrap = document.getElementById( 'ak-chart-slider-wrap' );
+ const chartSlider = document.getElementById( 'ak-chart-slider' );
+ const chartSliderLabel = document.getElementById( 'ak-chart-slider-label' );
+
+ const metricBtns = document.querySelectorAll( '[data-ak-metric]' );
+ const rangeBtns = document.querySelectorAll( '[data-ak-range]' );
+
+ // ── Helpers ──
+ function fmt( num ) {
+ return ( num ?? 0 ).toLocaleString();
+ }
+
+ function formatDate( dateStr ) {
+ if ( ! dateStr ) {
+ return '—';
+ }
+ const date = new Date( dateStr + 'T00:00:00' );
+ return date.toLocaleDateString( 'en-US', {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ } );
+ }
+
+ function escHtml( value ) {
+ return String( value )
+ .replace( /&/g, '&' )
+ .replace( //g, '>' )
+ .replace( /"/g, '"' );
+ }
+
+ function rangeLabel() {
+ const labels = {
+ '7d': 'Last 7 days',
+ '30d': 'Last 30 days',
+ '90d': 'Last 90 days',
+ all: 'All time',
+ };
+ return labels[ activeRange ] || 'All time';
+ }
+
+ const metricLabel = {
+ both: 'Views vs. Downloads',
+ views: 'Views',
+ downloads: 'Downloads',
+ };
+
+ // ── Fetch ──
+ async function fetchStats() {
+ const url = new URL( restUrl );
+ url.searchParams.set( 'metric', 'both' );
+ url.searchParams.set( 'range', activeRange );
+
+ if ( activeKit ) {
+ url.searchParams.set( 'kit', activeKit );
+ } else {
+ url.searchParams.delete( 'kit' );
+ }
+
+ const resp = await fetch( url.toString(), {
+ headers: { 'X-WP-Nonce': nonce },
+ } );
+ if ( ! resp.ok ) {
+ throw new Error( 'Failed to fetch stats.' );
+ }
+ return resp.json();
+ }
+
+ // ── Summary ──
+ function updateSummary( data ) {
+ const isSingle = !! activeKit;
+ const totalV = data.reduce( ( sum, row ) => sum + ( row.views ?? 0 ), 0 );
+ const totalD = data.reduce( ( sum, row ) => sum + ( row.downloads ?? 0 ), 0 );
+ const rate = totalV > 0 ? ( ( totalD / totalV ) * 100 ).toFixed( 1 ) + '%' : '—';
+
+ if ( summaryViews ) {
+ summaryViews.textContent = fmt( totalV );
+ }
+ if ( summaryDownloads ) {
+ summaryDownloads.textContent = fmt( totalD );
+ }
+ if ( summaryRate ) {
+ summaryRate.textContent = rate;
+ }
+ if ( totalKits ) {
+ totalKits.textContent = isSingle ? '1' : data.length;
+ }
+
+ if ( boxKits ) {
+ boxKits.style.display = isSingle ? 'none' : '';
+ }
+ if ( boxViews ) {
+ boxViews.style.display = activeMetric === 'downloads' ? 'none' : '';
+ }
+ if ( boxDownloads ) {
+ boxDownloads.style.display = activeMetric === 'views' ? 'none' : '';
+ }
+ if ( boxRate ) {
+ boxRate.style.display = isSingle ? '' : 'none';
+ }
+ }
+
+ // ── Chart ──
+ function initChart() {
+ const ctx = chartCanvas.getContext( '2d' );
+ chart = new Chart( ctx, {
+ type: 'bar',
+ data: { labels: [], datasets: [] },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: { display: false },
+ tooltip: {
+ callbacks: {
+ label: ( context ) =>
+ ' ' + context.dataset.label + ': ' + context.parsed.y.toLocaleString(),
+ },
+ },
+ },
+ scales: {
+ // eslint-disable-next-line id-length
+ x: {
+ grid: { display: false },
+ ticks: {
+ font: { size: 11 },
+ color: '#646970',
+ maxRotation: 30,
+ },
+ border: { color: '#c3c4c7' },
+ },
+ // eslint-disable-next-line id-length
+ y: {
+ beginAtZero: true,
+ grid: { color: '#f0f0f1' },
+ ticks: { font: { size: 11 }, color: '#646970' },
+ border: { color: '#c3c4c7' },
+ },
+ },
+ },
+ } );
+ }
+
+ function renderChart( data ) {
+ if ( ! chart ) {
+ return;
+ }
+
+ const total = data.length;
+ const paged = total > CHART_PAGE;
+ const sliced = paged ? data.slice( chartOffset, chartOffset + CHART_PAGE ) : data;
+
+ // Slider visibility and state.
+ if ( chartSliderWrap ) {
+ chartSliderWrap.classList.toggle( 'is-visible', paged );
+ }
+ if ( paged && chartSlider ) {
+ const maxOffset = Math.max( 0, total - CHART_PAGE );
+ chartSlider.max = maxOffset;
+ chartSlider.value = chartOffset;
+ }
+ if ( paged && chartSliderLabel ) {
+ const end = Math.min( chartOffset + CHART_PAGE, total );
+ chartSliderLabel.textContent = chartOffset + 1 + '–' + end + ' of ' + total;
+ }
+
+ const labels = sliced.map( ( row ) => ( row.title.length > 20 ? row.title.slice( 0, 18 ) + '…' : row.title ) );
+ const datasets = [];
+
+ if ( activeMetric !== 'downloads' ) {
+ datasets.push( {
+ label: 'Views',
+ backgroundColor: '#3858e9',
+ barPercentage: 0.75,
+ categoryPercentage: 0.7,
+ data: sliced.map( ( row ) => row.views ?? 0 ),
+ } );
+ }
+ if ( activeMetric !== 'views' ) {
+ datasets.push( {
+ label: 'Downloads',
+ backgroundColor: '#9fb1ff',
+ barPercentage: 0.75,
+ categoryPercentage: 0.7,
+ data: sliced.map( ( row ) => row.downloads ?? 0 ),
+ } );
+ }
+
+ chart.data.labels = labels;
+ chart.data.datasets = datasets;
+ chart.update();
+ }
+
+ // ── Table ──
+ function renderTable( data ) {
+ if ( ! data.length ) {
+ tableBody.innerHTML = '| No data found. |
';
+ return;
+ }
+
+ const sorted = [ ...data ].sort( ( a, b ) => {
+ if ( sortCol === 'title' ) {
+ const titleA = a.title.toLowerCase();
+ const titleB = b.title.toLowerCase();
+ if ( sortDir === 'asc' ) {
+ if ( titleA < titleB ) {
+ return -1;
+ }
+ if ( titleA > titleB ) {
+ return 1;
+ }
+ return 0;
+ }
+ if ( titleB < titleA ) {
+ return -1;
+ }
+ if ( titleB > titleA ) {
+ return 1;
+ }
+ return 0;
+ }
+ if ( sortCol === 'updated' ) {
+ return sortDir === 'asc'
+ ? ( a.updated || '' ).localeCompare( b.updated || '' )
+ : ( b.updated || '' ).localeCompare( a.updated || '' );
+ }
+ let valueA;
+ if ( sortCol === 'views' ) {
+ valueA = a.views ?? 0;
+ } else if ( sortCol === 'downloads' ) {
+ valueA = a.downloads ?? 0;
+ } else {
+ valueA = ( a.views ?? 0 ) > 0 ? ( a.downloads ?? 0 ) / ( a.views ?? 0 ) : 0;
+ }
+ let valueB;
+ if ( sortCol === 'views' ) {
+ valueB = b.views ?? 0;
+ } else if ( sortCol === 'downloads' ) {
+ valueB = b.downloads ?? 0;
+ } else {
+ valueB = ( b.views ?? 0 ) > 0 ? ( b.downloads ?? 0 ) / ( b.views ?? 0 ) : 0;
+ }
+ return sortDir === 'asc' ? valueA - valueB : valueB - valueA;
+ } );
+
+ tableBody.innerHTML = '';
+ sorted.forEach( ( row ) => {
+ const views = row.views ?? 0;
+ const downloads = row.downloads ?? 0;
+ const rate = views > 0 ? ( ( downloads / views ) * 100 ).toFixed( 1 ) + '%' : '—';
+ const isSelected = row.slug === activeKit;
+ const tableRow = document.createElement( 'tr' );
+ if ( isSelected ) {
+ tableRow.classList.add( 'is-selected' );
+ }
+
+ const viewsClass = 'ak-col-number' + ( activeMetric === 'downloads' ? ' ak-hidden-col' : '' );
+ const dlClass = 'ak-col-number' + ( activeMetric === 'views' ? ' ak-hidden-col' : '' );
+
+ tableRow.innerHTML = `
+ ${ escHtml( row.title ) } |
+ ${ fmt( views ) } |
+ ${ fmt( downloads ) } |
+ ${ rate } |
+ ${ formatDate( row.updated ) } |
+ `;
+
+ tableRow.querySelector( 'a' ).addEventListener( 'click', ( event ) => {
+ event.preventDefault();
+ setKit( event.currentTarget.dataset.slug );
+ } );
+ tableRow.addEventListener( 'click', ( event ) => {
+ if ( event.target.tagName !== 'A' ) {
+ setKit( row.slug );
+ }
+ } );
+
+ tableBody.appendChild( tableRow );
+ } );
+
+ // Update sort arrows.
+ document.querySelectorAll( '#ak-stats-table thead th' ).forEach( ( tableHeader ) => {
+ const col = tableHeader.dataset.col;
+ const arrow = tableHeader.querySelector( '.ak-sort-arrow' );
+ tableHeader.classList.remove( 'is-sorted' );
+ if ( arrow ) {
+ arrow.textContent = '';
+ }
+ if ( col === sortCol && arrow ) {
+ tableHeader.classList.add( 'is-sorted' );
+ arrow.textContent = sortDir === 'asc' ? ' ↑' : ' ↓';
+ }
+ } );
+ }
+
+ // ── UI state ──
+ function updateUI() {
+ const isSingle = !! activeKit;
+ const kitObj = isSingle ? allData.find( ( row ) => row.slug === activeKit ) : null;
+
+ if ( chartTitle ) {
+ chartTitle.textContent =
+ metricLabel[ activeMetric ] + ' — ' + ( isSingle && kitObj ? kitObj.title : 'All Kits' );
+ }
+ if ( chartSubtitle ) {
+ chartSubtitle.textContent = rangeLabel();
+ }
+
+ if ( legendViews ) {
+ legendViews.style.display = activeMetric === 'downloads' ? 'none' : '';
+ }
+ if ( legendDownloads ) {
+ legendDownloads.style.display = activeMetric === 'views' ? 'none' : '';
+ }
+
+ if ( thViews ) {
+ thViews.classList.toggle( 'ak-hidden-col', activeMetric === 'downloads' );
+ }
+ if ( thDownloads ) {
+ thDownloads.classList.toggle( 'ak-hidden-col', activeMetric === 'views' );
+ }
+
+ if ( backLinkBar ) {
+ backLinkBar.classList.toggle( 'is-visible', isSingle );
+ }
+ if ( kitBanner ) {
+ kitBanner.classList.toggle( 'is-visible', isSingle );
+ if ( isSingle && kitObj && kitBannerName ) {
+ kitBannerName.textContent = kitObj.title;
+ }
+ }
+
+ if ( tableSubtitle ) {
+ tableSubtitle.textContent = isSingle ? 'Showing single kit' : "Click a row to see a single kit's stats";
+ }
+ }
+
+ // ── Main render ──
+ async function render() {
+ if ( ! jetpackAvailable ) {
+ tableBody.innerHTML =
+ '| Jetpack Stats is not connected. View and download counts require a Jetpack connection to WordPress.com. |
';
+ return;
+ }
+
+ tableBody.innerHTML = '| Loading… |
';
+
+ try {
+ allData = await fetchStats();
+ const data = activeKit ? allData.filter( ( row ) => row.slug === activeKit ) : allData;
+ updateSummary( data );
+ updateUI();
+ renderChart( data );
+ renderTable( data );
+ } catch ( error ) {
+ tableBody.innerHTML = `| Error loading stats: ${ escHtml( error.message ) } |
`;
+ }
+ }
+
+ // ── Setters ──
+ function setMetric( metric ) {
+ activeMetric = metric;
+ metricBtns.forEach( ( button ) => button.classList.toggle( 'is-active', button.dataset.akMetric === metric ) );
+ const data = activeKit ? allData.filter( ( row ) => row.slug === activeKit ) : allData;
+ updateSummary( data );
+ updateUI();
+ renderChart( data );
+ renderTable( data );
+ }
+
+ function setRange( range ) {
+ activeRange = range;
+ rangeBtns.forEach( ( button ) => button.classList.toggle( 'is-active', button.dataset.akRange === range ) );
+ render();
+ }
+
+ function setKit( slug ) {
+ activeKit = slug === activeKit ? '' : slug;
+ chartOffset = 0;
+ if ( filterKit ) {
+ filterKit.value = activeKit;
+ }
+ render();
+ }
+
+ function resetKit() {
+ activeKit = '';
+ chartOffset = 0;
+ if ( filterKit ) {
+ filterKit.value = '';
+ }
+ render();
+ }
+
+ // ── Export CSV ──
+ function exportCSV() {
+ const data = activeKit ? allData.filter( ( row ) => row.slug === activeKit ) : allData;
+ const rows = [ [ 'Kit Name', 'Views', 'Downloads', 'Download Rate', 'Last Updated' ] ];
+ data.forEach( ( row ) => {
+ const views = row.views ?? 0;
+ const downloads = row.downloads ?? 0;
+ const rate = views > 0 ? ( ( downloads / views ) * 100 ).toFixed( 1 ) + '%' : '0%';
+ rows.push( [ row.title, views, downloads, rate, row.updated || '' ] );
+ } );
+ const csv = rows
+ .map( ( row ) => row.map( ( cell ) => `"${ String( cell ).replace( /"/g, '""' ) }"` ).join( ',' ) )
+ .join( '\n' );
+ const blob = new Blob( [ csv ], { type: 'text/csv' } );
+ const url = URL.createObjectURL( blob );
+ const a = document.createElement( 'a' );
+ a.href = url;
+ a.download = 'activity-kit-stats.csv';
+ a.click();
+ URL.revokeObjectURL( url );
+ }
+
+ // ── Event listeners ──
+ metricBtns.forEach( ( btn ) => {
+ btn.addEventListener( 'click', () => setMetric( btn.dataset.akMetric ) );
+ } );
+
+ rangeBtns.forEach( ( btn ) => {
+ btn.addEventListener( 'click', () => setRange( btn.dataset.akRange ) );
+ } );
+
+ filterKit.addEventListener( 'change', () => {
+ activeKit = filterKit.value;
+ chartOffset = 0;
+ render();
+ } );
+
+ if ( chartSlider ) {
+ chartSlider.addEventListener( 'input', () => {
+ chartOffset = parseInt( chartSlider.value, 10 );
+ const data = activeKit ? allData.filter( ( row ) => row.slug === activeKit ) : allData;
+ renderChart( data );
+ } );
+ }
+
+ const backLink = document.getElementById( 'ak-back-link' );
+ const bannerBack = document.getElementById( 'ak-kit-banner-back' );
+ if ( backLink ) {
+ backLink.addEventListener( 'click', ( event ) => {
+ event.preventDefault();
+ resetKit();
+ } );
+ }
+ if ( bannerBack ) {
+ bannerBack.addEventListener( 'click', ( event ) => {
+ event.preventDefault();
+ resetKit();
+ } );
+ }
+ if ( exportBtn ) {
+ exportBtn.addEventListener( 'click', ( event ) => {
+ event.preventDefault();
+ exportCSV();
+ } );
+ }
+
+ // Sortable column headers.
+ document.querySelectorAll( '#ak-stats-table thead th' ).forEach( ( tableHeader ) => {
+ tableHeader.addEventListener( 'click', () => {
+ const col = tableHeader.dataset.col;
+ if ( ! col ) {
+ return;
+ }
+ if ( sortCol === col ) {
+ sortDir = sortDir === 'asc' ? 'desc' : 'asc';
+ } else {
+ sortCol = col;
+ sortDir = tableHeader.dataset.type === 'number' ? 'desc' : 'asc';
+ }
+ const data = activeKit ? allData.filter( ( row ) => row.slug === activeKit ) : allData;
+ renderTable( data );
+ } );
+ } );
+
+ // ── Init ──
+ initChart();
+ render();
+} )();
diff --git a/wp-content/plugins/wporg-learn/js/lesson-plan-details/src/edit.js b/wp-content/plugins/wporg-learn/js/lesson-plan-details/src/edit.js
index c94b2d2e0..e38b442da 100644
--- a/wp-content/plugins/wporg-learn/js/lesson-plan-details/src/edit.js
+++ b/wp-content/plugins/wporg-learn/js/lesson-plan-details/src/edit.js
@@ -12,10 +12,7 @@ export default function Edit( { clientId } ) {
'lesson-plan',
useGetCurrentPostType(),
useIsBlockInSidebar( clientId, 'wporg-learn-lesson-plans' ),
- __(
- 'This will be dynamically populated based on settings in the Lesson Plan Details meta box.',
- 'wporg-learn'
- )
+ __( 'This will be dynamically populated based on settings in the Lesson Plan Details meta box.', 'wporg-learn' )
);
return (
diff --git a/wp-content/plugins/wporg-learn/js/locale-notice.js b/wp-content/plugins/wporg-learn/js/locale-notice.js
index 927780d44..7076ae51e 100644
--- a/wp-content/plugins/wporg-learn/js/locale-notice.js
+++ b/wp-content/plugins/wporg-learn/js/locale-notice.js
@@ -6,9 +6,7 @@
const localeNotice = window.WPOrgLearnLocaleNotice || {};
const app = $.extend( localeNotice, {
- $notice: $(),
-
- init: function () {
+ init() {
app.$notice = $( '.wporg-learn-locale-notice' );
app.$notice.on( 'click', '.wporg-learn-locale-notice-dismiss', function ( event ) {
@@ -17,7 +15,7 @@
} );
},
- dismissNotice: function () {
+ dismissNotice() {
app.$notice.fadeTo( 100, 0, function () {
app.$notice.slideUp( 100, function () {
app.$notice.remove();
@@ -34,6 +32,7 @@
);
},
} );
+ app.$notice = $();
$( document ).ready( function () {
app.init();
diff --git a/wp-content/plugins/wporg-learn/js/query-activity-kits/src/index.js b/wp-content/plugins/wporg-learn/js/query-activity-kits/src/index.js
new file mode 100644
index 000000000..e7205efa1
--- /dev/null
+++ b/wp-content/plugins/wporg-learn/js/query-activity-kits/src/index.js
@@ -0,0 +1,23 @@
+import { registerBlockVariation } from '@wordpress/blocks';
+import { __ } from '@wordpress/i18n';
+
+registerBlockVariation( 'core/query', {
+ name: 'wporg/activity-kits',
+ title: __( 'Activity Kits', 'wporg-learn' ),
+ description: __( 'Display a filterable grid of activity kits.', 'wporg-learn' ),
+ isActive: [ 'namespace' ],
+ attributes: {
+ namespace: 'wporg/activity-kits',
+ query: {
+ postType: 'activity_kit',
+ perPage: 12,
+ order: 'desc',
+ orderBy: 'date',
+ },
+ },
+ innerBlocks: [
+ [ 'core/post-template', {}, [ [ 'wporg/activity-kit-card', {} ] ] ],
+ [ 'core/query-pagination', {} ],
+ ],
+ scope: [ 'inserter' ],
+} );
diff --git a/wp-content/plugins/wporg-learn/js/workshop-details/src/edit.js b/wp-content/plugins/wporg-learn/js/workshop-details/src/edit.js
index 910ddf3f4..46b88e694 100644
--- a/wp-content/plugins/wporg-learn/js/workshop-details/src/edit.js
+++ b/wp-content/plugins/wporg-learn/js/workshop-details/src/edit.js
@@ -12,10 +12,7 @@ export default function Edit( { clientId } ) {
'wporg_workshop',
useGetCurrentPostType(),
useIsBlockInSidebar( clientId, 'wporg-learn-workshops' ),
- __(
- 'This will be dynamically populated based on settings in the Workshop Details meta box.',
- 'wporg-learn'
- )
+ __( 'This will be dynamically populated based on settings in the Workshop Details meta box.', 'wporg-learn' )
);
return (
diff --git a/wp-content/plugins/wporg-learn/views/block-activity-kit-card.php b/wp-content/plugins/wporg-learn/views/block-activity-kit-card.php
new file mode 100644
index 000000000..059895b62
--- /dev/null
+++ b/wp-content/plugins/wporg-learn/views/block-activity-kit-card.php
@@ -0,0 +1,99 @@
+context['postId'] ?? 0;
+
+if ( ! $kit_post_id || 'activity_kit' !== get_post_type( $kit_post_id ) ) {
+ return '';
+}
+
+$kit_post = get_post( $kit_post_id );
+$kit_title = get_the_title( $kit_post_id );
+$permalink = get_permalink( $kit_post_id );
+$excerpt = get_the_excerpt( $kit_post );
+
+$duration = get_post_meta( $kit_post_id, '_activity_duration', true );
+$zip_id = (int) get_post_meta( $kit_post_id, '_activity_zip_id', true );
+$zip_url = $zip_id ? wp_get_attachment_url( $zip_id ) : '';
+
+$level_terms = wp_get_post_terms( $kit_post_id, 'level', array( 'fields' => 'names' ) );
+$level_name = ! is_wp_error( $level_terms ) && ! empty( $level_terms ) ? $level_terms[0] : '';
+
+$thumbnail_html = '';
+if ( has_post_thumbnail( $kit_post_id ) ) {
+ $thumbnail_html = get_the_post_thumbnail(
+ $kit_post_id,
+ 'medium',
+ array(
+ 'style' => 'width:100%;height:100%;object-fit:cover;',
+ 'alt' => esc_attr( $kit_title ),
+ )
+ );
+}
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/wp-content/plugins/wporg-learn/webpack.config.js b/wp-content/plugins/wporg-learn/webpack.config.js
index a9fb05412..01e82e509 100644
--- a/wp-content/plugins/wporg-learn/webpack.config.js
+++ b/wp-content/plugins/wporg-learn/webpack.config.js
@@ -20,6 +20,10 @@ config.entry = {
'lesson-plan-details': './js/lesson-plan-details/src/index.js',
'course-data': './js/course-data/src/index.js',
'language-meta': './js/language-meta/index.js',
+ 'activity-kit-card': './js/activity-kit-card/src/index.js',
+ 'activity-kit-sidebar': './js/activity-kit-sidebar/src/index.js',
+ 'query-activity-kits': './js/query-activity-kits/src/index.js',
+ 'activity-kit-stats': './js/activity-kit-stats/index.js',
};
module.exports = config;
diff --git a/wp-content/plugins/wporg-learn/wporg-learn.php b/wp-content/plugins/wporg-learn/wporg-learn.php
index fcf31eac5..1c8da8389 100644
--- a/wp-content/plugins/wporg-learn/wporg-learn.php
+++ b/wp-content/plugins/wporg-learn/wporg-learn.php
@@ -89,6 +89,10 @@ function load_files() {
require_once get_includes_path() . 'taxonomy.php';
require_once get_includes_path() . 'export.php';
require_once get_includes_path() . 'utils.php';
+ require_once get_includes_path() . 'activity-kit-rest.php';
+ require_once get_includes_path() . 'activity-kit-settings.php';
+ require_once get_includes_path() . 'activity-kit-stats-page.php';
+ require_once get_includes_path() . 'activity-kit-import.php';
}
/**
diff --git a/wp-content/themes/pub/wporg-learn-2024/inc/block-config.php b/wp-content/themes/pub/wporg-learn-2024/inc/block-config.php
index e93b38f65..dd973427b 100644
--- a/wp-content/themes/pub/wporg-learn-2024/inc/block-config.php
+++ b/wp-content/themes/pub/wporg-learn-2024/inc/block-config.php
@@ -5,11 +5,14 @@
namespace WordPressdotorg\Theme\Learn_2024\Block_Config;
-use function WPOrg_Learn\Post_Meta\{get_available_post_type_locales};
use Sensei_Learner;
+use function WPOrg_Learn\Post_Meta\{get_available_post_type_locales};
add_filter( 'wporg_query_filter_options_content_type', __NAMESPACE__ . '\get_content_type_options' );
+add_filter( 'wporg_query_filter_options_activity_kit_topic', __NAMESPACE__ . '\get_activity_kit_topic_options' );
+add_filter( 'wporg_query_filter_options_activity_kit_level', __NAMESPACE__ . '\get_activity_kit_level_options' );
+
add_filter( 'wporg_query_filter_options_language', __NAMESPACE__ . '\get_language_options' );
add_filter( 'wporg_query_filter_options_archive_language', __NAMESPACE__ . '\get_language_options_by_post_type' );
@@ -49,21 +52,22 @@ function get_content_type_options( $options ) {
global $wp_query;
$options = array(
- 'any' => __( 'Any', 'wporg-learn' ),
- 'course' => __( 'Course', 'wporg-learn' ),
- 'lesson' => __( 'Lesson', 'wporg-learn' ),
+ 'any' => __( 'Any', 'wporg-learn' ),
+ 'course' => __( 'Course', 'wporg-learn' ),
+ 'lesson' => __( 'Lesson', 'wporg-learn' ),
+ 'activity_kit' => __( 'Activity Kit', 'wporg-learn' ),
);
- $post_type = $wp_query->get( 'post_type' );
+ $post_type = $wp_query->get( 'post_type' );
$selected_slug = is_string( $post_type ) ? $post_type : 'any';
- $label = $options[ $selected_slug ] ?? $options['any'];
+ $label = $options[ $selected_slug ] ?? $options['any'];
return array(
- 'label' => sprintf( __( 'Type: %s', 'wporg-learn' ), $label ),
- 'title' => __( 'Content type', 'wporg-learn' ),
- 'key' => 'post_type',
- 'action' => get_filtered_url(),
- 'options' => $options,
+ 'label' => sprintf( __( 'Type: %s', 'wporg-learn' ), $label ),
+ 'title' => __( 'Content type', 'wporg-learn' ),
+ 'key' => 'post_type',
+ 'action' => get_filtered_url(),
+ 'options' => $options,
'selected' => array( $selected_slug ),
);
}
@@ -109,18 +113,18 @@ function ( $a, $b ) {
$selected_level = wp_list_filter( $levels, array( 'slug' => $selected_slug ) );
if ( ! empty( $selected_level ) ) {
$selected_level = array_shift( $selected_level );
- $label = $selected_level->name;
+ $label = $selected_level->name;
}
} else {
$selected_slug = 'all';
}
return array(
- 'label' => sprintf( __( 'Level: %s', 'wporg-learn' ), $label ),
- 'title' => __( 'Level', 'wporg-learn' ),
- 'key' => 'wporg_lesson_level',
- 'action' => get_filtered_url(),
- 'options' => array_combine( wp_list_pluck( $levels, 'slug' ), wp_list_pluck( $levels, 'name' ) ),
+ 'label' => sprintf( __( 'Level: %s', 'wporg-learn' ), $label ),
+ 'title' => __( 'Level', 'wporg-learn' ),
+ 'key' => 'wporg_lesson_level',
+ 'action' => get_filtered_url(),
+ 'options' => array_combine( wp_list_pluck( $levels, 'slug' ), wp_list_pluck( $levels, 'name' ) ),
'selected' => array( $selected_slug ),
);
}
@@ -142,18 +146,18 @@ function get_level_options_by_post_type( $options ) {
// Get top 10 levels ordered by count, not empty, filtered by post_type.
$object_ids = get_posts(
array(
- 'post_type' => $wp_query->query_vars['post_type'],
- 'fields' => 'ids',
+ 'post_type' => $wp_query->query_vars['post_type'],
+ 'fields' => 'ids',
'posts_per_page' => -1,
- 'post_status' => 'publish',
+ 'post_status' => 'publish',
)
);
- $levels = get_terms(
+ $levels = get_terms(
array(
- 'taxonomy' => 'level',
- 'orderby' => 'count',
- 'order' => 'DESC',
- 'number' => 10,
+ 'taxonomy' => 'level',
+ 'orderby' => 'count',
+ 'order' => 'DESC',
+ 'number' => 10,
'hide_empty' => true,
'object_ids' => $object_ids,
)
@@ -173,10 +177,10 @@ function get_level_options( $options ) {
// Get top 10 levels ordered by count, not empty.
$levels = get_terms(
array(
- 'taxonomy' => 'level',
- 'orderby' => 'count',
- 'order' => 'DESC',
- 'number' => 10,
+ 'taxonomy' => 'level',
+ 'orderby' => 'count',
+ 'order' => 'DESC',
+ 'number' => 10,
'hide_empty' => true,
)
);
@@ -200,25 +204,25 @@ function get_learning_pathway_level_options( $options ) {
// Get top 10 levels ordered by count, not empty, filtered by post_type.
$object_ids = get_posts(
array(
- 'fields' => 'ids',
+ 'fields' => 'ids',
'posts_per_page' => -1,
- 'post_status' => 'publish',
- 'post_type' => 'course',
- 'tax_query' => array(
+ 'post_status' => 'publish',
+ 'post_type' => 'course',
+ 'tax_query' => array(
array(
'taxonomy' => 'learning-pathway',
- 'field' => 'slug',
- 'terms' => $wp_query->query_vars['wporg_learning_pathway'],
+ 'field' => 'slug',
+ 'terms' => $wp_query->query_vars['wporg_learning_pathway'],
),
),
)
);
- $levels = get_terms(
+ $levels = get_terms(
array(
- 'taxonomy' => 'level',
- 'orderby' => 'count',
- 'order' => 'DESC',
- 'number' => 10,
+ 'taxonomy' => 'level',
+ 'orderby' => 'count',
+ 'order' => 'DESC',
+ 'number' => 10,
'hide_empty' => true,
'object_ids' => $object_ids,
)
@@ -250,19 +254,19 @@ function ( $a, $b ) {
);
$selected = isset( $wp_query->query['wporg_workshop_topic'] ) ? (array) $wp_query->query['wporg_workshop_topic'] : array();
- $count = count( $selected );
- $label = sprintf(
+ $count = count( $selected );
+ $label = sprintf(
/* translators: The dropdown label for filtering, %s is the selected term count. */
_n( 'Topic %s', 'Topic %s', $count, 'wporg-learn' ),
$count
);
return array(
- 'label' => $label,
- 'title' => __( 'Filter', 'wporg-learn' ),
- 'key' => 'wporg_workshop_topic',
- 'action' => get_filtered_url(),
- 'options' => array_combine( wp_list_pluck( $topics, 'slug' ), wp_list_pluck( $topics, 'name' ) ),
+ 'label' => $label,
+ 'title' => __( 'Filter', 'wporg-learn' ),
+ 'key' => 'wporg_workshop_topic',
+ 'action' => get_filtered_url(),
+ 'options' => array_combine( wp_list_pluck( $topics, 'slug' ), wp_list_pluck( $topics, 'name' ) ),
'selected' => $selected,
);
}
@@ -282,18 +286,20 @@ function get_topic_options_by_post_type( $options ) {
}
// Get top 20 topics ordered by count, not empty, filtered by post_type.
- $object_ids = get_posts( array(
- 'fields' => 'ids',
- 'posts_per_page' => -1,
- 'post_status' => 'publish',
- 'post_type' => $wp_query->query_vars['post_type'],
- ) );
- $topics = get_terms(
+ $object_ids = get_posts(
array(
- 'taxonomy' => 'topic',
- 'orderby' => 'count',
- 'order' => 'DESC',
- 'number' => 20,
+ 'fields' => 'ids',
+ 'posts_per_page' => -1,
+ 'post_status' => 'publish',
+ 'post_type' => $wp_query->query_vars['post_type'],
+ )
+ );
+ $topics = get_terms(
+ array(
+ 'taxonomy' => 'topic',
+ 'orderby' => 'count',
+ 'order' => 'DESC',
+ 'number' => 20,
'hide_empty' => true,
'object_ids' => $object_ids,
)
@@ -313,10 +319,10 @@ function get_topic_options( $options ) {
// Get top 20 topics ordered by count, not empty.
$topics = get_terms(
array(
- 'taxonomy' => 'topic',
- 'orderby' => 'count',
- 'order' => 'DESC',
- 'number' => 20,
+ 'taxonomy' => 'topic',
+ 'orderby' => 'count',
+ 'order' => 'DESC',
+ 'number' => 20,
'hide_empty' => true,
)
);
@@ -340,25 +346,25 @@ function get_learning_pathway_topic_options( $options ) {
// Get top 20 topics ordered by count, not empty, filtered by post_type.
$object_ids = get_posts(
array(
- 'fields' => 'ids',
+ 'fields' => 'ids',
'posts_per_page' => -1,
- 'post_status' => 'publish',
- 'post_type' => 'course',
- 'tax_query' => array(
+ 'post_status' => 'publish',
+ 'post_type' => 'course',
+ 'tax_query' => array(
array(
'taxonomy' => 'learning-pathway',
- 'field' => 'slug',
- 'terms' => $wp_query->query_vars['wporg_learning_pathway'],
+ 'field' => 'slug',
+ 'terms' => $wp_query->query_vars['wporg_learning_pathway'],
),
),
)
);
- $topics = get_terms(
+ $topics = get_terms(
array(
- 'taxonomy' => 'topic',
- 'orderby' => 'count',
- 'order' => 'DESC',
- 'number' => 20,
+ 'taxonomy' => 'topic',
+ 'orderby' => 'count',
+ 'order' => 'DESC',
+ 'number' => 20,
'hide_empty' => true,
'object_ids' => $object_ids,
)
@@ -431,19 +437,19 @@ function create_language_options( $languages ) {
}
$selected = get_meta_query_values_by_key( $wp_query, 'language' );
- $count = count( $selected );
- $label = sprintf(
+ $count = count( $selected );
+ $label = sprintf(
/* translators: The dropdown label for filtering, %s is the selected term count. */
_n( 'Language %s', 'Language %s', $count, 'wporg-learn' ),
$count
);
return array(
- 'label' => $label,
- 'title' => __( 'Filter', 'wporg-learn' ),
- 'key' => 'language',
- 'action' => get_filtered_url(),
- 'options' => $languages,
+ 'label' => $label,
+ 'title' => __( 'Filter', 'wporg-learn' ),
+ 'key' => 'language',
+ 'action' => get_filtered_url(),
+ 'options' => $languages,
'selected' => $selected,
);
}
@@ -520,20 +526,20 @@ function get_student_course_options( $options ) {
$key = get_student_course_filter_query_var_name();
$options = array(
- 'all' => __( 'All', 'wporg-learn' ),
- 'active' => __( 'Active', 'wporg-learn' ),
+ 'all' => __( 'All', 'wporg-learn' ),
+ 'active' => __( 'Active', 'wporg-learn' ),
'completed' => __( 'Completed', 'wporg-learn' ),
);
$selected_slug = $wp_query->get( $key ) ? $wp_query->get( $key ) : 'all';
- $label = $options[ $selected_slug ] ?? $options['all'];
+ $label = $options[ $selected_slug ] ?? $options['all'];
return array(
- 'label' => sprintf( __( 'Status: %s', 'wporg-learn' ), $label ),
- 'title' => __( 'Completion status', 'wporg-learn' ),
- 'key' => $key,
- 'action' => get_filtered_url(),
- 'options' => $options,
+ 'label' => sprintf( __( 'Status: %s', 'wporg-learn' ), $label ),
+ 'title' => __( 'Completion status', 'wporg-learn' ),
+ 'key' => $key,
+ 'action' => get_filtered_url(),
+ 'options' => $options,
'selected' => array( $selected_slug ),
);
}
@@ -550,7 +556,7 @@ function get_student_course_options( $options ) {
function inject_other_filters( $key ) {
global $wp_query;
- $single_query_vars = array( 'wporg_lesson_level', 'wporg_learning_pathway', 'post_type' );
+ $single_query_vars = array( 'wporg_lesson_level', 'wporg_learning_pathway', 'post_type', 'level' );
foreach ( $single_query_vars as $single_query_var ) {
if ( ! isset( $wp_query->query[ $single_query_var ] ) ) {
continue;
@@ -565,7 +571,7 @@ function inject_other_filters( $key ) {
printf( '', esc_attr( $single_query_var ), esc_attr( $value ) );
}
- $multi_query_vars = array( 'wporg_workshop_topic' );
+ $multi_query_vars = array( 'wporg_workshop_topic', 'topic' );
foreach ( $multi_query_vars as $multi_query_var ) {
if ( ! isset( $wp_query->query[ $multi_query_var ] ) ) {
continue;
@@ -639,3 +645,113 @@ function modify_course_query( $query ) {
return $query;
}
+
+/**
+ * Get topic filter options for the Activity Kit archive.
+ *
+ * @return array
+ */
+function get_activity_kit_topic_options() {
+ $terms = get_terms(
+ array(
+ 'taxonomy' => 'topic',
+ 'object_ids' => get_posts(
+ array(
+ 'post_type' => 'activity_kit',
+ 'post_status' => 'publish',
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ )
+ ),
+ 'hide_empty' => true,
+ 'number' => 20,
+ 'orderby' => 'count',
+ 'order' => 'DESC',
+ )
+ );
+
+ if ( is_wp_error( $terms ) || count( $terms ) < 2 ) {
+ return array();
+ }
+
+ usort(
+ $terms,
+ function ( $a, $b ) {
+ return strcmp( strtolower( $a->name ), strtolower( $b->name ) );
+ }
+ );
+
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $selected = isset( $_GET['topic'] ) ? array_map( 'sanitize_text_field', (array) wp_unslash( $_GET['topic'] ) ) : array();
+ $count = count( $selected );
+ $label = $count
+ ? sprintf( _n( 'Topic %s', 'Topic %s', $count, 'wporg-learn' ), $count )
+ : __( 'Topic', 'wporg-learn' );
+
+ return array(
+ 'label' => $label,
+ 'title' => __( 'Topic', 'wporg-learn' ),
+ 'key' => 'topic',
+ 'action' => get_filtered_url(),
+ 'options' => array_combine(
+ wp_list_pluck( $terms, 'slug' ),
+ wp_list_pluck( $terms, 'name' )
+ ),
+ 'selected' => $selected,
+ );
+}
+
+/**
+ * Get level filter options for the Activity Kit archive.
+ *
+ * @return array
+ */
+function get_activity_kit_level_options() {
+ $terms = get_terms(
+ array(
+ 'taxonomy' => 'level',
+ 'object_ids' => get_posts(
+ array(
+ 'post_type' => 'activity_kit',
+ 'post_status' => 'publish',
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ )
+ ),
+ 'hide_empty' => true,
+ 'orderby' => 'name',
+ 'order' => 'ASC',
+ )
+ );
+
+ if ( is_wp_error( $terms ) || empty( $terms ) ) {
+ return array();
+ }
+
+ $all_levels = array_merge(
+ array(
+ (object) array(
+ 'slug' => 'all',
+ 'name' => __( 'All', 'wporg-learn' ),
+ ),
+ ),
+ $terms
+ );
+
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $selected_slug = isset( $_GET['level'] ) ? sanitize_text_field( wp_unslash( $_GET['level'] ) ) : 'all';
+ $matching = wp_list_filter( $all_levels, array( 'slug' => $selected_slug ) );
+ $selected_label = ! empty( $matching ) ? reset( $matching )->name : __( 'All', 'wporg-learn' );
+
+ return array(
+ 'label' => sprintf( __( 'Level: %s', 'wporg-learn' ), $selected_label ),
+ 'title' => __( 'Level', 'wporg-learn' ),
+ 'key' => 'level',
+ 'action' => get_filtered_url(),
+ 'options' => array_combine(
+ wp_list_pluck( $all_levels, 'slug' ),
+ wp_list_pluck( $all_levels, 'name' )
+ ),
+ 'selected' => array( $selected_slug ),
+ );
+}
diff --git a/wp-content/themes/pub/wporg-learn-2024/inc/block-hooks.php b/wp-content/themes/pub/wporg-learn-2024/inc/block-hooks.php
index 5f51defde..85da61850 100644
--- a/wp-content/themes/pub/wporg-learn-2024/inc/block-hooks.php
+++ b/wp-content/themes/pub/wporg-learn-2024/inc/block-hooks.php
@@ -14,6 +14,7 @@
add_filter( 'render_block_sensei-lms/course-outline', __NAMESPACE__ . '\update_course_outline_block_add_aria' );
add_filter( 'render_block_sensei-lms/course-theme-notices', __NAMESPACE__ . '\update_lesson_quiz_notice_text' );
add_filter( 'render_block_sensei-lms/quiz-actions', __NAMESPACE__ . '\update_quiz_actions' );
+add_filter( 'render_block_core/template-part', __NAMESPACE__ . '\replace_card_with_activity_kit_card', 10, 2 );
/**
* Update header template based on current query.
@@ -53,10 +54,10 @@ function modify_course_outline_lesson_block_attrs( $parsed_block ) {
}
$lesson_id = $parsed_block['attrs']['id'];
- $classes = array();
+ $classes = array();
$classes[] = $parsed_block['attrs']['className'] ?? '';
- $status = 'not-started';
+ $status = 'not-started';
$lesson_status = Sensei_Utils::user_lesson_status( $lesson_id );
if ( $lesson_status ) {
$status = $lesson_status->comment_approved;
@@ -95,7 +96,7 @@ function update_course_outline_block_add_aria( $block_content ) {
while ( $html->next_tag( array( 'class_name' => 'wp-block-sensei-lms-course-outline-lesson' ) ) ) {
if ( $html->has_class( 'is-complete' ) || $html->has_class( 'is-passed' ) ) {
$label = __( 'Completed', 'wporg-learn' );
- } else if ( $html->has_class( 'is-in-progress' ) ) {
+ } elseif ( $html->has_class( 'is-in-progress' ) ) {
$label = __( 'In progress', 'wporg-learn' );
} else {
$label = __( 'Not started', 'wporg-learn' );
@@ -149,17 +150,19 @@ function update_lesson_quiz_notice_text( $block_content ) {
*/
function update_quiz_actions( $block_content ) {
if ( is_singular( 'quiz' ) && is_quiz_ungraded() ) {
- $lesson_id = Sensei()->quiz->get_lesson_id();
+ $lesson_id = Sensei()->quiz->get_lesson_id();
$lesson_link = get_permalink( $lesson_id );
// Add a new button to go back to the lesson.
- $new_button_block = do_blocks( '
+ $new_button_block = do_blocks(
+ '
- ');
+ '
+ );
$block_content = str_replace(
'',
@@ -171,6 +174,114 @@ function update_quiz_actions( $block_content ) {
return $block_content;
}
+/**
+ * Replace the generic card template part with the activity kit card when the
+ * current post in the loop is an activity_kit.
+ *
+ * This ensures that search results pages (which use the generic `card` template
+ * part) render the same card component as the activity library archive grid.
+ *
+ * @param string $block_content The rendered block HTML.
+ * @param array $parsed_block The parsed block data.
+ * @return string The (possibly replaced) block HTML.
+ */
+function replace_card_with_activity_kit_card( $block_content, $parsed_block ) {
+ if ( ( $parsed_block['attrs']['slug'] ?? '' ) !== 'card' ) {
+ return $block_content;
+ }
+
+ global $post;
+ if ( ! $post || 'activity_kit' !== $post->post_type ) {
+ return $block_content;
+ }
+
+ $kit_post_id = $post->ID;
+ $kit_title = get_the_title( $kit_post_id );
+ $permalink = get_permalink( $kit_post_id );
+ $excerpt = get_the_excerpt( $post );
+ $duration = get_post_meta( $kit_post_id, '_activity_duration', true );
+ $zip_id = (int) get_post_meta( $kit_post_id, '_activity_zip_id', true );
+ $zip_url = $zip_id ? wp_get_attachment_url( $zip_id ) : '';
+
+ $level_terms = wp_get_post_terms( $kit_post_id, 'level', array( 'fields' => 'names' ) );
+ $level_name = ! is_wp_error( $level_terms ) && ! empty( $level_terms ) ? $level_terms[0] : '';
+
+ $thumbnail_html = '';
+ if ( has_post_thumbnail( $kit_post_id ) ) {
+ $thumbnail_html = get_the_post_thumbnail(
+ $kit_post_id,
+ 'medium',
+ array(
+ 'style' => 'width:100%;height:100%;object-fit:cover;',
+ 'alt' => esc_attr( $kit_title ),
+ )
+ );
+ }
+
+ ob_start();
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ quiz_progress_repository->get( $quiz_id, $user_id );
if ( $quiz_progress && 'ungraded' === $quiz_progress->get_status() ) {
diff --git a/wp-content/themes/pub/wporg-learn-2024/inc/query.php b/wp-content/themes/pub/wporg-learn-2024/inc/query.php
index 6e8fb2aa6..d610da0e7 100644
--- a/wp-content/themes/pub/wporg-learn-2024/inc/query.php
+++ b/wp-content/themes/pub/wporg-learn-2024/inc/query.php
@@ -8,6 +8,7 @@
add_action( 'pre_get_posts', __NAMESPACE__ . '\add_language_to_archive_queries' );
add_action( 'pre_get_posts', __NAMESPACE__ . '\filter_hidden_lessons_from_archive_and_search' );
add_action( 'pre_get_posts', __NAMESPACE__ . '\filter_search_queries_by_post_type' );
+add_action( 'pre_get_posts', __NAMESPACE__ . '\filter_activity_kit_archive' );
add_filter( 'request', __NAMESPACE__ . '\handle_all_level_query' );
add_filter( 'jetpack_search_es_wp_query_args', __NAMESPACE__ . '\filter_jetpack_wp_search_query', 10, 2 );
add_filter( 'jetpack_search_es_query_args', __NAMESPACE__ . '\filter_jetpack_es_search_query', 10, 2 );
@@ -65,7 +66,7 @@ function filter_hidden_lessons_from_archive_and_search( $query ) {
// If there's an existing tax query, add the new condition
if ( ! empty( $tax_query ) ) {
$tax_query['relation'] = 'AND';
- $tax_query[] = $exclude_lessons_by_taxonomy;
+ $tax_query[] = $exclude_lessons_by_taxonomy;
} else {
$tax_query = array( $exclude_lessons_by_taxonomy );
}
@@ -151,10 +152,59 @@ function filter_jetpack_es_search_query( $es_query_args, $query ) {
}
$es_query_args['query'] = array(
'bool' => array(
- 'must' => array( $es_query_args['query'] ),
+ 'must' => array( $es_query_args['query'] ),
'must_not' => $must_not,
),
);
return $es_query_args;
}
+
+/**
+ * Filter the activity kit archive by taxonomy terms from URL query params.
+ *
+ * @param \WP_Query $query
+ */
+function filter_activity_kit_archive( $query ) {
+ if ( is_admin() || ! $query->is_main_query() ) {
+ return;
+ }
+
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $requested_post_type = isset( $_GET['post_type'] ) ? sanitize_key( wp_unslash( $_GET['post_type'] ) ) : '';
+
+ if ( ! is_post_type_archive( 'activity_kit' ) && ! ( $query->is_search() && 'activity_kit' === $requested_post_type ) ) {
+ return;
+ }
+
+ $query->set( 'posts_per_page', 12 );
+
+ $tax_query = array();
+
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ if ( ! empty( $_GET['topic'] ) ) {
+ $tax_query[] = array(
+ 'taxonomy' => 'topic',
+ 'field' => 'slug',
+ 'terms' => array_map( 'sanitize_text_field', (array) wp_unslash( $_GET['topic'] ) ),
+ );
+ }
+
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $level_slug = isset( $_GET['level'] ) ? sanitize_text_field( wp_unslash( $_GET['level'] ) ) : '';
+ if ( $level_slug && 'all' !== $level_slug ) {
+ $tax_query[] = array(
+ 'taxonomy' => 'level',
+ 'field' => 'slug',
+ 'terms' => $level_slug,
+ );
+ }
+
+ if ( ! empty( $tax_query ) ) {
+ $query->set( 'tax_query', $tax_query );
+ }
+
+ if ( $query->is_search() && 'activity_kit' === $requested_post_type ) {
+ $query->set( 'post_type', 'activity_kit' );
+ }
+}
diff --git a/wp-content/themes/pub/wporg-learn-2024/patterns/archive-activity-kits-content.php b/wp-content/themes/pub/wporg-learn-2024/patterns/archive-activity-kits-content.php
new file mode 100644
index 000000000..3a63b95ba
--- /dev/null
+++ b/wp-content/themes/pub/wporg-learn-2024/patterns/archive-activity-kits-content.php
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/wp-content/themes/pub/wporg-learn-2024/patterns/single-activity-kit-content.php b/wp-content/themes/pub/wporg-learn-2024/patterns/single-activity-kit-content.php
new file mode 100644
index 000000000..02aa83cd2
--- /dev/null
+++ b/wp-content/themes/pub/wporg-learn-2024/patterns/single-activity-kit-content.php
@@ -0,0 +1,356 @@
+ 'names' ) );
+$topic_terms = wp_get_post_terms( $kit_id, 'topic', array( 'fields' => 'names' ) );
+
+$archive_url = get_post_type_archive_link( 'activity_kit' );
+
+// ZIP file size.
+$zip_path = $zip_id ? get_attached_file( $zip_id ) : '';
+$zip_size = ( $zip_path && file_exists( $zip_path ) ) ? size_format( filesize( $zip_path ) ) : '';
+
+// Build dynamic "what's inside" description for the download box.
+if ( $guide_url && $slides_url ) {
+ $download_desc = __( 'This download includes a facilitator guide and presentation slide deck.', 'wporg-learn' );
+} elseif ( $guide_url ) {
+ $download_desc = __( 'This download includes a facilitator guide.', 'wporg-learn' );
+} elseif ( $slides_url ) {
+ $download_desc = __( 'This download includes a presentation slide deck.', 'wporg-learn' );
+} else {
+ $download_desc = __( 'All files included as a single .zip download from the WordPress Media Library.', 'wporg-learn' );
+}
+
+// SVG icons (matching @wordpress/icons viewBox).
+$icon_file = '
';
+$icon_desktop = '
';
+$icon_download = '
';
+$icon_back = '
';
+$icon_clock = '
';
+$icon_calendar = '
';
+$icon_file_lg = '
';
+$icon_desk_lg = '
';
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
>
+
+
+
+
+
+
+
+
+
+
+
+
';
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ·
+
+ ·
+
+
+
+
+
+
diff --git a/wp-content/themes/pub/wporg-learn-2024/src/style/_activity-kit.scss b/wp-content/themes/pub/wporg-learn-2024/src/style/_activity-kit.scss
new file mode 100644
index 000000000..f03430d0e
--- /dev/null
+++ b/wp-content/themes/pub/wporg-learn-2024/src/style/_activity-kit.scss
@@ -0,0 +1,496 @@
+// -------------------------------------------------------------------------
+// Activity Kit Card (archive grid)
+// -------------------------------------------------------------------------
+.wporg-activity-kit-card {
+ display: flex;
+ flex-direction: column;
+ border: 1px solid var(--wp--custom--color--border);
+ border-radius: 2px;
+ overflow: hidden;
+ background: var(--wp--preset--color--white);
+ height: 100%;
+
+ &__image {
+ height: 152px;
+ flex-shrink: 0;
+ overflow: hidden;
+ background: var(--wp--preset--color--light-grey-2);
+
+ a {
+ display: block;
+ height: 100%;
+ }
+
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+
+ &-placeholder {
+ width: 100%;
+ height: 100%;
+ background: var(--wp--preset--color--light-grey-2);
+ }
+ }
+
+ &__body {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ padding: 16px;
+ gap: 12px;
+ }
+
+ &__title {
+ font-size: var(--wp--preset--font-size--normal);
+ font-weight: 600;
+ margin: 0;
+
+ a {
+ color: var(--wp--custom--link--color--text);
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ &__excerpt {
+ font-size: var(--wp--preset--font-size--small);
+ color: var(--wp--preset--color--charcoal-2);
+ margin: 0;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ }
+
+ &__meta {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: var(--wp--preset--spacing--10);
+ font-size: var(--wp--preset--font-size--small);
+ color: var(--wp--preset--color--charcoal-3);
+ margin-top: auto;
+
+ span + span::before {
+ content: "·";
+ margin-right: var(--wp--preset--spacing--10);
+ }
+ }
+
+ &__duration {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+
+ &::before {
+ content: "";
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+ background: url(../../assets/icon-clock.svg) no-repeat center / contain;
+ flex-shrink: 0;
+ }
+ }
+
+ &__actions {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ margin-top: auto;
+ }
+
+ &__view-btn,
+ &__download-btn {
+ flex: 1;
+ text-align: center;
+ padding: 7px 12px;
+ font-size: var(--wp--preset--font-size--small);
+ font-weight: 500;
+ border-radius: 2px;
+ text-decoration: none;
+ line-height: 1.4;
+ display: inline-block;
+ }
+
+ &__view-btn {
+ border: 1px solid var(--wp--preset--color--blueberry-1);
+ color: var(--wp--preset--color--blueberry-1);
+
+ &:hover {
+ background: color-mix(in sRGB, var(--wp--preset--color--blueberry-1) 8%, transparent);
+ text-decoration: none;
+ }
+ }
+
+ &__download-btn {
+ background: var(--wp--preset--color--blueberry-1);
+ color: var(--wp--preset--color--white) !important;
+
+ &:hover {
+ background: var(--wp--preset--color--deep-blueberry);
+ text-decoration: none;
+ }
+ }
+}
+
+// -------------------------------------------------------------------------
+// Activity Kit Single Page
+// -------------------------------------------------------------------------
+.wporg-activity-kit-breadcrumb {
+ font-size: var(--wp--preset--font-size--small);
+ color: var(--wp--preset--color--charcoal-3);
+ margin-bottom: var(--wp--preset--spacing--30);
+
+ a {
+ color: var(--wp--preset--color--blueberry-1);
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+}
+
+.wporg-activity-kit-action-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: var(--wp--preset--spacing--20);
+ margin-bottom: var(--wp--preset--spacing--40);
+
+ &__back {
+ font-size: var(--wp--preset--font-size--small);
+ color: var(--wp--preset--color--blueberry-1);
+ text-decoration: none;
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ &__download {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ background: var(--wp--preset--color--blueberry-1);
+ color: var(--wp--preset--color--white) !important;
+ padding: 9px 20px;
+ border-radius: 2px;
+ font-size: var(--wp--preset--font-size--small);
+ font-weight: 500;
+ text-decoration: none;
+
+ &:hover {
+ background: var(--wp--preset--color--deep-blueberry);
+ text-decoration: none;
+ }
+ }
+}
+
+.wporg-activity-kit-header {
+ margin-bottom: var(--wp--preset--spacing--30);
+}
+
+.wporg-activity-kit-title {
+ margin-bottom: var(--wp--preset--spacing--20);
+}
+
+.wporg-activity-kit-meta {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: var(--wp--preset--spacing--10);
+ font-size: var(--wp--preset--font-size--small);
+ color: var(--wp--preset--color--charcoal-2);
+
+ &__duration,
+ &__updated {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+
+ svg {
+ flex-shrink: 0;
+ color: var(--wp--preset--color--charcoal-3);
+ }
+ }
+
+ span + span::before {
+ content: "·";
+ margin-right: var(--wp--preset--spacing--10);
+ }
+}
+
+// Section label (e.g. "Kit Preview")
+.wporg-activity-kit-section-label {
+ font-size: 11px;
+ color: var(--wp--preset--color--charcoal-3);
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ font-weight: 500;
+ margin-bottom: var(--wp--preset--spacing--20);
+}
+
+.wporg-activity-kit-pdf-section {
+ margin-bottom: var(--wp--preset--spacing--40);
+}
+
+// PDF tab toggle
+.wporg-activity-kit-pdf-tabs {
+ &__header {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: var(--wp--preset--spacing--20);
+ }
+
+ &__dl-btn {
+ display: none; // shown via JS only for non-Chromium browsers (Firefox, Safari)
+ align-items: center;
+ gap: 5px;
+ font-size: var(--wp--preset--font-size--small);
+ color: var(--wp--preset--color--blueberry-1);
+ text-decoration: none;
+ padding-bottom: 9px; // align baseline with tab bottom border
+ white-space: nowrap;
+ flex-shrink: 0;
+
+ svg {
+ flex-shrink: 0;
+ }
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ &__nav {
+ display: flex;
+ gap: 0;
+ border: 1px solid var(--wp--custom--color--border);
+ border-bottom: none;
+ border-radius: 2px 2px 0 0;
+ overflow: hidden;
+ width: fit-content;
+ }
+
+ &__tab {
+ background: var(--wp--preset--color--light-grey-2);
+ border: none;
+ border-right: 1px solid var(--wp--custom--color--border);
+ padding: 9px 20px;
+ font-size: var(--wp--preset--font-size--small);
+ font-weight: 400;
+ color: var(--wp--preset--color--charcoal-2);
+ cursor: pointer;
+ transition: background 0.1s, color 0.1s;
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+
+ &:last-child {
+ border-right: none;
+ }
+
+ &:hover {
+ background: #ebebeb;
+ color: var(--wp--preset--color--charcoal-1);
+ }
+
+ &.is-active {
+ background: var(--wp--preset--color--white);
+ color: var(--wp--preset--color--charcoal-1);
+ font-weight: 500;
+ }
+ }
+
+ &__panel {
+ display: none;
+ border: 1px solid var(--wp--custom--color--border);
+ border-radius: 0 2px 2px 2px;
+
+ &.is-active {
+ display: block;
+ }
+
+ iframe {
+ width: 100%;
+ height: 520px;
+ border: none;
+ display: block;
+ }
+ }
+}
+
+.wporg-activity-kit-no-pdf {
+ color: var(--wp--preset--color--charcoal-3);
+ font-style: italic;
+}
+
+// Feedback strip — appears below PDF preview tabs, above post content.
+.wporg-activity-kit-feedback-strip {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 12px;
+ padding: 14px 20px;
+ margin-bottom: 44px;
+ background: #f6f7f7;
+ border: 1px solid #d9d9d9;
+ border-radius: 2px;
+
+ &__prompt {
+ font-size: var(--wp--preset--font-size--small);
+ color: var(--wp--preset--color--charcoal-2);
+ margin: 0;
+ }
+
+ &__btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 7px 18px;
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--wp--preset--color--blueberry-1);
+ background: var(--wp--preset--color--white);
+ border: 1px solid var(--wp--preset--color--blueberry-1);
+ border-radius: 2px;
+ text-decoration: none;
+ white-space: nowrap;
+ transition: background 0.1s ease, color 0.1s ease;
+
+ svg {
+ flex-shrink: 0;
+ }
+
+ &:hover {
+ background: var(--wp--preset--color--blueberry-1);
+ color: var(--wp--preset--color--white);
+ text-decoration: none;
+ }
+
+ &:focus-visible {
+ background: var(--wp--preset--color--blueberry-1);
+ color: var(--wp--preset--color--white);
+ outline: 2px solid var(--wp--preset--color--blueberry-1);
+ outline-offset: 2px;
+ text-decoration: none;
+ }
+ }
+}
+
+// "What's included" grid
+.wporg-activity-kit-included {
+ margin-bottom: var(--wp--preset--spacing--40);
+
+ h2 {
+ font-size: var(--wp--preset--font-size--large);
+ font-family: var(--wp--preset--font-family--eb-garamond);
+ margin-bottom: var(--wp--preset--spacing--30);
+ }
+
+ &__grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
+ gap: var(--wp--preset--spacing--30);
+ }
+
+ &__card {
+ border: 1px solid var(--wp--custom--color--border);
+ border-radius: 2px;
+ padding: var(--wp--preset--spacing--30);
+ display: flex;
+ flex-direction: row;
+ gap: var(--wp--preset--spacing--20);
+ align-items: flex-start;
+ background: var(--wp--preset--color--white);
+ }
+
+ &__card-body {
+ flex: 1;
+ min-width: 0;
+
+ h3 {
+ font-size: var(--wp--preset--font-size--normal);
+ font-weight: 600;
+ margin: 0 0 6px;
+ line-height: 1.4;
+ }
+
+ p {
+ font-size: var(--wp--preset--font-size--small);
+ color: var(--wp--preset--color--charcoal-2);
+ margin: 0;
+ line-height: 1.5;
+ }
+ }
+
+ &__icon {
+ flex-shrink: 0;
+ margin-top: 3px;
+ color: var(--wp--preset--color--blueberry-1);
+ display: flex;
+ }
+
+ // Higher-specificity override for parent theme's h3 margin-top rule.
+ .wporg-activity-kit-included__card-body h3 {
+ margin-top: 0;
+ }
+}
+
+// "Download this kit" box at bottom
+.wporg-activity-kit-download-box {
+ border: 1px solid var(--wp--custom--color--border);
+ border-radius: 2px;
+ padding: 32px;
+ text-align: center;
+ margin-top: var(--wp--preset--spacing--40);
+
+ h2 {
+ font-size: var(--wp--preset--font-size--large);
+ font-family: var(--wp--preset--font-family--eb-garamond);
+ margin-bottom: var(--wp--preset--spacing--20);
+ }
+
+ p {
+ font-size: var(--wp--preset--font-size--normal);
+ color: var(--wp--preset--color--charcoal-1);
+ margin-bottom: var(--wp--preset--spacing--40);
+ line-height: 1.7;
+ }
+
+ &__btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ background: var(--wp--preset--color--blueberry-1);
+ color: var(--wp--preset--color--white) !important;
+ padding: 12px 32px;
+ border-radius: 2px;
+ font-size: var(--wp--preset--font-size--normal);
+ font-weight: 500;
+ text-decoration: none;
+
+ &:hover {
+ background: var(--wp--preset--color--deep-blueberry);
+ text-decoration: none;
+ }
+ }
+
+ &__note {
+ font-size: var(--wp--preset--font-size--small);
+ color: var(--wp--preset--color--charcoal-3);
+ margin: var(--wp--preset--spacing--20) 0 0;
+ }
+}
diff --git a/wp-content/themes/pub/wporg-learn-2024/src/style/style.scss b/wp-content/themes/pub/wporg-learn-2024/src/style/style.scss
index c27657021..8dc54ff23 100644
--- a/wp-content/themes/pub/wporg-learn-2024/src/style/style.scss
+++ b/wp-content/themes/pub/wporg-learn-2024/src/style/style.scss
@@ -3,6 +3,7 @@
* templates or theme.json settings.
*/
+@import "activity-kit";
@import "card-grid";
@import "jetpack";
@import "playground";
diff --git a/wp-content/themes/pub/wporg-learn-2024/templates/archive-activity_kit.html b/wp-content/themes/pub/wporg-learn-2024/templates/archive-activity_kit.html
new file mode 100644
index 000000000..c6eb7db8f
--- /dev/null
+++ b/wp-content/themes/pub/wporg-learn-2024/templates/archive-activity_kit.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/wp-content/themes/pub/wporg-learn-2024/templates/single-activity_kit.html b/wp-content/themes/pub/wporg-learn-2024/templates/single-activity_kit.html
new file mode 100644
index 000000000..4dc17b285
--- /dev/null
+++ b/wp-content/themes/pub/wporg-learn-2024/templates/single-activity_kit.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+