Modernize to .gjs Glimmer component with renderInOutlet pattern

- Replace plugin outlet connectors with modern .gjs Glimmer component
- Use api.renderInOutlet() instead of connector templates (official pattern)
- Add service injection (@service router, @service site)
- Implement lifecycle hooks with didInsert/willDestroy modifiers
- Use native SearchMenu component integration
- Remove old connector directory structure
- Update CSS to target outlet wrapper classes
- Simplify API initializer to 10 lines (from 112 lines)
- Add route-based display logic with router service
- Direct settings access without this.theme wrapper

This follows the official discourse-search-banner implementation pattern.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
gabeszm 2025-10-16 17:16:53 +02:00
parent 62d97ffdc9
commit c683bc82a9
7 changed files with 173 additions and 378 deletions

View file

@ -15,6 +15,11 @@
border-radius: 4px; border-radius: 4px;
} }
.below-site-header-outlet,
.above-main-container-outlet {
display: block;
}
@media (min-width: 800px) { @media (min-width: 800px) {
.welcome-card { .welcome-card {
max-width: 1500px; max-width: 1500px;
@ -22,7 +27,7 @@
margin: 0 auto; margin: 0 auto;
} }
.below-site-header.welcome-card { .below-site-header-outlet .welcome-card {
max-width: calc(var(--d-sidebar-width, 0px) + var(--d-max-width, 1110px)); max-width: calc(var(--d-sidebar-width, 0px) + var(--d-max-width, 1110px));
} }
} }

View file

@ -1,112 +1,10 @@
import { apiInitializer } from "discourse/lib/api"; import { apiInitializer } from "discourse/lib/api";
import WelcomeBanner from "../components/welcome-banner";
export default apiInitializer("1.8.0", (api) => { export default apiInitializer("1.8.0", (api) => {
api.onPageChange((url) => { const outletName = settings.banner_position === "below_header"
const welcomeCard = document.querySelector(".welcome-card"); ? "below-site-header"
if (!welcomeCard) return; : "above-main-container";
const showOnPages = settings.show_on_pages || "homepage_only"; api.renderInOutlet(outletName, WelcomeBanner);
if (showOnPages === "all_pages") {
// Show on all pages
welcomeCard.style.display = "block";
} else {
// Show only on homepage
if (url === "/") {
welcomeCard.style.display = "block";
} else {
welcomeCard.style.display = "none";
}
}
});
const isMobileView = () => document.body.classList.contains("mobile-view");
// Render search input with SearchMenu integration
function renderSearchInput() {
// Check if search is enabled in settings (default to true if not set)
const searchEnabled = settings.enable_hero_search !== false;
if (isMobileView()) return;
// Try both possible positions for the banner
const container = document.querySelector(
".welcome-card .search-container"
);
if (!container) return;
// Idempotent: exit if already created
if (container.querySelector(".search-input")) return;
const wrapper = document.createElement("div");
wrapper.className = "search-wrapper";
wrapper.setAttribute("role", "search");
const input = document.createElement("input");
input.type = "text";
input.className = "search-input";
input.placeholder = settings.search_placeholder || "Keresés a fórumon…";
input.setAttribute("aria-label", "Search");
// Open and sync SearchMenu
const openSearchMenu = () => {
try {
api.openSearchMenu({
anchorElement: input,
mobileMode: false,
});
} catch (e) {
// Fallback if method not available
if (console?.debug) {
console.debug("SearchMenu API not available:", e);
}
}
};
const syncMenuQuery = () => {
const menuInput = document.querySelector(".search-menu input.search-query");
if (menuInput && menuInput !== input) {
menuInput.value = input.value;
menuInput.dispatchEvent(new Event("input", { bubbles: true }));
}
};
const handleSearchInteraction = () => {
openSearchMenu();
syncMenuQuery();
};
input.addEventListener("focus", handleSearchInteraction);
input.addEventListener("input", handleSearchInteraction);
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
const q = input.value.trim();
if (!q) return;
const form = document.querySelector(".search-menu form");
const menuInput = document.querySelector(".search-menu input.search-query");
if (form && menuInput) {
menuInput.value = q;
form.dispatchEvent(new Event("submit", { bubbles: true }));
} else {
window.location.assign(`/search?q=${encodeURIComponent(q)}`);
}
}
});
wrapper.appendChild(input);
container.appendChild(wrapper);
}
const apply = () => {
renderSearchInput();
};
api.onAppEvent("page:changed", () => {
requestAnimationFrame(apply);
});
// Initial application
requestAnimationFrame(apply);
}); });

View file

