From b46c70a221f38a0aa48ca1b983a9085df587d3a4 Mon Sep 17 00:00:00 2001 From: DPN MW Date: Sun, 8 Mar 2026 14:51:39 -0400 Subject: [PATCH] Ui improvements and bug fixes --- .../community-landing-admin-tabs.js | 157 +++++++++++++++--- .../stylesheets/community_landing/admin.css | 90 ++++++++++ .../stylesheets/community_landing/landing.css | 39 +++++ config/locales/en.yml | 4 + config/settings.yml | 16 ++ lib/community_landing/page_builder.rb | 3 + lib/community_landing/style_builder.rb | 6 + 7 files changed, 294 insertions(+), 21 deletions(-) diff --git a/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js b/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js index 7f6d191..cb53f91 100644 --- a/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js +++ b/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js @@ -194,7 +194,10 @@ const DESCRIPTIONS = { groups_description_max_length: "Max characters for group descriptions (30–500). Longer text is truncated.", groups_card_bg_dark: "Space card background for dark mode.", groups_card_bg_light: "Space card background for light mode.", - splits_background_image_url: "Background image for the splits section (Groups + FAQ row).", + splits_background_image_url: "Background image for the splits section (Groups + FAQ container).", + splits_bg_dark: "Background color for the splits section (dark mode).", + splits_bg_light: "Background color for the splits section (light mode).", + splits_min_height: "Minimum height in pixels for the splits section. 0 = auto.", // ── FAQ ── faq_enabled: "Show FAQ accordion alongside the Spaces section. One item opens at a time.", @@ -204,6 +207,7 @@ const DESCRIPTIONS = { faq_items: 'FAQ items as JSON array: [{\"q\":\"Question\",\"a\":\"Answer\"}]. HTML supported in answers.', faq_card_bg_dark: "FAQ card background for dark mode.", faq_card_bg_light: "FAQ card background for light mode.", + faq_mobile_max_height: "Max height of FAQ container on mobile (px). Scrollable if content overflows. 0 = no limit.", // ── App CTA ── show_app_ctas: "Show App Download CTA: gradient banner with headline, badges, and promo image.", @@ -343,23 +347,19 @@ const TABS = [ ]) }, { - id: "groups", - label: "Groups", + id: "splits", + label: "Splits", settings: new Set([ + "splits_background_image_url", + "splits_bg_dark", "splits_bg_light", "splits_min_height", "groups_enabled", "groups_title_enabled", "groups_title", "groups_title_size", "groups_count", "groups_selected", "groups_show_description", "groups_description_max_length", "groups_card_bg_dark", "groups_card_bg_light", - "splits_background_image_url" - ]) - }, - { - id: "faq", - label: "FAQ", - settings: new Set([ "faq_enabled", "faq_title_enabled", "faq_title", "faq_title_size", "faq_items", - "faq_card_bg_dark", "faq_card_bg_light" + "faq_card_bg_dark", "faq_card_bg_light", + "faq_mobile_max_height" ]) }, { @@ -403,8 +403,6 @@ const TAB_ENABLE_SETTINGS = { stats: { setting: "stats_enabled", label: "Stats" }, about: { setting: "about_enabled", label: "About" }, topics: { setting: "topics_enabled", label: "Topics" }, - groups: { setting: "groups_enabled", label: "Groups" }, - faq: { setting: "faq_enabled", label: "FAQ" }, appcta: { setting: "show_app_ctas", label: "App CTA" }, }; @@ -903,22 +901,139 @@ function updatePreviewThumbnail(wrapper, settingName) { row.querySelector(".setting-value textarea"); const url = input ? input.value.trim() : ""; - const preview = wrapper.querySelector(".cl-upload-preview"); - if (!preview) return; - const cfg = IMAGE_UPLOAD_SETTINGS[settingName]; if (cfg && cfg.multi) { - // For multi-image, show the last image in the list - const urls = url.split("|").filter(Boolean); - const lastUrl = urls.length > 0 ? urls[urls.length - 1] : ""; - preview.src = lastUrl; - preview.style.display = lastUrl ? "" : "none"; + // Multi-image: render a sortable image list + renderMultiImageList(wrapper, row, input, settingName); + // Hide single preview if it exists + const preview = wrapper.querySelector(".cl-upload-preview"); + if (preview) preview.style.display = "none"; } else { + const preview = wrapper.querySelector(".cl-upload-preview"); + if (!preview) return; preview.src = url; preview.style.display = url ? "" : "none"; } } +function renderMultiImageList(wrapper, row, input, settingName) { + let list = wrapper.querySelector(".cl-multi-image-list"); + if (!list) { + list = document.createElement("div"); + list.className = "cl-multi-image-list"; + wrapper.appendChild(list); + } + + const raw = input ? input.value.trim() : ""; + const urls = raw.split(/[\n\r]+/).map((u) => u.trim()).filter(Boolean); + + list.innerHTML = ""; + if (urls.length === 0) return; + + urls.forEach((url, idx) => { + const item = document.createElement("div"); + item.className = "cl-multi-image-item"; + item.draggable = true; + item.dataset.idx = idx; + + const thumb = document.createElement("img"); + thumb.src = url; + thumb.alt = `Image ${idx + 1}`; + thumb.className = "cl-multi-image-thumb"; + item.appendChild(thumb); + + const label = document.createElement("span"); + label.className = "cl-multi-image-label"; + label.textContent = `${idx + 1}`; + item.appendChild(label); + + const actions = document.createElement("span"); + actions.className = "cl-multi-image-actions"; + + // Move up + if (idx > 0) { + const upBtn = document.createElement("button"); + upBtn.type = "button"; + upBtn.className = "cl-multi-image-move"; + upBtn.innerHTML = "▲"; + upBtn.title = "Move up"; + upBtn.addEventListener("click", () => { + [urls[idx - 1], urls[idx]] = [urls[idx], urls[idx - 1]]; + input.value = urls.join("\n"); + input.dispatchEvent(new Event("input", { bubbles: true })); + input.dispatchEvent(new Event("change", { bubbles: true })); + renderMultiImageList(wrapper, row, input, settingName); + }); + actions.appendChild(upBtn); + } + + // Move down + if (idx < urls.length - 1) { + const downBtn = document.createElement("button"); + downBtn.type = "button"; + downBtn.className = "cl-multi-image-move"; + downBtn.innerHTML = "▼"; + downBtn.title = "Move down"; + downBtn.addEventListener("click", () => { + [urls[idx], urls[idx + 1]] = [urls[idx + 1], urls[idx]]; + input.value = urls.join("\n"); + input.dispatchEvent(new Event("input", { bubbles: true })); + input.dispatchEvent(new Event("change", { bubbles: true })); + renderMultiImageList(wrapper, row, input, settingName); + }); + actions.appendChild(downBtn); + } + + // Remove + const removeBtn = document.createElement("button"); + removeBtn.type = "button"; + removeBtn.className = "cl-multi-image-remove"; + removeBtn.innerHTML = "×"; + removeBtn.title = "Remove image"; + removeBtn.addEventListener("click", () => { + urls.splice(idx, 1); + input.value = urls.join("\n"); + input.dispatchEvent(new Event("input", { bubbles: true })); + input.dispatchEvent(new Event("change", { bubbles: true })); + renderMultiImageList(wrapper, row, input, settingName); + }); + actions.appendChild(removeBtn); + + item.appendChild(actions); + + // Drag-and-drop reorder + item.addEventListener("dragstart", (e) => { + e.dataTransfer.setData("text/plain", idx.toString()); + item.classList.add("cl-dragging"); + }); + item.addEventListener("dragend", () => { + item.classList.remove("cl-dragging"); + }); + item.addEventListener("dragover", (e) => { + e.preventDefault(); + item.classList.add("cl-drag-over"); + }); + item.addEventListener("dragleave", () => { + item.classList.remove("cl-drag-over"); + }); + item.addEventListener("drop", (e) => { + e.preventDefault(); + item.classList.remove("cl-drag-over"); + const fromIdx = parseInt(e.dataTransfer.getData("text/plain"), 10); + const toIdx = idx; + if (fromIdx === toIdx) return; + const [moved] = urls.splice(fromIdx, 1); + urls.splice(toIdx, 0, moved); + input.value = urls.join("\n"); + input.dispatchEvent(new Event("input", { bubbles: true })); + input.dispatchEvent(new Event("change", { bubbles: true })); + renderMultiImageList(wrapper, row, input, settingName); + }); + + list.appendChild(item); + }); +} + function injectUploadButtons() { const container = getContainer(); if (!container) return; diff --git a/assets/stylesheets/community_landing/admin.css b/assets/stylesheets/community_landing/admin.css index 2170382..8cf44d4 100644 --- a/assets/stylesheets/community_landing/admin.css +++ b/assets/stylesheets/community_landing/admin.css @@ -448,3 +448,93 @@ html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="f color: var(--primary-medium); font-style: italic; } + +/* ── Multi-image list (hero_image_urls) ── */ + +.cl-multi-image-list { + display: flex; + flex-wrap: wrap; + gap: 8px; + width: 100%; + margin-top: 8px; +} + +.cl-multi-image-item { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + background: var(--primary-very-low); + border: 1px solid var(--primary-low); + border-radius: 6px; + cursor: grab; + transition: box-shadow 0.15s ease, border-color 0.15s ease; +} + +.cl-multi-image-item:hover { + border-color: var(--primary-low-mid); +} + +.cl-multi-image-item.cl-dragging { + opacity: 0.4; +} + +.cl-multi-image-item.cl-drag-over { + border-color: var(--tertiary); + box-shadow: 0 0 0 2px var(--tertiary-low); +} + +.cl-multi-image-thumb { + width: 48px; + height: 36px; + object-fit: cover; + border-radius: 4px; + border: 1px solid var(--primary-low); + background: var(--secondary); +} + +.cl-multi-image-label { + font-size: var(--font-down-2); + font-weight: 600; + color: var(--primary-medium); + min-width: 14px; + text-align: center; +} + +.cl-multi-image-actions { + display: flex; + gap: 2px; +} + +.cl-multi-image-move { + padding: 2px 5px; + font-size: 10px; + line-height: 1; + background: none; + border: 1px solid var(--primary-low); + border-radius: 3px; + cursor: pointer; + color: var(--primary-medium); + transition: background 0.1s ease; +} + +.cl-multi-image-move:hover { + background: var(--primary-very-low); + color: var(--primary); +} + +.cl-multi-image-remove { + padding: 2px 6px; + font-size: 14px; + line-height: 1; + background: none; + border: 1px solid var(--danger-low); + border-radius: 3px; + cursor: pointer; + color: var(--danger); + transition: background 0.1s ease; +} + +.cl-multi-image-remove:hover { + background: var(--danger-low); +} diff --git a/assets/stylesheets/community_landing/landing.css b/assets/stylesheets/community_landing/landing.css index 6aab061..a282eae 100644 --- a/assets/stylesheets/community_landing/landing.css +++ b/assets/stylesheets/community_landing/landing.css @@ -2096,4 +2096,43 @@ html { .cl-faq__answer { animation: none; } +} + +/* ═══════════════════════════════════════════════════════════════════ + MOBILE — tighter padding, no horizontal overflow + ═══════════════════════════════════════════════════════════════════ */ +@media (max-width: 767px) { + .cl-container { + padding: 0 1rem; + } + + .cl-hero__inner { + padding: 0 1rem; + } + + .cl-navbar__inner { + padding: 0 1rem; + } + + .cl-about__left { + padding: 1.25rem 1rem; + } + + .cl-about__right { + padding: 1.25rem 1.25rem; + } + + .cl-app-cta__inner { + padding: 1.5rem 1.25rem; + align-items: center; + text-align: center; + } + + .cl-app-cta__image { + display: none; + } + + .cl-app-cta__badges { + justify-content: center; + } } \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index e83182f..1362526 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -178,6 +178,9 @@ en: groups_card_bg_dark: "Space card background color. Dark (left) and light (right) pickers. Leave blank for default card styling." groups_card_bg_light: "Light mode background for space cards." splits_background_image_url: "Background image for the splits section (Groups + FAQ row)." + splits_bg_dark: "Background color for the splits section (dark mode)." + splits_bg_light: "Background color for the splits section (light mode)." + splits_min_height: "Minimum height in pixels for the splits section. 0 = auto." 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." groups_title_size: "Spaces section title font size in pixels. 0 = use default." @@ -190,6 +193,7 @@ en: faq_title_size: "FAQ section title font size in pixels. 0 = use default." faq_card_bg_dark: "FAQ card background color (dark mode). Leave blank for default card styling." faq_card_bg_light: "FAQ card background color (light mode). Leave blank for default." + faq_mobile_max_height: "Max height of FAQ container on mobile (px). Content scrolls if it overflows. 0 = no limit." # ── 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." diff --git a/config/settings.yml b/config/settings.yml index eb9f9a4..8fecd76 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -622,6 +622,17 @@ plugins: splits_background_image_url: default: "" type: string + splits_bg_dark: + default: "" + type: color + splits_bg_light: + default: "" + type: color + splits_min_height: + default: 0 + type: integer + min: 0 + max: 800 groups_show_description: default: true type: bool @@ -662,6 +673,11 @@ plugins: faq_card_bg_light: default: "" type: color + faq_mobile_max_height: + default: 0 + type: integer + min: 0 + max: 1200 # ══════════════════════════════════════════ # 8. App Download CTA Section diff --git a/lib/community_landing/page_builder.rb b/lib/community_landing/page_builder.rb index ee2f6cb..b7182c8 100644 --- a/lib/community_landing/page_builder.rb +++ b/lib/community_landing/page_builder.rb @@ -514,10 +514,13 @@ module CommunityLanding show_desc = @s.groups_show_description rescue true desc_max = (@s.groups_description_max_length rescue 100).to_i + min_h = @s.splits_min_height rescue 0 + html = +"" groups_bg_img = (@s.splits_background_image_url.presence rescue nil) section_style_parts = [] section_style_parts << "background: url('#{groups_bg_img}') center/cover no-repeat;" if groups_bg_img + section_style_parts << "min-height: #{min_h}px;" if min_h.to_i > 0 section_attr = section_style_parts.any? ? " style=\"#{section_style_parts.join(' ')}\"" : "" html << "
\n" diff --git a/lib/community_landing/style_builder.rb b/lib/community_landing/style_builder.rb index cab4695..d352a34 100644 --- a/lib/community_landing/style_builder.rb +++ b/lib/community_landing/style_builder.rb @@ -222,6 +222,7 @@ module CommunityLanding ["#cl-about", safe_hex(:about_bg_dark), safe_hex(:about_bg_light)], ["#cl-participation", safe_hex(:participation_bg_dark), safe_hex(:participation_bg_light)], ["#cl-topics", safe_hex(:topics_bg_dark), safe_hex(:topics_bg_light)], + ["#cl-splits", safe_hex(:splits_bg_dark), safe_hex(:splits_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)], ] @@ -237,6 +238,11 @@ module CommunityLanding end end + faq_mh = (@s.faq_mobile_max_height rescue 0).to_i + if faq_mh > 0 + css << "@media (max-width: 767px) { .cl-faq { max-height: #{faq_mh}px; overflow-y: auto; } }\n" + end + css.present? ? "\n" : "" end