Bug Fixes and UI Tweaks

This commit is contained in:
2026-03-07 22:06:48 -04:00
parent 8700b35f3f
commit 4f41c0f900
8 changed files with 166 additions and 135 deletions

View File

@@ -28,13 +28,16 @@ const TABS = [
"hero_primary_button_label", "hero_primary_button_url", "hero_primary_button_label", "hero_primary_button_url",
"hero_secondary_button_label", "hero_secondary_button_url", "hero_secondary_button_label", "hero_secondary_button_url",
"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",
"contributors_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_title", "stat_icon_color", "stat_icon_bg_color", "stat_icon_shape", "stat_counter_color", "stats_title", "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",
@@ -60,14 +63,6 @@ const TABS = [
"topics_bg_dark", "topics_bg_light", "topics_min_height", "topics_border_style" "topics_bg_dark", "topics_bg_light", "topics_min_height", "topics_border_style"
]) ])
}, },
{
id: "contributors",
label: "Creators",
settings: new Set([
"contributors_enabled", "contributors_title", "contributors_days", "contributors_count",
"contributors_bg_dark", "contributors_bg_light", "contributors_min_height", "contributors_border_style"
])
},
{ {
id: "groups", id: "groups",
label: "Spaces", label: "Spaces",
@@ -99,6 +94,18 @@ const TABS = [
} }
]; ];
// Pairs of dark/light background settings to display side-by-side
const BG_PAIRS = [
["hero_bg_dark", "hero_bg_light"],
["hero_card_bg_dark", "hero_card_bg_light"],
["stats_bg_dark", "stats_bg_light"],
["about_bg_dark", "about_bg_light"],
["topics_bg_dark", "topics_bg_light"],
["groups_bg_dark", "groups_bg_light"],
["app_cta_bg_dark", "app_cta_bg_light"],
["footer_bg_dark", "footer_bg_light"],
];
let currentTab = "settings"; let currentTab = "settings";
let filterActive = false; let filterActive = false;
let isActive = false; let isActive = false;
@@ -126,6 +133,14 @@ function applyTabFilter() {
); );
}); });
// Toggle visibility on bg-pair wrappers
container.querySelectorAll(".cl-bg-pair").forEach((pair) => {
const firstRow = pair.querySelector(".row.setting[data-setting]");
if (firstRow) {
pair.classList.toggle("cl-tab-hidden", firstRow.classList.contains("cl-tab-hidden"));
}
});
// Update filter-active dimming on whichever tab container exists // Update filter-active dimming on whichever tab container exists
const nativeTabs = container.querySelector(".admin-plugin-config-area__tabs"); const nativeTabs = container.querySelector(".admin-plugin-config-area__tabs");
if (nativeTabs) { if (nativeTabs) {
@@ -179,6 +194,25 @@ function handleTabClick(container, tabId) {
applyTabFilter(); applyTabFilter();
} }
function wrapBgPairs() {
const container = getContainer();
if (!container) return;
BG_PAIRS.forEach(([darkName, lightName]) => {
const darkRow = container.querySelector(`.row.setting[data-setting="${darkName}"]`);
const lightRow = container.querySelector(`.row.setting[data-setting="${lightName}"]`);
if (!darkRow || !lightRow) return;
// Skip if already wrapped
if (darkRow.parentElement && darkRow.parentElement.classList.contains("cl-bg-pair")) return;
const wrapper = document.createElement("div");
wrapper.className = "cl-bg-pair";
darkRow.parentNode.insertBefore(wrapper, darkRow);
wrapper.appendChild(darkRow);
wrapper.appendChild(lightRow);
});
}
function buildTabsUI() { function buildTabsUI() {
const container = getContainer(); const container = getContainer();
if (!container) return false; if (!container) return false;
@@ -229,6 +263,7 @@ function buildTabsUI() {
nativeTabsEl.classList.add("cl-tabs-injected"); nativeTabsEl.classList.add("cl-tabs-injected");
container.classList.add("cl-tabs-active"); container.classList.add("cl-tabs-active");
wrapBgPairs();
applyTabFilter(); applyTabFilter();
return true; return true;
} }
@@ -273,6 +308,7 @@ function buildTabsUI() {
} }
container.classList.add("cl-tabs-active"); container.classList.add("cl-tabs-active");
wrapBgPairs();
applyTabFilter(); applyTabFilter();
return true; return true;
} }

