Admin Menu Sorting

Fix broken admin menu sorting and rearrange frontend ui elements
This commit is contained in:
2026-03-08 00:06:12 -04:00
parent 4f41c0f900
commit 3e5951dd81
7 changed files with 199 additions and 124 deletions

View File

@@ -30,15 +30,18 @@ const TABS = [
"hero_video_url", "hero_video_button_color", "hero_video_blur_on_hover", "hero_video_url", "hero_video_button_color", "hero_video_blur_on_hover",
"hero_bg_dark", "hero_bg_light", "hero_min_height", "hero_border_style", "hero_bg_dark", "hero_bg_light", "hero_min_height", "hero_border_style",
"hero_card_bg_dark", "hero_card_bg_light", "hero_card_opacity", "hero_card_bg_dark", "hero_card_bg_light", "hero_card_opacity",
"contributors_enabled", "contributors_days", "contributors_count" "contributors_enabled", "contributors_title", "contributors_title_enabled",
"contributors_count_label", "contributors_count_label_enabled",
"contributors_days", "contributors_count"
]) ])
}, },
{ {
id: "stats", id: "stats",
label: "Stats", label: "Stats",
settings: new Set([ settings: new Set([
"stats_enabled", "stat_labels_enabled", "stats_enabled", "stat_labels_enabled", "stats_title_enabled",
"stats_title", "stat_icon_color", "stat_icon_bg_color", "stat_icon_shape", "stat_counter_color", "stats_title", "stat_card_style",
"stat_icon_color", "stat_icon_bg_color", "stat_icon_shape", "stat_counter_color",
"stat_members_label", "stat_topics_label", "stat_posts_label", "stat_members_label", "stat_topics_label", "stat_posts_label",
"stat_likes_label", "stat_chats_label", "stat_round_numbers", "stat_likes_label", "stat_chats_label", "stat_round_numbers",
"stats_bg_dark", "stats_bg_light", "stats_min_height", "stats_border_style" "stats_bg_dark", "stats_bg_light", "stats_min_height", "stats_border_style"
@@ -141,31 +144,33 @@ function applyTabFilter() {
} }
}); });
// Update filter-active dimming on whichever tab container exists // Update filter-active dimming on native nav or standalone tab bar
const nativeTabs = container.querySelector(".admin-plugin-config-area__tabs"); const nativeNav = document.querySelector(".d-nav-submenu__tabs");
if (nativeTabs) { if (nativeNav) {
nativeTabs.classList.toggle("cl-filter-active", filterActive); nativeNav.classList.toggle("cl-filter-active", filterActive);
} }
const standaloneTabs = container.querySelector(".cl-admin-tabs"); const standaloneBar = document.querySelector(".cl-admin-tabs");
if (standaloneTabs) { if (standaloneBar) {
standaloneTabs.classList.toggle("filter-active", filterActive); standaloneBar.classList.toggle("filter-active", filterActive);
} }
} }
function updateActiveStates(activeId) { function updateActiveStates(activeId) {
const container = getContainer(); // Native nav: our injected <li> tabs
if (!container) return; document.querySelectorAll("li.cl-admin-tab").forEach((li) => {
li.classList.toggle("active", li.dataset.tab === activeId);
// Update all our injected tabs
container.querySelectorAll(".cl-admin-tab").forEach((btn) => {
btn.classList.toggle("active", btn.dataset.tab === activeId);
}); });
// Update native Settings link if present // Native nav: the original Settings <li>
const nativeLink = container.querySelector(".cl-native-settings-link"); const nativeItem = document.querySelector(".cl-native-settings-item");
if (nativeLink) { if (nativeItem) {
nativeLink.classList.toggle("active", activeId === "settings"); nativeItem.classList.toggle("active", activeId === "settings");
} }
// Standalone fallback: <button> tabs
document.querySelectorAll("button.cl-admin-tab").forEach((btn) => {
btn.classList.toggle("active", btn.dataset.tab === activeId);
});
} }
function findFilterInput(container) { function findFilterInput(container) {
@@ -218,7 +223,6 @@ function buildTabsUI() {
if (!container) return false; if (!container) return false;
// Already injected — just re-apply filter // Already injected — just re-apply filter
// Search broadly: native tabs may be a sibling of container, not a child
if (document.querySelector(".cl-admin-tab")) { if (document.querySelector(".cl-admin-tab")) {
applyTabFilter(); applyTabFilter();
return true; return true;
@@ -233,35 +237,46 @@ function buildTabsUI() {
); );
if (!hasOurs) return false; if (!hasOurs) return false;
// ── Strategy 1: Inject into native Discourse tab bar ── // ── Strategy 1: Inject into native Discourse nav tab bar ──
// Native tabs may be a sibling of our container, so search at page level // Native structure: <ul class="nav-pills action-list d-nav-submenu__tabs">
const page = container.closest(".admin-plugin-config-page") || container.parentElement; // <li class="admin-plugin-config-page__top-nav-item"><a>Settings</a></li>
const nativeTabsEl = (page && page.querySelector(".admin-plugin-config-area__tabs")) || const nativeTabsList = document.querySelector(".d-nav-submenu__tabs");
document.querySelector(".admin-plugin-config-area__tabs"); if (nativeTabsList) {
if (nativeTabsEl) { // Hook the native "Settings" <li> so clicking it activates our Settings tab
// Find the native "Settings" link and hook into it const nativeSettingsItem = nativeTabsList.querySelector(
const nativeLink = nativeTabsEl.querySelector("a"); ".admin-plugin-config-page__top-nav-item"
if (nativeLink) { );
nativeLink.classList.add("cl-native-settings-link", "active"); if (nativeSettingsItem) {
nativeLink.addEventListener("click", (e) => { nativeSettingsItem.classList.add("cl-native-settings-item");
e.preventDefault(); const nativeLink = nativeSettingsItem.querySelector("a");
handleTabClick(container, "settings"); if (nativeLink) {
}); nativeLink.addEventListener("click", (e) => {
e.preventDefault();
handleTabClick(container, "settings");
});
}
} }
// Inject our section tabs into the native bar (skip "settings" — native link handles that) // Inject our section tabs as <li> items (skip "settings" — native handles it)
TABS.forEach((tab) => { TABS.forEach((tab) => {
if (tab.id === "settings") return; if (tab.id === "settings") return;
const btn = document.createElement("button"); const li = document.createElement("li");
btn.className = "cl-admin-tab"; li.className = "admin-plugin-config-page__top-nav-item cl-admin-tab";
btn.textContent = tab.label; li.dataset.tab = tab.id;
btn.dataset.tab = tab.id; li.title = tab.label;
btn.addEventListener("click", () => handleTabClick(container, tab.id));
nativeTabsEl.appendChild(btn); const a = document.createElement("a");
a.textContent = tab.label;
a.addEventListener("click", (e) => {
e.preventDefault();
handleTabClick(container, tab.id);
});
li.appendChild(a);
nativeTabsList.appendChild(li);
}); });
nativeTabsEl.classList.add("cl-tabs-injected");
container.classList.add("cl-tabs-active"); container.classList.add("cl-tabs-active");
wrapBgPairs(); wrapBgPairs();
applyTabFilter(); applyTabFilter();

View File

@@ -9,64 +9,31 @@
display: none !important; display: none !important;
} }
/* ── Injected tabs inside native Discourse tab bar ── */ /* ── Injected tabs inside native Discourse nav bar ──
Native structure: <ul class="nav-pills d-nav-submenu__tabs">
<li class="admin-plugin-config-page__top-nav-item"><a>Settings</a></li>
<li class="admin-plugin-config-page__top-nav-item cl-admin-tab"><a>Hero</a></li>
</ul> */
.admin-plugin-config-area__tabs.cl-tabs-injected { .d-nav-submenu__tabs li.cl-admin-tab {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0;
}
.admin-plugin-config-area__tabs.cl-tabs-injected .cl-admin-tab {
padding: 10px 14px;
border: none;
background: none;
color: var(--primary-medium, #888);
font-size: 0.875em;
font-weight: 600;
cursor: pointer; cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
} }
.admin-plugin-config-area__tabs.cl-tabs-injected .cl-admin-tab:hover { .d-nav-submenu__tabs li.cl-admin-tab a {
color: var(--primary, #333); cursor: pointer;
}
.admin-plugin-config-area__tabs.cl-tabs-injected .cl-admin-tab.active {
color: var(--tertiary, #0088cc);
border-bottom-color: var(--tertiary, #0088cc);
}
/* Native "Settings" link — match our tab styling when active/inactive */
.admin-plugin-config-area__tabs.cl-tabs-injected .cl-native-settings-link {
transition: color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
}
.admin-plugin-config-area__tabs.cl-tabs-injected .cl-native-settings-link:not(.active) {
color: var(--primary-medium, #888);
border-bottom-color: transparent;
} }
/* Dimmed state when Discourse filter/search is active */ /* Dimmed state when Discourse filter/search is active */
.admin-plugin-config-area__tabs.cl-filter-active .cl-admin-tab, .d-nav-submenu__tabs.cl-filter-active > li {
.admin-plugin-config-area__tabs.cl-filter-active .cl-native-settings-link {
opacity: 0.4; opacity: 0.4;
} }
.admin-plugin-config-area__tabs.cl-filter-active .cl-admin-tab.active, .d-nav-submenu__tabs.cl-filter-active > li.active {
.admin-plugin-config-area__tabs.cl-filter-active .cl-native-settings-link.active { opacity: 1;
border-bottom-color: transparent; border-bottom-color: transparent;
} }
/* Dark mode */ /* ── Standalone tab bar (fallback for older Discourse without native nav) ── */
html.dark-scheme .admin-plugin-config-area__tabs.cl-tabs-injected .cl-admin-tab:hover {
color: var(--primary, #ddd);
}
/* ── Standalone tab bar (fallback for older Discourse without native tabs) ── */
.cl-admin-tabs { .cl-admin-tabs {
display: flex; display: flex;

View File

@@ -878,12 +878,21 @@
/* ── Hero Creators (top 3 ranked pills in hero) ── */ /* ── Hero Creators (top 3 ranked pills in hero) ── */
.cl-hero__creators { .cl-hero__creators {
display: flex; display: flex;
flex-wrap: nowrap; flex-direction: column;
justify-content: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
margin-top: 2rem; margin-top: 2rem;
} }
.cl-hero__creators-title {
font-size: 0.85rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--cl-muted);
margin: 0 0 0.25rem;
}
/* ═══════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════
3. COMPACT STATS BAR — icon + count + label inline 3. COMPACT STATS BAR — icon + count + label inline
═══════════════════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════════════════ */
@@ -986,6 +995,45 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
/* Stat card style variants */
.cl-stat-card--rectangle {
border-radius: var(--cl-radius-sm);
}
.cl-stat-card--rounded {
border-radius: 16px;
}
.cl-stat-card--pill {
border-radius: 50px;
padding: 0.85rem 1.5rem;
}
.cl-stat-card--minimal {
background: transparent;
border: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.cl-stat-card--minimal:hover {
background: transparent;
border: none;
box-shadow: none;
transform: translateY(-2px);
}
.cl-stat-card--minimal .cl-stat-card__icon-wrap {
background: transparent;
color: var(--cl-stat-icon-color);
}
.cl-stat-card--minimal:hover .cl-stat-card__icon-wrap {
background: transparent;
color: var(--cl-accent);
transform: scale(1.15);
}
/* ═══════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════
4. ABOUT — split layout: image left on gradient, text right 4. ABOUT — split layout: image left on gradient, text right
═══════════════════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════════════════ */
@@ -1208,16 +1256,18 @@
} }
/* ═══════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════
6. CREATOR PILLS — used in hero section 6. CREATOR PILLS — vertical stack in hero section
═══════════════════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════════════════ */
.cl-creator-pill { .cl-creator-pill {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
padding: 0.6rem 1.25rem 0.6rem 0.6rem; padding: 0.6rem 1.25rem 0.6rem 0.6rem;
width: 100%;
max-width: 340px;
background: var(--cl-card); background: var(--cl-card);
border: 1px solid var(--cl-border); border: 1px solid var(--cl-border);
border-radius: 50px; border-radius: 14px;
text-decoration: none; text-decoration: none;
position: relative; position: relative;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
@@ -1234,27 +1284,26 @@
.cl-creator-pill__rank { .cl-creator-pill__rank {
position: absolute; position: absolute;
top: -6px; top: -8px;
left: -6px; right: -8px;
width: 20px; padding: 0.15rem 0.5rem;
height: 20px; border-radius: 50px;
border-radius: 50%;
background: var(--rank-color); background: var(--rank-color);
color: #000; color: #000;
font-size: 0.65rem; font-size: 0.6rem;
font-weight: 900; font-weight: 900;
display: flex; letter-spacing: 0.03em;
align-items: center; white-space: nowrap;
justify-content: center;
z-index: 2; z-index: 2;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
} }
.cl-creator-pill__avatar { .cl-creator-pill__avatar {
width: 48px; width: 44px;
height: 48px; height: 44px;
border-radius: 50%; border-radius: 50%;
object-fit: cover; object-fit: cover;
flex-shrink: 0;
border: 2px solid var(--rank-color, var(--cl-border)); border: 2px solid var(--rank-color, var(--cl-border));
transition: border-color 0.3s ease; transition: border-color 0.3s ease;
} }
@@ -1263,24 +1312,27 @@
border-color: var(--rank-color, var(--cl-accent)); border-color: var(--rank-color, var(--cl-accent));
} }
.cl-creator-pill__info {
display: flex;
flex-direction: column;
min-width: 0;
flex: 1;
}
.cl-creator-pill__name { .cl-creator-pill__name {
font-size: 0.95rem; font-size: 0.9rem;
font-weight: 700; font-weight: 700;
color: var(--cl-text-strong); color: var(--cl-text-strong);
white-space: nowrap; white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.cl-creator-pill__count { .cl-creator-pill__count {
font-size: 0.8rem; font-size: 0.75rem;
color: var(--cl-accent); color: var(--cl-accent);
white-space: nowrap; white-space: nowrap;
font-weight: 800; font-weight: 600;
margin-left: auto;
padding: 0.15rem 0.6rem;
min-width: 1.5em;
text-align: center;
background: var(--cl-accent-subtle);
border-radius: 50px;
} }
/* ═══════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════

View File

@@ -49,7 +49,9 @@ en:
# ── 3. Stats Section ── # ── 3. Stats Section ──
stats_enabled: "━━ ROW 3: PREMIUM STATS ━━ — Show or hide the entire stats section." stats_enabled: "━━ ROW 3: PREMIUM STATS ━━ — Show or hide the entire stats section."
stat_labels_enabled: "Show text labels below stat counters (e.g. 'Members', 'Topics'). When off, only numbers and icons are displayed." stat_labels_enabled: "Show text labels below stat counters (e.g. 'Members', 'Topics'). When off, only numbers and icons are displayed."
stats_title_enabled: "Show the section heading above the stats row. Turn off to display only the stat cards."
stats_title: "Section heading text above the stats row." stats_title: "Section heading text above the stats row."
stat_card_style: "Visual style for stat cards: rectangle (sharp corners), rounded (soft corners), pill (fully rounded), or minimal (no background/border)."
stat_icon_color: "Color for all stat counter icons. Hex value (e.g. #d4a24e)." stat_icon_color: "Color for all stat counter icons. Hex value (e.g. #d4a24e)."
stat_icon_bg_color: "Background color behind each stat icon. Leave blank for a subtle accent tint." stat_icon_bg_color: "Background color behind each stat icon. Leave blank for a subtle accent tint."
stat_icon_shape: "Shape of the icon background: circle or rounded square." stat_icon_shape: "Shape of the icon background: circle or rounded square."
@@ -93,7 +95,11 @@ en:
topics_border_style: "Border style at the bottom of the trending section." topics_border_style: "Border style at the bottom of the trending section."
# ── 6. Hero Creators ── # ── 6. Hero Creators ──
contributors_enabled: "Show top 3 creators in the hero section with gold, silver, and bronze rank badges. Each pill displays avatar, @username, and post count." contributors_enabled: "Show top 3 creators in the hero section with gold, silver, and bronze rank badges."
contributors_title: "Heading text above the creators list (e.g. 'Top Creators', 'Community Leaders')."
contributors_title_enabled: "Show the heading above the creators list."
contributors_count_label: "Label shown before each creator's count (e.g. 'Cheers', 'Points'). Leave blank for no prefix."
contributors_count_label_enabled: "Show the count label prefix before each creator's activity count."
contributors_days: "Lookback period in days for calculating top contributors." contributors_days: "Lookback period in days for calculating top contributors."
contributors_count: "Number of top contributors to fetch (top 3 are shown in the hero)." contributors_count: "Number of top contributors to fetch (top 3 are shown in the hero)."

View File

@@ -174,9 +174,20 @@ plugins:
stat_labels_enabled: stat_labels_enabled:
default: true default: true
type: bool type: bool
stats_title_enabled:
default: true
type: bool
stats_title: stats_title:
default: "Premium Stats" default: "Premium Stats"
type: string type: string
stat_card_style:
default: "rectangle"
type: enum
choices:
- rectangle
- rounded
- pill
- minimal
stat_icon_color: stat_icon_color:
default: "d4a24e" default: "d4a24e"
type: color type: color
@@ -327,6 +338,18 @@ plugins:
contributors_enabled: contributors_enabled:
default: true default: true
type: bool type: bool
contributors_title:
default: "Top Creators"
type: string
contributors_title_enabled:
default: true
type: bool
contributors_count_label:
default: "Cheers"
type: string
contributors_count_label_enabled:
default: true
type: bool
contributors_days: contributors_days:
default: 90 default: 90
type: integer type: integer

View File

@@ -4,7 +4,7 @@ module CommunityLanding
module Icons module Icons
SUN_SVG = '<svg class="cl-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>' SUN_SVG = '<svg class="cl-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>'
MOON_SVG = '<svg class="cl-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>' MOON_SVG = '<svg class="cl-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>'
QUOTE_SVG = '<svg class="cl-about__quote-mark" viewBox="0 0 24 24" fill="currentColor" width="32" height="32"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg>' QUOTE_SVG = '<svg class="cl-about__quote-mark" viewBox="0 0 24 24" fill="currentColor" width="32" height="32"><path d="M18 7h-3l-2 4v6h6v-6h-3zm-8 0H7L5 11v6h6v-6H8z"/></svg>'
STAT_MEMBERS_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>' STAT_MEMBERS_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>'
STAT_TOPICS_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>' STAT_TOPICS_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>'

View File

@@ -160,16 +160,25 @@ module CommunityLanding
if (@s.contributors_enabled rescue false) && contributors&.any? if (@s.contributors_enabled rescue false) && contributors&.any?
top3 = contributors.first(3) top3 = contributors.first(3)
rank_colors = ["#FFD700", "#C0C0C0", "#CD7F32"] rank_colors = ["#FFD700", "#C0C0C0", "#CD7F32"]
creators_title = @s.contributors_title.presence || "Top Creators"
show_title = @s.contributors_title_enabled rescue true
count_label = @s.contributors_count_label.presence || ""
show_count_label = @s.contributors_count_label_enabled rescue true
html << "<div class=\"cl-hero__creators\">\n" html << "<div class=\"cl-hero__creators\">\n"
html << "<h3 class=\"cl-hero__creators-title\">#{e(creators_title)}</h3>\n" if show_title
top3.each_with_index do |user, idx| top3.each_with_index do |user, idx|
avatar_url = user.avatar_template.gsub("{size}", "120") avatar_url = user.avatar_template.gsub("{size}", "120")
activity_count = user.attributes["post_count"].to_i rescue 0 activity_count = user.attributes["post_count"].to_i rescue 0
rank_color = rank_colors[idx] rank_color = rank_colors[idx]
count_prefix = show_count_label && count_label.present? ? "#{e(count_label)} " : ""
html << "<a href=\"#{login_url}\" class=\"cl-creator-pill cl-creator-pill--rank-#{idx + 1}\" style=\"--rank-color: #{rank_color}\">\n" html << "<a href=\"#{login_url}\" class=\"cl-creator-pill cl-creator-pill--rank-#{idx + 1}\" style=\"--rank-color: #{rank_color}\">\n"
html << "<span class=\"cl-creator-pill__rank\">#{idx + 1}</span>\n" html << "<span class=\"cl-creator-pill__rank\">Ranked ##{idx + 1}</span>\n"
html << "<img src=\"#{avatar_url}\" alt=\"#{e(user.username)}\" class=\"cl-creator-pill__avatar\" loading=\"lazy\">\n" html << "<img src=\"#{avatar_url}\" alt=\"#{e(user.username)}\" class=\"cl-creator-pill__avatar\" loading=\"lazy\">\n"
html << "<div class=\"cl-creator-pill__info\">\n"
html << "<span class=\"cl-creator-pill__name\">@#{e(user.username)}</span>\n" html << "<span class=\"cl-creator-pill__name\">@#{e(user.username)}</span>\n"
html << "<span class=\"cl-creator-pill__count\">#{activity_count}</span>\n" html << "<span class=\"cl-creator-pill__count\">#{count_prefix}#{activity_count}</span>\n"
html << "</div>\n"
html << "</a>\n" html << "</a>\n"
end end
html << "</div>\n" html << "</div>\n"
@@ -217,21 +226,23 @@ module CommunityLanding
stats = @data[:stats] stats = @data[:stats]
stats_title = @s.stats_title.presence || "Premium Stats" stats_title = @s.stats_title.presence || "Premium Stats"
show_title = @s.stats_title_enabled rescue true
border = @s.stats_border_style rescue "none" border = @s.stats_border_style rescue "none"
min_h = @s.stats_min_height rescue 0 min_h = @s.stats_min_height rescue 0
icon_shape = @s.stat_icon_shape rescue "circle" icon_shape = @s.stat_icon_shape rescue "circle"
card_style = @s.stat_card_style rescue "rectangle"
round_nums = @s.stat_round_numbers rescue false round_nums = @s.stat_round_numbers rescue false
show_labels = @s.stat_labels_enabled rescue true show_labels = @s.stat_labels_enabled rescue true
html = +"" html = +""
html << "<section class=\"cl-stats cl-anim\" id=\"cl-stats-row\"#{section_style(border, min_h)}><div class=\"cl-container\">\n" html << "<section class=\"cl-stats cl-anim\" id=\"cl-stats-row\"#{section_style(border, min_h)}><div class=\"cl-container\">\n"
html << "<h2 class=\"cl-section-title\">#{e(stats_title)}</h2>\n" html << "<h2 class=\"cl-section-title\">#{e(stats_title)}</h2>\n" if show_title
html << "<div class=\"cl-stats__grid\">\n" html << "<div class=\"cl-stats__grid\">\n"
html << stat_card(Icons::STAT_MEMBERS_SVG, stats[:members], @s.stat_members_label, icon_shape, round_nums, show_labels) html << stat_card(Icons::STAT_MEMBERS_SVG, stats[:members], @s.stat_members_label, icon_shape, card_style, round_nums, show_labels)
html << stat_card(Icons::STAT_TOPICS_SVG, stats[:topics], @s.stat_topics_label, icon_shape, round_nums, show_labels) html << stat_card(Icons::STAT_TOPICS_SVG, stats[:topics], @s.stat_topics_label, icon_shape, card_style, round_nums, show_labels)
html << stat_card(Icons::STAT_POSTS_SVG, stats[:posts], @s.stat_posts_label, icon_shape, round_nums, show_labels) html << stat_card(Icons::STAT_POSTS_SVG, stats[:posts], @s.stat_posts_label, icon_shape, card_style, round_nums, show_labels)
html << stat_card(Icons::STAT_LIKES_SVG, stats[:likes], @s.stat_likes_label, icon_shape, round_nums, show_labels) html << stat_card(Icons::STAT_LIKES_SVG, stats[:likes], @s.stat_likes_label, icon_shape, card_style, round_nums, show_labels)
html << stat_card(Icons::STAT_CHATS_SVG, stats[:chats], @s.stat_chats_label, icon_shape, round_nums, show_labels) html << stat_card(Icons::STAT_CHATS_SVG, stats[:chats], @s.stat_chats_label, icon_shape, card_style, round_nums, show_labels)
html << "</div>\n</div></section>\n" html << "</div>\n</div></section>\n"
html html
end end
@@ -439,11 +450,12 @@ module CommunityLanding
# ── Shared helpers ── # ── Shared helpers ──
def stat_card(icon_svg, count, label, icon_shape = "circle", round_numbers = false, show_label = true) def stat_card(icon_svg, count, label, icon_shape = "circle", card_style = "rectangle", round_numbers = false, show_label = true)
shape_class = icon_shape == "rounded" ? "cl-stat-icon--rounded" : "cl-stat-icon--circle" shape_class = icon_shape == "rounded" ? "cl-stat-icon--rounded" : "cl-stat-icon--circle"
style_class = "cl-stat-card--#{card_style}"
round_attr = round_numbers ? ' data-round="true"' : '' round_attr = round_numbers ? ' data-round="true"' : ''
label_html = show_label ? "<span class=\"cl-stat-card__label\">#{e(label)}</span>\n" : "" label_html = show_label ? "<span class=\"cl-stat-card__label\">#{e(label)}</span>\n" : ""
"<div class=\"cl-stat-card\">\n" \ "<div class=\"cl-stat-card #{style_class}\">\n" \
"<div class=\"cl-stat-card__icon-wrap #{shape_class}\">#{icon_svg}</div>\n" \ "<div class=\"cl-stat-card__icon-wrap #{shape_class}\">#{icon_svg}</div>\n" \
"<div class=\"cl-stat-card__text\">\n" \ "<div class=\"cl-stat-card__text\">\n" \
"<span class=\"cl-stat-card__value\" data-count=\"#{count}\"#{round_attr}>0</span>\n" \ "<span class=\"cl-stat-card__value\" data-count=\"#{count}\"#{round_attr}>0</span>\n" \