@ -0,0 +1,162 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import { on } from "@ember/modifier";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
import SearchMenu from "discourse/components/search-menu";
export default class WelcomeBanner extends Component {
@service router;
@service site;
get displayForRoute() {
const showOnPages = settings.show_on_pages || "homepage_only";
const currentRoute = this.router.currentRouteName;
if (showOnPages === "all_pages") {
return true;
}
// Show only on homepage
return currentRoute === "discovery.latest" || currentRoute === "discovery.categories";
}
get shouldDisplay() {
return settings.enable_welcome_banner && this.displayForRoute;
}
get isMobile() {
return this.site.mobileView;
}
didInsert() {
document.documentElement.classList.add("display-welcome-banner");
}
willDestroy() {
document.documentElement.classList.remove("display-welcome-banner");
}
openSearchMenu(event) {
event.preventDefault();
// Search menu will be handled by Discourse's native component
}
<template>
{{#if this.shouldDisplay}}
<section class={{concat settings.banner_position "-outlet"}} {{didInsert this.didInsert}} {{willDestroy this.willDestroy}}>
<div class="welcome-card">
<div class="hero-section">
<div class="hero-deco"></div>
<div class="hero-content">
<h2 class="hero-title">
{{htmlSafe settings.hero_title_html}}
</h2>
<div class="hero-description">
{{htmlSafe settings.hero_content_html}}
</div>
{{#if (and settings.enable_hero_search (not this.isMobile))}}
<div class="search-container" role="search">
<SearchMenu @searchInputId="welcome-banner-search" />
</div>
{{/if}}
</div>
</div>
<div class="cta-section">
<ul class="cta-list">
{{#if settings.cta_card_1_enabled}}
<li>
<a href={{settings.cta_card_1_url}} class="cta-card" aria-label={{settings.cta_card_1_title}}>
<div class="cta-icon">
{{#if (eq settings.cta_card_1_icon_type "font_awesome")}}
<i class="fas fa-{{settings.cta_card_1_icon_font_awesome}}"></i>
{{else}}
{{settings.cta_card_1_icon_emoji}}
{{/if}}
</div>
<div class="cta-content">
<span class="cta-title">{{settings.cta_card_1_title}}</span>
<span class="cta-desc">{{settings.cta_card_1_description}}</span>
<span class="cta-cta">
<i class="fas fa-angle-double-right"></i>
{{settings.cta_card_1_cta_text}}
</span>
</div>
</a>
</li>
{{/if}}
{{#if settings.cta_card_2_enabled}}
<li>
<a href={{settings.cta_card_2_url}} class="cta-card" aria-label={{settings.cta_card_2_title}}>
<div class="cta-icon">
{{#if (eq settings.cta_card_2_icon_type "font_awesome")}}
<i class="fas fa-{{settings.cta_card_2_icon_font_awesome}}"></i>
{{else}}
{{settings.cta_card_2_icon_emoji}}
{{/if}}
</div>
<div class="cta-content">
<span class="cta-title">{{settings.cta_card_2_title}}</span>
<span class="cta-desc">{{settings.cta_card_2_description}}</span>
<span class="cta-cta">
<i class="fas fa-angle-double-right"></i>
{{settings.cta_card_2_cta_text}}
</span>
</div>
</a>
</li>
{{/if}}
{{#if settings.cta_card_3_enabled}}
<li>
<a href={{settings.cta_card_3_url}} class="cta-card" aria-label={{settings.cta_card_3_title}}>
<div class="cta-icon">
{{#if (eq settings.cta_card_3_icon_type "font_awesome")}}
<i class="fas fa-{{settings.cta_card_3_icon_font_awesome}}"></i>
{{else}}
{{settings.cta_card_3_icon_emoji}}
{{/if}}
</div>
<div class="cta-content">
<span class="cta-title">{{settings.cta_card_3_title}}</span>
<span class="cta-desc">{{settings.cta_card_3_description}}</span>
<span class="cta-cta">
<i class="fas fa-angle-double-right"></i>
{{settings.cta_card_3_cta_text}}
</span>
</div>
</a>
</li>
{{/if}}
{{#if settings.cta_card_4_enabled}}
<li>
<a href={{settings.cta_card_4_url}} class="cta-card" aria-label={{settings.cta_card_4_title}}>
<div class="cta-icon">
{{#if (eq settings.cta_card_4_icon_type "font_awesome")}}
<i class="fas fa-{{settings.cta_card_4_icon_font_awesome}}"></i>
{{else}}
{{settings.cta_card_4_icon_emoji}}
{{/if}}
</div>
<div class="cta-content">
<span class="cta-title">{{settings.cta_card_4_title}}</span>
<span class="cta-desc">{{settings.cta_card_4_description}}</span>
<span class="cta-cta">
<i class="fas fa-angle-double-right"></i>
{{settings.cta_card_4_cta_text}}
</span>
</div>
</a>
</li>
{{/if}}
</ul>
</div>
</div>
</section>
{{/if}}
</template>
}

View file

@ -1,128 +0,0 @@
{{#if this.theme.enable_welcome_banner}}
{{#unless (eq this.theme.banner_position "below_header")}}
<section class="above-main-outlet welcome-card">
<div class="hero-section">
<div class="hero-deco"></div>
<div class="hero-content">
<h2 class="hero-title">
{{{this.theme.hero_title_html}}}
</h2>
<div class="hero-description">
{{{this.theme.hero_content_html}}}
</div>
{{#if this.theme.enable_hero_search}}
<div class="search-container" aria-label="Search"></div>
{{/if}}
</div>
</div>
<div class="cta-section">
<ul class="cta-list">
{{#if this.theme.cta_card_1_enabled}}
<li>
<a
href="{{this.theme.cta_card_1_url}}"
class="cta-card"
aria-label="{{this.theme.cta_card_1_title}}"
>
<div class="cta-icon">
{{#if (eq this.theme.cta_card_1_icon_type "font_awesome")}}
<i class="fas fa-{{this.theme.cta_card_1_icon_font_awesome}}"></i>
{{else}}
{{this.theme.cta_card_1_icon_emoji}}
{{/if}}
</div>
<div class="cta-content">
<span class="cta-title">{{this.theme.cta_card_1_title}}</span>
<span class="cta-desc">{{this.theme.cta_card_1_description}}</span>
<span class="cta-cta">
<i class="fas fa-angle-double-right"></i>
{{this.theme.cta_card_1_cta_text}}
</span>
</div>
</a>
</li>
{{/if}}
{{#if this.theme.cta_card_2_enabled}}
<li>
<a
href="{{this.theme.cta_card_2_url}}"
class="cta-card"
aria-label="{{this.theme.cta_card_2_title}}"
>
<div class="cta-icon">
{{#if (eq this.theme.cta_card_2_icon_type "font_awesome")}}
<i class="fas fa-{{this.theme.cta_card_2_icon_font_awesome}}"></i>
{{else}}
{{this.theme.cta_card_2_icon_emoji}}
{{/if}}
</div>
<div class="cta-content">
<span class="cta-title">{{this.theme.cta_card_2_title}}</span>
<span class="cta-desc">{{this.theme.cta_card_2_description}}</span>
<span class="cta-cta">
<i class="fas fa-angle-double-right"></i>
{{this.theme.cta_card_2_cta_text}}
</span>
</div>
</a>
</li>
{{/if}}
{{#if this.theme.cta_card_3_enabled}}
<li>
<a
href="{{this.theme.cta_card_3_url}}"
class="cta-card"
aria-label="{{this.theme.cta_card_3_title}}"
>
<div class="cta-icon">
{{#if (eq this.theme.cta_card_3_icon_type "font_awesome")}}
<i class="fas fa-{{this.theme.cta_card_3_icon_font_awesome}}"></i>
{{else}}
{{this.theme.cta_card_3_icon_emoji}}
{{/if}}
</div>
<div class="cta-content">
<span class="cta-title">{{this.theme.cta_card_3_title}}</span>
<span class="cta-desc">{{this.theme.cta_card_3_description}}</span>
<span class="cta-cta">
<i class="fas fa-angle-double-right"></i>
{{this.theme.cta_card_3_cta_text}}
</span>
</div>
</a>
</li>
{{/if}}
{{#if this.theme.cta_card_4_enabled}}
<li>
<a
href="{{this.theme.cta_card_4_url}}"
class="cta-card"
aria-label="{{this.theme.cta_card_4_title}}"
>
<div class="cta-icon">
{{#if (eq this.theme.cta_card_4_icon_type "font_awesome")}}
<i class="fas fa-{{this.theme.cta_card_4_icon_font_awesome}}"></i>
{{else}}
{{this.theme.cta_card_4_icon_emoji}}
{{/if}}
</div>
<div class="cta-content">
<span class="cta-title">{{this.theme.cta_card_4_title}}</span>
<span class="cta-desc">{{this.theme.cta_card_4_description}}</span>
<span class="cta-cta">
<i class="fas fa-angle-double-right"></i>
{{this.theme.cta_card_4_cta_text}}
</span>
</div>
</a>
</li>
{{/if}}
</ul>
</div>
</section>
{{/unless}}
{{/if}}

View file

@ -1,7 +0,0 @@
import Component from "@glimmer/component";
export default class WelcomeBanner extends Component {
get theme() {
return settings;
}
}

View file

@ -1,128 +0,0 @@
{{#if this.theme.enable_welcome_banner}}
{{#if (eq this.theme.banner_position "below_header")}}
<section class="below-site-header welcome-card">
<div class="hero-section">
<div class="hero-deco"></div>
<div class="hero-content">
<h2 class="hero-title">
{{{this.theme.hero_title_html}}}
</h2>
<div class="hero-description">
{{{this.theme.hero_content_html}}}
</div>
{{#if this.theme.enable_hero_search}}
<div class="search-container" aria-label="Search"></div>
{{/if}}
</div>
</div>
<div class="cta-section">
<ul class="cta-list">
{{#if this.theme.cta_card_1_enabled}}
<li>
<a
href="{{this.theme.cta_card_1_url}}"
class="cta-card"
aria-label="{{this.theme.cta_card_1_title}}"
>
<div class="cta-icon">
{{#if (eq this.theme.cta_card_1_icon_type "font_awesome")}}
<i class="fas fa-{{this.theme.cta_card_1_icon_font_awesome}}"></i>
{{else}}
{{this.theme.cta_card_1_icon_emoji}}
{{/if}}
</div>
<div class="cta-content">
<span class="cta-title">{{this.theme.cta_card_1_title}}</span>
<span class="cta-desc">{{this.theme.cta_card_1_description}}</span>
<span class="cta-cta">
<i class="fas fa-angle-double-right"></i>
{{this.theme.cta_card_1_cta_text}}
</span>
</div>
</a>
</li>
{{/if}}
{{#if this.theme.cta_card_2_enabled}}
<li>
<a
href="{{this.theme.cta_card_2_url}}"
class="cta-card"
aria-label="{{this.theme.cta_card_2_title}}"
>
<div class="cta-icon">
{{#if (eq this.theme.cta_card_2_icon_type "font_awesome")}}
<i class="fas fa-{{this.theme.cta_card_2_icon_font_awesome}}"></i>
{{else}}
{{this.theme.cta_card_2_icon_emoji}}
{{/if}}
</div>
<div class="cta-content">
<span class="cta-title">{{this.theme.cta_card_2_title}}</span>
<span class="cta-desc">{{this.theme.cta_card_2_description}}</span>
<span class="cta-cta">
<i class="fas fa-angle-double-right"></i>
{{this.theme.cta_card_2_cta_text}}
</span>
</div>
</a>
</li>
{{/if}}
{{#if this.theme.cta_card_3_enabled}}
<li>
<a
href="{{this.theme.cta_card_3_url}}"
class="cta-card"
aria-label="{{this.theme.cta_card_3_title}}"
>
<div class="cta-icon">
{{#if (eq this.theme.cta_card_3_icon_type "font_awesome")}}
<i class="fas fa-{{this.theme.cta_card_3_icon_font_awesome}}"></i>
{{else}}
{{this.theme.cta_card_3_icon_emoji}}
{{/if}}
</div>
<div class="cta-content">
<span class="cta-title">{{this.theme.cta_card_3_title}}</span>
<span class="cta-desc">{{this.theme.cta_card_3_description}}</span>
<span class="cta-cta">
<i class="fas fa-angle-double-right"></i>
{{this.theme.cta_card_3_cta_text}}
</span>
</div>
</a>
</li>
{{/if}}
{{#if this.theme.cta_card_4_enabled}}
<li>
<a
href="{{this.theme.cta_card_4_url}}"
class="cta-card"
aria-label="{{this.theme.cta_card_4_title}}"
>
<div class="cta-icon">
{{#if (eq this.theme.cta_card_4_icon_type "font_awesome")}}
<i class="fas fa-{{this.theme.cta_card_4_icon_font_awesome}}"></i>
{{else}}
{{this.theme.cta_card_4_icon_emoji}}
{{/if}}
</div>
<div class="cta-content">
<span class="cta-title">{{this.theme.cta_card_4_title}}</span>
<span class="cta-desc">{{this.theme.cta_card_4_description}}</span>
<span class="cta-cta">
<i class="fas fa-angle-double-right"></i>
{{this.theme.cta_card_4_cta_text}}
</span>
</div>
</a>
</li>
{{/if}}
</ul>
</div>
</section>
{{/if}}
{{/if}}

View file

@ -1,7 +0,0 @@
import Component from "@glimmer/component";
export default class WelcomeBannerHeader extends Component {
get theme() {
return settings;
}
}