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 @@ + + + +
+ + +
+ + + +
+ + +
+ + +