View File

@@ -1,6 +1,6 @@
/* ═══════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════
Community Landing — Admin Settings Panel Styles Community Landing — Admin Settings Panel Styles
Tab navigation + fallback separators Tab navigation + fallback separators + bg-pair layout
═══════════════════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════════════════ */
/* ── Tab-hidden class (used instead of inline display:none) ── */ /* ── Tab-hidden class (used instead of inline display:none) ── */
@@ -123,6 +123,30 @@ html.dark-scheme .cl-admin-tabs .cl-admin-tab:hover {
padding-top: 0 !important; padding-top: 0 !important;
} }
/* ── Dual-color background pairs (side-by-side dark/light) ── */
.cl-bg-pair {
display: flex;
gap: 16px;
width: 100%;
}
.cl-bg-pair > .row.setting {
flex: 1;
min-width: 0;
margin-bottom: 0 !important;
}
.cl-bg-pair.cl-tab-hidden {
display: none !important;
}
@media (max-width: 600px) {
.cl-bg-pair {
flex-direction: column;
}
}
/* ── Fallback: Separator borders when tabs are NOT active ── /* ── Fallback: Separator borders when tabs are NOT active ──
(e.g. if JS fails to load or on older Discourse) */ (e.g. if JS fails to load or on older Discourse) */
@@ -155,10 +179,9 @@ html.dark-scheme .cl-admin-tabs .cl-admin-tab:hover {
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="scroll_animation"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="scroll_animation"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="navbar_signin_label"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="navbar_signin_label"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="hero_title"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="hero_title"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="stats_title"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="stats_enabled"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="about_enabled"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="about_enabled"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="topics_enabled"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="topics_enabled"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="contributors_enabled"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="groups_enabled"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="groups_enabled"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="show_app_ctas"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="show_app_ctas"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="footer_description"] { .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="footer_description"] {
@@ -173,10 +196,9 @@ html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="a
html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="scroll_animation"], html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="scroll_animation"],
html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="navbar_signin_label"], html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="navbar_signin_label"],
html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="hero_title"], html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="hero_title"],
html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="stats_title"], html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="stats_enabled"],
html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="about_enabled"], html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="about_enabled"],
html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="topics_enabled"], html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="topics_enabled"],
html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="contributors_enabled"],
html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="groups_enabled"], html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="groups_enabled"],
html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="show_app_ctas"], html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="show_app_ctas"],
html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="footer_description"], html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="footer_description"],

View File

