From 49072574ecece99c2ac4e8a9fcf65c84f4c6dd89 Mon Sep 17 00:00:00 2001 From: Mattias Bodlund Date: Wed, 28 May 2025 12:27:30 +0200 Subject: [PATCH] na --- app/assets/stylesheets/application.css | 3 +- app/helpers/questions_helper.rb | 2 +- app/javascript/application.js | 325 ++++++++++++++----------- app/views/players/new.html.erb | 2 +- app/views/questions/result.html.erb | 2 +- config/locales/en.yml | 3 +- 6 files changed, 189 insertions(+), 148 deletions(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 10cf3a2..1977843 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -429,7 +429,7 @@ main { line-height: 1.3; align-self: end; - min-height: 160px; + /*min-height: 160px;*/ display: flex; flex-direction: column; @@ -524,6 +524,7 @@ label { content: '|'; animation: typewriter-blink 1s infinite; color: currentColor; + font-weight: 400; position: absolute; } diff --git a/app/helpers/questions_helper.rb b/app/helpers/questions_helper.rb index 4c638b9..332a5fd 100644 --- a/app/helpers/questions_helper.rb +++ b/app/helpers/questions_helper.rb @@ -3,7 +3,7 @@ module QuestionsHelper def decorate_divs_for_typewriting_effect(html) doc = Nokogiri::HTML.fragment(html, 'utf-8') - doc.xpath('./*').each do |div| + doc.xpath('./*[position() > last()-2]').each do |div| div['class'] = 'typewriter-text' end diff --git a/app/javascript/application.js b/app/javascript/application.js index 36ef0ce..0a37523 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -25,12 +25,12 @@ document.addEventListener('turbo:render', (event) => { let errorElements = document.querySelectorAll('.field_with_errors') if (errorElements.length > 0) { - animationElements = document.querySelectorAll('.animation-element'); + animationElements = document.querySelectorAll('.animation-element') gsap.set(animationElements, { opacity: 1 }) } -}); +}) // document.addEventListener("turbo:before-stream-render", animateElements) @@ -51,9 +51,9 @@ function animateElements(event) { text: e.currentTarget.getAttribute('data-share-text'), url: e.currentTarget.getAttribute('data-share-url'), }).then(() => { - console.log('Thanks for sharing!'); + console.log('Thanks for sharing!') }) - .catch(console.error); + .catch(console.error) } else { navigator.clipboard.writeText(window.location.href) alert('URL copied to clipboard!') @@ -62,7 +62,7 @@ function animateElements(event) { }) originalHeights = [] - animationElements = document.querySelectorAll('.animation-element'); + animationElements = document.querySelectorAll('.animation-element') animationElements.forEach((animationElement, index) => { const height = animationElement.offsetHeight @@ -76,7 +76,7 @@ function animateElements(event) { overflow: 'hidden' }) - let typewriterElements = document.querySelectorAll('.typewriter-text'); + let typewriterElements = document.querySelectorAll('.typewriter-text') if (typewriterElements.length > 0) { gsap.set(typewriterElements, { opacity: 0 @@ -84,7 +84,7 @@ function animateElements(event) { } // Start the sequential animation - animateElementsSequentially(); + animateElementsSequentially() } @@ -94,8 +94,8 @@ function animateElements(event) { function animateElementsSequentially(index = 0) { if (index >= animationElements.length) { - console.log('All animations complete'); - return; + console.log('All animations complete') + return } // 'margin-top': (index == (animationElements.length-1) ? "12px" : "8px"), @@ -108,14 +108,29 @@ function animateElementsSequentially(index = 0) { ease: "power2.out", onComplete: () => { + // Focus input field + animationElements[index].querySelector('input[type=text]')?.focus() + + // Typewriter const typewriterTexts = animationElements[index].querySelectorAll('.typewriter-text') if (typewriterTexts.length > 0) { const typewriter = typeWriterConsoleSequential(typewriterTexts[0].parentElement, { - baseSpeed: 20, + baseSpeed: 10, onAllComplete: () => { + + if (index + 1 < animationElements.length) { + let nextAnimationElementInputField = animationElements[index+1].querySelector('input[type=text]') + if (nextAnimationElementInputField) { + animationElements[index].querySelectorAll('.typewriter-cursor').forEach((node) => { + node.classList.remove('typewriter-cursor') + }) + } + } + // Start next element after typewriter finishes - animateElementsSequentially(index + 1); + animateElementsSequentially(index + 1) + } }) typewriter.start() @@ -126,31 +141,56 @@ function animateElementsSequentially(index = 0) { const answerDistributions = animationElements[index].querySelectorAll('.answer-distribution') if (answerDistributions.length > 0 ) { - const containerHeight = animationElements[index].offsetHeight - 8; // - padding + const containerHeight = animationElements[index].offsetHeight - 8 // - padding + + const percentages = Array.from(answerDistributions).map(bar => parseInt(bar.dataset.value)) + const maxPercentage = Math.max(...percentages) + + const targetHeights = [] - const percentages = Array.from(answerDistributions).map(bar => parseInt(bar.dataset.value)); - const maxPercentage = Math.max(...percentages); - - console.info(maxPercentage) - answerDistributions.forEach((bar, i) => { - const percentage = parseInt(bar.dataset.value); - const relativePercentage = (percentage / maxPercentage) * 100; - const calculatedHeight = (relativePercentage / 100) * containerHeight; - + const percentage = parseInt(bar.dataset.value) + const relativePercentage = (percentage / maxPercentage) * 100 + const calculatedHeight = (relativePercentage / 100) * containerHeight + + const computedStyle = window.getComputedStyle(bar) + const minHeight = parseInt(computedStyle.minHeight) || 0 - const computedStyle = window.getComputedStyle(bar); - const minHeight = parseInt(computedStyle.minHeight) || 0; + let finalMinHeight = minHeight + if (bar.querySelector('.explanation')) { + finalMinHeight = Math.max(minHeight, 160) + } - const targetHeight = Math.max(calculatedHeight, minHeight); + targetHeights[i] = Math.max(calculatedHeight, finalMinHeight) + }) - gsap.to(bar, { - height: targetHeight, - duration: 0.4, - ease: "power2.out", - delay: index * 0.1, - onAllComplete: () => { + const percentageValues = percentages.map((val, idx) => ({ value: val, index: idx })) + const sortedPercentages = [...percentageValues].sort((a, b) => b.value - a.value) + const highestBar = sortedPercentages[0] + const lowestBar = sortedPercentages[sortedPercentages.length - 1] + + let equalHeight = answerDistributions[0].offsetHeight + answerDistributions.forEach((bar, i) => { + let fakeHeight, finalHeight + + if (i === highestBar.index) { + fakeHeight = containerHeight * 0.3 + } else if (i === lowestBar.index) { + fakeHeight = containerHeight * 0.8 + } else { + fakeHeight = containerHeight * 0.5 + } + + finalHeight = targetHeights[i] + + gsap.to(bar, { + keyframes: [ + { height: fakeHeight, duration: 0.4, ease: "power2.out" }, + { height: equalHeight, duration: 0.4, ease: "power2.inOut" }, + { height: finalHeight, duration: 0.4, ease: "power2.out" } + ], + onComplete: () => { if (bar.dataset.answered != undefined) { bar.classList.add('answered') } @@ -158,32 +198,31 @@ function animateElementsSequentially(index = 0) { gsap.to(bar.querySelectorAll('.percentage'), { opacity: 1, duration: 0.4, - onAllComplete: function() { + onComplete: function () { const explanationElement = this.targets()[0].nextElementSibling if (explanationElement) { explanationElement.style.display = 'block' - } + } } }) - if (i == (answerDistributions.length - 1)) { - setTimeout(() => animateElementsSequentially(index + 1), 1000); - + setTimeout(() => animateElementsSequentially(index + 1), 1000) } } - }); - + }) }) + return + } - animateElementsSequentially(index + 1); + animateElementsSequentially(index + 1) } - }); + }) } @@ -196,156 +235,156 @@ function typeWriterConsoleSequential(container, options = {}) { const { selector = '.typewriter-text', baseSpeed = 60, - divDelay = 600, + divDelay = 400, showCursor = true, onDivComplete = null, onAllComplete = null - } = options; + } = options - const textDivs = container.querySelectorAll(selector); - if (textDivs.length === 0) return null; + const textDivs = container.querySelectorAll(selector) + if (textDivs.length === 0) return null - let currentDivIndex = 0; - let isActive = false; - let animationId = null; - let timeoutIds = new Set(); // Track all timeouts for cleanup + let currentDivIndex = 0 + let isActive = false + let animationId = null + let timeoutIds = new Set() // Track all timeouts for cleanup // Cleanup function for Turbo navigation function cleanup() { - isActive = false; + isActive = false if (animationId) { - cancelAnimationFrame(animationId); - animationId = null; + cancelAnimationFrame(animationId) + animationId = null } // Clear all timeouts - timeoutIds.forEach(id => clearTimeout(id)); - timeoutIds.clear(); + timeoutIds.forEach(id => clearTimeout(id)) + timeoutIds.clear() } // Set up Turbo event listeners for cleanup function setupTurboListeners() { - const turboEvents = ['turbo:before-visit', 'turbo:before-cache', 'beforeunload']; + const turboEvents = ['turbo:before-visit', 'turbo:before-cache', 'beforeunload'] turboEvents.forEach(eventName => { - document.addEventListener(eventName, cleanup, { once: false }); - }); + document.addEventListener(eventName, cleanup, { once: false }) + }) // Store cleanup function on container for manual cleanup - container._typewriterCleanup = cleanup; + container._typewriterCleanup = cleanup } // Enhanced setTimeout that tracks IDs function safeSetTimeout(callback, delay) { const id = setTimeout(() => { - timeoutIds.delete(id); - if (isActive) callback(); - }, delay); - timeoutIds.add(id); - return id; + timeoutIds.delete(id) + if (isActive) callback() + }, delay) + timeoutIds.add(id) + return id } // Store original text and prepare divs function initializeDivs() { // Set container height to prevent layout shifts - const containerHeight = container.offsetHeight; - container.style.minHeight = `${containerHeight}px`; + const containerHeight = container.offsetHeight + container.style.minHeight = `${containerHeight}px` textDivs.forEach(div => { if (!div.dataset.originalText) { - div.dataset.originalText = div.textContent.trim(); + div.dataset.originalText = div.textContent.trim() } // Use visibility instead of blanking content to maintain layout - div.style.visibility = 'hidden'; - div.innerHTML = ''; - div.style.opacity = '1'; - }); + div.style.visibility = 'hidden' + div.innerHTML = '' + div.style.opacity = '1' + }) // Force reflow to ensure styles are applied - container.offsetHeight; + container.offsetHeight } function createCursorSpan() { // No longer needed - cursor is now handled via CSS - return null; + return null } function updateDivContent(div, text, withCursor = false) { // Use textContent for better performance - div.textContent = text; + div.textContent = text // Toggle cursor class instead of adding DOM element if (withCursor && showCursor) { - div.classList.add('typewriter-cursor'); + div.classList.add('typewriter-cursor') } else { - div.classList.remove('typewriter-cursor'); + div.classList.remove('typewriter-cursor') } } function animateNextDiv() { if (currentDivIndex >= textDivs.length || !isActive) { - isActive = false; + isActive = false if (onAllComplete) { // Use setTimeout to avoid potential timing issues requestAnimationFrame(() => { - safeSetTimeout(() => onAllComplete(container), 200); - }); + safeSetTimeout(() => onAllComplete(container), 200) + }) } - return; + return } - const currentDiv = textDivs[currentDivIndex]; - const originalText = currentDiv.dataset.originalText; - const isLastDiv = currentDivIndex === textDivs.length - 1; + const currentDiv = textDivs[currentDivIndex] + const originalText = currentDiv.dataset.originalText + const isLastDiv = currentDivIndex === textDivs.length - 1 // Make div visible and prepare for typing - currentDiv.style.visibility = 'visible'; - updateDivContent(currentDiv, '', showCursor); - currentDiv.classList.add('typing-active'); + currentDiv.style.visibility = 'visible' + updateDivContent(currentDiv, '', showCursor) + currentDiv.classList.add('typing-active') - let charIndex = 0; - let nextCharTime = performance.now() + 200; // Initial delay + let charIndex = 0 + let nextCharTime = performance.now() + 100 // Initial delay function typeFrame(currentTime) { if (!isActive) { if (animationId) { - cancelAnimationFrame(animationId); - animationId = null; + cancelAnimationFrame(animationId) + animationId = null } - return; + return } if (currentTime >= nextCharTime && charIndex < originalText.length) { - charIndex++; - const currentText = originalText.substring(0, charIndex); - updateDivContent(currentDiv, currentText, true); + charIndex++ + const currentText = originalText.substring(0, charIndex) + updateDivContent(currentDiv, currentText, true) // Calculate delay for next character - let delay = baseSpeed + Math.random() * 30; - const char = originalText.charAt(charIndex - 1); + let delay = baseSpeed + Math.random() * 15 + const char = originalText.charAt(charIndex - 1) if (char === '.' || char === '!' || char === '?') { - delay += 250; - } else if (char === ',' || char === ';') { - delay += 120; + delay += 250 + } else if (char === ',' || char === '') { + delay += 120 } else if (char === ' ') { - delay += 60; + delay += 60 } - nextCharTime = currentTime + delay; + nextCharTime = currentTime + delay } // Check if current div is complete if (charIndex >= originalText.length) { - currentDiv.classList.remove('typing-active'); + currentDiv.classList.remove('typing-active') // Final content update - updateDivContent(currentDiv, originalText, showCursor); + updateDivContent(currentDiv, originalText, showCursor) if (onDivComplete) { - onDivComplete(currentDiv, currentDivIndex, container); + onDivComplete(currentDiv, currentDivIndex, container) } - currentDivIndex++; + currentDivIndex++ if (isActive && currentDivIndex < textDivs.length) { // Move to next div after delay @@ -353,106 +392,106 @@ function typeWriterConsoleSequential(container, options = {}) { if (isActive) { // Remove cursor from previous div (except last one) if (!isLastDiv) { - updateDivContent(currentDiv, originalText, false); + updateDivContent(currentDiv, originalText, false) } - animateNextDiv(); + animateNextDiv() } - }, divDelay); + }, divDelay) } else if (isActive) { // This was the last div safeSetTimeout(() => { if (onAllComplete) { - onAllComplete(container); + onAllComplete(container) } - }, 200); + }, 200) } - return; + return } // Continue animation if (isActive) { - animationId = requestAnimationFrame(typeFrame); + animationId = requestAnimationFrame(typeFrame) } } // Start the animation - animationId = requestAnimationFrame(typeFrame); + animationId = requestAnimationFrame(typeFrame) } // Initialize Turbo listeners - setupTurboListeners(); + setupTurboListeners() // Add required CSS for cursor pseudo-element function addCursorStyles() { - const styleId = 'typewriter-cursor-styles'; + const styleId = 'typewriter-cursor-styles' if (!document.getElementById(styleId)) { - const style = document.createElement('style'); - style.id = styleId; + const style = document.createElement('style') + style.id = styleId style.textContent = ` .typewriter-cursor::after { - content: '|'; - animation: typewriter-blink 1s infinite; - color: currentColor; + content: '|' + animation: typewriter-blink 1s infinite + color: currentColor } @keyframes typewriter-blink { - 0%, 50% { opacity: 1; } - 51%, 100% { opacity: 0; } + 0%, 50% { opacity: 1 } + 51%, 100% { opacity: 0 } } - `; - document.head.appendChild(style); + ` + document.head.appendChild(style) } } // Initialize styles - addCursorStyles(); + addCursorStyles() return { start: () => { - if (isActive) return; // Prevent multiple starts + if (isActive) return // Prevent multiple starts - initializeDivs(); - isActive = true; - currentDivIndex = 0; + initializeDivs() + isActive = true + currentDivIndex = 0 // Use requestAnimationFrame for smoother start requestAnimationFrame(() => { if (isActive) { - animateNextDiv(); + animateNextDiv() } - }); + }) }, stop: () => { - cleanup(); + cleanup() }, reset: () => { - cleanup(); + cleanup() textDivs.forEach(div => { if (div.dataset.originalText) { - div.style.visibility = 'visible'; - updateDivContent(div, div.dataset.originalText, false); - div.classList.remove('typing-active'); - div.classList.remove('typewriter-cursor'); + div.style.visibility = 'visible' + updateDivContent(div, div.dataset.originalText, false) + div.classList.remove('typing-active') + div.classList.remove('typewriter-cursor') } - }); - currentDivIndex = 0; + }) + currentDivIndex = 0 }, // Manual cleanup method for explicit cleanup destroy: () => { - cleanup(); + cleanup() // Remove Turbo event listeners - const turboEvents = ['turbo:before-visit', 'turbo:before-cache', 'beforeunload']; + const turboEvents = ['turbo:before-visit', 'turbo:before-cache', 'beforeunload'] turboEvents.forEach(eventName => { - document.removeEventListener(eventName, cleanup); - }); + document.removeEventListener(eventName, cleanup) + }) // Remove cleanup reference - delete container._typewriterCleanup; + delete container._typewriterCleanup }, - get isActive() { return isActive; } - }; + get isActive() { return isActive } + } } \ No newline at end of file diff --git a/app/views/players/new.html.erb b/app/views/players/new.html.erb index 8d2310a..7a166a8 100644 --- a/app/views/players/new.html.erb +++ b/app/views/players/new.html.erb @@ -25,7 +25,7 @@
<%= form.label :name, class: "question-answer" do %> - <%= form.text_field :name, placeholder: t('what_is_your_name'), autofocus: true %> + <%= form.text_field :name, placeholder: t('write_your_name'), autofocus: true, autocomplete: 'off' %> <% end %>
diff --git a/app/views/questions/result.html.erb b/app/views/questions/result.html.erb index 5cbb92c..8198604 100644 --- a/app/views/questions/result.html.erb +++ b/app/views/questions/result.html.erb @@ -26,7 +26,7 @@
- <%= link_to tag.span(t('read_more')), "#", class: 'button__base' %> + <%= link_to tag.span(t('read_more')), t('read_more_link'), class: 'button__base' %> <%= button_tag tag.span(t('share_on_story')), data: { share_title: t('share.title'), diff --git a/config/locales/en.yml b/config/locales/en.yml index c7f33be..c00e180 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -5,7 +5,7 @@ en: get_started: Let’s go! what_is_your_name: What’s your name? - please_type_your_name_here: Please type your name here… + write_your_name: Write your name. submit: Submit next_question: Next question of_people_worldwide_think_just_lik_you: of people worldwide think like you. @@ -13,6 +13,7 @@ en: results: Results read_more: Read more share_on_story: Share + read_more_link: https://ikeafoundation.org/25 please_type_your_name_to_continue: Please type your name to continue share: