Mattias Bodlund 6 months ago
parent
commit
237f30ad58
10 changed files with 248 additions and 37 deletions
  1. +48
    -18
      app/assets/stylesheets/application.css
  2. +12
    -0
      app/helpers/application_helper.rb
  3. +27
    -0
      app/javascript/application.js
  4. +117
    -0
      app/javascript/quiz_preloader.js
  5. +15
    -4
      app/views/languages/index.html.erb
  6. +8
    -2
      app/views/layouts/application.html.erb
  7. +1
    -1
      app/views/players/new.html.erb
  8. +1
    -1
      app/views/questions/answer.html.erb
  9. +18
    -11
      app/views/questions/show.html.erb
  10. +1
    -0
      config/importmap.rb

+ 48
- 18
app/assets/stylesheets/application.css View File

@ -125,19 +125,35 @@ footer {
z-index: 2; 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) */ /* Forward nav (normal clicks) */
html[data-turbo-visit-direction="forward"] { 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 */ /* Back button reverses the sense */
html[data-turbo-visit-direction="back"] { 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 { body {
@ -151,8 +167,14 @@ body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
touch-action: manipulation; 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 { path.logo-text {
fill: var(--clr-black);
fill: var(--clr-white);
} }
:has(main .background-container) {
:has(main .with-backdrop) {
& header path.logo-text { & header path.logo-text {
fill: var(--clr-white);
fill: var(--clr-black);
} }
& footer { & footer {
color: var(--clr-white);
color: var(--clr-black);
} }
} }
@ -197,7 +220,8 @@ footer {
font-family: var(--ff-ikea); font-family: var(--ff-ikea);
writing-mode: vertical-rl; writing-mode: vertical-rl;
text-orientation: mixed; text-orientation: mixed;
z-index: 10
z-index: 10;
color: var(--clr-white);
} }
@ -265,10 +289,13 @@ footer {
justify-content: center; justify-content: center;
padding: 0 1em; padding: 0 1em;
box-sizing: border-box; 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 { main {
margin: 0 1.2rem 1.6rem 1.2rem;
padding: 50px 1.2rem 1.6rem 1.2rem;
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: end; justify-content: end;
&:has(.with-backdrop) {
background-color: var(--clr-dark-yellow);
}
} }


+ 12
- 0
app/helpers/application_helper.rb View File

@ -11,4 +11,16 @@ module ApplicationHelper
end end
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 end

+ 27
- 0
app/javascript/application.js View File

@ -4,6 +4,8 @@ import { Application } from "@hotwired/stimulus"
import LocaleController from "locale_controller" import LocaleController from "locale_controller"
import ImageController from "image_controller" import ImageController from "image_controller"
import QuizImagePreloader from "quiz_preloader"
const application = Application.start() const application = Application.start()
application.register("locale", LocaleController) application.register("locale", LocaleController)
@ -15,10 +17,35 @@ window.Stimulus = application
export { application } export { application }
const quizPreloader = new QuizImagePreloader()
quizPreloader.init()
var animationElements var animationElements
var originalHeights 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) => { document.addEventListener('turbo:render', (event) => {
console.info('turbo:render') console.info('turbo:render')


+ 117
- 0
app/javascript/quiz_preloader.js View File

@ -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'
}
}

+ 15
- 4
app/views/languages/index.html.erb View File

@ -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
<div class="background-container">
<%= responsive_picture_tag_for_question(@node) %>
</div>
# 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(' ')
%>
<div class="intro-container"> <div class="intro-container">


+ 8
- 2
app/views/layouts/application.html.erb View File

@ -23,10 +23,16 @@
<%= stylesheet_link_tag "application" %> <%= stylesheet_link_tag "application" %>
<%= javascript_include_tag 'gsap/gsap.min.js' %> <%= 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' %>
</head> </head>
<% if content_for? :body_style %>
<body style="<%= yield :body_style %>">
<% else %>
<body> <body>
<header>
<% end %>
<header data-quiz-images="<%= quiz_image_urls.to_json if defined?(quiz_image_urls) %>">
<%= link_to svg('ikea-foundation-203x22'), url_for(controller: 'languages', action: (I18n.default_locale == I18n.locale ? 'index' : 'show')) %> <%= link_to svg('ikea-foundation-203x22'), url_for(controller: 'languages', action: (I18n.default_locale == I18n.locale ? 'index' : 'show')) %>
</header> </header>


+ 1
- 1
app/views/players/new.html.erb View File

@ -5,7 +5,7 @@
<%= form_with model: @player, url: url_for(controller: 'players', action: 'create') do |form| %> <%= form_with model: @player, url: url_for(controller: 'players', action: 'create') do |form| %>
<div class="question-container">
<div class="question-container with-backdrop">
<div class="animation-element"> <div class="animation-element">
<div class="question-header"> <div class="question-header">


+ 1
- 1
app/views/questions/answer.html.erb View File

@ -4,7 +4,7 @@
%> %>
<div class="question-container">
<div class="question-container with-backdrop">
<div class="animation-element"> <div class="animation-element">
<div class="question-step"> <div class="question-step">


+ 18
- 11
app/views/questions/show.html.erb View File

@ -1,19 +1,26 @@
<%- <%-
content_for :title, question.page_title.blank? ? question.title : question.page_title content_for :title, question.page_title.blank? ? question.title : question.page_title
content_for :meta_description, question.page_description 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| %>
<div class="background-container">
<%# 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) %>
</div>
<%# end %>
<%= form_with model: @answer, url: url_for(controller: 'answers', action: 'create'), id: 'questionForm' do |form| %> <%= form_with model: @answer, url: url_for(controller: 'answers', action: 'create'), id: 'questionForm' do |form| %>


+ 1
- 0
config/importmap.rb View File

@ -16,3 +16,4 @@ pin "trix" # @2.1.15
pin "application", preload: false pin "application", preload: false
pin "locale_controller", preload: false pin "locale_controller", preload: false
pin "image_controller", preload: false pin "image_controller", preload: false
pin "quiz_preloader", preload: false

Loading…
Cancel
Save