@@ -602,6 +602,8 @@
min-height: 80vh; min-height: 80vh;
display: flex; display: flex;
align-items: center; align-items: center;
background-size: cover;
background-position: center;
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
@@ -692,34 +694,15 @@
transition: opacity 0.6s ease; transition: opacity 0.6s ease;
} }
/* ── Hero Background Image (flat mode) ── */
.cl-hero__bg {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
z-index: 0;
opacity: 0.25;
}
.cl-hero__bg::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(to bottom, transparent 60%, var(--cl-bg));
}
/* ── Hero Card Mode ── */ /* ── Hero Card Mode ── */
.cl-hero--card .cl-hero__inner { .cl-hero--card .cl-hero__inner {
background: var(--cl-card); background: var(--cl-hero-card-bg, var(--cl-card));
border: 1px solid var(--cl-border); border: 1px solid var(--cl-border);
border-radius: var(--cl-radius); border-radius: var(--cl-radius);
padding: 3rem 2.5rem; padding: 3rem 2.5rem;
backdrop-filter: var(--cl-blur); backdrop-filter: var(--cl-blur);
-webkit-backdrop-filter: var(--cl-blur); -webkit-backdrop-filter: var(--cl-blur);
box-shadow: 0 8px 32px var(--cl-shadow); box-shadow: 0 8px 32px var(--cl-shadow);
background-size: cover;
background-position: center;
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
@@ -892,10 +875,11 @@
transition: filter 0.4s ease; transition: filter 0.4s ease;
} }
/* ── Hero Creators (top 3 pills in hero) ── */ /* ── Hero Creators (top 3 ranked pills in hero) ── */
.cl-hero__creators { .cl-hero__creators {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: nowrap;
justify-content: center;
gap: 0.75rem; gap: 0.75rem;
margin-top: 2rem; margin-top: 2rem;
} }
@@ -1224,18 +1208,8 @@
} }
/* ═══════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════
6. TOP CREATORS — pill badges 6. CREATOR PILLS — used in hero section
═══════════════════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════════════════ */
.cl-creators {
padding: 2.5rem 0 3rem;
}
.cl-creators__list {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.cl-creator-pill { .cl-creator-pill {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1245,29 +1219,48 @@
border: 1px solid var(--cl-border); border: 1px solid var(--cl-border);
border-radius: 50px; border-radius: 50px;
text-decoration: none; text-decoration: none;
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);
backdrop-filter: var(--cl-blur); backdrop-filter: var(--cl-blur);
-webkit-backdrop-filter: var(--cl-blur); -webkit-backdrop-filter: var(--cl-blur);
} }
.cl-creator-pill:hover { .cl-creator-pill:hover {
border-color: var(--cl-border-hover); border-color: var(--rank-color, var(--cl-border-hover));
background: var(--cl-accent-subtle); background: var(--cl-accent-subtle);
transform: translateY(-3px); transform: translateY(-3px);
box-shadow: 0 8px 24px var(--cl-shadow); box-shadow: 0 8px 24px var(--cl-shadow);
} }
.cl-creator-pill__rank {
position: absolute;
top: -6px;
left: -6px;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--rank-color);
color: #000;
font-size: 0.65rem;
font-weight: 900;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
.cl-creator-pill__avatar { .cl-creator-pill__avatar {
width: 48px; width: 48px;
height: 48px; height: 48px;
border-radius: 50%; border-radius: 50%;
object-fit: cover; object-fit: cover;
border: 2px solid var(--cl-border); border: 2px solid var(--rank-color, var(--cl-border));
transition: border-color 0.3s ease; transition: border-color 0.3s ease;
} }
.cl-creator-pill:hover .cl-creator-pill__avatar { .cl-creator-pill:hover .cl-creator-pill__avatar {
border-color: var(--cl-accent); border-color: var(--rank-color, var(--cl-accent));
} }
.cl-creator-pill__name { .cl-creator-pill__name {

View File

@@ -42,9 +42,14 @@ en:
hero_bg_light: "Background color for the hero section in light mode. Hex value. Leave blank for default." hero_bg_light: "Background color for the hero section in light mode. Hex value. Leave blank for default."
hero_min_height: "Minimum height for the hero section in pixels. Set to 0 for auto height." hero_min_height: "Minimum height for the hero section in pixels. Set to 0 for auto height."
hero_border_style: "Border style at the bottom of the hero section." hero_border_style: "Border style at the bottom of the hero section."
hero_card_bg_dark: "Background color for the hero card overlay in dark mode. Only applies when hero card mode is enabled. Leave blank for default glass effect."
hero_card_bg_light: "Background color for the hero card overlay in light mode. Only applies when hero card mode is enabled. Leave blank for default glass effect."
hero_card_opacity: "Opacity of the hero card background (0 to 1, default 0.85). Lower values make the card more transparent, showing the background image through."
# ── 3. Stats Section ── # ── 3. Stats Section ──
stats_title: "━━ ROW 3: PREMIUM STATS ━━ — Full-width row of live community statistics (members, topics, posts, likes, chats) with animated counters and icons. Each card shows icon + label on one line with the counter below. This is the section heading." 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."
stats_title: "Section heading text above the stats row."
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."
@@ -87,15 +92,10 @@ en:
topics_min_height: "Minimum height for the trending section in pixels. Set to 0 for auto height." topics_min_height: "Minimum height for the trending section in pixels. Set to 0 for auto height."
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. Top Creators ── # ── 6. Hero Creators ──
contributors_enabled: "━━ ROW 6: CREATORS ━━ — Show the Top Creators section: pill-shaped badges showing your most active community members. Each pill displays avatar, @username, and post count from the lookback period." 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_title: "Heading text above the creator pills."
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 contributor pills to display. Recommended: 612." contributors_count: "Number of top contributors to fetch (top 3 are shown in the hero)."
contributors_bg_dark: "Background color for the creators section in dark mode. Leave blank for default."
contributors_bg_light: "Background color for the creators section in light mode. Leave blank for default."
contributors_min_height: "Minimum height for the creators section in pixels. Set to 0 for auto height."
contributors_border_style: "Border style at the bottom of the creators section."
# ── 7. Community Spaces ── # ── 7. Community Spaces ──
groups_enabled: "━━ ROW 7: SPACES ━━ — Show the Community Spaces section: a grid of colorful cards representing your public groups. Each card shows a colored icon (with group's first letter or flair), group name, and member count. Only public, non-automatic groups are shown." groups_enabled: "━━ ROW 7: SPACES ━━ — Show the Community Spaces section: a grid of colorful cards representing your public groups. Each card shows a colored icon (with group's first letter or flair), group name, and member count. Only public, non-automatic groups are shown."

View File

@@ -155,10 +155,25 @@ plugins:
- solid - solid
- dashed - dashed
- dotted - dotted
hero_card_bg_dark:
default: ""
type: color
hero_card_bg_light:
default: ""
type: color
hero_card_opacity:
default: "0.85"
type: string
# ══════════════════════════════════════════ # ══════════════════════════════════════════
# 3. Premium Stats Section # 3. Premium Stats Section
# ══════════════════════════════════════════ # ══════════════════════════════════════════
stats_enabled:
default: true
type: bool
stat_labels_enabled:
default: true
type: bool
stats_title: stats_title:
default: "Premium Stats" default: "Premium Stats"
type: string type: string
@@ -307,39 +322,17 @@ plugins:
- dotted - dotted
# ══════════════════════════════════════════ # ══════════════════════════════════════════
# 6. Top Creators Section # 6. Hero Creators (Top 3 in Hero)
# ══════════════════════════════════════════ # ══════════════════════════════════════════
contributors_enabled: contributors_enabled:
default: true default: true
type: bool type: bool
contributors_title:
default: "Top Creators"
type: string
contributors_days: contributors_days:
default: 90 default: 90
type: integer type: integer
contributors_count: contributors_count:
default: 10 default: 10
type: integer type: integer
contributors_bg_dark:
default: ""
type: color
contributors_bg_light:
default: ""
type: color
contributors_min_height:
default: 0
type: integer
min: 0
max: 2000
contributors_border_style:
default: "none"
type: enum
choices:
- none
- solid
- dashed
- dotted
# ══════════════════════════════════════════ # ══════════════════════════════════════════
# 7. Community Spaces Section # 7. Community Spaces Section

View File

@@ -24,7 +24,6 @@ module CommunityLanding
html << render_stats html << render_stats
html << render_about html << render_about
html << render_topics html << render_topics
html << render_contributors
html << render_groups html << render_groups
html << render_app_cta html << render_app_cta
html << render_footer_desc html << render_footer_desc
@@ -127,18 +126,15 @@ module CommunityLanding
site_name = @s.title site_name = @s.title
html = +"" html = +""
html << "<section class=\"cl-hero#{hero_card ? ' cl-hero--card' : ''}\" id=\"cl-hero\"#{section_style(hero_border, hero_min_h)}>\n" # Build hero section style: bg image on the section itself + border/min-height
hero_style_parts = []
hero_style_parts << "background-image: url('#{hero_bg_img}');" if hero_bg_img
hero_style_parts << "border-bottom: 1px #{hero_border} var(--cl-border);" if hero_border.present? && hero_border != "none"
hero_style_parts << "min-height: #{hero_min_h}px;" if hero_min_h.to_i > 0
hero_attr = hero_style_parts.any? ? " style=\"#{hero_style_parts.join(' ')}\"" : ""
html << "<section class=\"cl-hero#{hero_card ? ' cl-hero--card' : ''}\" id=\"cl-hero\"#{hero_attr}>\n"
if hero_bg_img && !hero_card html << "<div class=\"cl-hero__inner\">\n<div class=\"cl-hero__content\">\n"
html << "<div class=\"cl-hero__bg\" style=\"background-image: url('#{hero_bg_img}');\"></div>\n"
end
inner_style = ""
if hero_card && hero_bg_img
inner_style = " style=\"background-image: linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4)), url('#{hero_bg_img}'); background-size: cover; background-position: center;\""
end
html << "<div class=\"cl-hero__inner\"#{inner_style}>\n<div class=\"cl-hero__content\">\n"
title_words = @s.hero_title.to_s.split(" ") title_words = @s.hero_title.to_s.split(" ")
if title_words.length > 1 if title_words.length > 1
@@ -159,15 +155,18 @@ module CommunityLanding
html << "<a href=\"#{secondary_url}\" class=\"cl-btn cl-btn--ghost cl-btn--lg\">#{e(secondary_label)}</a>\n" html << "<a href=\"#{secondary_url}\" class=\"cl-btn cl-btn--ghost cl-btn--lg\">#{e(secondary_label)}</a>\n"
html << "</div>\n" html << "</div>\n"
# Hero creators (top 3) # Hero creators (top 3 with gold/silver/bronze ranks)
contributors = @data[:contributors] contributors = @data[:contributors]
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"]
html << "<div class=\"cl-hero__creators\">\n" html << "<div class=\"cl-hero__creators\">\n"
top3.each do |user| 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
html << "<a href=\"#{login_url}\" class=\"cl-creator-pill\">\n" rank_color = rank_colors[idx]
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 << "<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 << "<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\">#{activity_count}</span>\n"
@@ -214,22 +213,25 @@ module CommunityLanding
# ── 3. STATS ── # ── 3. STATS ──
def render_stats def render_stats
return "" unless (@s.stats_enabled rescue true)
stats = @data[:stats] stats = @data[:stats]
stats_title = @s.stats_title.presence || "Premium Stats" stats_title = @s.stats_title.presence || "Premium Stats"
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"
round_nums = @s.stat_round_numbers rescue false round_nums = @s.stat_round_numbers rescue false
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"
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) html << stat_card(Icons::STAT_MEMBERS_SVG, stats[:members], @s.stat_members_label, icon_shape, round_nums, show_labels)
html << stat_card(Icons::STAT_TOPICS_SVG, stats[:topics], @s.stat_topics_label, icon_shape, round_nums) html << stat_card(Icons::STAT_TOPICS_SVG, stats[:topics], @s.stat_topics_label, icon_shape, round_nums, show_labels)
html << stat_card(Icons::STAT_POSTS_SVG, stats[:posts], @s.stat_posts_label, icon_shape, round_nums) html << stat_card(Icons::STAT_POSTS_SVG, stats[:posts], @s.stat_posts_label, icon_shape, round_nums, show_labels)
html << stat_card(Icons::STAT_LIKES_SVG, stats[:likes], @s.stat_likes_label, icon_shape, round_nums) html << stat_card(Icons::STAT_LIKES_SVG, stats[:likes], @s.stat_likes_label, icon_shape, round_nums, show_labels)
html << stat_card(Icons::STAT_CHATS_SVG, stats[:chats], @s.stat_chats_label, icon_shape, round_nums) html << stat_card(Icons::STAT_CHATS_SVG, stats[:chats], @s.stat_chats_label, icon_shape, round_nums, show_labels)
html << "</div>\n</div></section>\n" html << "</div>\n</div></section>\n"
html html
end end
@@ -309,35 +311,6 @@ module CommunityLanding
html html
end end
# ── 6. TOP CREATORS ──
def render_contributors
contributors = @data[:contributors]
return "" unless @s.contributors_enabled && contributors&.any?
border = @s.contributors_border_style rescue "none"
min_h = @s.contributors_min_height rescue 0
html = +""
html << "<section class=\"cl-creators cl-anim\" id=\"cl-contributors\"#{section_style(border, min_h)}><div class=\"cl-container\">\n"
html << "<h2 class=\"cl-section-title\">#{e(@s.contributors_title)}</h2>\n"
html << "<div class=\"cl-creators__list\">\n"
contributors.each do |user|
avatar_url = user.avatar_template.gsub("{size}", "120")
activity_count = user.attributes["post_count"].to_i rescue 0
html << "<a href=\"#{login_url}\" class=\"cl-creator-pill\">\n"
html << "<img src=\"#{avatar_url}\" alt=\"#{e(user.username)}\" class=\"cl-creator-pill__avatar\" loading=\"lazy\">\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 << "</a>\n"
end
html << "</div>\n</div></section>\n"
html
end
# ── 7. COMMUNITY SPACES ── # ── 7. COMMUNITY SPACES ──
def render_groups def render_groups
@@ -466,14 +439,15 @@ module CommunityLanding
# ── Shared helpers ── # ── Shared helpers ──
def stat_card(icon_svg, count, label, icon_shape = "circle", round_numbers = false) def stat_card(icon_svg, count, label, icon_shape = "circle", 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"
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" : ""
"<div class=\"cl-stat-card\">\n" \ "<div class=\"cl-stat-card\">\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" \
"<span class=\"cl-stat-card__label\">#{e(label)}</span>\n" \ "#{label_html}" \
"</div>\n" \ "</div>\n" \
"</div>\n" "</div>\n"
end end

View File

@@ -27,6 +27,10 @@ module CommunityLanding
space_card_bg = hex(@s.groups_card_bg_color.presence) rescue nil space_card_bg = hex(@s.groups_card_bg_color.presence) rescue nil
topic_card_bg = hex(@s.topics_card_bg_color.presence) rescue nil topic_card_bg = hex(@s.topics_card_bg_color.presence) rescue nil
video_btn_bg = hex(@s.hero_video_button_color.presence) rescue nil video_btn_bg = hex(@s.hero_video_button_color.presence) rescue nil
hero_card_dark = hex(@s.hero_card_bg_dark.presence) rescue nil
hero_card_light = hex(@s.hero_card_bg_light.presence) rescue nil
hero_card_opacity = [[@s.hero_card_opacity.to_s.to_f, 0].max, 1].min rescue 0.85
hero_card_opacity = 0.85 if hero_card_opacity == 0.0 && (@s.hero_card_opacity.to_s.strip.empty? rescue true)
accent_rgb = hex_to_rgb(accent) accent_rgb = hex_to_rgb(accent)
stat_icon_rgb = hex_to_rgb(stat_icon) stat_icon_rgb = hex_to_rgb(stat_icon)
@@ -35,6 +39,12 @@ module CommunityLanding
space_card_bg_val = space_card_bg || "var(--cl-card)" space_card_bg_val = space_card_bg || "var(--cl-card)"
topic_card_bg_val = topic_card_bg || "var(--cl-card)" topic_card_bg_val = topic_card_bg || "var(--cl-card)"
# Hero card bg: convert hex + opacity to rgba
hero_card_dark_rgb = hero_card_dark ? hex_to_rgb(hero_card_dark) : "12, 12, 25"
hero_card_light_rgb = hero_card_light ? hex_to_rgb(hero_card_light) : "255, 255, 255"
hero_card_dark_val = "rgba(#{hero_card_dark_rgb}, #{hero_card_opacity})"
hero_card_light_val = "rgba(#{hero_card_light_rgb}, #{hero_card_opacity})"
about_bg_extra = about_bg_img ? ", url('#{about_bg_img}') center/cover no-repeat" : "" about_bg_extra = about_bg_img ? ", url('#{about_bg_img}') center/cover no-repeat" : ""
video_btn_line = video_btn_bg ? "\n --cl-video-btn-bg: #{video_btn_bg};" : "" video_btn_line = video_btn_bg ? "\n --cl-video-btn-bg: #{video_btn_bg};" : ""
@@ -54,6 +64,7 @@ module CommunityLanding
--cl-stat-counter-color: #{stat_counter_val}; --cl-stat-counter-color: #{stat_counter_val};
--cl-space-card-bg: #{space_card_bg_val}; --cl-space-card-bg: #{space_card_bg_val};
--cl-topic-card-bg: #{topic_card_bg_val};#{video_btn_line} --cl-topic-card-bg: #{topic_card_bg_val};#{video_btn_line}
--cl-hero-card-bg: #{hero_card_dark_val};
--cl-about-gradient: linear-gradient(135deg, #{about_g1}, #{about_g2}, #{about_g3})#{about_bg_extra}; --cl-about-gradient: linear-gradient(135deg, #{about_g1}, #{about_g2}, #{about_g3})#{about_bg_extra};
--cl-app-gradient: linear-gradient(135deg, #{app_g1}, #{app_g2}, #{app_g3}); --cl-app-gradient: linear-gradient(135deg, #{app_g1}, #{app_g2}, #{app_g3});
} }
@@ -72,6 +83,7 @@ module CommunityLanding
--cl-stat-counter-color: #{stat_counter_val}; --cl-stat-counter-color: #{stat_counter_val};
--cl-space-card-bg: #{space_card_bg_val}; --cl-space-card-bg: #{space_card_bg_val};
--cl-topic-card-bg: #{topic_card_bg_val};#{video_btn_line} --cl-topic-card-bg: #{topic_card_bg_val};#{video_btn_line}
--cl-hero-card-bg: #{hero_card_light_val};
--cl-about-gradient: linear-gradient(135deg, #{about_g1}, #{about_g2}, #{about_g3})#{about_bg_extra}; --cl-about-gradient: linear-gradient(135deg, #{about_g1}, #{about_g2}, #{about_g3})#{about_bg_extra};
--cl-app-gradient: linear-gradient(135deg, #{app_g1}, #{app_g2}, #{app_g3}); --cl-app-gradient: linear-gradient(135deg, #{app_g1}, #{app_g2}, #{app_g3});
} }
@@ -102,7 +114,6 @@ module CommunityLanding
["#cl-stats-row", safe_hex(:stats_bg_dark), safe_hex(:stats_bg_light)], ["#cl-stats-row", safe_hex(:stats_bg_dark), safe_hex(:stats_bg_light)],
["#cl-about", safe_hex(:about_bg_dark), safe_hex(:about_bg_light)], ["#cl-about", safe_hex(:about_bg_dark), safe_hex(:about_bg_light)],
["#cl-topics", safe_hex(:topics_bg_dark), safe_hex(:topics_bg_light)], ["#cl-topics", safe_hex(:topics_bg_dark), safe_hex(:topics_bg_light)],
["#cl-contributors", safe_hex(:contributors_bg_dark), safe_hex(:contributors_bg_light)],
["#cl-groups", safe_hex(:groups_bg_dark), safe_hex(:groups_bg_light)], ["#cl-groups", safe_hex(:groups_bg_dark), safe_hex(:groups_bg_light)],
["#cl-app-cta", safe_hex(:app_cta_bg_dark), safe_hex(:app_cta_bg_light)], ["#cl-app-cta", safe_hex(:app_cta_bg_dark), safe_hex(:app_cta_bg_light)],
["#cl-footer", safe_hex(:footer_bg_dark), safe_hex(:footer_bg_light)], ["#cl-footer", safe_hex(:footer_bg_dark), safe_hex(:footer_bg_light)],

View File

@@ -42,10 +42,12 @@ after_initialize do
base_url = Discourse.base_url base_url = Discourse.base_url
csp = "default-src 'self' #{base_url}; " \ csp = "default-src 'self' #{base_url}; " \
"script-src 'self' 'unsafe-inline'; " \ "script-src 'self' 'unsafe-inline'; " \
"style-src 'self' 'unsafe-inline'; " \ "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " \
"style-src-elem 'self' 'unsafe-inline' https://fonts.googleapis.com; " \
"img-src 'self' #{base_url} data: https:; " \ "img-src 'self' #{base_url} data: https:; " \
"font-src 'self' #{base_url} https://fonts.gstatic.com; " \ "font-src 'self' #{base_url} https://fonts.gstatic.com; " \
"media-src 'self' https:; " \ "media-src 'self' https:; " \
"connect-src 'self' #{base_url}; " \
"frame-src https://www.youtube.com https://www.youtube-nocookie.com; " \ "frame-src https://www.youtube.com https://www.youtube-nocookie.com; " \
"frame-ancestors 'self'" "frame-ancestors 'self'"
response.headers["Content-Security-Policy"] = csp response.headers["Content-Security-Policy"] = csp