From 237f30ad58eec3cc8277d74a0989b3a2af166110 Mon Sep 17 00:00:00 2001 From: Mattias Bodlund Date: Wed, 28 May 2025 18:32:23 +0200 Subject: [PATCH] na --- app/assets/stylesheets/application.css | 66 ++++++++++---- app/helpers/application_helper.rb | 12 +++ app/javascript/application.js | 27 ++++++ app/javascript/quiz_preloader.js | 117 +++++++++++++++++++++++++ app/views/languages/index.html.erb | 19 +++- app/views/layouts/application.html.erb | 10 ++- app/views/players/new.html.erb | 2 +- app/views/questions/answer.html.erb | 2 +- app/views/questions/show.html.erb | 29 +++--- config/importmap.rb | 1 + 10 files changed, 248 insertions(+), 37 deletions(-) create mode 100644 app/javascript/quiz_preloader.js diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index b8fb449..008dd79 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -125,19 +125,35 @@ footer { z-index: 2; } -@keyframes slide-in { from { transform: translateY(100dvh); } to { transform: translateY(0); } } -@keyframes slide-out { from { transform: translateY(0); } to { transform: translateY(-100dvh); } } +@keyframes slide-in { + from { + transform: translateY(100dvh); + } + + to { + transform: translateY(0); + } +} +@keyframes slide-out { + from { + transform: translateY(0); + } + + to { + transform: translateY(-100dvh); + } +} /* Forward nav (normal clicks) */ html[data-turbo-visit-direction="forward"] { - &::view-transition-new(main) { animation: slide-in .4s ease-out both; } - &::view-transition-old(main) { animation: slide-out .4s ease-in both; } + &::view-transition-new(main) { animation: slide-in .3s ease-out both; } + &::view-transition-old(main) { animation: slide-out .3s ease-in both; } } /* Back button reverses the sense */ html[data-turbo-visit-direction="back"] { - &::view-transition-new(main), - &::view-transition-old(main) { animation: slide-in .4s ease-out both reverse; } + &::view-transition-new(main) { animation: none } + &::view-transition-old(main) { animation: none } } body { @@ -151,8 +167,14 @@ body { -webkit-font-smoothing: antialiased; touch-action: manipulation; - &:has(main .background-container) { - background-color: var(--clr-black); + background-image: var(--bg-landscape); + background-position: center center; + background-attachment: fixed; + background-repeat: no-repeat; + background-size: cover; + + @media (orientation: portrait) { + background-image: var(--bg-portrait); } } @@ -176,15 +198,16 @@ header { } path.logo-text { - fill: var(--clr-black); + fill: var(--clr-white); } -:has(main .background-container) { + +:has(main .with-backdrop) { & header path.logo-text { - fill: var(--clr-white); + fill: var(--clr-black); } & footer { - color: var(--clr-white); + color: var(--clr-black); } } @@ -197,7 +220,8 @@ footer { font-family: var(--ff-ikea); writing-mode: vertical-rl; text-orientation: mixed; - z-index: 10 + z-index: 10; + color: var(--clr-white); } @@ -265,10 +289,13 @@ footer { justify-content: center; padding: 0 1em; box-sizing: border-box; - transition: scale 0.08s ease-in-out; + transition: transform 0.08s ease-in-out; + user-select: none; + -webkit-user-select: none; + -webkit-tap-highlight-color: transparent; - &:not(.wiggle):active { - scale: 0.96; + &:not(.wiggle):active { + transform: scale(0.96); } } @@ -293,13 +320,16 @@ footer { main { - margin: 0 1.2rem 1.6rem 1.2rem; + padding: 50px 1.2rem 1.6rem 1.2rem; flex-grow: 1; display: flex; flex-direction: column; justify-content: end; - + &:has(.with-backdrop) { + background-color: var(--clr-dark-yellow); + } + } diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 9f1e2f5..7557ca3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -11,4 +11,16 @@ module ApplicationHelper end end + + def quiz_image_urls + + urls = [] + + assets = Node.viewable.map {|node| node.assets.includes(file_attachment: :blob).select{ |asset| asset.file.image? } }.flatten + assets.each do |asset| + urls << rails_storage_proxy_path(asset.file.variant(resize_to_limit: [((image_orientation(asset.file) == :landscape) ? 2400 : 1600), nil])) + end + + urls.compact.uniq + end end diff --git a/app/javascript/application.js b/app/javascript/application.js index 94fe2f7..cd6e9eb 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -4,6 +4,8 @@ import { Application } from "@hotwired/stimulus" import LocaleController from "locale_controller" import ImageController from "image_controller" +import QuizImagePreloader from "quiz_preloader" + const application = Application.start() application.register("locale", LocaleController) @@ -15,10 +17,35 @@ window.Stimulus = application export { application } +const quizPreloader = new QuizImagePreloader() +quizPreloader.init() + var animationElements var originalHeights +document.addEventListener("turbo:before-visit", (event) => { + const bodyStyle = document.body.getAttribute('style') + if (bodyStyle) { + sessionStorage.setItem('previousBodyStyle', bodyStyle) + } else { + sessionStorage.removeItem('previousBodyStyle') + } +}) + +// Apply stored style on backdrop pages +document.addEventListener("turbo:render", (event) => { + const hasBackdrop = document.querySelector('.with-backdrop') + + if (hasBackdrop) { + const previousStyle = sessionStorage.getItem('previousBodyStyle') + if (previousStyle) { + document.body.setAttribute('style', previousStyle) + sessionStorage.removeItem('previousBodyStyle') + } + } +}) + document.addEventListener('turbo:render', (event) => { console.info('turbo:render') diff --git a/app/javascript/quiz_preloader.js b/app/javascript/quiz_preloader.js new file mode 100644 index 0000000..e7c267c --- /dev/null +++ b/app/javascript/quiz_preloader.js @@ -0,0 +1,117 @@ +export default class QuizImagePreloader { + constructor() { + this.isPreloading = false + this.preloadedImages = new Map() + } + + init() { + // Start preloading on initial page load + document.addEventListener('DOMContentLoaded', () => { + sessionStorage.removeItem('quiz_images_preloaded') + // this.startPreloadingFromHeader() + // console.info('startPreloadingFromHeader'); + }) + + // Continue preloading on Turbo navigation + document.addEventListener('turbo:load', () => { + this.startPreloadingFromHeader() + console.info('startPreloadingFromHeader'); + }) + + + } + + startPreloadingFromHeader() { + + if (sessionStorage.getItem('quiz_images_preloaded') === 'true') { + return + } + + // Skip if already preloading + if (this.isPreloading) { + return + } + + const imageUrls = this.getImageUrlsFromHeader() + + if (imageUrls.length > 0) { + this.preloadImages(imageUrls) + } + } + + getImageUrlsFromHeader() { + const headerElement = document.querySelector('header[data-quiz-images]') || + document.querySelector('[data-quiz-images]') + + if (!headerElement) return [] + + + try { + const urls = JSON.parse(headerElement.dataset.quizImages || '[]') + return urls.filter(url => url && url.trim()) + } catch (e) { + console.warn('Failed to parse quiz image URLs from header:', e) + return [] + } + } + + preloadImages(imageUrls) { + this.isPreloading = true + let loadedCount = 0 + const totalImages = imageUrls.length + + imageUrls.forEach((url, index) => { + // Skip if already preloaded + if (this.preloadedImages.has(url)) { + loadedCount++ + if (loadedCount === totalImages) { + this.onPreloadComplete() + } + return + } + + const img = new Image() + + img.onload = () => { + this.preloadedImages.set(url, img) + loadedCount++ + + if (loadedCount === totalImages) { + this.onPreloadComplete() + } + } + + img.onerror = () => { + console.warn(`Failed to preload image: ${url}`) + loadedCount++ + + if (loadedCount === totalImages) { + this.onPreloadComplete() + } + } + + // Stagger requests to avoid overwhelming server + setTimeout(() => { + img.src = url + }, index * 30) + }) + } + + onPreloadComplete() { + this.isPreloading = false + sessionStorage.setItem('quiz_images_preloaded', 'true') + + console.info('PreloadComplete'); + // Dispatch completion event for components that need to know + document.dispatchEvent(new CustomEvent('quiz:preload:complete', { + detail: { totalImages: this.preloadedImages.size } + })) + } + + // Check if preloading is complete + isComplete() { + return sessionStorage.getItem('quiz_images_preloaded') === 'true' + } + + +} \ No newline at end of file diff --git a/app/views/languages/index.html.erb b/app/views/languages/index.html.erb index 32d7f54..8605cf4 100644 --- a/app/views/languages/index.html.erb +++ b/app/views/languages/index.html.erb @@ -1,10 +1,21 @@ -<%- content_for :title, t('project_name') %> +<%- + content_for :title, t('project_name') + assets = @node.assets.includes(file_attachment: :blob).select{ |asset| asset.file.image? }.to_a -
- <%= responsive_picture_tag_for_question(@node) %> -
+ # Find one landscape and one portrait image + landscape_asset = assets.find { |asset| image_orientation(asset.file) == :landscape } + portrait_asset = assets.find { |asset| image_orientation(asset.file) == :portrait } + + landscape_asset ||= portrait_asset + portrait_asset ||= landscape_asset + + body_styles = [] + body_styles << "--bg-landscape: url(#{rails_storage_proxy_path(landscape_asset.file.variant(resize_to_limit: [2400, nil]))});" + body_styles << "--bg-portrait: url(#{rails_storage_proxy_path(portrait_asset.file.variant(resize_to_limit: [1600, nil]))});" + content_for :body_style, body_styles.join(' ') +%>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 9ba0d78..454e95a 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -23,10 +23,16 @@ <%= stylesheet_link_tag "application" %> <%= javascript_include_tag 'gsap/gsap.min.js' %> - <%= frontend_javascript_importmap_tags %w'application @hotwired/turbo-rails @hotwired/stimulus locale_controller image_controller' %> + <%= frontend_javascript_importmap_tags %w'application @hotwired/turbo-rails @hotwired/stimulus locale_controller image_controller quiz_preloader' %> + + <% if content_for? :body_style %> + + <% else %> -
+ <% end %> + +
<%= link_to svg('ikea-foundation-203x22'), url_for(controller: 'languages', action: (I18n.default_locale == I18n.locale ? 'index' : 'show')) %>
diff --git a/app/views/players/new.html.erb b/app/views/players/new.html.erb index 22d0343..10adb9e 100644 --- a/app/views/players/new.html.erb +++ b/app/views/players/new.html.erb @@ -5,7 +5,7 @@ <%= form_with model: @player, url: url_for(controller: 'players', action: 'create') do |form| %> -
+
diff --git a/app/views/questions/answer.html.erb b/app/views/questions/answer.html.erb index b430eff..3d7aafd 100644 --- a/app/views/questions/answer.html.erb +++ b/app/views/questions/answer.html.erb @@ -4,7 +4,7 @@ %> -
+
diff --git a/app/views/questions/show.html.erb b/app/views/questions/show.html.erb index fb8cee6..87a31a7 100644 --- a/app/views/questions/show.html.erb +++ b/app/views/questions/show.html.erb @@ -1,19 +1,26 @@ <%- content_for :title, question.page_title.blank? ? question.title : question.page_title content_for :meta_description, question.page_description + + assets = question.assets.includes(file_attachment: :blob).select{ |asset| asset.file.image? }.to_a + + if assets.any? + # Find one landscape and one portrait image + landscape_asset = assets.find { |asset| image_orientation(asset.file) == :landscape } + portrait_asset = assets.find { |asset| image_orientation(asset.file) == :portrait } + + landscape_asset ||= portrait_asset + portrait_asset ||= landscape_asset + + body_styles = [] + body_styles << "--bg-landscape: url(#{rails_storage_proxy_path(landscape_asset.file.variant(resize_to_limit: [2400, nil]))});" + body_styles << "--bg-portrait: url(#{rails_storage_proxy_path(portrait_asset.file.variant(resize_to_limit: [1600, nil]))});" + + content_for :body_style, body_styles.join(' ') + end %> -<%# Array(question.assets.select{ |asset| asset.file.image? }.first).each do |asset| %> -
- <%# image_tag rails_storage_proxy_path(asset.file.representation(resize_to_limit: [800,nil], format: :jpg)), - srcset: "#{rails_storage_proxy_path(asset.file.representation(resize_to_limit: [800,nil], format: :jpg))} 800w, - #{rails_storage_proxy_path(asset.file.representation(resize_to_limit: [1600,nil], format: :jpg))} 1600w, - #{rails_storage_proxy_path(asset.file.representation(resize_to_limit: [2400,nil], format: :jpg))} 2400w", - sizes: "100vw" - %> - <%= responsive_picture_tag_for_question(question) %> -
-<%# end %> + <%= form_with model: @answer, url: url_for(controller: 'answers', action: 'create'), id: 'questionForm' do |form| %> diff --git a/config/importmap.rb b/config/importmap.rb index 53f3863..95465ab 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -16,3 +16,4 @@ pin "trix" # @2.1.15 pin "application", preload: false pin "locale_controller", preload: false pin "image_controller", preload: false +pin "quiz_preloader", preload: false