From af75ea54a61e7144a2fc2572582791c254e79ac5 Mon Sep 17 00:00:00 2001 From: DPN MW Date: Fri, 6 Mar 2026 17:45:04 -0400 Subject: [PATCH] Plugin Rewrite v2.3.0 This update constitutes a plugin rewrite to manage all options needed in the plugin into separate tabs and split files for maintenance. --- .../community-landing-admin-tabs.js | 189 ++++++ .../stylesheets/community_landing/admin.css | 169 ++++-- .../stylesheets/community_landing/landing.css | 7 +- config/locales/en.yml | 207 ++++--- config/settings.yml | 283 +++++---- lib/community_landing/data_fetcher.rb | 60 ++ lib/community_landing/helpers.rb | 43 ++ lib/community_landing/icons.rb | 21 + lib/community_landing/page_builder.rb | 464 +++++++++++++++ lib/community_landing/style_builder.rb | 114 ++++ plugin.rb | 548 +----------------- 11 files changed, 1300 insertions(+), 805 deletions(-) create mode 100644 assets/javascripts/discourse/initializers/community-landing-admin-tabs.js create mode 100644 lib/community_landing/data_fetcher.rb create mode 100644 lib/community_landing/helpers.rb create mode 100644 lib/community_landing/icons.rb create mode 100644 lib/community_landing/page_builder.rb create mode 100644 lib/community_landing/style_builder.rb diff --git a/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js b/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js new file mode 100644 index 0000000..805a0b2 --- /dev/null +++ b/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js @@ -0,0 +1,189 @@ +import { withPluginApi } from "discourse/lib/plugin-api"; + +const TABS = [ + { + id: "general", + label: "General", + settings: new Set([ + "community_landing_enabled", + "logo_dark_url", "logo_light_url", "logo_height", "footer_logo_url", + "accent_color", "accent_hover_color", "dark_bg_color", "light_bg_color", + "scroll_animation" + ]) + }, + { + id: "navbar", + label: "Navbar", + settings: new Set([ + "navbar_signin_label", "navbar_join_label", "navbar_bg_color", "navbar_border_style" + ]) + }, + { + id: "hero", + label: "Hero", + settings: new Set([ + "hero_title", "hero_subtitle", "hero_card_enabled", "hero_background_image_url", + "hero_image_urls", "hero_image_max_height", + "hero_primary_button_label", "hero_primary_button_url", + "hero_secondary_button_label", "hero_secondary_button_url", + "hero_bg_dark", "hero_bg_light", "hero_min_height", "hero_border_style" + ]) + }, + { + id: "stats", + label: "Stats", + settings: new Set([ + "stats_title", "stat_icon_color", + "stat_members_label", "stat_topics_label", "stat_posts_label", + "stat_likes_label", "stat_chats_label", + "stats_bg_dark", "stats_bg_light", "stats_min_height", "stats_border_style" + ]) + }, + { + id: "about", + label: "About", + settings: new Set([ + "about_enabled", "about_heading_enabled", "about_heading", + "about_title", "about_role", "about_body", "about_image_url", + "about_gradient_start", "about_gradient_mid", "about_gradient_end", + "about_background_image_url", + "about_bg_dark", "about_bg_light", "about_min_height", "about_border_style" + ]) + }, + { + id: "topics", + label: "Trending", + settings: new Set([ + "topics_enabled", "topics_title", "topics_count", + "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", + label: "Spaces", + settings: new Set([ + "groups_enabled", "groups_title", "groups_count", + "groups_bg_dark", "groups_bg_light", "groups_min_height", "groups_border_style" + ]) + }, + { + id: "appcta", + label: "App CTA", + settings: new Set([ + "show_app_ctas", "ios_app_url", "android_app_url", + "ios_app_badge_image_url", "android_app_badge_image_url", + "app_badge_height", "app_badge_style", + "app_cta_headline", "app_cta_subtext", + "app_cta_gradient_start", "app_cta_gradient_mid", "app_cta_gradient_end", + "app_cta_image_url", + "app_cta_bg_dark", "app_cta_bg_light", "app_cta_min_height", "app_cta_border_style" + ]) + }, + { + id: "footer", + label: "Footer", + settings: new Set([ + "footer_description", "footer_text", "footer_links", + "footer_bg_dark", "footer_bg_light", "footer_border_style" + ]) + }, + { + id: "css", + label: "Custom CSS", + settings: new Set(["custom_css"]) + } +]; + +let currentTab = "general"; + +function applyTabFilter(container) { + const tab = TABS.find((t) => t.id === currentTab); + if (!tab) return; + + container.querySelectorAll(".row.setting[data-setting]").forEach((row) => { + const name = row.getAttribute("data-setting"); + row.style.display = tab.settings.has(name) ? "" : "none"; + }); +} + +function buildTabsUI() { + const container = + document.querySelector(".admin-plugin-config-area") || + document.querySelector(".admin-detail"); + if (!container) return false; + + // Already injected? + if (container.querySelector(".cl-admin-tabs")) return true; + + const allRows = container.querySelectorAll(".row.setting[data-setting]"); + if (allRows.length < 5) return false; + + // Verify our settings are present + const firstTab = TABS[0]; + const hasOurs = Array.from(allRows).some((row) => + firstTab.settings.has(row.getAttribute("data-setting")) + ); + if (!hasOurs) return false; + + // Create tab bar + const tabBar = document.createElement("div"); + tabBar.className = "cl-admin-tabs"; + + TABS.forEach((tab) => { + const btn = document.createElement("button"); + btn.className = "cl-admin-tab" + (tab.id === currentTab ? " active" : ""); + btn.textContent = tab.label; + btn.setAttribute("data-tab", tab.id); + btn.addEventListener("click", () => { + currentTab = tab.id; + tabBar + .querySelectorAll(".cl-admin-tab") + .forEach((b) => b.classList.remove("active")); + btn.classList.add("active"); + applyTabFilter(container); + }); + tabBar.appendChild(btn); + }); + + // Insert tab bar before the first setting row + const settingsParent = allRows[0].parentNode; + settingsParent.insertBefore(tabBar, allRows[0]); + + // Add class to disable separator borders + container.classList.add("cl-tabs-active"); + + // Apply initial filter + applyTabFilter(container); + return true; +} + +export default { + name: "community-landing-admin-tabs", + + initialize() { + withPluginApi("1.0", (api) => { + api.onPageChange((url) => { + if ( + url.includes("community-landing") || + url.includes("community_landing") + ) { + let attempts = 0; + const tryInject = () => { + if (buildTabsUI() || attempts > 15) return; + attempts++; + setTimeout(tryInject, 200); + }; + tryInject(); + } + }); + }); + }, +}; diff --git a/assets/stylesheets/community_landing/admin.css b/assets/stylesheets/community_landing/admin.css index 36ec2df..4bb73ae 100644 --- a/assets/stylesheets/community_landing/admin.css +++ b/assets/stylesheets/community_landing/admin.css @@ -1,74 +1,123 @@ /* ═══════════════════════════════════════════════════════════════════ Community Landing — Admin Settings Panel Styles - Adds visual separators and spacing between setting groups + Tab navigation + fallback separators ═══════════════════════════════════════════════════════════════════ */ -/* All community_landing settings get extra bottom spacing */ -.admin-detail .row.setting[data-setting^="community_landing_"] { +/* ── Tab Navigation ── */ + +.cl-admin-tabs { + display: flex; + flex-wrap: wrap; + gap: 0; + margin: 0 0 24px 0; + padding: 0; + border-bottom: 1px solid var(--primary-low, rgba(0, 0, 0, 0.1)); +} + +.cl-admin-tab { + padding: 10px 14px; + border: none; + background: none; + color: var(--primary-medium, #888); + font-size: 13px; + font-weight: 600; + cursor: pointer; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + transition: color 0.15s ease, border-color 0.15s ease; +} + +.cl-admin-tab:hover { + color: var(--primary, #333); +} + +.cl-admin-tab.active { + color: var(--tertiary, #0088cc); + border-bottom-color: var(--tertiary, #0088cc); +} + +/* Dark mode tabs */ +html.dark-scheme .cl-admin-tabs { + border-bottom-color: rgba(255, 255, 255, 0.1); +} + +html.dark-scheme .cl-admin-tab:hover { + color: var(--primary, #ddd); +} + +/* ── When tabs are active, remove separator borders ── */ + +.cl-tabs-active .row.setting[data-setting] { + border-top: none !important; + margin-top: 0 !important; + padding-top: 0 !important; +} + +/* ── Fallback: Separator borders when tabs are NOT active ── + (e.g. if JS fails to load or on older Discourse) */ + +/* All plugin settings spacing */ +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="community_landing_enabled"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="logo_"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="accent_"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="dark_bg_color"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="light_bg_color"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="scroll_"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="navbar_"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="hero_"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="stats_"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="stat_"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="about_"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="topics_"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="contributors_"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="groups_"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="show_app_ctas"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="ios_"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="android_"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="app_"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="footer_"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="custom_css"] { margin-bottom: 20px; } -/* ── Section separator borders ── - These selectors target the FIRST setting of each group to add - a visible border-top separator with extra padding above it. */ - -/* Branding section */ -.admin-detail .row.setting[data-setting="community_landing_logo_dark_url"], -/* Global Colors section */ -.admin-detail .row.setting[data-setting="community_landing_accent_color"], -/* Custom CSS section */ -.admin-detail .row.setting[data-setting="community_landing_custom_css"], -/* Scroll Animations section */ -.admin-detail .row.setting[data-setting="community_landing_scroll_animation"], -/* Row 1: Navbar */ -.admin-detail .row.setting[data-setting="community_landing_navbar_signin_label"], -/* Row 2: Hero Section */ -.admin-detail .row.setting[data-setting="community_landing_hero_title"], -/* Row 3: Premium Stats */ -.admin-detail .row.setting[data-setting="community_landing_stats_title"], -/* Row 4: About Community */ -.admin-detail .row.setting[data-setting="community_landing_about_enabled"], -/* Row 5: Trending Discussions */ -.admin-detail .row.setting[data-setting="community_landing_topics_enabled"], -/* Row 6: Top Creators */ -.admin-detail .row.setting[data-setting="community_landing_contributors_enabled"], -/* Row 7: Community Spaces */ -.admin-detail .row.setting[data-setting="community_landing_groups_enabled"], -/* Row 8: App Download CTA */ -.admin-detail .row.setting[data-setting="community_landing_show_app_ctas"], -/* Row 9: Footer */ -.admin-detail .row.setting[data-setting="community_landing_footer_description"] { +/* Section separator borders (fallback only) */ +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="logo_dark_url"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="accent_color"], +.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="hero_title"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="stats_title"], +.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="contributors_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="footer_description"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="custom_css"] { border-top: 2px solid rgba(0, 0, 0, 0.12); margin-top: 28px; padding-top: 24px; } -/* Dark mode admin panel support */ -.admin-detail.dark .row.setting[data-setting="community_landing_logo_dark_url"], -.admin-detail.dark .row.setting[data-setting="community_landing_accent_color"], -.admin-detail.dark .row.setting[data-setting="community_landing_custom_css"], -.admin-detail.dark .row.setting[data-setting="community_landing_scroll_animation"], -.admin-detail.dark .row.setting[data-setting="community_landing_navbar_signin_label"], -.admin-detail.dark .row.setting[data-setting="community_landing_hero_title"], -.admin-detail.dark .row.setting[data-setting="community_landing_stats_title"], -.admin-detail.dark .row.setting[data-setting="community_landing_about_enabled"], -.admin-detail.dark .row.setting[data-setting="community_landing_topics_enabled"], -.admin-detail.dark .row.setting[data-setting="community_landing_contributors_enabled"], -.admin-detail.dark .row.setting[data-setting="community_landing_groups_enabled"], -.admin-detail.dark .row.setting[data-setting="community_landing_show_app_ctas"], -.admin-detail.dark .row.setting[data-setting="community_landing_footer_description"], -html.dark-scheme .admin-detail .row.setting[data-setting="community_landing_logo_dark_url"], -html.dark-scheme .admin-detail .row.setting[data-setting="community_landing_accent_color"], -html.dark-scheme .admin-detail .row.setting[data-setting="community_landing_custom_css"], -html.dark-scheme .admin-detail .row.setting[data-setting="community_landing_scroll_animation"], -html.dark-scheme .admin-detail .row.setting[data-setting="community_landing_navbar_signin_label"], -html.dark-scheme .admin-detail .row.setting[data-setting="community_landing_hero_title"], -html.dark-scheme .admin-detail .row.setting[data-setting="community_landing_stats_title"], -html.dark-scheme .admin-detail .row.setting[data-setting="community_landing_about_enabled"], -html.dark-scheme .admin-detail .row.setting[data-setting="community_landing_topics_enabled"], -html.dark-scheme .admin-detail .row.setting[data-setting="community_landing_contributors_enabled"], -html.dark-scheme .admin-detail .row.setting[data-setting="community_landing_groups_enabled"], -html.dark-scheme .admin-detail .row.setting[data-setting="community_landing_show_app_ctas"], -html.dark-scheme .admin-detail .row.setting[data-setting="community_landing_footer_description"] { +/* Dark mode separators (fallback only) */ +html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="logo_dark_url"], +html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="accent_color"], +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="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="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="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="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="custom_css"] { border-top-color: rgba(255, 255, 255, 0.12); } + +/* ── Tab content spacing ── */ + +.cl-tabs-active .row.setting[data-setting] { + margin-bottom: 16px; +} diff --git a/assets/stylesheets/community_landing/landing.css b/assets/stylesheets/community_landing/landing.css index ffe3be5..3982c37 100644 --- a/assets/stylesheets/community_landing/landing.css +++ b/assets/stylesheets/community_landing/landing.css @@ -493,9 +493,10 @@ font-size: 0.82rem; font-weight: 600; color: var(--cl-text-strong); white-space: nowrap; } -.cl-creator-pill__activity { - font-size: 0.72rem; color: var(--cl-muted); white-space: nowrap; - font-weight: 500; +.cl-creator-pill__count { + font-size: 0.72rem; color: var(--cl-accent); white-space: nowrap; + font-weight: 700; margin-left: auto; padding-left: 0.4rem; + min-width: 1.2em; text-align: center; } /* ═══════════════════════════════════════════════════════════════════ diff --git a/config/locales/en.yml b/config/locales/en.yml index 467851c..9ae9ff4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,114 +1,129 @@ en: site_settings: # ── Master Switch ── - community_landing_enabled: "━━ LANDING PAGE ━━ — Enable the branded community landing page. When turned on, logged-out visitors see a custom welcome page instead of the default Discourse homepage. All sections below are configurable." + community_landing_enabled: "Enable the branded community landing page. When on, logged-out visitors see a custom welcome page instead of the default Discourse homepage. All sections below are configurable." # ── Branding: Logo ── - community_landing_logo_dark_url: "━━ BRANDING ━━ — Logo image URL for dark mode. Displayed in the navbar header and footer. Leave blank to show the site name as text instead." - community_landing_logo_light_url: "Logo image URL for light mode. If not set, the dark logo is used for both themes." - community_landing_logo_height: "Logo height in pixels (16–80). Applies to both the navbar and footer logos." - community_landing_footer_logo_url: "Override logo specifically for the footer. If not set, the navbar logo is reused." + logo_dark_url: "━━ BRANDING ━━ — Logo image URL for dark mode. Displayed in the navbar and footer. Leave blank to show the site name as text." + logo_light_url: "Logo image URL for light mode. If not set, the dark logo is used for both themes." + logo_height: "Logo height in pixels (16–80). Applies to both the navbar and footer logos." + footer_logo_url: "Override logo specifically for the footer. If not set, the navbar logo is reused." # ── Appearance: Color Scheme ── - community_landing_accent_color: "━━ GLOBAL COLORS ━━ — Primary accent color used across the entire landing page: buttons, links, highlights, gradients, and stat icons. Enter a hex value (e.g. #d4a24e)." - community_landing_accent_hover_color: "Accent color on hover states. Should be slightly lighter or darker than the primary accent. Hex value." - community_landing_dark_bg_color: "Background color for dark mode. Sets the overall page background when in dark theme. Hex value." - community_landing_light_bg_color: "Background color for light mode. Sets the overall page background when in light theme. Hex value." - - # ── Custom CSS ── - community_landing_custom_css: "━━ CUSTOM CSS ━━ — Inject your own CSS directly into the landing page. Use this for fine-tuning styles, overriding any default, or adding completely custom design touches. Paste raw CSS rules here (no <style> tags needed). This CSS loads after all other styles, giving it the highest priority." + accent_color: "━━ GLOBAL COLORS ━━ — Primary accent color used across the page: buttons, links, highlights, gradients, and stat icons. Hex value (e.g. #d4a24e)." + accent_hover_color: "Accent color on hover states. Should be slightly lighter or darker than the primary accent. Hex value." + dark_bg_color: "Overall page background color for dark mode. Hex value." + light_bg_color: "Overall page background color for light mode. Hex value." # ── Scroll Animations ── - community_landing_scroll_animation: "━━ SCROLL ANIMATIONS ━━ — Choose how sections animate into view as the visitor scrolls down the page. Options: fade_up (slide up from below), fade_in (appear in place), slide_left / slide_right (horizontal entrance), zoom_in (scale up from smaller), flip_up (3D rotation), or none to disable all scroll animations." + scroll_animation: "━━ SCROLL ANIMATIONS ━━ — How sections animate into view on scroll. Options: fade_up, fade_in, slide_left, slide_right, zoom_in, flip_up, or none." - # ── Navbar ── - community_landing_navbar_signin_label: "━━ ROW 1: NAVBAR ━━ — The fixed navigation bar at the top of the page with your logo, theme toggle, sign-in link, and join button. This setting controls the text for the sign-in link." - community_landing_navbar_join_label: "Text for the join/register call-to-action button in the navbar." - community_landing_navbar_bg_color: "Custom background color for the navbar when scrolled. Leave blank for the default frosted glass effect." - community_landing_navbar_border_style: "Border line style at the bottom of the navbar when scrolled. Choose none, solid, dashed, or dotted." + # ── 1. Navbar ── + navbar_signin_label: "━━ ROW 1: NAVBAR ━━ — Fixed navigation bar at the top with logo, theme toggle, sign-in link, and join button. This setting controls the sign-in link text." + navbar_join_label: "Text for the join/register CTA button in the navbar." + navbar_bg_color: "Custom background color for the navbar when scrolled. Leave blank for the default frosted glass effect." + navbar_border_style: "Border style at the bottom of the navbar when scrolled." - # ── Hero Section ── - community_landing_hero_title: "━━ ROW 2: HERO SECTION ━━ — The large welcome area at the top of the page with headline, subtitle, CTA buttons, and optional imagery. This setting is the main headline text. The last word is automatically highlighted with your accent color gradient." - community_landing_hero_subtitle: "Supporting text shown below the hero headline. Describe your community's purpose or value proposition." - community_landing_hero_card_enabled: "Wrap the hero content inside a rounded card container with a subtle border and shadow. Turn off for a flat, full-width hero layout." - community_landing_hero_background_image_url: "Full-bleed background image behind the hero section. In card mode, the image fills the card with a dark overlay. In flat mode, it covers the entire section." - community_landing_hero_image_urls: "Images displayed on the right side of the hero. Add up to 5 image URLs — a random one is shown on each page load. Click + to add rows." - community_landing_hero_image_max_height: "Maximum height in pixels for the hero image (100–1200). Prevents tall images from overwhelming the section." - community_landing_hero_primary_button_label: "Text on the primary (filled, accent-colored) call-to-action button." - community_landing_hero_primary_button_url: "URL the primary button links to. Use a relative path like /latest or an absolute URL." - community_landing_hero_secondary_button_label: "Text on the secondary (outlined) call-to-action button." - community_landing_hero_secondary_button_url: "URL the secondary button links to. Defaults to the login page." - community_landing_hero_bg_color: "Custom background color for the hero section wrapper. Leave blank for the default page background." - community_landing_hero_border_style: "Border line style at the bottom of the hero section. Choose none, solid, dashed, or dotted." + # ── 2. Hero Section ── + hero_title: "━━ ROW 2: HERO ━━ — Large welcome area at the top with headline, subtitle, CTA buttons, and optional imagery. This is the main headline text. The last word is highlighted with your accent color." + hero_subtitle: "Supporting text below the hero headline. Describe your community's purpose or value proposition." + hero_card_enabled: "Display the hero content inside a rounded card container with border and shadow. When off, the hero uses a flat full-width layout." + hero_background_image_url: "Full-bleed background image behind the hero section. In card mode, fills the card with a dark overlay. In flat mode, covers the entire section." + hero_image_urls: "Images displayed on the right side of the hero. Add up to 5 URLs — a random one is shown on each page load." + hero_image_max_height: "Maximum height in pixels for the hero image (100–1200)." + hero_primary_button_label: "Text on the primary (filled, accent-colored) CTA button." + hero_primary_button_url: "URL the primary button links to. Use a relative path like /latest or an absolute URL." + hero_secondary_button_label: "Text on the secondary (outlined) CTA button." + hero_secondary_button_url: "URL the secondary button links to." + hero_bg_dark: "Background color for the hero section in dark 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_border_style: "Border style at the bottom of the hero section." - # ── Stats Section ── - community_landing_stats_title: "━━ ROW 3: PREMIUM STATS ━━ — A full-width row displaying live community statistics (members, topics, posts, likes, chats) with animated counters and icons. Each card shows the icon and label on one line with the counter number below. This setting is the section heading text." - community_landing_stat_icon_color: "Color for all stat counter icons. Enter a hex value (e.g. #d4a24e). Matches your accent color by default." - community_landing_stat_members_label: "Custom label for the Members stat counter card." - community_landing_stat_topics_label: "Custom label for the Topics stat counter card." - community_landing_stat_posts_label: "Custom label for the Posts stat counter card." - community_landing_stat_likes_label: "Custom label for the Likes stat counter card." - community_landing_stat_chats_label: "Custom label for the Chats stat counter card. Shows total chat messages if the Chat plugin is active." - community_landing_stats_bg_color: "Custom background color for the stats section wrapper. Leave blank for default." - community_landing_stats_border_style: "Border line style at the bottom of the 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." + stat_icon_color: "Color for all stat counter icons. Hex value (e.g. #d4a24e)." + stat_members_label: "Custom label for the Members stat card." + stat_topics_label: "Custom label for the Topics stat card." + stat_posts_label: "Custom label for the Posts stat card." + stat_likes_label: "Custom label for the Likes stat card." + stat_chats_label: "Custom label for the Chats stat card. Shows total chat messages if the Chat plugin is active." + stats_bg_dark: "Background color for the stats section in dark mode. Leave blank for default." + stats_bg_light: "Background color for the stats section in light mode. Leave blank for default." + stats_min_height: "Minimum height for the stats section in pixels. Set to 0 for auto height." + stats_border_style: "Border style at the bottom of the stats section." - # ── About Section ── - community_landing_about_enabled: "━━ ROW 4: ABOUT COMMUNITY ━━ — A full-width gradient card with a bold heading, decorative quote icon, community description text, and author attribution (avatar, name, role). Supports a 3-color gradient background and an optional overlay image. Toggle this to show or hide the entire section." - community_landing_about_heading_enabled: "Show the bold heading text (e.g. 'About Community') at the top of the card. Turn off to start directly with the quote icon and description." - community_landing_about_heading: "The bold heading text displayed at the top of the About card (e.g. 'About Community', 'Our Story', 'Who We Are')." - community_landing_about_title: "Author or community name shown in the card's bottom attribution area, next to the avatar." - community_landing_about_role: "Subtitle below the author name (e.g. 'Community Manager', 'Founded 2020'). If blank, the site name is used." - community_landing_about_body: "Main body text for the About card. This is the community description. Supports basic HTML: p, a, strong, em, ul, li, br." - community_landing_about_image_url: "Small avatar image shown next to the author name in the card footer. Square images work best." - community_landing_about_gradient_start: "First color (left) of the About card's 3-color gradient background. Hex value (e.g. #fdf6ec)." - community_landing_about_gradient_mid: "Middle color of the About card gradient. Hex value (e.g. #fef9f0)." - community_landing_about_gradient_end: "Third color (right) of the About card gradient. Hex value (e.g. #fdf6ec)." - community_landing_about_background_image_url: "Background image layered on top of the gradient. Use a subtle pattern or texture. The gradient shows through any transparent areas." - community_landing_about_bg_color: "Custom background color for the about section wrapper (the area around the card). Leave blank for default." - community_landing_about_border_style: "Border line style at the bottom of the about section." + # ── 4. About Section ── + about_enabled: "━━ ROW 4: ABOUT ━━ — Show the About section: a gradient card with bold heading, decorative quote icon, community description, and author attribution (avatar, name, role). Supports a 3-color gradient background and optional overlay image." + about_heading_enabled: "Show the bold heading text at the top of the About card. Turn off to start with the quote icon and description." + about_heading: "The heading text at the top of the About card (e.g. 'About Community', 'Our Story')." + about_title: "Author or community name shown in the card's bottom attribution, next to the avatar." + about_role: "Subtitle below the author name (e.g. 'Community Manager'). If blank, the site name is used." + about_body: "Main body text for the About card. Supports basic HTML: p, a, strong, em, ul, li, br." + about_image_url: "Small avatar image shown next to the author name. Square images work best." + about_gradient_start: "First color (left) of the About card's 3-color gradient. Hex value." + about_gradient_mid: "Middle color of the About card gradient. Hex value." + about_gradient_end: "Third color (right) of the About card gradient. Hex value." + about_background_image_url: "Background image layered on top of the gradient. Use a subtle pattern or texture." + about_bg_dark: "Background color for the about section wrapper in dark mode. Leave blank for default." + about_bg_light: "Background color for the about section wrapper in light mode. Leave blank for default." + about_min_height: "Minimum height for the about section in pixels. Set to 0 for auto height." + about_border_style: "Border style at the bottom of the about section." - # ── Trending Discussions ── - community_landing_topics_enabled: "━━ ROW 5: TRENDING DISCUSSIONS ━━ — A horizontally scrollable row of topic cards showing the most active discussions. Each card displays the category badge, topic title, reply count, and like count — all pulled live from your forum. Supports drag-to-scroll on desktop and native swipe on mobile. Toggle to show or hide." - community_landing_topics_title: "Heading text displayed above the scrollable topic cards (e.g. 'Trending Discussions', 'Hot Topics')." - community_landing_topics_count: "Number of trending topic cards to display. Shows the most-replied topics from the last 30 days." - community_landing_topics_bg_color: "Custom background color for the trending discussions section wrapper. Leave blank for default." - community_landing_topics_border_style: "Border line style at the bottom of the trending section." + # ── 5. Trending Discussions ── + topics_enabled: "━━ ROW 5: TRENDING ━━ — Show the Trending Discussions section: a horizontally scrollable row of topic cards showing the most active discussions. Each card displays category badge, title, reply count, and like count — all live data. Supports drag-to-scroll and native swipe." + topics_title: "Heading text above the scrollable topic cards." + topics_count: "Number of trending topic cards to display." + topics_bg_dark: "Background color for the trending section in dark mode. Leave blank for default." + topics_bg_light: "Background color for the trending section in light mode. Leave blank for default." + 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." - # ── Top Creators ── - community_landing_contributors_enabled: "━━ ROW 6: TOP CREATORS ━━ — A row of pill-shaped badges showing your most active community members. Each pill displays the user's avatar, @username, and activity count (number of posts in the lookback period). Toggle to show or hide the entire section." - community_landing_contributors_title: "Heading text displayed above the creator pills (e.g. 'Top Creators', 'Most Active Members')." - community_landing_contributors_days: "Lookback period in days for calculating top contributors. A larger number captures a wider time range of activity." - community_landing_contributors_count: "Number of top contributor pills to display. Recommended: 6–12 for a balanced layout." - community_landing_contributors_bg_color: "Custom background color for the creators section wrapper. Leave blank for default." - community_landing_contributors_border_style: "Border line style at the bottom of the creators section." + # ── 6. Top 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_title: "Heading text above the creator pills." + contributors_days: "Lookback period in days for calculating top contributors." + contributors_count: "Number of top contributor pills to display. Recommended: 6–12." + 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." - # ── Community Spaces ── - community_landing_groups_enabled: "━━ ROW 7: COMMUNITY SPACES ━━ — A grid of colorful cards representing your public community groups. Each card shows a large colored icon (with the group's first letter if no flair is set), the group name, and member count. Only publicly visible, non-automatic groups are displayed. Toggle to show or hide." - community_landing_groups_title: "Heading text displayed above the group cards (e.g. 'Community Spaces', 'Our Groups')." - community_landing_groups_count: "Number of group cards to display. Only public, non-automatic groups are shown." - community_landing_groups_bg_color: "Custom background color for the community spaces section wrapper. Leave blank for default." - community_landing_groups_border_style: "Border line style at the bottom of the spaces section." + # ── 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_title: "Heading text above the group cards." + groups_count: "Number of group cards to display." + groups_bg_dark: "Background color for the spaces section in dark mode. Leave blank for default." + groups_bg_light: "Background color for the spaces section in light mode. Leave blank for default." + groups_min_height: "Minimum height for the spaces section in pixels. Set to 0 for auto height." + groups_border_style: "Border style at the bottom of the spaces section." - # ── App Download CTA ── - community_landing_show_app_ctas: "━━ ROW 8: APP DOWNLOAD CTA ━━ — A gradient banner promoting your mobile app with a headline, subtitle, download badges (App Store / Google Play), and an optional promotional image on the right side. The gradient, badge style, and all text are fully configurable. Requires at least one app store URL. Toggle to show or hide." - community_landing_ios_app_url: "Apple App Store URL for your iOS app. Leave blank to hide the iOS badge entirely." - community_landing_android_app_url: "Google Play Store URL for your Android app. Leave blank to hide the Android badge entirely." - community_landing_ios_app_badge_image_url: "Replace the entire iOS badge button with a custom image (e.g. an official App Store badge PNG/SVG). Leave blank to use the default icon+text button." - community_landing_android_app_badge_image_url: "Replace the entire Android badge button with a custom image (e.g. an official Google Play badge PNG/SVG). Leave blank to use the default icon+text button." - community_landing_app_badge_height: "Height of app download badges in pixels (30–80). Applies to both default and custom image badges." - community_landing_app_badge_style: "Border-radius style for default badges: rounded (soft corners), pill (fully rounded), or square (minimal rounding)." - community_landing_app_cta_headline: "Bold headline text in the app download banner (e.g. 'Get the best experience on our app')." - community_landing_app_cta_subtext: "Supporting text below the headline (e.g. 'Available free on iOS and Android')." - community_landing_app_cta_gradient_start: "First color (left) of the app CTA 3-color gradient background. Hex value." - community_landing_app_cta_gradient_mid: "Middle color of the app CTA gradient. Hex value." - community_landing_app_cta_gradient_end: "Third color (right) of the app CTA gradient. Hex value." - community_landing_app_cta_image_url: "Promotional image on the right side of the CTA (e.g. a phone mockup). Supports PNG for transparent backgrounds. The image extends above the gradient area." - community_landing_app_cta_bg_color: "Custom background color for the app CTA section wrapper (behind the gradient card). Leave blank for default." - community_landing_app_cta_border_style: "Border line style at the bottom of the app CTA section." + # ── 8. App Download CTA ── + show_app_ctas: "━━ ROW 8: APP CTA ━━ — Show the App Download CTA: a gradient banner promoting your mobile app with headline, subtitle, download badges (App Store / Google Play), and optional promotional image. Requires at least one app store URL." + ios_app_url: "Apple App Store URL for your iOS app. Leave blank to hide the iOS badge." + android_app_url: "Google Play Store URL for your Android app. Leave blank to hide the Android badge." + ios_app_badge_image_url: "Replace the iOS badge button with a custom image (e.g. official App Store badge). Leave blank for default." + android_app_badge_image_url: "Replace the Android badge button with a custom image (e.g. official Google Play badge). Leave blank for default." + app_badge_height: "Height of app download badges in pixels (30–80)." + app_badge_style: "Badge border-radius: rounded (soft corners), pill (fully rounded), or square (minimal rounding)." + app_cta_headline: "Bold headline text in the app download banner." + app_cta_subtext: "Supporting text below the headline." + app_cta_gradient_start: "First color (left) of the app CTA 3-color gradient. Hex value." + app_cta_gradient_mid: "Middle color of the app CTA gradient. Hex value." + app_cta_gradient_end: "Third color (right) of the app CTA gradient. Hex value." + app_cta_image_url: "Promotional image on the right side of the CTA (e.g. phone mockup). PNG for transparent backgrounds." + app_cta_bg_dark: "Background color for the app CTA section in dark mode. Leave blank for default." + app_cta_bg_light: "Background color for the app CTA section in light mode. Leave blank for default." + app_cta_min_height: "Minimum height for the app CTA section in pixels. Set to 0 for auto height." + app_cta_border_style: "Border style at the bottom of the app CTA section." - # ── Footer ── - community_landing_footer_description: "━━ ROW 9: FOOTER ━━ — The bottom of the page with your logo, navigation links, copyright notice, and optional description text. This setting adds a description paragraph between the app CTA and the footer bar. Useful for a brief community tagline or mission statement." - community_landing_footer_text: "Optional HTML text displayed inside the footer bar area. Supports: p, a, strong, em, ul, li, br." - community_landing_footer_links: 'Footer navigation links as a JSON array. Format: [{"label":"Terms","url":"/tos"},{"label":"Privacy","url":"/privacy"}]. Logo and links appear on the left, copyright on the right.' - community_landing_footer_bg_color: "Custom background color for the footer bar. Leave blank for the default footer background." - community_landing_footer_border_style: "Border line style at the top of the footer bar. Defaults to solid." + # ── 9. Footer ── + footer_description: "━━ ROW 9: FOOTER ━━ — Bottom of the page with logo, navigation links, copyright, and optional description. This adds a description paragraph above the footer bar." + footer_text: "Optional HTML text inside the footer bar. Supports: p, a, strong, em, ul, li, br." + footer_links: 'Footer navigation links as a JSON array. Format: [{"label":"Terms","url":"/tos"},{"label":"Privacy","url":"/privacy"}].' + footer_bg_dark: "Background color for the footer bar in dark mode. Leave blank for default." + footer_bg_light: "Background color for the footer bar in light mode. Leave blank for default." + footer_border_style: "Border style at the top of the footer bar." + + # ── Custom CSS ── + custom_css: "━━ CUSTOM CSS ━━ — Paste your own CSS rules directly into the landing page. This CSS loads after all other styles, giving it the highest priority. No style tags needed." diff --git a/config/settings.yml b/config/settings.yml index 3549151..0bd7909 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -9,41 +9,41 @@ plugins: # ══════════════════════════════════════════ # Branding: Logo # ══════════════════════════════════════════ - community_landing_logo_dark_url: + logo_dark_url: default: "" type: string - community_landing_logo_light_url: + logo_light_url: default: "" type: string - community_landing_logo_height: + logo_height: default: 30 type: integer min: 16 max: 80 - community_landing_footer_logo_url: + footer_logo_url: default: "" type: string # ══════════════════════════════════════════ # Appearance: Color Scheme # ══════════════════════════════════════════ - community_landing_accent_color: - default: "#d4a24e" - type: string - community_landing_accent_hover_color: - default: "#c4922e" - type: string - community_landing_dark_bg_color: - default: "#06060f" - type: string - community_landing_light_bg_color: - default: "#faf6f0" - type: string + accent_color: + default: "d4a24e" + type: color + accent_hover_color: + default: "c4922e" + type: color + dark_bg_color: + default: "06060f" + type: color + light_bg_color: + default: "faf6f0" + type: color # ══════════════════════════════════════════ # Scroll Animations # ══════════════════════════════════════════ - community_landing_scroll_animation: + scroll_animation: default: "fade_up" type: enum choices: @@ -55,26 +55,19 @@ plugins: - flip_up - none - # ══════════════════════════════════════════ - # Custom CSS - # ══════════════════════════════════════════ - community_landing_custom_css: - default: "" - type: string - # ══════════════════════════════════════════ # 1. Navbar # ══════════════════════════════════════════ - community_landing_navbar_signin_label: + navbar_signin_label: default: "Sign In" type: string - community_landing_navbar_join_label: + navbar_join_label: default: "Get Started" type: string - community_landing_navbar_bg_color: + navbar_bg_color: default: "" type: string - community_landing_navbar_border_style: + navbar_border_style: default: "none" type: enum choices: @@ -86,42 +79,50 @@ plugins: # ══════════════════════════════════════════ # 2. Hero Section # ══════════════════════════════════════════ - community_landing_hero_title: + hero_title: default: "Welcome to Our Creative HeadQuarters" type: string - community_landing_hero_subtitle: + hero_subtitle: default: "Are you ready to start your creative journey?" type: string - community_landing_hero_card_enabled: + hero_card_enabled: default: true type: bool - community_landing_hero_background_image_url: + hero_background_image_url: default: "" type: string - community_landing_hero_image_urls: + hero_image_urls: default: "" type: list - community_landing_hero_image_max_height: + hero_image_max_height: default: 500 type: integer min: 100 max: 1200 - community_landing_hero_primary_button_label: + hero_primary_button_label: default: "View Latest Topics" type: string - community_landing_hero_primary_button_url: + hero_primary_button_url: default: "/latest" type: string - community_landing_hero_secondary_button_label: + hero_secondary_button_label: default: "Explore Our Spaces" type: string - community_landing_hero_secondary_button_url: + hero_secondary_button_url: default: "/login" type: string - community_landing_hero_bg_color: + hero_bg_dark: default: "" type: string - community_landing_hero_border_style: + hero_bg_light: + default: "" + type: string + hero_min_height: + default: 0 + type: integer + min: 0 + max: 2000 + hero_border_style: default: "none" type: enum choices: @@ -133,31 +134,39 @@ plugins: # ══════════════════════════════════════════ # 3. Premium Stats Section # ══════════════════════════════════════════ - community_landing_stats_title: + stats_title: default: "Premium Stats" type: string - community_landing_stat_icon_color: - default: "#d4a24e" - type: string - community_landing_stat_members_label: + stat_icon_color: + default: "d4a24e" + type: color + stat_members_label: default: "Members" type: string - community_landing_stat_topics_label: + stat_topics_label: default: "Topics" type: string - community_landing_stat_posts_label: + stat_posts_label: default: "Posts" type: string - community_landing_stat_likes_label: + stat_likes_label: default: "Likes" type: string - community_landing_stat_chats_label: + stat_chats_label: default: "Chats" type: string - community_landing_stats_bg_color: + stats_bg_dark: default: "" type: string - community_landing_stats_border_style: + stats_bg_light: + default: "" + type: string + stats_min_height: + default: 0 + type: integer + min: 0 + max: 2000 + stats_border_style: default: "none" type: enum choices: @@ -169,43 +178,51 @@ plugins: # ══════════════════════════════════════════ # 4. About Community Section # ══════════════════════════════════════════ - community_landing_about_enabled: + about_enabled: default: true type: bool - community_landing_about_heading_enabled: + about_heading_enabled: default: true type: bool - community_landing_about_heading: + about_heading: default: "About Community" type: string - community_landing_about_title: + about_title: default: "Community Name" type: string - community_landing_about_role: + about_role: default: "" type: string - community_landing_about_body: + about_body: default: "" type: string - community_landing_about_image_url: + about_image_url: default: "" type: string - community_landing_about_gradient_start: - default: "#fdf6ec" - type: string - community_landing_about_gradient_mid: - default: "#fef9f0" - type: string - community_landing_about_gradient_end: - default: "#fdf6ec" - type: string - community_landing_about_background_image_url: + about_gradient_start: + default: "fdf6ec" + type: color + about_gradient_mid: + default: "fef9f0" + type: color + about_gradient_end: + default: "fdf6ec" + type: color + about_background_image_url: default: "" type: string - community_landing_about_bg_color: + about_bg_dark: default: "" type: string - community_landing_about_border_style: + about_bg_light: + default: "" + type: string + about_min_height: + default: 0 + type: integer + min: 0 + max: 2000 + about_border_style: default: "none" type: enum choices: @@ -217,19 +234,27 @@ plugins: # ══════════════════════════════════════════ # 5. Trending Discussions Section # ══════════════════════════════════════════ - community_landing_topics_enabled: + topics_enabled: default: true type: bool - community_landing_topics_title: + topics_title: default: "Trending Discussions" type: string - community_landing_topics_count: + topics_count: default: 5 type: integer - community_landing_topics_bg_color: + topics_bg_dark: default: "" type: string - community_landing_topics_border_style: + topics_bg_light: + default: "" + type: string + topics_min_height: + default: 0 + type: integer + min: 0 + max: 2000 + topics_border_style: default: "none" type: enum choices: @@ -241,22 +266,30 @@ plugins: # ══════════════════════════════════════════ # 6. Top Creators Section # ══════════════════════════════════════════ - community_landing_contributors_enabled: + contributors_enabled: default: true type: bool - community_landing_contributors_title: + contributors_title: default: "Top Creators" type: string - community_landing_contributors_days: + contributors_days: default: 90 type: integer - community_landing_contributors_count: + contributors_count: default: 10 type: integer - community_landing_contributors_bg_color: + contributors_bg_dark: default: "" type: string - community_landing_contributors_border_style: + contributors_bg_light: + default: "" + type: string + contributors_min_height: + default: 0 + type: integer + min: 0 + max: 2000 + contributors_border_style: default: "none" type: enum choices: @@ -268,19 +301,27 @@ plugins: # ══════════════════════════════════════════ # 7. Community Spaces Section # ══════════════════════════════════════════ - community_landing_groups_enabled: + groups_enabled: default: true type: bool - community_landing_groups_title: + groups_title: default: "Community Spaces" type: string - community_landing_groups_count: + groups_count: default: 5 type: integer - community_landing_groups_bg_color: + groups_bg_dark: default: "" type: string - community_landing_groups_border_style: + groups_bg_light: + default: "" + type: string + groups_min_height: + default: 0 + type: integer + min: 0 + max: 2000 + groups_border_style: default: "none" type: enum choices: @@ -292,55 +333,63 @@ plugins: # ══════════════════════════════════════════ # 8. App Download CTA Section # ══════════════════════════════════════════ - community_landing_show_app_ctas: + show_app_ctas: default: true type: bool - community_landing_ios_app_url: + ios_app_url: default: "" type: string - community_landing_android_app_url: + android_app_url: default: "" type: string - community_landing_ios_app_badge_image_url: + ios_app_badge_image_url: default: "" type: string - community_landing_android_app_badge_image_url: + android_app_badge_image_url: default: "" type: string - community_landing_app_badge_height: + app_badge_height: default: 45 type: integer min: 30 max: 80 - community_landing_app_badge_style: + app_badge_style: default: "rounded" type: enum choices: - rounded - pill - square - community_landing_app_cta_headline: + app_cta_headline: default: "Get the best experience on our app" type: string - community_landing_app_cta_subtext: + app_cta_subtext: default: "Available free on iOS and Android" type: string - community_landing_app_cta_gradient_start: - default: "#d4a24e" - type: string - community_landing_app_cta_gradient_mid: - default: "#c4922e" - type: string - community_landing_app_cta_gradient_end: - default: "#b8862e" - type: string - community_landing_app_cta_image_url: + app_cta_gradient_start: + default: "d4a24e" + type: color + app_cta_gradient_mid: + default: "c4922e" + type: color + app_cta_gradient_end: + default: "b8862e" + type: color + app_cta_image_url: default: "" type: string - community_landing_app_cta_bg_color: + app_cta_bg_dark: default: "" type: string - community_landing_app_cta_border_style: + app_cta_bg_light: + default: "" + type: string + app_cta_min_height: + default: 0 + type: integer + min: 0 + max: 2000 + app_cta_border_style: default: "none" type: enum choices: @@ -352,19 +401,22 @@ plugins: # ══════════════════════════════════════════ # 9. Footer # ══════════════════════════════════════════ - community_landing_footer_description: + footer_description: default: "" type: string - community_landing_footer_text: + footer_text: default: "" type: string - community_landing_footer_links: + footer_links: default: '[{"label":"Brand","url":"/about"},{"label":"Links","url":"/links"},{"label":"Terms","url":"/tos"},{"label":"Privacy","url":"/privacy"}]' type: string - community_landing_footer_bg_color: + footer_bg_dark: default: "" type: string - community_landing_footer_border_style: + footer_bg_light: + default: "" + type: string + footer_border_style: default: "solid" type: enum choices: @@ -372,3 +424,10 @@ plugins: - solid - dashed - dotted + + # ══════════════════════════════════════════ + # Custom CSS (last) + # ══════════════════════════════════════════ + custom_css: + default: "" + type: text diff --git a/lib/community_landing/data_fetcher.rb b/lib/community_landing/data_fetcher.rb new file mode 100644 index 0000000..5a5e03a --- /dev/null +++ b/lib/community_landing/data_fetcher.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module CommunityLanding + class DataFetcher + def self.fetch + s = SiteSetting + data = {} + + # Top contributors + data[:contributors] = if s.contributors_enabled + User + .joins(:posts) + .where(posts: { created_at: s.contributors_days.days.ago.. }) + .where.not(username: %w[system discobot]) + .where(active: true, staged: false) + .group("users.id") + .order("COUNT(posts.id) DESC") + .limit(s.contributors_count) + .select("users.*, COUNT(posts.id) AS post_count") + end + + # Public groups + data[:groups] = if s.groups_enabled + Group + .where(visibility_level: Group.visibility_levels[:public]) + .where(automatic: false) + .limit(s.groups_count) + end + + # Trending topics + data[:topics] = if s.topics_enabled + Topic + .listable_topics + .where(visible: true) + .where("topics.created_at > ?", 30.days.ago) + .order(posts_count: :desc) + .limit(s.topics_count) + .includes(:category, :user) + end + + # Aggregate stats + chat_count = 0 + begin + chat_count = Chat::Message.count if defined?(Chat::Message) + rescue + chat_count = 0 + end + + data[:stats] = { + members: User.real.count, + topics: Topic.listable_topics.count, + posts: Post.where(user_deleted: false).count, + likes: Post.sum(:like_count), + chats: chat_count, + } + + data + end + end +end diff --git a/lib/community_landing/helpers.rb b/lib/community_landing/helpers.rb new file mode 100644 index 0000000..297eb2e --- /dev/null +++ b/lib/community_landing/helpers.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module CommunityLanding + module Helpers + def e(text) + ERB::Util.html_escape(text.to_s) + end + + # Normalize color values — Discourse color picker stores without # + def hex(val) + return nil if val.blank? + v = val.to_s.delete("#") + v.present? ? "##{v}" : nil + end + + def hex_to_rgb(hex_val) + hex_val = hex_val.to_s.gsub("#", "") + return "0, 0, 0" unless hex_val.match?(/\A[0-9a-fA-F]{6}\z/) + "#{hex_val[0..1].to_i(16)}, #{hex_val[2..3].to_i(16)}, #{hex_val[4..5].to_i(16)}" + end + + # Inline style for section border + min-height (no background — handled by CSS) + def section_style(border_style, min_height = 0) + parts = [] + parts << "border-bottom: 1px #{border_style} var(--cl-border);" if border_style.present? && border_style != "none" + parts << "min-height: #{min_height}px;" if min_height.to_i > 0 + parts.any? ? " style=\"#{parts.join(' ')}\"" : "" + end + + def logo_img(url, alt, css_class, height) + "\"#{e(alt)}\"" + end + + def render_logo(dark_url, light_url, site_name, base_class, height) + if dark_url && light_url + logo_img(dark_url, site_name, "#{base_class} cl-logo--dark", height) + + logo_img(light_url, site_name, "#{base_class} cl-logo--light", height) + else + logo_img(dark_url || light_url, site_name, base_class, height) + end + end + end +end diff --git a/lib/community_landing/icons.rb b/lib/community_landing/icons.rb new file mode 100644 index 0000000..ae2f9c6 --- /dev/null +++ b/lib/community_landing/icons.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module CommunityLanding + module Icons + SUN_SVG = '' + MOON_SVG = '' + QUOTE_SVG = '' + + STAT_MEMBERS_SVG = '' + STAT_TOPICS_SVG = '' + STAT_POSTS_SVG = '' + STAT_LIKES_SVG = '' + STAT_CHATS_SVG = '' + + COMMENT_SVG = '' + HEART_SVG = '' + + IOS_BADGE_SVG = '' + ANDROID_BADGE_SVG = '' + end +end diff --git a/lib/community_landing/page_builder.rb b/lib/community_landing/page_builder.rb new file mode 100644 index 0000000..c414459 --- /dev/null +++ b/lib/community_landing/page_builder.rb @@ -0,0 +1,464 @@ +# frozen_string_literal: true + +module CommunityLanding + class PageBuilder + include Helpers + + def initialize(data:, css:, js:) + @data = data + @css = css + @js = js + @s = SiteSetting + @styles = StyleBuilder.new(@s) + end + + def build + html = +"" + html << render_head + html << "\n" + html << render_navbar + html << render_hero + html << render_stats + html << render_about + html << render_topics + html << render_contributors + html << render_groups + html << render_app_cta + html << render_footer_desc + html << render_footer + html << "\n" + html << "\n" + html + end + + private + + # ── ── + + def render_head + site_name = @s.title + anim_class = @s.scroll_animation rescue "fade_up" + anim_class = "none" if anim_class.blank? + og_logo = logo_dark_url || logo_light_url + + html = +"" + html << "\n\n\n" + html << "\n" + html << "\n" + html << "\n" + html << "#{e(@s.hero_title)} | #{e(site_name)}\n" + html << "\n" + html << "\n" + html << "\n" + html << "\n" + html << "\n" if og_logo + html << "\n" + html << "\n" + html << "\n" + html << @styles.color_overrides + html << @styles.section_backgrounds + + custom_css = @s.custom_css.presence rescue nil + html << "\n" if custom_css + + html << "\n" + html + end + + # ── 1. NAVBAR ── + + def render_navbar + site_name = @s.title + signin_label = @s.navbar_signin_label.presence || "Sign In" + join_label = @s.navbar_join_label.presence || "Get Started" + navbar_bg = hex(@s.navbar_bg_color) rescue nil + navbar_border = @s.navbar_border_style rescue "none" + + navbar_data = "" + navbar_data << " data-nav-bg=\"#{e(navbar_bg)}\"" if navbar_bg + navbar_data << " data-nav-border=\"#{e(navbar_border)}\"" if navbar_border && navbar_border != "none" + + html = +"" + html << "\n" + html + end + + # ── 2. HERO ── + + def render_hero + hero_card = @s.hero_card_enabled rescue true + hero_bg_img = @s.hero_background_image_url.presence + hero_border = @s.hero_border_style rescue "none" + hero_min_h = @s.hero_min_height rescue 0 + site_name = @s.title + + html = +"" + html << "
\n" + + if hero_bg_img && !hero_card + html << "
\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 << "
\n
\n" + + title_words = @s.hero_title.to_s.split(" ") + if title_words.length > 1 + html << "

#{e(title_words[0..-2].join(' '))} #{e(title_words.last)}

\n" + else + html << "

#{e(@s.hero_title)}

\n" + end + + html << "

#{e(@s.hero_subtitle)}

\n" + + primary_label = @s.hero_primary_button_label.presence || "View Latest Topics" + primary_url = @s.hero_primary_button_url.presence || "/latest" + secondary_label = @s.hero_secondary_button_label.presence || "Explore Our Spaces" + secondary_url = @s.hero_secondary_button_url.presence || login_url + + html << "
\n" + html << "#{e(primary_label)}\n" + html << "#{e(secondary_label)}\n" + html << "
\n
\n" + + hero_image_urls_raw = @s.hero_image_urls.presence + if hero_image_urls_raw + urls = hero_image_urls_raw.split("|").map(&:strip).reject(&:empty?).first(5) + if urls.any? + img_max_h = @s.hero_image_max_height rescue 500 + html << "
\n" + html << "\"#{e(site_name)}\"\n" + html << "
\n" + end + end + + html << "
\n" + html + end + + # ── 3. STATS ── + + def render_stats + stats = @data[:stats] + stats_title = @s.stats_title.presence || "Premium Stats" + border = @s.stats_border_style rescue "none" + min_h = @s.stats_min_height rescue 0 + + html = +"" + html << "
\n" + html << "

#{e(stats_title)}

\n" + html << "
\n" + html << stat_card(Icons::STAT_MEMBERS_SVG, stats[:members], @s.stat_members_label) + html << stat_card(Icons::STAT_TOPICS_SVG, stats[:topics], @s.stat_topics_label) + html << stat_card(Icons::STAT_POSTS_SVG, stats[:posts], @s.stat_posts_label) + html << stat_card(Icons::STAT_LIKES_SVG, stats[:likes], @s.stat_likes_label) + html << stat_card(Icons::STAT_CHATS_SVG, stats[:chats], @s.stat_chats_label) + html << "
\n
\n" + html + end + + # ── 4. ABOUT ── + + def render_about + return "" unless @s.about_enabled + + about_body = @s.about_body.presence || "" + about_image = @s.about_image_url.presence + about_role = @s.about_role.presence || @s.title + about_heading_on = @s.about_heading_enabled rescue true + about_heading = @s.about_heading.presence || "About Community" + border = @s.about_border_style rescue "none" + min_h = @s.about_min_height rescue 0 + + html = +"" + html << "
\n" + html << "
\n" + html << "

#{e(about_heading)}

\n" if about_heading_on + html << Icons::QUOTE_SVG + html << "
#{about_body}
\n" if about_body.present? + html << "
\n" + html << "\"\"\n" if about_image + html << "
\n" + html << "#{e(@s.about_title)}\n" + html << "#{e(about_role)}\n" + html << "
\n
\n
\n" + html + end + + # ── 5. TRENDING DISCUSSIONS ── + + def render_topics + topics = @data[:topics] + return "" unless @s.topics_enabled && topics&.any? + + border = @s.topics_border_style rescue "none" + min_h = @s.topics_min_height rescue 0 + + html = +"" + html << "
\n" + html << "

#{e(@s.topics_title)}

\n" + html << "
\n" + + topics.each do |topic| + topic_likes = topic.like_count rescue 0 + topic_replies = topic.posts_count.to_i + + html << "\n" + if topic.category + html << "#{e(topic.category.name)}\n" + end + html << "#{e(topic.title)}\n" + html << "
" + html << "#{Icons::COMMENT_SVG} #{topic_replies}" + html << "#{Icons::HEART_SVG} #{topic_likes}" + html << "
\n" + end + + html << "
\n
\n" + html + 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 << "
\n" + html << "

#{e(@s.contributors_title)}

\n" + html << "
\n" + + contributors.each do |user| + avatar_url = user.avatar_template.gsub("{size}", "120") + activity_count = user.attributes["post_count"].to_i rescue 0 + + html << "\n" + html << "\"#{e(user.username)}\"\n" + html << "@#{e(user.username)}\n" + html << "#{activity_count}\n" + html << "\n" + end + + html << "
\n
\n" + html + end + + # ── 7. COMMUNITY SPACES ── + + def render_groups + groups = @data[:groups] + return "" unless @s.groups_enabled && groups&.any? + + border = @s.groups_border_style rescue "none" + min_h = @s.groups_min_height rescue 0 + + html = +"" + html << "
\n" + html << "

#{e(@s.groups_title)}

\n" + html << "
\n" + + groups.each do |group| + display_name = group.name.tr("_-", " ").gsub(/\b\w/, &:upcase) + hue = group.name.bytes.sum % 360 + sat = 50 + (group.name.bytes.first.to_i % 20) + light = 40 + (group.name.bytes.last.to_i % 15) + + html << "\n" + html << "
" + if group.flair_url.present? + html << "\"\"" + else + html << "#{group.name[0].upcase}" + end + html << "
\n" + html << "#{e(display_name)}\n" + html << "#{group.user_count} members\n" + html << "
\n" + end + + html << "
\n
\n" + html + end + + # ── 8. APP CTA ── + + def render_app_cta + return "" unless @s.show_app_ctas && (@s.ios_app_url.present? || @s.android_app_url.present?) + + badge_h = @s.app_badge_height rescue 45 + badge_style = @s.app_badge_style rescue "rounded" + app_image = @s.app_cta_image_url.presence + ios_custom = @s.ios_app_badge_image_url.presence rescue nil + android_custom = @s.android_app_badge_image_url.presence rescue nil + border = @s.app_cta_border_style rescue "none" + min_h = @s.app_cta_min_height rescue 0 + + html = +"" + html << "
\n" + html << "
\n
\n" + html << "

#{e(@s.app_cta_headline)}

\n" + html << "

#{e(@s.app_cta_subtext)}

\n" if @s.app_cta_subtext.present? + html << "
\n" + + html << app_badge(:ios, @s.ios_app_url, ios_custom, badge_h, badge_style) if @s.ios_app_url.present? + html << app_badge(:android, @s.android_app_url, android_custom, badge_h, badge_style) if @s.android_app_url.present? + + html << "
\n
\n" + if app_image + html << "
\n\"App\n
\n" + end + html << "
\n
\n" + html + end + + # ── 9. FOOTER DESCRIPTION ── + + def render_footer_desc + return "" unless @s.footer_description.present? + + html = +"" + html << "\n" + html + end + + # ── 10. FOOTER ── + + def render_footer + site_name = @s.title + footer_border = @s.footer_border_style rescue "solid" + + style_parts = [] + style_parts << "border-top: 1px #{footer_border} var(--cl-border);" if footer_border && footer_border != "none" + style_attr = style_parts.any? ? " style=\"#{style_parts.join(' ')}\"" : "" + + html = +"" + html << "\n" + html + end + + # ── Shared helpers ── + + def stat_card(icon_svg, count, label) + "
\n" \ + "
\n" \ + "#{icon_svg}\n" \ + "#{e(label)}\n" \ + "
\n" \ + "0\n" \ + "
\n" + end + + def app_badge(platform, url, custom_img, badge_h, badge_style) + label = platform == :ios ? "App Store" : "Google Play" + icon = platform == :ios ? Icons::IOS_BADGE_SVG : Icons::ANDROID_BADGE_SVG + style_class = case badge_style + when "pill" then "cl-app-badge--pill" + when "square" then "cl-app-badge--square" + else "cl-app-badge--rounded" + end + + if custom_img + "" \ + "\"#{label}\"" \ + "\n" + else + "" \ + "#{icon}" \ + "#{label}" \ + "\n" + end + end + + def theme_toggle + "\n" + end + + def login_url + "/login" + end + + # ── Logo memoization ── + + def logo_dark_url + return @logo_dark_url if defined?(@logo_dark_url) + dark = @s.logo_dark_url.presence + light = @s.logo_light_url.presence + if dark.nil? && light.nil? + dark = @s.respond_to?(:logo_url) ? @s.logo_url.presence : nil + end + @logo_dark_url = dark + end + + def logo_light_url + return @logo_light_url if defined?(@logo_light_url) + @logo_light_url = @s.logo_light_url.presence + end + + def has_logo? + logo_dark_url.present? || logo_light_url.present? + end + + def logo_height + @logo_height ||= (@s.logo_height rescue 30) + end + end +end diff --git a/lib/community_landing/style_builder.rb b/lib/community_landing/style_builder.rb new file mode 100644 index 0000000..fdb7c60 --- /dev/null +++ b/lib/community_landing/style_builder.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +module CommunityLanding + class StyleBuilder + include Helpers + + def initialize(settings = SiteSetting) + @s = settings + end + + # CSS custom properties for accent colors, gradients, backgrounds + def color_overrides + accent = hex(@s.accent_color) || "#d4a24e" + accent_hover = hex(@s.accent_hover_color) || "#c4922e" + dark_bg = hex(@s.dark_bg_color) || "#06060f" + light_bg = hex(@s.light_bg_color) || "#faf6f0" + stat_icon = hex(@s.stat_icon_color) || accent + about_g1 = hex(@s.about_gradient_start) || "#fdf6ec" + about_g2 = hex(@s.about_gradient_mid) || "#fef9f0" + about_g3 = hex(@s.about_gradient_end) || "#fdf6ec" + about_bg_img = @s.about_background_image_url.presence + app_g1 = hex(@s.app_cta_gradient_start) || accent + app_g2 = hex(@s.app_cta_gradient_mid) || accent_hover + app_g3 = hex(@s.app_cta_gradient_end) || accent_hover + accent_rgb = hex_to_rgb(accent) + + about_bg_extra = about_bg_img ? ", url('#{about_bg_img}') center/cover no-repeat" : "" + + "\n" + end + + # Per-section dark/light background overrides + def section_backgrounds + css = +"" + sections = [ + ["#cl-hero", safe_hex(:hero_bg_dark), safe_hex(:hero_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-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-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)], + ] + + sections.each do |sel, dark_bg, light_bg| + next unless dark_bg || light_bg + if dark_bg + css << ":root #{sel}, [data-theme=\"dark\"] #{sel} { background: #{dark_bg}; }\n" + end + if light_bg + css << "[data-theme=\"light\"] #{sel} { background: #{light_bg}; }\n" + css << "@media (prefers-color-scheme: light) { :root:not([data-theme=\"dark\"]) #{sel} { background: #{light_bg}; } }\n" + end + end + + css.present? ? "\n" : "" + end + + private + + # Safe accessor — returns nil if the setting doesn't exist + def safe_hex(setting_name) + hex(@s.public_send(setting_name)) + rescue + nil + end + end +end diff --git a/plugin.rb b/plugin.rb index 3b50444..e893c44 100644 --- a/plugin.rb +++ b/plugin.rb @@ -2,9 +2,10 @@ # name: community-landing # about: Branded public landing page for unauthenticated visitors -# version: 2.1.0 -# authors: Community -# url: https://github.com/community/community-landing +# version: 2.3.0 +# authors: DPN MEDiA WORKS +# url: https://github.com/dpnmw/community-landing +# meta_url: https://dpnmediaworks.com enabled_site_setting :community_landing_enabled @@ -16,6 +17,12 @@ after_initialize do PLUGIN_DIR = File.expand_path("..", __FILE__) end + require_relative "lib/community_landing/icons" + require_relative "lib/community_landing/helpers" + require_relative "lib/community_landing/data_fetcher" + require_relative "lib/community_landing/style_builder" + require_relative "lib/community_landing/page_builder" + class ::CommunityLanding::LandingController < ::ApplicationController requires_plugin CommunityLanding::PLUGIN_NAME @@ -25,11 +32,13 @@ after_initialize do content_security_policy false def index - fetch_community_data + data = CommunityLanding::DataFetcher.fetch css = load_file("assets", "stylesheets", "community_landing", "landing.css") js = load_file("assets", "javascripts", "community_landing", "landing.js") + html = CommunityLanding::PageBuilder.new(data: data, css: css, js: js).build + base_url = Discourse.base_url csp = "default-src 'self' #{base_url}; " \ "script-src 'self' 'unsafe-inline'; " \ @@ -39,7 +48,7 @@ after_initialize do "frame-ancestors 'self'" response.headers["Content-Security-Policy"] = csp - render html: build_html(css, js).html_safe, layout: false, content_type: "text/html" + render html: html.html_safe, layout: false, content_type: "text/html" end private @@ -49,535 +58,6 @@ after_initialize do rescue StandardError => e "/* Error loading #{path_parts.last}: #{e.message} */" end - - def fetch_community_data - s = SiteSetting - - if s.community_landing_contributors_enabled - @top_contributors = User - .joins(:posts) - .where(posts: { created_at: s.community_landing_contributors_days.days.ago.. }) - .where.not(username: %w[system discobot]) - .where(active: true, staged: false) - .group("users.id") - .order("COUNT(posts.id) DESC") - .limit(s.community_landing_contributors_count) - .select("users.*, COUNT(posts.id) AS post_count") - end - - if s.community_landing_groups_enabled - @groups = Group - .where(visibility_level: Group.visibility_levels[:public]) - .where(automatic: false) - .limit(s.community_landing_groups_count) - end - - if s.community_landing_topics_enabled - @hot_topics = Topic - .listable_topics - .where(visible: true) - .where("topics.created_at > ?", 30.days.ago) - .order(posts_count: :desc) - .limit(s.community_landing_topics_count) - .includes(:category, :user) - end - - chat_count = 0 - begin - chat_count = Chat::Message.count if defined?(Chat::Message) - rescue - chat_count = 0 - end - - @stats = { - members: User.real.count, - topics: Topic.listable_topics.count, - posts: Post.where(user_deleted: false).count, - likes: Post.sum(:like_count), - chats: chat_count, - } - end - - def e(text) - ERB::Util.html_escape(text.to_s) - end - - # ── SVG Icons ── - - SUN_SVG = '' - MOON_SVG = '' - QUOTE_SVG = '' - - STAT_MEMBERS_SVG = '' - STAT_TOPICS_SVG = '' - STAT_POSTS_SVG = '' - STAT_LIKES_SVG = '' - STAT_CHATS_SVG = '' - - COMMENT_SVG = '' - HEART_SVG = '' - - IOS_BADGE_SVG = '' - ANDROID_BADGE_SVG = '' - - def hex_to_rgb(hex) - hex = hex.to_s.gsub("#", "") - return "0, 0, 0" unless hex.match?(/\A[0-9a-fA-F]{6}\z/) - "#{hex[0..1].to_i(16)}, #{hex[2..3].to_i(16)}, #{hex[4..5].to_i(16)}" - end - - def build_color_overrides(s) - accent = s.community_landing_accent_color.presence || "#d4a24e" - accent_hover = s.community_landing_accent_hover_color.presence || "#c4922e" - dark_bg = s.community_landing_dark_bg_color.presence || "#06060f" - light_bg = s.community_landing_light_bg_color.presence || "#faf6f0" - stat_icon_color = s.community_landing_stat_icon_color.presence || accent - about_g1 = s.community_landing_about_gradient_start.presence || "#fdf6ec" - about_g2 = s.community_landing_about_gradient_mid.presence || "#fef9f0" - about_g3 = s.community_landing_about_gradient_end.presence || "#fdf6ec" - about_bg_img = s.community_landing_about_background_image_url.presence - app_g1 = s.community_landing_app_cta_gradient_start.presence || accent - app_g2 = s.community_landing_app_cta_gradient_mid.presence || accent_hover - app_g3 = s.community_landing_app_cta_gradient_end.presence || accent_hover - accent_rgb = hex_to_rgb(accent) - anim = s.community_landing_scroll_animation rescue "fade_up" - - about_bg_extra = about_bg_img ? ", url('#{about_bg_img}') center/cover no-repeat" : "" - - "\n" - end - - # ── Section row style helpers ── - - def section_style(bg_color, border_style) - parts = [] - parts << "background: #{bg_color};" if bg_color.present? - parts << "border-bottom: 1px #{border_style} var(--cl-border);" if border_style.present? && border_style != "none" - parts.any? ? " style=\"#{parts.join(" ")}\"" : "" - end - - # ── Logo helpers ── - - def logo_img(url, alt, css_class, height) - "\"#{e(alt)}\"" - end - - def render_logo(dark_url, light_url, site_name, base_class, height) - if dark_url && light_url - logo_img(dark_url, site_name, "#{base_class} cl-logo--dark", height) + - logo_img(light_url, site_name, "#{base_class} cl-logo--light", height) - else - logo_img(dark_url || light_url, site_name, base_class, height) - end - end - - def build_html(css, js) - s = SiteSetting - site_name = s.title - login_url = "/login" - anim_class = s.community_landing_scroll_animation rescue "fade_up" - anim_class = "none" if anim_class.blank? - - # Logo URLs - logo_dark_url = s.community_landing_logo_dark_url.presence - logo_light_url = s.community_landing_logo_light_url.presence - if logo_dark_url.nil? && logo_light_url.nil? - fallback = s.respond_to?(:logo_url) ? s.logo_url.presence : nil - logo_dark_url = fallback - end - has_logo = logo_dark_url.present? || logo_light_url.present? - logo_h = s.community_landing_logo_height rescue 30 - og_logo = logo_dark_url || logo_light_url - footer_logo_url = s.community_landing_footer_logo_url.presence - - html = +"" - html << "\n\n\n" - html << "\n" - html << "\n" - html << "\n" - html << "#{e(s.community_landing_hero_title)} | #{e(site_name)}\n" - html << "\n" - html << "\n" - html << "\n" - html << "\n" - html << "\n" if og_logo - html << "\n" - html << "\n" - html << "\n" - html << build_color_overrides(s) - - # Custom CSS injection - custom_css = s.community_landing_custom_css.presence rescue nil - if custom_css - html << "\n" - end - - html << "\n\n" - - signin_label = s.community_landing_navbar_signin_label.presence || "Sign In" - join_label = s.community_landing_navbar_join_label.presence || "Get Started" - - # ── 1. NAVBAR ── - navbar_bg = s.community_landing_navbar_bg_color.presence rescue nil - navbar_border = s.community_landing_navbar_border_style rescue "none" - navbar_data = "" - navbar_data << " data-nav-bg=\"#{e(navbar_bg)}\"" if navbar_bg - navbar_data << " data-nav-border=\"#{e(navbar_border)}\"" if navbar_border && navbar_border != "none" - html << "\n" - - # ── 2. HERO ── - hero_card = s.community_landing_hero_card_enabled rescue true - hero_bg_img = s.community_landing_hero_background_image_url.presence - hero_bg_color = s.community_landing_hero_bg_color.presence - hero_border = s.community_landing_hero_border_style rescue "none" - - hero_section_style = section_style(hero_bg_color, hero_border) - html << "
\n" - - if hero_bg_img && !hero_card - html << "
\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 << "
\n" - html << "
\n" - - title_words = s.community_landing_hero_title.to_s.split(" ") - if title_words.length > 1 - html << "

#{e(title_words[0..-2].join(" "))} #{e(title_words.last)}

\n" - else - html << "

#{e(s.community_landing_hero_title)}

\n" - end - - html << "

#{e(s.community_landing_hero_subtitle)}

\n" - - primary_label = s.community_landing_hero_primary_button_label.presence || "View Latest Topics" - primary_url = s.community_landing_hero_primary_button_url.presence || "/latest" - secondary_label = s.community_landing_hero_secondary_button_label.presence || "Explore Our Spaces" - secondary_url = s.community_landing_hero_secondary_button_url.presence || login_url - - html << "
\n" - html << "#{e(primary_label)}\n" - html << "#{e(secondary_label)}\n" - html << "
\n" - html << "
\n" - - hero_image_urls_raw = s.community_landing_hero_image_urls.presence - if hero_image_urls_raw - urls = hero_image_urls_raw.split("|").map(&:strip).reject(&:empty?).first(5) - if urls.any? - img_max_h = s.community_landing_hero_image_max_height rescue 500 - html << "
\n" - html << "\"#{e(site_name)}\"\n" - html << "
\n" - end - end - - html << "
\n" - - # ── 3. PREMIUM STATS ── - stats_title = s.community_landing_stats_title.presence || "Premium Stats" - stats_bg = s.community_landing_stats_bg_color.presence - stats_border = s.community_landing_stats_border_style rescue "none" - html << "
\n" - html << "

#{e(stats_title)}

\n" - html << "
\n" - html << stats_counter_card(STAT_MEMBERS_SVG, @stats[:members], s.community_landing_stat_members_label) - html << stats_counter_card(STAT_TOPICS_SVG, @stats[:topics], s.community_landing_stat_topics_label) - html << stats_counter_card(STAT_POSTS_SVG, @stats[:posts], s.community_landing_stat_posts_label) - html << stats_counter_card(STAT_LIKES_SVG, @stats[:likes], s.community_landing_stat_likes_label) - html << stats_counter_card(STAT_CHATS_SVG, @stats[:chats], s.community_landing_stat_chats_label) - html << "
\n
\n" - - # ── 4. ABOUT COMMUNITY ── - if s.community_landing_about_enabled - about_body = s.community_landing_about_body.presence || "" - about_image = s.community_landing_about_image_url.presence - about_role = s.community_landing_about_role.presence || site_name - about_heading_on = s.community_landing_about_heading_enabled rescue true - about_heading = s.community_landing_about_heading.presence || "About Community" - about_bg = s.community_landing_about_bg_color.presence - about_border = s.community_landing_about_border_style rescue "none" - - html << "
\n" - html << "
\n" - html << "

#{e(about_heading)}

\n" if about_heading_on - html << QUOTE_SVG - if about_body.present? - html << "
#{about_body}
\n" - end - html << "
\n" - if about_image - html << "\"\"\n" - end - html << "
\n" - html << "#{e(s.community_landing_about_title)}\n" - html << "#{e(about_role)}\n" - html << "
\n" - html << "
\n" - html << "
\n" - end - - # ── 5. TRENDING DISCUSSIONS ── - if s.community_landing_topics_enabled && @hot_topics&.any? - topics_bg = s.community_landing_topics_bg_color.presence - topics_border = s.community_landing_topics_border_style rescue "none" - html << "
\n" - html << "

#{e(s.community_landing_topics_title)}

\n" - html << "
\n" - @hot_topics.each do |topic| - topic_likes = topic.like_count rescue 0 - topic_replies = topic.posts_count.to_i - html << "\n" - if topic.category - html << "#{e(topic.category.name)}\n" - end - html << "#{e(topic.title)}\n" - html << "
" - html << "#{COMMENT_SVG} #{topic_replies}" - html << "#{HEART_SVG} #{topic_likes}" - html << "
" - html << "
\n" - end - html << "
\n
\n" - end - - # ── 6. TOP CREATORS ── - if s.community_landing_contributors_enabled && @top_contributors&.any? - contrib_bg = s.community_landing_contributors_bg_color.presence - contrib_border = s.community_landing_contributors_border_style rescue "none" - html << "
\n" - html << "

#{e(s.community_landing_contributors_title)}

\n" - html << "
\n" - @top_contributors.each do |user| - avatar_url = user.avatar_template.gsub("{size}", "120") - activity_count = user.attributes["post_count"].to_i rescue 0 - html << "\n" - html << "\"#{e(user.username)}\"\n" - html << "@#{e(user.username)}\n" - html << "#{activity_count} Activity\n" - html << "\n" - end - html << "
\n
\n" - end - - # ── 7. COMMUNITY SPACES ── - if s.community_landing_groups_enabled && @groups&.any? - groups_bg = s.community_landing_groups_bg_color.presence - groups_border = s.community_landing_groups_border_style rescue "none" - html << "
\n" - html << "

#{e(s.community_landing_groups_title)}

\n" - html << "
\n" - @groups.each do |group| - display_name = group.name.tr("_-", " ").gsub(/\b\w/, &:upcase) - hue = group.name.bytes.sum % 360 - sat = 50 + (group.name.bytes.first.to_i % 20) - light = 40 + (group.name.bytes.last.to_i % 15) - html << "\n" - html << "
" - if group.flair_url.present? - html << "\"\"" - else - html << "#{group.name[0].upcase}" - end - html << "
\n" - html << "#{e(display_name)}\n" - html << "#{group.user_count} members\n" - html << "
\n" - end - html << "
\n
\n" - end - - # ── 8. APP CTA ── - if s.community_landing_show_app_ctas && (s.community_landing_ios_app_url.present? || s.community_landing_android_app_url.present?) - badge_h = s.community_landing_app_badge_height rescue 45 - badge_style = s.community_landing_app_badge_style rescue "rounded" - app_image = s.community_landing_app_cta_image_url.presence - ios_custom = s.community_landing_ios_app_badge_image_url.presence rescue nil - android_custom = s.community_landing_android_app_badge_image_url.presence rescue nil - app_bg = s.community_landing_app_cta_bg_color.presence - app_border = s.community_landing_app_cta_border_style rescue "none" - - html << "
\n" - html << "
\n" - html << "
\n" - html << "

#{e(s.community_landing_app_cta_headline)}

\n" - html << "

#{e(s.community_landing_app_cta_subtext)}

\n" if s.community_landing_app_cta_subtext.present? - html << "
\n" - if s.community_landing_ios_app_url.present? - if ios_custom - style_class = case badge_style - when "pill" then "cl-app-badge--pill" - when "square" then "cl-app-badge--square" - else "cl-app-badge--rounded" - end - html << "" - html << "\"App" - html << "\n" - else - html << "" - html << "#{IOS_BADGE_SVG}" - html << "App Store" - html << "\n" - end - end - if s.community_landing_android_app_url.present? - if android_custom - style_class = case badge_style - when "pill" then "cl-app-badge--pill" - when "square" then "cl-app-badge--square" - else "cl-app-badge--rounded" - end - html << "" - html << "\"Google" - html << "\n" - else - html << "" - html << "#{ANDROID_BADGE_SVG}" - html << "Google Play" - html << "\n" - end - end - html << "
\n" - html << "
\n" - if app_image - html << "
\n" - html << "\"App\n" - html << "
\n" - end - html << "
\n" - html << "
\n" - end - - # ── 9. FOOTER DESCRIPTION ── - if s.community_landing_footer_description.present? - html << "\n" - end - - # ── 10. FOOTER ── - footer_bg = s.community_landing_footer_bg_color.presence rescue nil - footer_border = s.community_landing_footer_border_style rescue "solid" - footer_style_parts = [] - footer_style_parts << "background: #{footer_bg};" if footer_bg - footer_style_parts << "border-top: 1px #{footer_border} var(--cl-border);" if footer_border && footer_border != "none" - footer_style_attr = footer_style_parts.any? ? " style=\"#{footer_style_parts.join(" ")}\"" : "" - html << "\n" - - html << "\n" - html << "\n" - html - end - - def stats_counter_card(icon_svg, count, label) - "
\n" \ - "
\n" \ - "#{icon_svg}\n" \ - "#{e(label)}\n" \ - "
\n" \ - "0\n" \ - "
\n" - end - end Discourse::Application.routes.prepend do