add_action( 'init', $this, 'register_shortcode' ); $loader->add_action( 'init', $this, 'register_block' ); } public function register_shortcode(): void { add_shortcode( 'acc_recent_courses', [ $this, 'render' ] ); } public function register_block(): void { if ( ! function_exists( 'register_block_type' ) ) { return; } wp_register_script( 'acc-block-recent-courses', false, [ 'wp-blocks', 'wp-element', 'wp-server-side-render', 'wp-block-editor', 'wp-components', 'wp-i18n' ], ACADEMICCREW_VERSION, true ); wp_add_inline_script( 'acc-block-recent-courses', $this->editor_script() ); register_block_type( 'academiccrew/recent-courses', [ 'api_version' => 2, 'title' => __( 'AcademicCrew: Recent Courses', 'academiccrew' ), 'description' => __( 'Grid of the courses currently being taught (current academic year and teaching period), ordered by level.', 'academiccrew' ), 'category' => 'academiccrew', 'icon' => 'clock', 'supports' => [ 'html' => false, 'align' => [ 'wide', 'full' ] ], 'attributes' => [ 'count' => [ 'type' => 'integer', 'default' => 3 ], 'gap' => [ 'type' => 'integer', 'default' => 12 ], 'card_color' => [ 'type' => 'string', 'default' => '#f8f9fb' ], 'max_card_width' => [ 'type' => 'integer', 'default' => 0 ], ], 'render_callback' => [ $this, 'render' ], 'editor_script' => 'acc-block-recent-courses', 'editor_style_handles' => [ 'acc-system', 'acc-featured-courses' ], ] ); } // ------------------------------------------------------------------------- // Render (block callback + shortcode handler) // ------------------------------------------------------------------------- public function render( array $atts ): string { $atts = shortcode_atts( [ 'count' => 3, 'gap' => 12, 'card_color' => '#f8f9fb', 'max_card_width' => 0, ], $atts, 'acc_recent_courses' ); $count = max( 1, min( 12, (int) $atts['count'] ) ); $gap = max( 0, min( 80, (int) $atts['gap'] ) ); $card_color = $this->sanitize_color( (string) $atts['card_color'] ) ?: '#f8f9fb'; $max_card_width = max( 0, (int) $atts['max_card_width'] ); $courses = $this->get_recent_courses( $count ); if ( ! $courses ) { return sprintf( '

%s

', esc_html__( 'No courses are currently being taught this academic year and period.', 'academiccrew' ) ); } $cols = count( $courses ); $css_vars = sprintf( '--acc-fc-cols:%d;--acc-fc-gap:%dpx;--acc-fc-card-bg:%s', $cols, $gap, esc_attr( $card_color ) ); if ( $max_card_width > 0 ) { $css_vars .= sprintf( ';--acc-fc-card-max-w:%dpx', $max_card_width ); } $cards = implode( '', array_map( [ $this, 'render_card' ], $courses ) ); return sprintf( '
%s
', esc_attr( $css_vars ), $cards ); } // ------------------------------------------------------------------------- // Card HTML — identical to Featured Courses. // ------------------------------------------------------------------------- private function render_card( array $course ): string { $level_labels = [ 'bsc' => 'BSc', 'msc' => 'MSc', 'phd' => 'PhD', 'professional' => __( 'Executive', 'academiccrew' ), 'other' => __( 'Other', 'academiccrew' ), ]; $code = $course['code'] ? sprintf( '%s', esc_html( $course['code'] ) ) : ''; $level = $course['level'] ? sprintf( '%s', esc_attr( $course['level'] ), esc_html( $level_labels[ $course['level'] ] ?? strtoupper( $course['level'] ) ) ) : ''; $title_inner = $course['permalink'] ? sprintf( '%s', esc_url( $course['permalink'] ), esc_html( $course['title'] ) ) : esc_html( $course['title'] ); $degree = $course['degree_program'] ? sprintf( '

%s

', esc_html( $course['degree_program'] ) ) : ''; $meta_items = []; if ( $course['credits'] ) { $meta_items[] = $this->meta_item( __( 'CFU', 'academiccrew' ), (string) $course['credits'] ); } if ( $course['period'] ) { $meta_items[] = $this->meta_item( __( 'Period', 'academiccrew' ), 'P' . $course['period'] ); } if ( $course['language'] ) { $meta_items[] = $this->meta_item( __( 'Lang', 'academiccrew' ), $course['language'] ); } if ( $course['lecture_hours'] ) { $meta_items[] = $this->meta_item( __( 'Hours', 'academiccrew' ), $course['lecture_hours'] . 'h' ); } $meta = $meta_items ? sprintf( '
%s
', implode( '', $meta_items ) ) : ''; $cta = $course['permalink'] ? sprintf( '%s ', esc_url( $course['permalink'] ), esc_html__( 'Course details', 'academiccrew' ) ) : ''; return sprintf( '
%s%s

%s

