diff --git a/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js b/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js index 3eaf17c..26b142a 100644 --- a/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js +++ b/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js @@ -74,7 +74,7 @@ const DESCRIPTIONS = { hero_background_image_url: "Full-bleed background image behind the hero. In card mode, fills the card with overlay.", hero_image_url: "Single hero image displayed on the right side of the hero. Use the upload button or paste a URL.", hero_multiple_images_enabled: "Enable multiple hero images (up to 5) that rotate randomly on each page load. Disables the single image upload.", - hero_image_urls: "Images for the right side of the hero. One URL per line, up to 5. One is shown randomly per page load.", + hero_image_urls: "Images for the right side of the hero. Paste a URL and click Create to add. Up to 5 — one is shown randomly per page load.", hero_image_max_height: "Maximum height for the hero image in pixels (100–1200).", hero_primary_button_enabled: "Show the primary CTA button in the hero.", hero_primary_button_label: "Primary button text. Use 'icon | Label' for FA icon before or 'Label | icon' for after (e.g. 'rocket | Get Started').", @@ -86,6 +86,8 @@ const DESCRIPTIONS = { hero_primary_btn_color_light: "Primary button background for light mode.", hero_secondary_btn_color_dark: "Secondary button background for dark mode. Leave blank for glass style.", hero_secondary_btn_color_light: "Secondary button background for light mode.", + hero_video_upload: "Upload a video file to Discourse. Check your site's allowed file types and maximum file size in site settings before uploading.", + hero_video_url_enabled: "Use an external video URL instead of uploading a file.", hero_video_url: "Hero video URL (MP4 or YouTube). Play button opens a lightbox modal.", hero_video_button_color: "Custom color for the video play button. Leave blank for accent color.", hero_video_blur_on_hover: "Blur the hero image when hovering the play button.", @@ -288,7 +290,8 @@ const TABS = [ "hero_secondary_button_enabled", "hero_secondary_button_label", "hero_secondary_button_url", "hero_primary_btn_color_dark", "hero_primary_btn_color_light", "hero_secondary_btn_color_dark", "hero_secondary_btn_color_light", - "hero_video_url", "hero_video_button_color", "hero_video_blur_on_hover", + "hero_video_upload", "hero_video_url_enabled", "hero_video_url", + "hero_video_button_color", "hero_video_blur_on_hover", "hero_bg_dark", "hero_bg_light", "hero_min_height", "hero_border_style", "hero_card_bg_dark", "hero_card_bg_light", "hero_card_opacity", "contributors_enabled", "contributors_title", "contributors_title_enabled", @@ -419,6 +422,7 @@ const IMAGE_UPLOAD_SETTINGS = { footer_logo_url: { label: "Upload Logo", multi: false }, hero_background_image_url: { label: "Upload Image", multi: false }, hero_image_url: { label: "Upload Image", multi: false }, + hero_video_upload: { label: "Upload Video", multi: false, accept: "video/*" }, about_image_url: { label: "Upload Image", multi: false }, about_background_image_url: { label: "Upload Image", multi: false }, ios_app_badge_image_url: { label: "Upload Badge", multi: false }, @@ -513,10 +517,10 @@ function handleTabClick(container, tabId) { clearDisabledNotice(container); updateActiveStates(tabId); applyTabFilter(); - applyHeroImageVisibility(container); + applyConditionalVisibility(container); injectUploadButtons(); updateDisabledNotice(container); - listenForHeroImageToggle(container); + listenForConditionalToggles(container); } /** @@ -765,10 +769,10 @@ function buildTabsUI() { cleanBooleanLabels(); injectUploadButtons(); applyTabFilter(); - applyHeroImageVisibility(container); + applyConditionalVisibility(container); updateDisabledNotice(container); listenForEnableToggles(container); - listenForHeroImageToggle(container); + listenForConditionalToggles(container); return true; } @@ -816,10 +820,10 @@ function buildTabsUI() { cleanBooleanLabels(); injectUploadButtons(); applyTabFilter(); - applyHeroImageVisibility(container); + applyConditionalVisibility(container); updateDisabledNotice(container); listenForEnableToggles(container); - listenForHeroImageToggle(container); + listenForConditionalToggles(container); return true; } @@ -843,43 +847,98 @@ function listenForEnableToggles(container) { // ── Conditional Visibility: Hero Single vs Multi Image ── -const HERO_IMAGE_CONDITIONAL = { - toggle: "hero_multiple_images_enabled", - whenOff: ["hero_image_url"], // single image with upload button - whenOn: ["hero_image_urls"], // multi-image textarea (paste URLs) -}; +// Toggle groups: each toggle shows one set of settings and hides the other. +const CONDITIONAL_TOGGLES = [ + { + toggle: "hero_multiple_images_enabled", + whenOff: ["hero_image_url"], + whenOn: ["hero_image_urls"], + }, + { + toggle: "hero_video_url_enabled", + whenOff: ["hero_video_upload"], + whenOn: ["hero_video_url"], + }, +]; -function applyHeroImageVisibility(container) { - const toggleRow = container.querySelector( - `.row.setting[data-setting="${HERO_IMAGE_CONDITIONAL.toggle}"]` - ); - if (!toggleRow) return; +function applyConditionalVisibility(container) { + CONDITIONAL_TOGGLES.forEach((group) => { + const toggleRow = container.querySelector( + `.row.setting[data-setting="${group.toggle}"]` + ); + if (!toggleRow) return; - const cb = toggleRow.querySelector('input[type="checkbox"]'); - const multiEnabled = cb ? cb.checked : false; + const cb = toggleRow.querySelector('input[type="checkbox"]'); + const isOn = cb ? cb.checked : false; - HERO_IMAGE_CONDITIONAL.whenOff.forEach((name) => { - const row = container.querySelector(`.row.setting[data-setting="${name}"]`); - if (row) row.classList.toggle("cl-tab-hidden", multiEnabled); + group.whenOff.forEach((name) => { + const row = container.querySelector(`.row.setting[data-setting="${name}"]`); + if (row) row.classList.toggle("cl-tab-hidden", isOn); + }); + + group.whenOn.forEach((name) => { + const row = container.querySelector(`.row.setting[data-setting="${name}"]`); + if (row) row.classList.toggle("cl-tab-hidden", !isOn); + }); }); - HERO_IMAGE_CONDITIONAL.whenOn.forEach((name) => { - const row = container.querySelector(`.row.setting[data-setting="${name}"]`); - if (row) row.classList.toggle("cl-tab-hidden", !multiEnabled); - }); + // Check if file attachments are allowed for video upload toggle + injectVideoUploadNotice(container); } -function listenForHeroImageToggle(container) { +function injectVideoUploadNotice(container) { const toggleRow = container.querySelector( - `.row.setting[data-setting="${HERO_IMAGE_CONDITIONAL.toggle}"]` + '.row.setting[data-setting="hero_video_upload"]' ); if (!toggleRow) return; + if (toggleRow.dataset.clVideoNoticeInjected) return; + toggleRow.dataset.clVideoNoticeInjected = "1"; - const cb = toggleRow.querySelector('input[type="checkbox"]'); - if (!cb || cb.dataset.clHeroToggleListening) return; - cb.dataset.clHeroToggleListening = "1"; - cb.addEventListener("change", () => { - applyHeroImageVisibility(container); + // Check Discourse site settings for file attachment support + const siteSettings = window.Discourse && window.Discourse.SiteSettings; + const authorizedExtensions = ( + (siteSettings && siteSettings.authorized_extensions) || "" + ).toLowerCase(); + const attachmentsAllowed = + authorizedExtensions.includes("mp4") || + authorizedExtensions.includes("webm") || + authorizedExtensions.includes("mov") || + authorizedExtensions.includes("*"); + + const notice = document.createElement("div"); + notice.className = "cl-upload-notice"; + + if (!attachmentsAllowed) { + notice.classList.add("cl-upload-notice--warn"); + notice.textContent = + "Video file uploads require video extensions (mp4, webm, mov) to be added to your site\u2019s authorized extensions in Settings \u2192 Files."; + // Disable the upload button if injected + const uploadBtn = toggleRow.querySelector(".cl-upload-btn"); + if (uploadBtn) uploadBtn.disabled = true; + } else { + const maxSize = siteSettings && siteSettings.max_attachment_size_kb; + const maxMB = maxSize ? (maxSize / 1024).toFixed(0) : "?"; + notice.textContent = + `Check allowed video file types and max upload size (${maxMB} MB) in Settings \u2192 Files before uploading.`; + } + + const valueDiv = toggleRow.querySelector(".setting-value") || toggleRow; + valueDiv.appendChild(notice); +} + +function listenForConditionalToggles(container) { + CONDITIONAL_TOGGLES.forEach((group) => { + const toggleRow = container.querySelector( + `.row.setting[data-setting="${group.toggle}"]` + ); + if (!toggleRow) return; + + const cb = toggleRow.querySelector('input[type="checkbox"]'); + if (!cb || cb.dataset.clConditionalListening) return; + cb.dataset.clConditionalListening = "1"; + cb.addEventListener("change", () => { + applyConditionalVisibility(container); + }); }); } @@ -1112,7 +1171,7 @@ function injectUploadButtons() { btn.addEventListener("click", () => { const fileInput = document.createElement("input"); fileInput.type = "file"; - fileInput.accept = "image/*"; + fileInput.accept = cfg.accept || "image/*"; fileInput.style.display = "none"; document.body.appendChild(fileInput); diff --git a/assets/stylesheets/community_landing/admin.css b/assets/stylesheets/community_landing/admin.css index 8cf44d4..afb3d42 100644 --- a/assets/stylesheets/community_landing/admin.css +++ b/assets/stylesheets/community_landing/admin.css @@ -449,6 +449,19 @@ html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="f font-style: italic; } +.cl-upload-notice { + margin-top: 6px; + font-size: var(--font-down-2); + color: var(--primary-medium); + font-style: italic; +} + +.cl-upload-notice--warn { + color: var(--danger); + font-style: normal; + font-weight: 600; +} + /* ── Multi-image list (hero_image_urls) ── */ .cl-multi-image-list { diff --git a/assets/stylesheets/community_landing/landing.css b/assets/stylesheets/community_landing/landing.css index 157a532..80487f2 100644 --- a/assets/stylesheets/community_landing/landing.css +++ b/assets/stylesheets/community_landing/landing.css @@ -395,11 +395,9 @@ html { top: 0; left: 0; width: 100%; - max-width: 100vw; z-index: 1000; padding: 0.85rem 0; transition: all 0.3s ease; - overflow: hidden; } .cl-navbar.scrolled { @@ -2149,6 +2147,11 @@ html { padding: 0 1rem; } + .cl-hero { + padding: 6rem 0 2rem; + min-height: auto; + } + .cl-hero__inner { padding: 0 1rem; } @@ -2157,6 +2160,18 @@ html { padding: 0 1rem; } + .cl-stats { + padding: 1.5rem 0 1rem; + } + + .cl-stat-card:hover { + transform: none; + } + + .cl-about { + padding: 1rem 0 1.5rem; + } + .cl-about__left { padding: 1.25rem 1rem; } @@ -2165,6 +2180,22 @@ html { padding: 1.25rem 1.25rem; } + .cl-participation { + padding: 2rem 0; + } + + .cl-topics { + padding: 1rem 0 1.5rem; + } + + .cl-spaces { + padding: 1.5rem 0 2rem; + } + + .cl-app-cta { + padding: 1.5rem 0; + } + .cl-app-cta__inner { padding: 1.5rem 1.25rem; align-items: center; diff --git a/config/locales/en.yml b/config/locales/en.yml index 4842f26..7d90d5f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -68,7 +68,7 @@ en: 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_url: "Single hero image displayed on the right side of the hero. Use the upload button or paste a URL." hero_multiple_images_enabled: "Enable multiple hero images (up to 5) that rotate randomly on each page load. Disables the single image upload." - hero_image_urls: "Images displayed on the right side of the hero. One URL per line, up to 5. A random one is shown on each page load." + hero_image_urls: "Images displayed on the right side of the hero. Paste a URL and click Create to add. Up to 5 — 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_enabled: "Show the primary CTA button in the hero section." hero_primary_button_label: "Text on the primary (filled, accent-colored) CTA button." @@ -80,6 +80,8 @@ en: hero_primary_btn_color_light: "Light mode background for the primary button." hero_secondary_btn_color_dark: "Secondary button background color. Dark (left) and light (right) pickers. Leave blank for default glass style." hero_secondary_btn_color_light: "Light mode background for the secondary button." + hero_video_upload: "Upload a video file to Discourse. Check your site's allowed file types and max file size before uploading." + hero_video_url_enabled: "Use an external video URL instead of uploading a file." hero_video_url: "URL for a hero video. Supports MP4 and YouTube links. A play button appears in the hero area; clicking opens a lightbox modal with the video." hero_video_button_color: "Custom background color for the hero video play button. Leave blank to use the accent color." hero_video_blur_on_hover: "Apply a blur effect to the hero image when hovering the play button." diff --git a/config/settings.yml b/config/settings.yml index 7832c82..3d6de0b 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -222,7 +222,7 @@ plugins: type: bool hero_image_urls: default: "" - type: text_area + type: list hero_image_max_height: default: 500 type: integer @@ -258,6 +258,12 @@ plugins: hero_secondary_btn_color_light: default: "" type: color + hero_video_upload: + default: "" + type: string + hero_video_url_enabled: + default: false + type: bool hero_video_url: default: "" type: string diff --git a/lib/community_landing/page_builder.rb b/lib/community_landing/page_builder.rb index 2b7c419..6834eeb 100644 --- a/lib/community_landing/page_builder.rb +++ b/lib/community_landing/page_builder.rb @@ -298,7 +298,11 @@ module CommunityLanding else hero_image_urls_raw = (@s.hero_image_url.presence rescue nil) end - hero_video = (@s.hero_video_url.presence rescue nil) + if (@s.hero_video_url_enabled rescue false) + hero_video = (@s.hero_video_url.presence rescue nil) + else + hero_video = (@s.hero_video_upload.presence rescue nil) + end blur_attr = (@s.hero_video_blur_on_hover rescue true) ? " data-blur-hover=\"true\"" : "" has_images = false diff --git a/plugin.rb b/plugin.rb index 688b3cd..b11d27c 100644 --- a/plugin.rb +++ b/plugin.rb @@ -82,19 +82,19 @@ after_initialize do requires_plugin CommunityLanding::PLUGIN_NAME before_action :ensure_admin - ALLOWED_IMAGE_SETTINGS = %w[ + ALLOWED_UPLOAD_SETTINGS = %w[ og_image_url favicon_url logo_dark_url logo_light_url footer_logo_url - hero_background_image_url hero_image_urls about_image_url + hero_background_image_url hero_image_url hero_image_urls about_image_url about_background_image_url ios_app_badge_image_url android_app_badge_image_url app_cta_image_url - splits_background_image_url + splits_background_image_url hero_video_upload ].freeze # POST /community-landing/admin/pin-upload def pin_upload upload = Upload.find(params[:upload_id]) setting_name = params[:setting_name].to_s - raise Discourse::InvalidParameters unless ALLOWED_IMAGE_SETTINGS.include?(setting_name) + raise Discourse::InvalidParameters unless ALLOWED_UPLOAD_SETTINGS.include?(setting_name) key = "upload_pin_#{setting_name}" existing = PluginStore.get("community-landing", key)