loader = new Loader(); } public static function get_instance(): static { if ( null === static::$instance ) { static::$instance = new static(); } return static::$instance; } public function run(): void { // Plugin updates don't fire the activation hook: bring the schema up to date // on the first admin or cron request after an update. if ( ( is_admin() || wp_doing_cron() ) && version_compare( (string) get_option( 'academiccrew_db_version', '0' ), '1.10', '<' ) ) { add_action( 'init', [ Activator::class, 'upgrade' ], 20 ); } $this->register_shared_assets(); $this->register_hooks(); $this->loader->run(); } private function register_shared_assets(): void { add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_frontend_css' ], 99 ); // Priority 5 so handles are registered before blocks fire at default priority 10. add_action( 'init', [ $this, 'register_plugin_styles' ], 5 ); // Priority 100 — after Avada has had a chance to register its meta boxes. add_action( 'add_meta_boxes', [ $this, 'remove_avada_meta_boxes_from_cpt_screens' ], 100 ); // Open Fusion-built content in the builder instead of showing raw shortcodes. add_action( 'load-post.php', [ $this, 'activate_builder_for_fusion_content' ] ); } /** * Registers all plugin CSS handles with wp_register_style() so that both * register_block_type() (via editor_style_handles) and enqueue_frontend_css() * below can reference them. Does NOT enqueue. */ public function register_plugin_styles(): void { $v = ACADEMICCREW_VERSION; $u = ACADEMICCREW_URL . 'assets/css/'; $tabler = ACADEMICCREW_URL . 'assets/fonts/tabler-icons/tabler-icons.min.css'; wp_register_style( 'acc-tabler-icons', $tabler, [], $v ); wp_register_style( 'acc-system', $u . 'system.css', [], $v ); wp_register_style( 'acc-portrait', $u . 'portrait.css', [ 'acc-system' ], $v ); wp_register_style( 'acc-page-hero', $u . 'page-hero.css', [ 'acc-system' ], $v ); wp_register_style( 'acc-member-profile', $u . 'member-profile.css', [ 'acc-system' ], $v ); wp_register_style( 'acc-course-profile', $u . 'course-profile.css', [ 'acc-system' ], $v ); wp_register_style( 'acc-members-archive', $u . 'members-archive.css', [ 'acc-system', 'acc-portrait' ], $v ); wp_register_style( 'acc-courses-archive', $u . 'courses-archive.css', [ 'acc-system', 'acc-page-hero' ], $v ); wp_register_style( 'acc-publications-archive', $u . 'publications-archive.css', [ 'acc-system', 'acc-page-hero' ], $v ); wp_register_style( 'acc-featured-projects', $u . 'featured-projects.css', [ 'acc-system' ], $v ); wp_register_style( 'acc-featured-courses', $u . 'featured-courses.css', [ 'acc-system' ], $v ); // Inline-only handle (no file) for the Settings → Appearance overrides. // Depends only on 'acc-system' — always enqueued in the same // needs_any-gated code path as this one (see enqueue_frontend_css()) — // never on any of the conditionally-loaded archive/profile handles: // listing an unenqueued-but-registered handle as a dependency makes // WP print it anyway (defeating the whole point of loading only what // a given page needs), and listing an unregistered one makes WP // silently drop this handle from printing altogether. It is always // enqueued last in enqueue_frontend_css(), which — with no // conditional dependency to reorder it — is what keeps it printed // last and winning the cascade for the shared --acc-* design tokens. wp_register_style( 'acc-appearance-overrides', false, [ 'acc-system' ], $v ); } /** * Enqueues the plugin's frontend CSS handles on wp_enqueue_scripts, based on * per-page needs detected from post-meta flags set by the page generators * (always present on plugin-managed pages) and, for standalone shortcode/ * block placements, has_shortcode()/has_block() checks on the current post. */ public function enqueue_frontend_css(): void { if ( is_admin() ) { return; } $page_id = get_queried_object_id(); if ( ! $page_id ) { return; } $meta = static function ( string $key ) use ( $page_id ): bool { return (bool) get_post_meta( $page_id, $key, true ); }; $need_member_profile = $meta( '_academiccrew_is_member_page' ); $need_course_profile = $meta( '_academiccrew_is_course_page' ); $need_members_archive = $meta( '_academiccrew_is_members_archive' ); $need_courses_archive = $meta( '_academiccrew_is_courses_archive' ); $need_publications_archive = $meta( '_academiccrew_is_publications_archive' ); $need_projects_archive = $meta( '_academiccrew_is_collaborations_archive' ); $need_featured_projects = false; $need_featured_courses = false; $need_featured_people = false; // Single project page: auto-generated WP page OR CPT direct URL. $need_single_project = $meta( '_academiccrew_is_collaboration_page' ) || is_singular( 'acc_collaboration' ); // Project/collaboration pages use member-profile.css (shared chip + header styles). if ( ! $need_member_profile && ( $need_single_project || $need_projects_archive ) ) { $need_member_profile = true; } // Standalone shortcodes on any arbitrary page if ( ! $need_member_profile && ! $need_course_profile && ! $need_members_archive && ! $need_courses_archive ) { $post = get_queried_object(); if ( $post instanceof \WP_Post ) { if ( has_shortcode( $post->post_content, 'academiccrew_publications_archive' ) ) { $need_publications_archive = true; } if ( has_shortcode( $post->post_content, 'academiccrew_projects' ) || has_shortcode( $post->post_content, 'academiccrew_collaborations' ) ) { $need_projects_archive = true; $need_member_profile = true; } if ( has_shortcode( $post->post_content, 'academiccrew_publications_list' ) || has_shortcode( $post->post_content, 'academiccrew_member_card' ) ) { $need_member_profile = true; } if ( has_shortcode( $post->post_content, 'academiccrew_members_archive' ) ) { $need_members_archive = true; } if ( has_shortcode( $post->post_content, 'academiccrew_courses_archive' ) ) { $need_courses_archive = true; } } } // Featured widgets: can appear on ANY page — detect unconditionally. $any_post = get_queried_object(); if ( $any_post instanceof \WP_Post ) { if ( ! $need_featured_projects ) { // Recent Projects reuses the Featured Projects index-strip markup/CSS. $need_featured_projects = has_shortcode( $any_post->post_content, 'acc_featured_projects' ) || has_shortcode( $any_post->post_content, 'minds_featured_projects' ) || has_shortcode( $any_post->post_content, 'acc_featured_strip' ) || has_shortcode( $any_post->post_content, 'acc_recent_projects' ) || ( function_exists( 'has_block' ) && ( has_block( 'academiccrew/featured-projects', $any_post ) || has_block( 'academiccrew/recent-projects', $any_post ) ) ); } if ( ! $need_featured_courses ) { // Recent Courses reuses the Featured Courses card-grid markup/CSS. $need_featured_courses = has_shortcode( $any_post->post_content, 'acc_featured_courses' ) || has_shortcode( $any_post->post_content, 'acc_recent_courses' ) || ( function_exists( 'has_block' ) && ( has_block( 'academiccrew/featured-courses', $any_post ) || has_block( 'academiccrew/recent-courses', $any_post ) ) ); } if ( ! $need_member_profile ) { $need_member_profile = has_shortcode( $any_post->post_content, 'acc_recent_publications' ) || has_shortcode( $any_post->post_content, 'acc_featured_publications' ) || ( function_exists( 'has_block' ) && ( has_block( 'academiccrew/recent-publications', $any_post ) || has_block( 'academiccrew/featured-publications', $any_post ) ) ); } if ( ! $need_featured_people ) { $need_featured_people = has_shortcode( $any_post->post_content, 'acc_featured_people' ) || ( function_exists( 'has_block' ) && has_block( 'academiccrew/featured-people', $any_post ) ); } } // Member self-edit form: own stylesheet (edit-form.css), enqueued via the // normal WP queue by the shortcode itself — only needed here so the // Appearance override below still reaches this page. $need_edit_form = $any_post instanceof \WP_Post && has_shortcode( $any_post->post_content, 'academiccrew_member_edit_form' ); $needs_any = $need_member_profile || $need_course_profile || $need_members_archive || $need_courses_archive || $need_publications_archive || $need_projects_archive || $need_featured_projects || $need_featured_courses || $need_featured_people || $need_edit_form; if ( ! $needs_any ) { return; } wp_enqueue_style( 'acc-tabler-icons' ); wp_enqueue_style( 'acc-system' ); if ( $need_member_profile || $need_course_profile || $need_members_archive || $need_featured_people ) { wp_enqueue_style( 'acc-portrait' ); } if ( $need_members_archive || $need_courses_archive || $need_publications_archive || $need_projects_archive ) { wp_enqueue_style( 'acc-page-hero' ); } if ( $need_member_profile || $need_course_profile ) { wp_enqueue_style( 'acc-member-profile' ); } if ( $need_course_profile ) { wp_enqueue_style( 'acc-course-profile' ); } if ( $need_members_archive ) { wp_enqueue_style( 'acc-members-archive' ); } if ( $need_courses_archive ) { wp_enqueue_style( 'acc-courses-archive' ); } if ( $need_publications_archive ) { wp_enqueue_style( 'acc-publications-archive' ); } if ( $need_featured_projects ) { wp_enqueue_style( 'acc-featured-projects' ); } if ( $need_featured_courses ) { wp_enqueue_style( 'acc-featured-courses' ); } if ( $need_featured_people ) { wp_enqueue_style( 'acc-members-archive' ); wp_enqueue_style( 'acc-featured-people' ); } // Suppress the theme-generated page title on our own single/archive // pages (Gutenberg / block-theme fallback — Avada is handled // separately via pyre_page_title in CollaborationProfileInjector). $need_hide_title = $need_single_project || $need_course_profile || $need_members_archive || $need_courses_archive || $need_publications_archive; if ( $need_hide_title ) { wp_add_inline_style( 'acc-system', '.entry-title,.page-title,h1.page-title,.wp-block-post-title{display:none}' ); } $this->enqueue_appearance_overrides(); } /** * Enqueues the Settings → Appearance palette/font overrides. * * Always called last within enqueue_frontend_css() so 'acc-appearance-overrides' * is the last plugin style handle added to the queue that request, which — * per the note on its registration in register_plugin_styles() — keeps it * printed last and winning the cascade for the shared --acc-* design tokens. */ private function enqueue_appearance_overrides(): void { $fonts_url = \AcademicCrew\Core\Appearance::google_fonts_url(); if ( $fonts_url ) { wp_enqueue_style( 'acc-appearance-fonts', $fonts_url, [], ACADEMICCREW_VERSION ); } wp_add_inline_style( 'acc-appearance-overrides', wp_strip_all_tags( \AcademicCrew\Core\Appearance::css_overrides() ) ); wp_enqueue_style( 'acc-appearance-overrides' ); } /** * Marks plugin posts whose body is built with Fusion shortcodes as "builder active". * * Avada opens the builder by itself only when `fusion_builder_status` is `active`. * Imported / pasted courses carry [fusion_builder_container] markup without that * flag, so the classic editor showed the raw shortcodes. Runs on the edit screen, * only for the plugin's post types, and only when the markup is really there. */ public function activate_builder_for_fusion_content(): void { $post_id = isset( $_GET['post'] ) ? (int) $_GET['post'] : 0; // phpcs:ignore WordPress.Security.NonceVerification if ( $post_id <= 0 || ! in_array( get_post_type( $post_id ), [ 'academiccrew_member', 'academiccrew_course', 'acc_collaboration' ], true ) ) { return; } if ( ! current_user_can( 'edit_post', $post_id ) || 'blocks' === get_post_meta( $post_id, '_acc_editor', true ) || 'active' === get_post_meta( $post_id, 'fusion_builder_status', true ) ) { return; } if ( false === strpos( (string) get_post_field( 'post_content', $post_id ), '[fusion_builder_container' ) ) { return; } update_post_meta( $post_id, 'fusion_builder_status', 'active' ); } /** * Removes Avada's "Fusion Page Options" meta box from our CPT edit screens. * * Avada registers `pyre_metabox` for every post type. On our CPT screens the * panel is irrelevant (options are set programmatically via page generators) * and its presence pushes the WP Publish button far off-screen. * * Runs at add_meta_boxes priority 100, after Avada has already registered. */ public function remove_avada_meta_boxes_from_cpt_screens(): void { $our_cpts = [ 'academiccrew_member', 'academiccrew_course', 'acc_collaboration' ]; // Known Avada / Fusion Builder meta box IDs. $avada_boxes = [ 'pyre_metabox', // Avada Page Options (all Avada versions) 'avada_page_options', // Alternative ID used in some builds 'fusion_builder_options', // Fusion Builder options panel ]; foreach ( $our_cpts as $cpt ) { foreach ( $avada_boxes as $box_id ) { remove_meta_box( $box_id, $cpt, 'normal' ); remove_meta_box( $box_id, $cpt, 'advanced' ); remove_meta_box( $box_id, $cpt, 'side' ); } } } /** * Instantiates all modules and calls their register( Loader ) method. * * Each phase appends to the $phase_modules array below. * The optional ...$extra_modules parameter lets callers inject additional * modules without subclassing (used in tests and future phases). * * Phases 3-7 placeholders show what will be added next. */ public function register_hooks( object ...$extra_modules ): void { $phase_modules = [ // Phase 2 — CPTs, meta boxes, roles new \AcademicCrew\CPT\MemberCPT(), new \AcademicCrew\CPT\CourseCPT(), new \AcademicCrew\CPT\PublicationCPT(), new \AcademicCrew\CPT\CollaborationCPT(), new \AcademicCrew\Meta\MemberMeta(), new \AcademicCrew\Meta\CourseMeta(), new \AcademicCrew\Meta\PublicationMeta(), new \AcademicCrew\Meta\CollaborationMeta(), new \AcademicCrew\Roles\RoleManager(), // Phase 3 — Page generators and locked injectors: new \AcademicCrew\PageGenerator\NavMenuGuard(), new \AcademicCrew\PageGenerator\MemberPageGenerator(), new \AcademicCrew\PageGenerator\CoursePageGenerator(), new \AcademicCrew\PageGenerator\CollaborationPageGenerator(), new \AcademicCrew\PageGenerator\MembersArchivePageGenerator(), new \AcademicCrew\PageGenerator\CoursesArchivePageGenerator(), new \AcademicCrew\PageGenerator\CollaborationsArchivePageGenerator(), new \AcademicCrew\PageGenerator\PublicationsArchivePageGenerator(), new \AcademicCrew\PageGenerator\MemberProfileInjector(), new \AcademicCrew\PageGenerator\CourseProfileInjector(), new \AcademicCrew\PageGenerator\CollaborationProfileInjector(), new \AcademicCrew\Roles\MemberPageCapabilities(), new \AcademicCrew\Roles\CoursePageCapabilities(), // Phase 4 — Shortcodes and REST endpoints: new \AcademicCrew\Shortcodes\MembersArchiveShortcode(), new \AcademicCrew\Shortcodes\CoursesArchiveShortcode(), new \AcademicCrew\Shortcodes\PublicationsListShortcode(), new \AcademicCrew\Shortcodes\PublicationsArchiveShortcode(), new \AcademicCrew\Shortcodes\MemberCardShortcode(), new \AcademicCrew\Shortcodes\CollaborationsShortcode(), new \AcademicCrew\Shortcodes\MemberEditFormShortcode(), new \AcademicCrew\Shortcodes\ResearchAreasCloudShortcode(), new \AcademicCrew\Shortcodes\FeaturedProjectsStripShortcode(), new \AcademicCrew\Shortcodes\RecentProjectsStripShortcode(), new \AcademicCrew\Shortcodes\StatCountersShortcode(), new \AcademicCrew\REST\MembersArchiveController(), new \AcademicCrew\REST\CoursesArchiveController(), new \AcademicCrew\REST\MemberPublicationsController(), new \AcademicCrew\REST\PublicationsArchiveController(), // Phase 5 — Publications sync (Pro only: unlimited automated sync // from ORCID/Scholar/IRIS/CrossRef, including its cron jobs and // the cross-linking of synced publications to members/courses/projects): ...( \AcademicCrew\Core\Freemius::is_pro() ? [ new \AcademicCrew\Sync\SyncScheduler() ] : [] ), // Phase 6 — Admin UI. Free: manage Members/Courses/Projects/ // Publications by hand, no limit on how many. Pro adds CSV/Web // import, full customization (Appearance, Roles & Classes, // archive page text, academic year settings) and the sync log — // none of that code may ship in the wordpress.org build, so it's // gated here the same way as Sync/Avada/Elementor below. new \AcademicCrew\Admin\AdminMenu(), new \AcademicCrew\Admin\PublicationsPage(), new \AcademicCrew\Admin\PubTagsPage(), new \AcademicCrew\Admin\AssetLoader(), new \AcademicCrew\Admin\MetaBoxLayout(), new \AcademicCrew\Admin\EditorSwitcher(), ...( \AcademicCrew\Core\Freemius::is_pro() ? [ new \AcademicCrew\Admin\SettingsPage() ] : [] ), ...( \AcademicCrew\Core\Freemius::is_pro() ? [ new \AcademicCrew\Admin\TaxonomiesPage() ] : [] ), ...( \AcademicCrew\Core\Freemius::is_pro() ? [ new \AcademicCrew\Admin\SyncLogPage() ] : [] ), ...( \AcademicCrew\Core\Freemius::is_pro() ? [ new \AcademicCrew\Admin\ImportPage() ] : [] ), // Phase 7 — Avada + Elementor (Pro only) + Gutenberg (always free) integration. // TemplateHierarchy stays free — it's the any-theme rendering fallback, // not a page-builder extra. ...( \AcademicCrew\Core\Freemius::is_pro() ? [ new \AcademicCrew\Avada\FusionElementsRegistrar() ] : [] ), new \AcademicCrew\Avada\TemplateHierarchy(), ...( \AcademicCrew\Core\Freemius::is_pro() ? [ new \AcademicCrew\Elementor\ElementorIntegration() ] : [] ), new \AcademicCrew\Blocks\PublicationsBlock(), new \AcademicCrew\Blocks\MemberCardBlock(), new \AcademicCrew\Blocks\CoursesArchiveBlock(), new \AcademicCrew\Blocks\FeaturedProjectsBlock(), new \AcademicCrew\Blocks\FeaturedCoursesBlock(), new \AcademicCrew\Blocks\RecentProjectsBlock(), new \AcademicCrew\Blocks\RecentCoursesBlock(), new \AcademicCrew\Blocks\RecentPublicationsBlock(), new \AcademicCrew\Blocks\FeaturedPublicationsBlock(), new \AcademicCrew\Blocks\FeaturedPeopleBlock(), new \AcademicCrew\Blocks\StatCountersBlock(), ]; foreach ( array_merge( $phase_modules, $extra_modules ) as $module ) { if ( method_exists( $module, 'register' ) ) { $module->register( $this->loader ); } } } public function get_loader(): Loader { return $this->loader; } }