%s%s%s
', $code, $level, $title_inner, $degree, $meta, $cta ); } private function meta_item( string $label, string $value ): string { return sprintf( '
%s
%s
', esc_html( $label ), esc_html( $value ) ); } // ------------------------------------------------------------------------- // Data // ------------------------------------------------------------------------- /** Courses at the current academic year (or later) AND the currently-running teaching period. */ private function get_recent_courses( int $limit ): array { $calc = new AcademicYearCalculator(); $current_ay = $calc->calculate( 1 ); $current_period = $calc->current_period(); $posts = get_posts( [ 'post_type' => 'academiccrew_course', 'post_status' => 'publish', 'posts_per_page' => -1, 'no_found_rows' => true, 'meta_query' => [ 'relation' => 'AND', [ 'key' => '_acc_academic_year', 'value' => $current_ay, 'compare' => '>=', 'type' => 'NUMERIC', ], [ 'key' => '_acc_period', 'value' => $current_period, 'compare' => '=', 'type' => 'NUMERIC', ], ], ] ); if ( ! $posts ) { return []; } $level_order = [ 'bsc' => 0, 'msc' => 1, 'phd' => 2, 'professional' => 3, 'other' => 4, '' => 5 ]; $courses = array_map( function ( \WP_Post $post ) { $codes = (array) ( get_post_meta( $post->ID, '_acc_course_codes', true ) ?: [] ); if ( empty( $codes ) ) { $old = get_post_meta( $post->ID, '_acc_course_code', true ); if ( $old ) { $codes = [ $old ]; } } $page_id = (int) get_post_meta( $post->ID, '_academiccrew_linked_page_id', true ); return [ 'id' => $post->ID, 'title' => get_the_title( $post ), 'permalink' => $page_id ? get_permalink( $page_id ) : get_permalink( $post ), 'code' => $codes[0] ?? '', 'level' => (string) ( get_post_meta( $post->ID, '_acc_level', true ) ?: '' ), 'credits' => (int) ( get_post_meta( $post->ID, '_acc_credits', true ) ?: 0 ), 'degree_program' => (string) ( get_post_meta( $post->ID, '_acc_degree_program', true ) ?: '' ), 'language' => (string) ( get_post_meta( $post->ID, '_acc_language', true ) ?: '' ), 'period' => (int) ( get_post_meta( $post->ID, '_acc_period', true ) ?: 0 ), 'lecture_hours' => (int) ( get_post_meta( $post->ID, '_acc_lecture_hours', true ) ?: 0 ), 'academic_year' => (int) ( get_post_meta( $post->ID, '_acc_academic_year', true ) ?: 0 ), ]; }, $posts ); usort( $courses, static function ( array $a, array $b ) use ( $level_order ): int { if ( $a['academic_year'] !== $b['academic_year'] ) { return $b['academic_year'] - $a['academic_year']; } if ( ( $level_order[ $a['level'] ] ?? 5 ) !== ( $level_order[ $b['level'] ] ?? 5 ) ) { return ( $level_order[ $a['level'] ] ?? 5 ) - ( $level_order[ $b['level'] ] ?? 5 ); } return strcasecmp( $a['title'], $b['title'] ); } ); return array_slice( $courses, 0, $limit ); } private function sanitize_color( string $color ): string { $color = trim( $color ); if ( $hex = sanitize_hex_color( $color ) ) { return $hex; } if ( preg_match( '/^(?:rgba?|hsla?)\([\d\s%.,]+\)$/', $color ) ) { return $color; } return ''; } // ------------------------------------------------------------------------- // Gutenberg editor script // ------------------------------------------------------------------------- private function editor_script(): string { return <<<'JS' (function(blocks, el, SSR, blockEditor, components, i18n) { var __ = i18n.__; var InspectorControls = blockEditor.InspectorControls; var useBlockProps = blockEditor.useBlockProps; var PanelBody = components.PanelBody; var RangeControl = components.RangeControl; var BaseControl = components.BaseControl; blocks.registerBlockType('academiccrew/recent-courses', { edit: function(props) { var attrs = props.attributes; var setA = props.setAttributes; var blockProps = useBlockProps({ style: { position: 'relative' } }); return el('div', blockProps, el(InspectorControls, { key: 'controls' }, el(PanelBody, { title: __('Settings', 'academiccrew'), initialOpen: true }, el(RangeControl, { label: __('Number of courses', 'academiccrew'), value: attrs.count, min: 1, max: 12, step: 1, onChange: function(v) { setA({ count: v }); } }), el(RangeControl, { label: __('Gap between cards (px)', 'academiccrew'), value: attrs.gap, min: 0, max: 80, step: 2, onChange: function(v) { setA({ gap: v }); } }), el(RangeControl, { label: __('Max card width (px, 0 = unlimited)', 'academiccrew'), value: attrs.max_card_width, min: 0, max: 600, step: 10, onChange: function(v) { setA({ max_card_width: v }); } }), el(BaseControl, { label: __('Card background', 'academiccrew'), id: 'acc-rc-card-color' }, el('input', { id: 'acc-rc-card-color', type: 'color', value: attrs.card_color, style: { display: 'block', marginTop: '6px', width: '48px', height: '32px', border: 'none', padding: 0, cursor: 'pointer' }, onChange: function(e) { setA({ card_color: e.target.value }); } }) ) ) ), el('div', { key: 'preview', style: { pointerEvents: 'none' } }, el(SSR, { block: 'academiccrew/recent-courses', attributes: attrs }) ) ); }, save: function() { return null; } }); }(wp.blocks, wp.element.createElement, wp.serverSideRender, wp.blockEditor, wp.components, wp.i18n)); JS; } }