diff --git a/assets/javascripts/community_landing/landing.js b/assets/javascripts/community_landing/landing.js index 716e6cc..f08431b 100644 --- a/assets/javascripts/community_landing/landing.js +++ b/assets/javascripts/community_landing/landing.js @@ -25,7 +25,26 @@ var progressBar = $(".cl-progress-bar"); // ═══════════════════════════════════════════════════════════════════ - // 2. NAVBAR & SCROLL + // 2. HERO IMAGE RANDOM CYCLE + // ═══════════════════════════════════════════════════════════════════ + (function initHeroImage() { + var container = $(".cl-hero__image[data-hero-images]"); + if (!container) return; + try { + var images = JSON.parse(container.getAttribute("data-hero-images")); + if (!images || images.length < 2) return; + var img = $(".cl-hero__image-img", container); + if (!img) return; + var pick = images[Math.floor(Math.random() * images.length)]; + img.style.opacity = "0"; + img.src = pick; + img.onload = function () { img.style.opacity = ""; }; + img.onerror = function () { img.src = images[0]; img.style.opacity = ""; }; + } catch (e) {} + })(); + + // ═══════════════════════════════════════════════════════════════════ + // 3. NAVBAR & SCROLL // ═══════════════════════════════════════════════════════════════════ var navbar = $("#cl-navbar"); if (navbar) { @@ -53,7 +72,7 @@ } // ═══════════════════════════════════════════════════════════════════ - // 3. ENHANCED REVEAL (Staggered) + // 4. ENHANCED REVEAL (Staggered) // ═══════════════════════════════════════════════════════════════════ if ("IntersectionObserver" in window) { var revealObserver = new IntersectionObserver(function (entries) { @@ -74,7 +93,7 @@ } // ═══════════════════════════════════════════════════════════════════ - // 4. MOUSE PARALLAX + // 5. MOUSE PARALLAX // ═══════════════════════════════════════════════════════════════════ var heroImage = $(".cl-hero__image-img"); var orbs = $$(".cl-orb"); @@ -97,7 +116,7 @@ } // ═══════════════════════════════════════════════════════════════════ - // 5. STAT COUNTER + // 6. STAT COUNTER // ═══════════════════════════════════════════════════════════════════ function animateCount(el) { if (el.classList.contains("counted")) return; @@ -130,4 +149,57 @@ var sr = $("#cl-stats-row"); if (sr) statsObs.observe(sr); } + // ═══════════════════════════════════════════════════════════════════ + // 7. VIDEO MODAL + // ═══════════════════════════════════════════════════════════════════ + var videoModal = $("#cl-video-modal"); + var videoPlayer = $("#cl-video-player"); + + if (videoModal && videoPlayer) { + function parseYouTubeId(url) { + var match = url.match(/(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([^&?#]+)/); + return match ? match[1] : null; + } + + function openVideoModal(url) { + var ytId = parseYouTubeId(url); + if (ytId) { + videoPlayer.innerHTML = ''; + } else { + videoPlayer.innerHTML = ''; + } + videoModal.classList.add("active"); + document.body.style.overflow = "hidden"; + } + + function closeVideoModal() { + videoModal.classList.remove("active"); + videoPlayer.innerHTML = ""; + document.body.style.overflow = ""; + } + + $$(".cl-hero-play").forEach(function (btn) { + btn.addEventListener("click", function () { + var url = btn.getAttribute("data-video-url"); + if (url) openVideoModal(url); + }); + }); + + var closeBtn = $(".cl-video-modal__close", videoModal); + if (closeBtn) { + closeBtn.addEventListener("click", closeVideoModal); + } + + var backdrop = $(".cl-video-modal__backdrop", videoModal); + if (backdrop) { + backdrop.addEventListener("click", closeVideoModal); + } + + document.addEventListener("keydown", function (e) { + if (e.key === "Escape" && videoModal.classList.contains("active")) { + closeVideoModal(); + } + }); + } + })(); diff --git a/assets/stylesheets/community_landing/landing.css b/assets/stylesheets/community_landing/landing.css index 862a424..b9ea8ff 100644 --- a/assets/stylesheets/community_landing/landing.css +++ b/assets/stylesheets/community_landing/landing.css @@ -362,6 +362,18 @@ padding: 0.55rem 0; } +.cl-progress-bar { + position: absolute; + top: 0; + left: 0; + height: 3px; + width: 0%; + background: var(--cl-accent); + z-index: 1001; + transition: width 0.1s linear; + border-radius: 0 2px 2px 0; +} + .cl-navbar__inner { max-width: 1200px; margin: 0 auto; @@ -663,6 +675,213 @@ gap: 1rem; } +/* ── Hero Image ── */ +.cl-hero__image { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.cl-hero__image-img { + width: 100%; + height: auto; + border-radius: var(--cl-radius); + object-fit: cover; + transition: opacity 0.6s ease; +} + +/* ── Hero Background Image (flat mode) ── */ +.cl-hero__bg { + position: absolute; + inset: 0; + background-size: cover; + background-position: center; + z-index: 0; + opacity: 0.25; +} + +.cl-hero__bg::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(to bottom, transparent 60%, var(--cl-bg)); +} + +/* ── Hero Card Mode ── */ +.cl-hero--card .cl-hero__inner { + background: var(--cl-card); + border: 1px solid var(--cl-border); + border-radius: var(--cl-radius); + padding: 3rem 2.5rem; + backdrop-filter: var(--cl-blur); + -webkit-backdrop-filter: var(--cl-blur); + box-shadow: 0 8px 32px var(--cl-shadow); + background-size: cover; + background-position: center; +} + +@media (min-width: 1024px) { + .cl-hero--card .cl-hero__inner { + padding: 4rem 3.5rem; + } +} + +/* ── Hero Video Play Button ── */ +.cl-hero-play { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 5; + width: 80px; + height: 80px; + border-radius: 50%; + border: none; + background: var(--cl-accent); + color: #fff; + cursor: pointer; + 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); + transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); + animation: cl-play-pulse 2s infinite; +} + +.cl-hero-play:hover { + transform: translate(-50%, -50%) scale(1.1); + background: var(--cl-accent-hover); + box-shadow: 0 12px 40px var(--cl-accent-glow); +} + +.cl-hero-play__icon { + display: flex; + align-items: center; + justify-content: center; + margin-left: 4px; +} + +.cl-hero-play__icon svg { + width: 32px; + height: 32px; +} + +@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); } +} + +.cl-hero__image--video-only { + display: flex; + align-items: center; + justify-content: center; + min-height: 200px; + position: relative; +} + +.cl-hero__image--video-only .cl-hero-play { + position: relative; + top: auto; + left: auto; + transform: none; + width: 100px; + height: 100px; +} + +.cl-hero__image--video-only .cl-hero-play:hover { + transform: scale(1.1); +} + +.cl-hero__image--video-only .cl-hero-play__icon svg { + width: 40px; + height: 40px; +} + +/* ── Video Modal / Lightbox ── */ +.cl-video-modal { + display: none; + position: fixed; + inset: 0; + z-index: 10000; + align-items: center; + justify-content: center; +} + +.cl-video-modal.active { + display: flex; +} + +.cl-video-modal__backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.85); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); +} + +.cl-video-modal__content { + position: relative; + z-index: 1; + width: 90vw; + max-width: 960px; + aspect-ratio: 16 / 9; + border-radius: var(--cl-radius); + overflow: hidden; + box-shadow: 0 32px 64px rgba(0, 0, 0, 0.5); +} + +.cl-video-modal__close { + position: absolute; + top: -48px; + right: 0; + z-index: 2; + width: 40px; + height: 40px; + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(0, 0, 0, 0.5); + color: #fff; + font-size: 1.5rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; +} + +.cl-video-modal__close:hover { + background: rgba(255, 255, 255, 0.15); + border-color: rgba(255, 255, 255, 0.4); +} + +.cl-video-modal__player { + width: 100%; + height: 100%; + background: #000; +} + +.cl-video-modal__player iframe, +.cl-video-modal__player video { + width: 100%; + height: 100%; + border: none; + display: block; +} + +@media (max-width: 640px) { + .cl-video-modal__content { + width: 95vw; + border-radius: var(--cl-radius-sm); + } + + .cl-video-modal__close { + top: -44px; + right: 4px; + } +} + /* ═══════════════════════════════════════════════════════════════════ 3. PREMIUM STATS — icon with bg, label, animated counter ═══════════════════════════════════════════════════════════════════ */ @@ -909,7 +1128,7 @@ display: flex; flex-direction: column; padding: 1.5rem; - background: var(--cl-card); + background: var(--cl-topic-card-bg, var(--cl-card)); border: 1px solid var(--cl-border); border-radius: var(--cl-radius); text-decoration: none; @@ -983,56 +1202,67 @@ 6. TOP CREATORS — pill badges ═══════════════════════════════════════════════════════════════════ */ .cl-creators { - padding: 1.5rem 0 2rem; + padding: 2.5rem 0 3rem; } .cl-creators__list { display: flex; flex-wrap: wrap; - gap: 0.6rem; + gap: 1rem; } .cl-creator-pill { display: flex; align-items: center; - gap: 0.5rem; - padding: 0.35rem 0.85rem 0.35rem 0.35rem; + gap: 0.75rem; + padding: 0.6rem 1.25rem 0.6rem 0.6rem; background: var(--cl-card); border: 1px solid var(--cl-border); border-radius: 50px; text-decoration: none; - transition: all 0.2s ease; + transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); + backdrop-filter: var(--cl-blur); + -webkit-backdrop-filter: var(--cl-blur); } .cl-creator-pill:hover { border-color: var(--cl-border-hover); background: var(--cl-accent-subtle); + transform: translateY(-3px); + box-shadow: 0 8px 24px var(--cl-shadow); } .cl-creator-pill__avatar { - width: 32px; - height: 32px; + width: 48px; + height: 48px; border-radius: 50%; object-fit: cover; border: 2px solid var(--cl-border); + transition: border-color 0.3s ease; +} + +.cl-creator-pill:hover .cl-creator-pill__avatar { + border-color: var(--cl-accent); } .cl-creator-pill__name { - font-size: 0.82rem; - font-weight: 600; + font-size: 0.95rem; + font-weight: 700; color: var(--cl-text-strong); white-space: nowrap; } .cl-creator-pill__count { - font-size: 0.72rem; + font-size: 0.8rem; color: var(--cl-accent); white-space: nowrap; - font-weight: 700; + font-weight: 800; margin-left: auto; - padding-left: 0.4rem; - min-width: 1.2em; + padding: 0.15rem 0.6rem; + min-width: 1.5em; text-align: center; + background: var(--cl-accent-subtle); + border-radius: 50px; } /* ═══════════════════════════════════════════════════════════════════ @@ -1435,4 +1665,8 @@ opacity: 1; transform: none; } + + .cl-hero-play { + animation: none; + } } \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 3d7bab8..48b583f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -35,6 +35,7 @@ en: 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_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_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." @@ -77,6 +78,7 @@ en: 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_card_bg_color: "Background color for each trending topic card. Leave blank for default card styling." 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." diff --git a/config/settings.yml b/config/settings.yml index bde0ae8..a9a4fa3 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -82,7 +82,7 @@ plugins: type: string navbar_bg_color: default: "" - type: string + type: color navbar_border_style: default: "none" type: enum @@ -127,12 +127,15 @@ plugins: hero_secondary_button_url: default: "/login" type: string + hero_video_url: + default: "" + type: string hero_bg_dark: default: "" - type: string + type: color hero_bg_light: default: "" - type: string + type: color hero_min_height: default: 0 type: integer @@ -158,7 +161,7 @@ plugins: type: color stat_icon_bg_color: default: "" - type: string + type: color stat_icon_shape: default: "circle" type: enum @@ -167,7 +170,7 @@ plugins: - rounded stat_counter_color: default: "" - type: string + type: color stat_members_label: default: "Members" type: string @@ -185,10 +188,10 @@ plugins: type: string stats_bg_dark: default: "" - type: string + type: color stats_bg_light: default: "" - type: string + type: color stats_min_height: default: 0 type: integer @@ -241,10 +244,10 @@ plugins: type: string about_bg_dark: default: "" - type: string + type: color about_bg_light: default: "" - type: string + type: color about_min_height: default: 0 type: integer @@ -271,12 +274,15 @@ plugins: topics_count: default: 5 type: integer + topics_card_bg_color: + default: "" + type: color topics_bg_dark: default: "" - type: string + type: color topics_bg_light: default: "" - type: string + type: color topics_min_height: default: 0 type: integer @@ -308,10 +314,10 @@ plugins: type: integer contributors_bg_dark: default: "" - type: string + type: color contributors_bg_light: default: "" - type: string + type: color contributors_min_height: default: 0 type: integer @@ -343,13 +349,13 @@ plugins: type: list groups_card_bg_color: default: "" - type: string + type: color groups_bg_dark: default: "" - type: string + type: color groups_bg_light: default: "" - type: string + type: color groups_min_height: default: 0 type: integer @@ -414,10 +420,10 @@ plugins: type: string app_cta_bg_dark: default: "" - type: string + type: color app_cta_bg_light: default: "" - type: string + type: color app_cta_min_height: default: 0 type: integer @@ -446,10 +452,10 @@ plugins: type: string footer_bg_dark: default: "" - type: string + type: color footer_bg_light: default: "" - type: string + type: color footer_border_style: default: "solid" type: enum diff --git a/lib/community_landing/icons.rb b/lib/community_landing/icons.rb index 678ff6d..7deea3a 100644 --- a/lib/community_landing/icons.rb +++ b/lib/community_landing/icons.rb @@ -12,6 +12,8 @@ module CommunityLanding STAT_LIKES_SVG = '' STAT_CHATS_SVG = '' + PLAY_SVG = '' + COMMENT_SVG = '' HEART_SVG = '' diff --git a/lib/community_landing/page_builder.rb b/lib/community_landing/page_builder.rb index 0023c2b..24d63a9 100644 --- a/lib/community_landing/page_builder.rb +++ b/lib/community_landing/page_builder.rb @@ -29,6 +29,7 @@ module CommunityLanding html << render_app_cta html << render_footer_desc html << render_footer + html << render_video_modal html << "\n" html << "\n" html @@ -80,12 +81,13 @@ module CommunityLanding 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" + nav_style_parts = [] + nav_style_parts << "--cl-nav-bg: #{navbar_bg}" if navbar_bg + nav_style_parts << "--cl-nav-border: 1px #{navbar_border} var(--cl-border)" if navbar_border && navbar_border != "none" + nav_style = nav_style_parts.any? ? " style=\"#{nav_style_parts.join('; ')}\"" : "" html = +"" - html << "