diff --git a/assets/javascripts/community_landing/landing.js b/assets/javascripts/community_landing/landing.js index d865342..70736a7 100644 --- a/assets/javascripts/community_landing/landing.js +++ b/assets/javascripts/community_landing/landing.js @@ -211,4 +211,19 @@ }); } + // ═══════════════════════════════════════════════════════════════════ + // 8. FAQ EXCLUSIVE ACCORDION + // ═══════════════════════════════════════════════════════════════════ + $$("details[data-faq-exclusive]").forEach(function (detail) { + detail.addEventListener("toggle", function () { + if (detail.open) { + $$("details[data-faq-exclusive]").forEach(function (other) { + if (other !== detail && other.open) { + other.removeAttribute("open"); + } + }); + } + }); + }); + })(); diff --git a/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js b/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js index 6141b0c..5da0b4e 100644 --- a/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js +++ b/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js @@ -6,6 +6,8 @@ const TABS = [ label: "Settings", settings: new Set([ "community_landing_enabled", + "section_order", "custom_css", + "meta_description", "og_image_url", "favicon_url", "json_ld_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", "staggered_reveal_enabled", "dynamic_background_enabled", @@ -20,7 +22,9 @@ const TABS = [ "navbar_signin_color_dark", "navbar_signin_color_light", "navbar_join_label", "navbar_join_enabled", "navbar_join_color_dark", "navbar_join_color_light", - "navbar_bg_color", "navbar_border_style" + "navbar_bg_color", "navbar_border_style", + "social_twitter_url", "social_facebook_url", "social_instagram_url", + "social_youtube_url", "social_tiktok_url", "social_github_url" ]) }, { @@ -44,6 +48,18 @@ const TABS = [ "contributors_days", "contributors_count" ]) }, + { + id: "participation", + label: "Participation", + settings: new Set([ + "participation_enabled", "participation_title_enabled", + "participation_title", "participation_bio_max_length", + "participation_icon_color", + "participation_card_bg_dark", "participation_card_bg_light", + "participation_bg_dark", "participation_bg_light", + "participation_min_height", "participation_border_style" + ]) + }, { id: "stats", label: "Stats", @@ -79,12 +95,14 @@ const TABS = [ }, { id: "groups", - label: "Spaces", + label: "Spaces & FAQ", settings: new Set([ "groups_enabled", "groups_title_enabled", "groups_title", "groups_count", "groups_selected", + "groups_show_description", "groups_description_max_length", "groups_card_bg_dark", "groups_card_bg_light", - "groups_bg_dark", "groups_bg_light", "groups_min_height", "groups_border_style" + "groups_bg_dark", "groups_bg_light", "groups_min_height", "groups_border_style", + "faq_enabled", "faq_title_enabled", "faq_title", "faq_items" ]) }, { @@ -95,7 +113,9 @@ const TABS = [ "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_gradient_start_dark", "app_cta_gradient_start_light", + "app_cta_gradient_mid_dark", "app_cta_gradient_mid_light", + "app_cta_gradient_end_dark", "app_cta_gradient_end_light", "app_cta_image_url", "app_cta_bg_dark", "app_cta_bg_light", "app_cta_min_height", "app_cta_border_style" ]) @@ -121,6 +141,9 @@ const BG_PAIRS = [ ["hero_bg_dark", "hero_bg_light"], ["hero_card_bg_dark", "hero_card_bg_light"], ["contributors_pill_bg_dark", "contributors_pill_bg_light"], + // Participation + ["participation_card_bg_dark", "participation_card_bg_light"], + ["participation_bg_dark", "participation_bg_light"], // Stats ["stat_card_bg_dark", "stat_card_bg_light"], ["stats_bg_dark", "stats_bg_light"], @@ -134,6 +157,9 @@ const BG_PAIRS = [ ["groups_card_bg_dark", "groups_card_bg_light"], ["groups_bg_dark", "groups_bg_light"], // App CTA + ["app_cta_gradient_start_dark", "app_cta_gradient_start_light"], + ["app_cta_gradient_mid_dark", "app_cta_gradient_mid_light"], + ["app_cta_gradient_end_dark", "app_cta_gradient_end_light"], ["app_cta_bg_dark", "app_cta_bg_light"], // Footer ["footer_bg_dark", "footer_bg_light"], @@ -159,8 +185,8 @@ function applyTabFilter() { if (!tab) return; container.querySelectorAll(".row.setting[data-setting]").forEach((row) => { - // Keep merged light rows permanently hidden - if (row.classList.contains("cl-merged-hidden")) return; + // Skip rows inside a merge wrapper — handled at wrapper level + if (row.closest(".cl-merge-wrapper")) return; const name = row.getAttribute("data-setting"); row.classList.toggle( "cl-tab-hidden", @@ -168,6 +194,17 @@ function applyTabFilter() { ); }); + // Handle merge wrappers — show/hide based on dark row's setting + container.querySelectorAll(".cl-merge-wrapper").forEach((wrapper) => { + const darkRow = wrapper.querySelector(".cl-merged-dark"); + if (!darkRow) return; + const name = darkRow.getAttribute("data-setting"); + wrapper.classList.toggle( + "cl-tab-hidden", + !filterActive && !tab.settings.has(name) + ); + }); + // Update filter-active dimming on native nav or standalone tab bar const nativeNav = document.querySelector(".d-nav-submenu__tabs"); if (nativeNav) { @@ -256,6 +293,17 @@ function cleanupTabs() { container.querySelectorAll(".cl-tab-hidden").forEach((el) => { el.classList.remove("cl-tab-hidden"); }); + + // Unwrap merge wrappers — restore rows to their original position + container.querySelectorAll(".cl-merge-wrapper").forEach((wrapper) => { + const parent = wrapper.parentNode; + while (wrapper.firstChild) { + const child = wrapper.firstChild; + child.classList.remove("cl-merged-dark", "cl-merged-light"); + parent.insertBefore(child, wrapper); + } + wrapper.remove(); + }); } // Reset state @@ -264,8 +312,9 @@ function cleanupTabs() { } /** - * Merge dark/light bg color pairs into a single row. - * Moves the light setting-value into the dark row and hides the light row. + * Merge dark/light bg color pairs into a single visual row. + * Uses a CSS wrapper approach — both rows stay intact in the DOM + * (preserving Ember bindings and undo/reset buttons). */ function mergeBgPairs() { const container = getContainer(); @@ -276,55 +325,41 @@ function mergeBgPairs() { const lightRow = container.querySelector(`.row.setting[data-setting="${lightName}"]`); if (!darkRow || !lightRow) return; // Already merged - if (darkRow.querySelector(".cl-merged-value")) return; + if (darkRow.classList.contains("cl-merged-dark")) return; - const lightValue = lightRow.querySelector(".setting-value"); - const lightLabel = lightRow.querySelector(".setting-label"); - if (!lightValue) return; - - // Rename the dark row label to just show the base name (e.g. "Hero BG" instead of "Hero BG dark") + // Rename the dark row label (remove " dark" suffix) const darkH3 = darkRow.querySelector(".setting-label h3"); if (darkH3) { darkH3.textContent = darkH3.textContent.replace(/\s*dark$/i, "").trim(); } - // Create a wrapper that holds both color pickers side by side + // Add "Dark" / "Light" labels to each row's setting-value const darkValue = darkRow.querySelector(".setting-value"); - if (!darkValue) return; + const lightValue = lightRow.querySelector(".setting-value"); + if (darkValue && !darkValue.querySelector(".cl-color-col__label")) { + const lbl = document.createElement("span"); + lbl.className = "cl-color-col__label"; + lbl.textContent = "Dark"; + darkValue.insertBefore(lbl, darkValue.firstChild); + } + if (lightValue && !lightValue.querySelector(".cl-color-col__label")) { + const lbl = document.createElement("span"); + lbl.className = "cl-color-col__label"; + lbl.textContent = "Light"; + lightValue.insertBefore(lbl, lightValue.firstChild); + } - // Wrap existing dark value + // Wrap both rows in a flex container const wrapper = document.createElement("div"); - wrapper.className = "cl-merged-value"; + wrapper.className = "cl-merge-wrapper"; + darkRow.parentNode.insertBefore(wrapper, darkRow); + wrapper.appendChild(darkRow); + wrapper.appendChild(lightRow); - const darkCol = document.createElement("div"); - darkCol.className = "cl-color-col"; - const darkLbl = document.createElement("span"); - darkLbl.className = "cl-color-col__label"; - darkLbl.textContent = "Dark"; - darkCol.appendChild(darkLbl); - // Move dark value's children into the column - while (darkValue.firstChild) { - darkCol.appendChild(darkValue.firstChild); - } - - const lightCol = document.createElement("div"); - lightCol.className = "cl-color-col"; - const lightLbl = document.createElement("span"); - lightLbl.className = "cl-color-col__label"; - lightLbl.textContent = "Light"; - lightCol.appendChild(lightLbl); - // Move light value's children into the column - while (lightValue.firstChild) { - lightCol.appendChild(lightValue.firstChild); - } - - wrapper.appendChild(darkCol); - wrapper.appendChild(lightCol); - darkValue.appendChild(wrapper); - - // Hide the now-empty light row permanently - lightRow.classList.add("cl-merged-hidden"); - lightRow.style.display = "none"; + // Mark rows for CSS styling + darkRow.classList.add("cl-merged-dark"); + lightRow.classList.add("cl-merged-light"); + // Light row is NOT hidden — it stays in the DOM with full Ember bindings }); } diff --git a/assets/stylesheets/community_landing/admin.css b/assets/stylesheets/community_landing/admin.css index 34caa67..fbf99fd 100644 --- a/assets/stylesheets/community_landing/admin.css +++ b/assets/stylesheets/community_landing/admin.css @@ -82,16 +82,35 @@ html.dark-scheme .cl-admin-tabs .cl-admin-tab:hover { color: var(--primary, #ddd); } -/* ── Merged dark/light color pairs (two pickers in one row) ── */ +/* ── Merged dark/light color pairs (wrapper approach) ── */ -.cl-merged-value { +.cl-merge-wrapper { display: flex; - gap: 24px; + gap: 16px; + width: 100%; + padding-bottom: 20px; } -.cl-color-col { +.cl-merge-wrapper > .row.setting { flex: 1; min-width: 0; + padding-bottom: 0 !important; + margin-bottom: 0 !important; +} + +/* Hide the light row's label + description — dark row's label covers both */ +.cl-merge-wrapper > .cl-merged-light > .setting-label { + display: none; +} + +.cl-merge-wrapper > .cl-merged-light .desc { + display: none; +} + +/* Light row's value area fills the full width since label is hidden */ +.cl-tabs-active .cl-merge-wrapper > .cl-merged-light > .setting-value { + width: 100%; + float: none; } .cl-color-col__label { @@ -105,9 +124,13 @@ html.dark-scheme .cl-admin-tabs .cl-admin-tab:hover { } @media (max-width: 767px) { - .cl-merged-value { + .cl-merge-wrapper { flex-direction: column; - gap: 12px; + gap: 0; + } + + .cl-merge-wrapper > .cl-merged-light > .setting-label { + display: none; } } @@ -125,6 +148,12 @@ html.dark-scheme .cl-admin-tabs .cl-admin-tab:hover { /* 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="section_order"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="custom_css"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="meta_"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="og_image_url"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="favicon_url"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="json_ld_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"], @@ -137,25 +166,34 @@ html.dark-scheme .cl-admin-tabs .cl-admin-tab:hover { .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^="participation_"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="groups_"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="faq_"], .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^="social_"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="footer_"] { margin-bottom: 20px; } /* Section separator borders (fallback only) */ +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="section_order"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="custom_css"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="meta_description"], .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="social_twitter_url"], .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_enabled"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="about_enabled"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="participation_enabled"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="topics_enabled"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="groups_enabled"], +.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="faq_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"] { border-top: 2px solid rgba(0, 0, 0, 0.12); @@ -164,18 +202,23 @@ html.dark-scheme .cl-admin-tabs .cl-admin-tab:hover { } /* Dark mode separators (fallback only) */ +html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="section_order"], +html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="custom_css"], +html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="meta_description"], 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="social_twitter_url"], 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_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="participation_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="groups_enabled"], +html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="faq_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"] { +html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="footer_description"] { border-top-color: rgba(255, 255, 255, 0.12); } diff --git a/assets/stylesheets/community_landing/landing.css b/assets/stylesheets/community_landing/landing.css index ec361e0..d003718 100644 --- a/assets/stylesheets/community_landing/landing.css +++ b/assets/stylesheets/community_landing/landing.css @@ -125,10 +125,49 @@ box-sizing: border-box; } +/* ── Smooth Scroll ── */ +html { + scroll-behavior: smooth; +} + +/* ── Focus-visible Accessibility ── */ +.cl-body a:focus-visible, +.cl-body button:focus-visible, +.cl-body [role="button"]:focus-visible, +.cl-body summary:focus-visible { + outline: 2px solid var(--cl-accent); + outline-offset: 2px; + border-radius: 4px; +} + +.cl-body .cl-btn:focus-visible { + outline: 2px solid var(--cl-accent); + outline-offset: 3px; + box-shadow: 0 0 0 4px var(--cl-accent-glow); +} + +.cl-body .cl-topic-card:focus-visible, +.cl-body .cl-space-card:focus-visible, +.cl-body .cl-participation-card:focus-visible, +.cl-body .cl-stat-card:focus-visible, +.cl-body .cl-creator-pill:focus-visible { + outline: 2px solid var(--cl-accent); + outline-offset: 2px; +} + +.cl-body .cl-theme-toggle:focus-visible { + outline: 2px solid var(--cl-accent); + outline-offset: 2px; + border-radius: 50%; +} + +.cl-body .cl-navbar__hamburger:focus-visible { + outline: 2px solid var(--cl-accent); + outline-offset: 2px; +} + /* ── Background FX ── */ .cl-orb-container { - position: absolute; - inset: 0; overflow: hidden; pointer-events: none; z-index: -1; @@ -394,6 +433,8 @@ display: flex; align-items: center; gap: 0.6rem; + min-width: 0; + overflow: hidden; } .cl-navbar__brand { @@ -413,6 +454,9 @@ font-size: 1.05rem; font-weight: 700; letter-spacing: -0.02em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .cl-navbar__right { @@ -430,6 +474,7 @@ .cl-navbar__hamburger { display: flex; flex-direction: column; + flex-shrink: 0; gap: 4px; background: none; border: none; @@ -572,6 +617,49 @@ } } +/* ── Social Icons ── */ +.cl-social-icons { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.cl-social-icon { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 50%; + color: var(--cl-muted); + transition: color 0.2s, background 0.2s; + text-decoration: none; +} + +.cl-social-icon:hover { + color: var(--cl-accent); + background: var(--cl-accent-subtle); +} + +.cl-social-icon svg { + width: 16px; + height: 16px; +} + +.cl-navbar__mobile-menu .cl-social-icons { + gap: 0.5rem; +} + +.cl-navbar__mobile-menu .cl-social-icon { + width: 40px; + height: 40px; +} + +.cl-navbar__mobile-menu .cl-social-icon svg { + width: 20px; + height: 20px; +} + /* Logo theme switching */ .cl-logo--light { display: none; @@ -737,7 +825,8 @@ display: flex; align-items: center; justify-content: center; - box-shadow: 0 8px 32px var(--cl-accent-glow), 0 0 0 0 var(--cl-accent-glow); + --_play-glow: var(--cl-video-btn-glow, var(--cl-accent-glow)); + box-shadow: 0 8px 32px var(--_play-glow), 0 0 0 0 var(--_play-glow); transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); animation: cl-play-pulse 2s infinite; } @@ -745,7 +834,7 @@ .cl-hero-play:hover { transform: translate(-50%, -50%) scale(1.1); background: var(--cl-video-btn-bg, var(--cl-accent-hover)); - box-shadow: 0 12px 40px var(--cl-accent-glow); + box-shadow: 0 12px 40px var(--_play-glow); } .cl-hero-play__icon { @@ -761,8 +850,8 @@ } @keyframes cl-play-pulse { - 0%, 100% { box-shadow: 0 8px 32px var(--cl-accent-glow), 0 0 0 0 var(--cl-accent-glow); } - 50% { box-shadow: 0 8px 32px var(--cl-accent-glow), 0 0 0 16px rgba(212, 162, 78, 0); } + 0%, 100% { box-shadow: 0 8px 32px var(--_play-glow), 0 0 0 0 var(--_play-glow); } + 50% { box-shadow: 0 8px 32px var(--_play-glow), 0 0 0 16px transparent; } } .cl-hero__image--video-only { @@ -1163,6 +1252,105 @@ color: var(--cl-muted); } +/* ═══════════════════════════════════════════════════════════════════ + 5b. PARTICIPATION — testimonial-style bio cards + ═══════════════════════════════════════════════════════════════════ */ +.cl-participation { + padding: 4rem 0; +} + +.cl-participation__grid { + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; +} + +@media (min-width: 640px) { + .cl-participation__grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (min-width: 1024px) { + .cl-participation__grid { + grid-template-columns: repeat(3, 1fr); + } +} + +.cl-participation-card { + background: var(--cl-participation-card-bg, var(--cl-card)); + border: 1px solid var(--cl-border); + border-radius: 16px; + padding: 1.5rem; + transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), + box-shadow 0.3s cubic-bezier(0.16, 1, 0.3, 1), + border-color 0.3s ease; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.cl-participation-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + border-color: var(--cl-border-hover); +} + +.cl-participation-card__quote { + color: var(--cl-participation-icon-color, var(--cl-accent)); + opacity: 0.6; +} + +.cl-participation-card__quote svg { + width: 28px; + height: 28px; +} + +.cl-participation-card__bio { + font-size: 0.95rem; + line-height: 1.65; + color: var(--cl-text); + margin: 0; + flex: 1; +} + +.cl-participation-card__footer { + display: flex; + align-items: center; + gap: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid var(--cl-border); +} + +.cl-participation-card__avatar { + width: 44px; + height: 44px; + border-radius: 50%; + border: 2px solid var(--cl-participation-icon-color, var(--cl-accent)); + object-fit: cover; + flex-shrink: 0; +} + +.cl-participation-card__meta { + display: flex; + flex-direction: column; + min-width: 0; +} + +.cl-participation-card__name { + font-weight: 600; + font-size: 0.9rem; + color: var(--cl-text-strong); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.cl-participation-card__count { + font-size: 0.8rem; + color: var(--cl-participation-icon-color, var(--cl-accent)); +} + /* ═══════════════════════════════════════════════════════════════════ 5. TRENDING DISCUSSIONS — 4-per-row grid ═══════════════════════════════════════════════════════════════════ */ @@ -1349,36 +1537,49 @@ } /* ═══════════════════════════════════════════════════════════════════ - 7. COMMUNITY SPACES — compact horizontal pills with accent stripe + 7. COMMUNITY SPACES + FAQ — split layout ═══════════════════════════════════════════════════════════════════ */ .cl-spaces { - padding: 1.5rem 0 2rem; + padding: 2.5rem 0 3rem; } +/* ── Split layout: groups left, FAQ right ── */ +.cl-spaces__split { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + align-items: start; + min-height: 400px; +} + +.cl-spaces__full { + display: block; +} + +.cl-spaces__full .cl-faq { + max-width: 700px; +} + +/* ── Groups grid (2×2 inside left column) ── */ .cl-spaces__grid { display: grid; - grid-template-columns: 1fr; + grid-template-columns: repeat(2, 1fr); gap: 0.6rem; } -@media (min-width: 480px) { +@media (max-width: 479px) { .cl-spaces__grid { - grid-template-columns: repeat(2, 1fr); - } -} - -@media (min-width: 768px) { - .cl-spaces__grid { - grid-template-columns: repeat(3, 1fr); + grid-template-columns: 1fr; } } +/* ── Group cards ── */ .cl-space-card { display: flex; flex-direction: row; - align-items: center; + align-items: flex-start; gap: 0.75rem; - padding: 0.6rem 1rem 0.6rem 0.6rem; + padding: 0.8rem 1rem; background: var(--cl-space-card-bg, var(--cl-card)); border: 1px solid var(--cl-border); border-left: 3px solid var(--space-color); @@ -1408,6 +1609,7 @@ flex-shrink: 0; background: var(--space-color); transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); + margin-top: 2px; } .cl-space-card:hover .cl-space-card__icon { @@ -1427,7 +1629,6 @@ line-height: 1; } -/* Card body */ .cl-space-card__body { display: flex; flex-direction: column; @@ -1451,6 +1652,109 @@ letter-spacing: 0.05em; } +.cl-space-card__desc { + margin: 0.25rem 0 0; + font-size: 0.78rem; + color: var(--cl-text); + line-height: 1.45; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* ── FAQ Accordion ── */ +.cl-faq { + display: flex; + flex-direction: column; + gap: 0; +} + +.cl-faq__title { + font-size: 1.2rem; + font-weight: 800; + color: var(--cl-text-strong); + margin: 0 0 1rem; +} + +.cl-faq__item { + border-bottom: 1px solid var(--cl-border); +} + +.cl-faq__item:first-of-type { + border-top: 1px solid var(--cl-border); +} + +.cl-faq__question { + padding: 1rem 2rem 1rem 0; + font-size: 0.92rem; + font-weight: 700; + color: var(--cl-text-strong); + cursor: pointer; + list-style: none; + position: relative; + transition: color 0.2s; +} + +.cl-faq__question::-webkit-details-marker { + display: none; +} + +.cl-faq__question::marker { + display: none; + content: ""; +} + +/* Chevron indicator */ +.cl-faq__question::after { + content: ""; + position: absolute; + right: 0; + top: 50%; + width: 8px; + height: 8px; + border-right: 2px solid var(--cl-muted); + border-bottom: 2px solid var(--cl-muted); + transform: translateY(-60%) rotate(45deg); + transition: transform 0.3s ease, border-color 0.2s; +} + +.cl-faq__item[open] > .cl-faq__question::after { + transform: translateY(-30%) rotate(-135deg); + border-color: var(--cl-accent); +} + +.cl-faq__question:hover { + color: var(--cl-accent); +} + +.cl-faq__answer { + padding: 0 0 1rem; + font-size: 0.85rem; + color: var(--cl-text); + line-height: 1.7; + animation: cl-faq-open 0.3s ease; +} + +@keyframes cl-faq-open { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* ── Mobile: stack split vertically ── */ +@media (max-width: 767px) { + .cl-spaces__split { + grid-template-columns: 1fr; + min-height: auto; + } +} + /* ═══════════════════════════════════════════════════════════════════ 8. APP CTA — split layout with gradient ═══════════════════════════════════════════════════════════════════ */ @@ -1710,6 +2014,10 @@ ═══════════════════════════════════════════════════════════════════ */ @media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + .cl-anim, .cl-hero__content, .cl-hero__image, @@ -1726,4 +2034,8 @@ .cl-hero-play { animation: none; } + + .cl-faq__answer { + animation: none; + } } \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 7e98d9e..bbb94e0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3,6 +3,18 @@ en: # ── Master Switch ── community_landing_enabled: "Enable the community landing page." + # ── Layout ── + section_order: "━━ LAYOUT ━━ — Order of content sections, pipe-separated. IDs: hero, stats, about, participation, topics, groups, app_cta. Navbar and footer are always fixed." + + # ── Custom CSS ── + custom_css: "━━ CUSTOM CSS ━━ — Raw CSS injected after all plugin styles. Use for overrides and tweaks. No style tags needed." + + # ── SEO & Meta ── + meta_description: "━━ SEO & META ━━ — Custom meta description for search engines and social sharing. If blank, the hero subtitle is used." + og_image_url: "Custom Open Graph image URL for social sharing (1200×630px recommended). If blank, the site logo is used." + favicon_url: "Custom favicon URL (.ico, .png, .svg). If blank, the browser default is used." + json_ld_enabled: "Add JSON-LD structured data (Organization + WebSite schema) for search engines." + # ── Branding: Logo ── 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." @@ -29,6 +41,12 @@ en: navbar_join_color_light: "Light mode background for the join button." 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." + social_twitter_url: "━━ SOCIAL LINKS ━━ — Twitter / X profile URL. Leave blank to hide. Icons appear in the navbar before the auth buttons." + social_facebook_url: "Facebook page or profile URL. Leave blank to hide." + social_instagram_url: "Instagram profile URL. Leave blank to hide." + social_youtube_url: "YouTube channel URL. Leave blank to hide." + social_tiktok_url: "TikTok profile URL. Leave blank to hide." + social_github_url: "GitHub organization or profile URL. Leave blank to hide." # ── 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." @@ -122,7 +140,20 @@ en: contributors_pill_bg_dark: "Creator pill background color. Dark (left) and light (right) pickers. Leave blank for default glass styling." contributors_pill_bg_light: "Light mode background for creator pills." contributors_days: "Lookback period in days for calculating top contributors." - contributors_count: "Number of top contributors to fetch (top 3 are shown in the hero)." + contributors_count: "Number of top contributors to fetch (top 3 are shown in the hero, 4–10 appear in the Participation section)." + + # ── 5b. Participation ── + participation_enabled: "━━ ROW 5b: PARTICIPATION ━━ — Show the Participation section: testimonial-style cards displaying leaderboard positions 4–10 with their public bio/summary. Only users who have written a bio are shown. Uses the same contributor data and lookback period as the Hero Creators." + participation_title_enabled: "Show the section heading above the participation cards." + participation_title: "Heading text above the participation cards." + participation_bio_max_length: "Maximum number of characters to show from each user's bio (50–500). Longer bios are truncated with an ellipsis." + participation_icon_color: "Color for the decorative quote icon on each participation card. Leave blank to use the accent color." + participation_card_bg_dark: "Participation card background color. Dark (left) and light (right) pickers. Leave blank for default card styling." + participation_card_bg_light: "Light mode background for participation cards." + participation_bg_dark: "Section background color override. Dark (left) and light (right) color pickers. Leave blank for default." + participation_bg_light: "Light mode background for the participation section." + participation_min_height: "Minimum height for the participation section in pixels. Set to 0 for auto height." + participation_border_style: "Border style at the bottom of the participation 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." @@ -136,6 +167,14 @@ en: groups_bg_light: "Light mode background for the spaces section." 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." + groups_show_description: "Show group description text (from the group's bio) below the group name on each card." + groups_description_max_length: "Maximum characters for group description text (30–500). Longer descriptions are truncated." + + # ── 7b. FAQ Accordion ── + faq_enabled: "━━ FAQ ACCORDION ━━ — Show an FAQ accordion alongside the Spaces section. Only one item opens at a time." + faq_title_enabled: "Show a heading above the FAQ accordion." + faq_title: "Heading text above the FAQ accordion." + faq_items: 'FAQ items as a JSON array. Format: [{"q":"Question","a":"Answer"}]. HTML is supported in answers.' # ── 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." @@ -147,9 +186,12 @@ en: 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_gradient_start_dark: "Gradient start color. Dark (left) and light (right) pickers. Leave blank for accent color." + app_cta_gradient_start_light: "Light mode gradient start color." + app_cta_gradient_mid_dark: "Gradient middle color. Dark (left) and light (right) pickers. Leave blank for accent hover color." + app_cta_gradient_mid_light: "Light mode gradient middle color." + app_cta_gradient_end_dark: "Gradient end color. Dark (left) and light (right) pickers. Leave blank for accent hover color." + app_cta_gradient_end_light: "Light mode gradient end color." 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: "Section background color override. Dark (left) and light (right) color pickers. Leave blank for default." app_cta_bg_light: "Light mode background for the app CTA section." diff --git a/config/settings.yml b/config/settings.yml index b79aea2..0ef0f91 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -6,6 +6,36 @@ plugins: default: true type: bool + # ══════════════════════════════════════════ + # Layout & Section Order + # ══════════════════════════════════════════ + section_order: + default: "hero|stats|about|participation|topics|groups|app_cta" + type: string + + # ══════════════════════════════════════════ + # Custom CSS + # ══════════════════════════════════════════ + custom_css: + default: "" + type: text_area + + # ══════════════════════════════════════════ + # SEO & Meta + # ══════════════════════════════════════════ + meta_description: + default: "" + type: string + og_image_url: + default: "" + type: string + favicon_url: + default: "" + type: string + json_ld_enabled: + default: true + type: bool + # ══════════════════════════════════════════ # Branding: Logo # ══════════════════════════════════════════ @@ -109,6 +139,24 @@ plugins: - solid - dashed - dotted + social_twitter_url: + default: "" + type: string + social_facebook_url: + default: "" + type: string + social_instagram_url: + default: "" + type: string + social_youtube_url: + default: "" + type: string + social_tiktok_url: + default: "" + type: string + social_github_url: + default: "" + type: string # ══════════════════════════════════════════ # 2. Hero Section @@ -427,6 +475,52 @@ plugins: default: 10 type: integer + # ══════════════════════════════════════════ + # 5b. Participation (Leaderboard Bios) + # ══════════════════════════════════════════ + participation_enabled: + default: true + type: bool + participation_title_enabled: + default: true + type: bool + participation_title: + default: "Participation" + type: string + participation_bio_max_length: + default: 150 + type: integer + min: 50 + max: 500 + participation_icon_color: + default: "" + type: color + participation_card_bg_dark: + default: "" + type: color + participation_card_bg_light: + default: "" + type: color + participation_bg_dark: + default: "" + type: color + participation_bg_light: + default: "" + type: color + participation_min_height: + default: 0 + type: integer + min: 0 + max: 2000 + participation_border_style: + default: "none" + type: enum + choices: + - none + - solid + - dashed + - dotted + # ══════════════════════════════════════════ # 7. Community Spaces Section # ══════════════════════════════════════════ @@ -470,6 +564,30 @@ plugins: - solid - dashed - dotted + groups_show_description: + default: true + type: bool + groups_description_max_length: + default: 100 + type: integer + min: 30 + max: 500 + + # ══════════════════════════════════════════ + # 7b. FAQ Accordion + # ══════════════════════════════════════════ + faq_enabled: + default: false + type: bool + faq_title_enabled: + default: true + type: bool + faq_title: + default: "Frequently Asked Questions" + type: string + faq_items: + default: '[{"q":"What is this community about?","a":"A creative community focused on sharing knowledge and building together."},{"q":"How do I join?","a":"Click the Get Started button to create your free account."},{"q":"Is it free?","a":"Yes! Basic membership is completely free."}]' + type: text_area # ══════════════════════════════════════════ # 8. App Download CTA Section @@ -507,15 +625,24 @@ plugins: app_cta_subtext: default: "Available free on iOS and Android" type: string - app_cta_gradient_start: + app_cta_gradient_start_dark: default: "d4a24e" type: color - app_cta_gradient_mid: + app_cta_gradient_start_light: + default: "" + type: color + app_cta_gradient_mid_dark: default: "c4922e" type: color - app_cta_gradient_end: + app_cta_gradient_mid_light: + default: "" + type: color + app_cta_gradient_end_dark: default: "b8862e" type: color + app_cta_gradient_end_light: + default: "" + type: color app_cta_image_url: default: "" type: string diff --git a/lib/community_landing/data_fetcher.rb b/lib/community_landing/data_fetcher.rb index 580e7c8..8b6a44d 100644 --- a/lib/community_landing/data_fetcher.rb +++ b/lib/community_landing/data_fetcher.rb @@ -11,6 +11,7 @@ module CommunityLanding if s.contributors_enabled User .joins(:posts) + .includes(:user_profile) .where(posts: { created_at: s.contributors_days.days.ago.. }) .where.not(username: %w[system discobot]) .where(active: true, staged: false) diff --git a/lib/community_landing/icons.rb b/lib/community_landing/icons.rb index 438a495..44b333e 100644 --- a/lib/community_landing/icons.rb +++ b/lib/community_landing/icons.rb @@ -17,6 +17,14 @@ module CommunityLanding COMMENT_SVG = '' HEART_SVG = '' + # Social media icons (18×18, fill currentColor) + SOCIAL_TWITTER_SVG = '' + SOCIAL_FACEBOOK_SVG = '' + SOCIAL_INSTAGRAM_SVG = '' + SOCIAL_YOUTUBE_SVG = '' + SOCIAL_TIKTOK_SVG = '' + SOCIAL_GITHUB_SVG = '' + IOS_BADGE_SVG = '' ANDROID_BADGE_SVG = '' end diff --git a/lib/community_landing/page_builder.rb b/lib/community_landing/page_builder.rb index 1cb088e..fcfe1bf 100644 --- a/lib/community_landing/page_builder.rb +++ b/lib/community_landing/page_builder.rb @@ -4,6 +4,16 @@ module CommunityLanding class PageBuilder include Helpers + SECTION_MAP = { + "hero" => :render_hero, + "stats" => :render_stats, + "about" => :render_about, + "participation" => :render_participation, + "topics" => :render_topics, + "groups" => :render_groups, + "app_cta" => :render_app_cta, + }.freeze + def initialize(data:, css:, js:) @data = data @css = css @@ -20,12 +30,14 @@ module CommunityLanding html << "