Mattias Bodlund 3 weeks ago
parent
commit
165abb7f33
29 changed files with 344 additions and 771 deletions
  1. +0
    -1
      app/assets/images/ico-arrow-down.svg
  2. +1
    -1
      app/assets/images/ico-arrow-right.svg
  3. +0
    -1
      app/assets/images/ico-arrow-updown.svg
  4. +1
    -0
      app/assets/images/ico-clock.svg
  5. +1
    -1
      app/assets/images/ico-close.svg
  6. +1
    -0
      app/assets/images/ico-globe.svg
  7. +0
    -1
      app/assets/images/ico-h1.svg
  8. +0
    -1
      app/assets/images/ico-h2.svg
  9. +0
    -1
      app/assets/images/ico-h3.svg
  10. +2
    -2
      app/assets/images/ikea-foundation-203x22.svg
  11. BIN
      app/assets/images/wase.png
  12. BIN
      app/assets/images/windmill.png
  13. +173
    -3
      app/assets/stylesheets/application.css
  14. +1
    -40
      app/controllers/languages_controller.rb
  15. +19
    -25
      app/helpers/site_helper.rb
  16. +2
    -567
      app/javascript/application.js
  17. +53
    -0
      app/javascript/language_menu_controller.js
  18. +0
    -56
      app/javascript/locale_controller.js
  19. +0
    -15
      app/javascript/plausible_controller.js
  20. +2
    -2
      app/models/concerns/has_attachments.rb
  21. +1
    -10
      app/models/node.rb
  22. +38
    -6
      app/views/layouts/application.html.erb
  23. +17
    -0
      app/views/site/start.html.erb
  24. +0
    -5
      app/views/site/tmpl_chance.html.erb
  25. +0
    -2
      app/views/site/tmpl_choise.html.erb
  26. +1
    -2
      config/importmap.rb
  27. +12
    -14
      config/locales/da.yml
  28. +12
    -10
      config/locales/en.yml
  29. +7
    -5
      config/routes.rb

+ 0
- 1
app/assets/images/ico-arrow-down.svg View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13.59 18.27"><path d="M6.93,0c.53,0,.96.43.96.96v13.75l4.08-3.91c.38-.37.99-.35,1.36.03s.35.99-.03,1.36l-6.36,6.08L.31,12.2c-.39-.36-.42-.97-.06-1.36.36-.39.97-.42,1.36-.06l4.36,3.99V.96c0-.53.43-.96.96-.96Z" style="fill:#FFF;"/></svg>

+ 1
- 1
app/assets/images/ico-arrow-right.svg View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M6.86,15.87c0-.53.43-.96.96-.96h13.75l-3.91-4.08c-.37-.38-.35-.99.03-1.36.38-.37.99-.35,1.36.03l6.08,6.36-6.07,6.63c-.36.39-.97.42-1.36.06-.39-.36-.42-.97-.06-1.36l3.99-4.36H7.82c-.53,0-.96-.43-.96-.96Z" style="fill:#010101; fill-rule:evenodd; stroke-width:0px;"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="21" viewBox="0 0 19 21"><path d="M9.1,21l-.97-.14c.43-3,1.59-5.48,3.45-7.36,1.05-1.06,2.28-1.89,3.69-2.5H0v-.99h15.27c-1.41-.6-2.64-1.44-3.69-2.5-1.86-1.88-3.02-4.36-3.45-7.36l.97-.14c.4,2.79,1.46,5.07,3.17,6.8,1.71,1.73,3.97,2.81,6.73,3.21l-.05.66.05.32c-2.76.4-5.02,1.48-6.73,3.21-1.71,1.73-2.78,4.02-3.17,6.8Z"/></svg>

+ 0
- 1
app/assets/images/ico-arrow-updown.svg View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 79 96"><path d="M18.77.92l17.69,17.69c.93.93.93,2.43,0,3.35-.93.93-2.43.93-3.35,0l-11.96-11.96v28.02c0,4.97,4.03,9.01,9.01,9.01h19.45c7.59,0,13.75,6.16,13.75,13.75v26.08l10.99-10.99c.93-.93,2.43-.93,3.35,0,.93.93.93,2.43,0,3.35l-16.72,16.72-16.72-16.72c-.93-.93-.93-2.43,0-3.35.93-.93,2.43-.93,3.35,0l10.99,10.99v-26.08c0-4.97-4.03-9.01-9.01-9.01h-19.45c-7.59,0-13.75-6.16-13.75-13.75V9.99l-11.96,11.96c-.93.93-2.43.93-3.35,0-.93-.93-.93-2.43,0-3.35L18.77.92Z" style="fill:#000; fill-rule:evenodd; stroke-width:0px;"/></svg>

+ 1
- 0
app/assets/images/ico-clock.svg View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><path d="M7,14c-3.86,0-7-3.14-7-7S3.14,0,7,0s7,3.14,7,7-3.14,7-7,7ZM7,.98C3.68.98.98,3.68.98,7s2.7,6.02,6.02,6.02,6.02-2.7,6.02-6.02S10.32.98,7,.98ZM9.6,8.79c-.07,0-.15-.02-.22-.05l-2.6-1.3c-.17-.08-.27-.25-.27-.44v-3.91c0-.27.22-.49.49-.49s.49.22.49.49v3.61l2.34,1.17c.24.12.34.41.22.66-.09.17-.26.27-.44.27Z"/></svg>

+ 1
- 1
app/assets/images/ico-close.svg View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><path d="M.66,2.62c-.54-.54-.54-1.41,0-1.95.54-.54,1.41-.54,1.95,0l6.38,6.38L15.38.66c.54-.54,1.41-.54,1.95,0,.54.54.54,1.41,0,1.95l-6.38,6.38,6.38,6.38c.54.54.54,1.41,0,1.95s-1.41.54-1.95,0l-6.38-6.38-6.38,6.38c-.54.54-1.41.54-1.95,0-.54-.54-.54-1.41,0-1.95l6.38-6.38L.66,2.62Z" style="fill:#fff;"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 11 11"><polygon points="9.95 11 5.5 6.55 1.05 11 0 9.95 4.45 5.5 0 1.05 1.05 0 5.5 4.45 9.95 0 11 1.05 6.55 5.5 11 9.95 9.95 11"/></svg>

+ 1
- 0
app/assets/images/ico-globe.svg View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 19 19"><path d="M9.5,19h0c-5.24,0-9.5-4.26-9.5-9.5S4.26,0,9.5,0h0c5.24,0,9.5,4.26,9.5,9.5s-4.26,9.5-9.5,9.5h0ZM13.58,10c-.16,2.89-1.19,5.65-2.94,7.92,3.99-.54,7.11-3.85,7.35-7.92h-4.41ZM1.01,10c.24,4.08,3.36,7.39,7.35,7.92-1.76-2.28-2.79-5.04-2.95-7.92H1.01ZM6.42,10c.17,2.83,1.25,5.55,3.08,7.74,1.83-2.18,2.91-4.9,3.08-7.74h-6.16ZM13.58,9h4.4c-.24-4.08-3.36-7.39-7.35-7.92,1.76,2.28,2.79,5.04,2.95,7.92ZM6.42,9h6.16c-.17-2.83-1.25-5.55-3.08-7.74-1.83,2.18-2.91,4.9-3.08,7.74ZM1.01,9h4.4c.16-2.89,1.19-5.65,2.94-7.92-3.99.54-7.11,3.85-7.35,7.92Z"/></svg>

+ 0
- 1
app/assets/images/ico-h1.svg View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12.68,19.46H9.83V13H5.61v6.47H2.75V4.54H5.61v6.12H9.83V4.54h2.85Zm8.57,0H18.37V8L14.6,9.36V6.88l6.5-2.34h.15Z"/></svg>

+ 0
- 1
app/assets/images/ico-h2.svg View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11.29,19.57H8.44V13.1H4.22v6.47H1.36V4.64H4.22v6.12H8.44V4.64h2.85Zm11.35,0H12.46V17.62l4.83-5.19c.38-.41.7-.78,1-1.12a8.46,8.46,0,0,0,.63-.91,4.3,4.3,0,0,0,.35-.78,2.47,2.47,0,0,0,.1-.7A2.72,2.72,0,0,0,19.19,8a1.85,1.85,0,0,0-.41-.66A1.64,1.64,0,0,0,18.16,7a2.09,2.09,0,0,0-.81-.15,2.2,2.2,0,0,0-1.75.66A2.66,2.66,0,0,0,15,9.3H12.15a4.78,4.78,0,0,1,.38-1.88,4.66,4.66,0,0,1,1.08-1.55,4.75,4.75,0,0,1,1.67-1,5.78,5.78,0,0,1,2.17-.39,5.93,5.93,0,0,1,2,.32,4.08,4.08,0,0,1,1.47.89A3.86,3.86,0,0,1,21.82,7a4.86,4.86,0,0,1,.31,1.78,4.17,4.17,0,0,1-.23,1.41,5.68,5.68,0,0,1-.66,1.33,11.34,11.34,0,0,1-1,1.34c-.39.45-.83.93-1.32,1.43l-2.81,3h6.55Z"/></svg>

+ 0
- 1
app/assets/images/ico-h3.svg View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11.28,19.46H8.43V13H4.2v6.47H1.34V4.54H4.2v6.12H8.43V4.54h2.85Zm4.46-8.71h1.55A2.31,2.31,0,0,0,19,10.16a2.12,2.12,0,0,0,.56-1.54,2.55,2.55,0,0,0-.12-.81,1.69,1.69,0,0,0-1-1,2.82,2.82,0,0,0-.92-.14,2.58,2.58,0,0,0-.8.12,2.08,2.08,0,0,0-.66.35,1.59,1.59,0,0,0-.44.56,1.52,1.52,0,0,0-.17.74H12.58A3.65,3.65,0,0,1,13,6.75a4,4,0,0,1,1-1.29,4.9,4.9,0,0,1,1.55-.83,5.93,5.93,0,0,1,1.91-.3,7.22,7.22,0,0,1,2,.27,4.38,4.38,0,0,1,1.58.81,3.66,3.66,0,0,1,1,1.34,4.41,4.41,0,0,1,.36,1.84,3,3,0,0,1-.14.94,3.63,3.63,0,0,1-.42.89,3.58,3.58,0,0,1-.69.78,4.62,4.62,0,0,1-.94.61,3.94,3.94,0,0,1,1.08.57,3.34,3.34,0,0,1,.76.79,3.77,3.77,0,0,1,.44,1,4.6,4.6,0,0,1,.14,1.13,4.18,4.18,0,0,1-.4,1.87,4.1,4.1,0,0,1-1.1,1.38,5,5,0,0,1-1.65.85,7.24,7.24,0,0,1-2.07.29,6.79,6.79,0,0,1-1.86-.26A5.2,5.2,0,0,1,14,18.62a3.93,3.93,0,0,1-1.13-1.33,4,4,0,0,1-.42-1.89h2.85a1.91,1.91,0,0,0,.16.8,1.79,1.79,0,0,0,.47.62,2,2,0,0,0,.71.41,2.57,2.57,0,0,0,.9.15,2.39,2.39,0,0,0,1.7-.57,2,2,0,0,0,.62-1.56,2.94,2.94,0,0,0-.17-1,1.82,1.82,0,0,0-.51-.7,2.09,2.09,0,0,0-.8-.41A3.76,3.76,0,0,0,17.29,13H15.74Z"/></svg>

+ 2
- 2
app/assets/images/ikea-foundation-203x22.svg View File

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 203 22">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 203 22" width="203" height="22">
<path d="M54.15,22H0V0h54.15v22Z" style="fill:#0058a3;"/>
<path d="M1.06,11c0,5.32,10.91,9.73,26.02,9.73s26.02-4.41,26.02-9.73S42.19,1.27,27.08,1.27,1.06,5.68,1.06,11Z" style="fill:#ffdb00;"/>
<path d="M25.54,14.12c.16.24.34.46.57.69h-5.86c0-.23-.22-.7-.47-1.08-.25-.37-1.59-2.41-1.59-2.41v2.8c0,.23,0,.46.11.69h-4.88c.11-.23.11-.46.11-.69v-6.66c0-.23,0-.46-.11-.69h4.88c-.11.23-.11.46-.11.69v2.91s1.56-2.03,1.92-2.5c.27-.36.61-.86.61-1.09h5.09c-.35.23-.74.66-1.05,1.04-.28.34-1.87,2.27-1.87,2.27,0,0,2.35,3.58,2.65,4.04ZM27.08,7.46v6.66c0,.23,0,.46-.11.69h9.42v-2.23c-.23.11-.46.11-.69.11h-3.96v-1.06h3.81v-1.69h-3.81v-1.06h3.96c.23,0,.46,0,.69.11v-2.23h-9.42c.11.23.11.46.11.69ZM49.44,14.12c.09.23.18.46.38.69h-5.11c.03-.23-.06-.46-.15-.69,0,0-.08-.19-.18-.46,0-.02-.05-.12-.05-.12h-2.94l-.04.12s-.08.23-.17.46c-.08.23-.17.46-.13.69h-4.03c.2-.23.28-.46.36-.69.13-.37,2.23-6.14,2.42-6.66.08-.23.17-.46.13-.69h6.81c-.06.23.06.46.15.69.2.51,2.38,6.21,2.55,6.66ZM43.69,11.85c-.37-.97-.68-1.78-.71-1.86-.09-.23-.16-.53-.16-.53,0,0-.06.3-.14.53-.03.07-.32.89-.67,1.86h1.68ZM11.11,6.77h-5.31c.11.23.11.46.11.69v6.66c0,.23,0,.46-.11.69h5.31c-.11-.23-.11-.46-.11-.69v-6.66c0-.23,0-.46.11-.69ZM47.81,7.4c0-.61.44-1.06,1.06-1.06s1.06.44,1.06,1.06-.44,1.06-1.06,1.06-1.06-.44-1.06-1.06ZM48.02,7.4c0,.47.35.85.85.85.47,0,.85-.35.85-.85,0-.47-.35-.85-.85-.85s-.85.35-.85.85ZM48.65,8.04h-.19v-1.27h.48c.22,0,.4.19.4.41,0,.16-.09.31-.22.37l.27.49h-.21l-.25-.45h-.27v.45h0ZM48.65,7.4h.26c.13,0,.24-.09.24-.22s-.11-.22-.24-.22h-.26v.45Z" style="fill:#0058a3;"/>
<path class="logo-text" d="M108.37,17.6V4.4h7.38v1.46h-5.71v4.68h5.36v1.46h-5.36v5.6h-1.66,0ZM121.58,17.78c-2.62,0-4.55-1.87-4.55-5.16s1.76-5.12,4.6-5.12c2.68,0,4.57,1.85,4.57,5.12s-1.79,5.16-4.62,5.16ZM121.62,16.44c2,0,2.9-1.46,2.9-3.81s-.91-3.75-2.92-3.75-2.88,1.4-2.88,3.75.89,3.81,2.9,3.81ZM136.59,17.6h-1.33l-.24-1.31h-.07c-.65,1.04-1.87,1.5-3.14,1.5-2.38,0-3.6-1.09-3.6-3.61v-6.49h1.65v6.38c0,1.59.7,2.37,2.2,2.37,2.2,0,2.92-1.28,2.92-3.59v-5.16h1.63v9.91h0ZM147.7,11.15v6.45h-1.61v-6.34c0-1.59-.7-2.39-2.22-2.39-2.2,0-2.9,1.28-2.9,3.59v5.14h-1.63V7.69h1.31l.24,1.35h.09c.65-1.04,1.87-1.53,3.12-1.53,2.37,0,3.59,1.09,3.59,3.64ZM150.06,12.66c0-3.4,1.63-5.16,4.08-5.16,1.54,0,2.46.65,3.07,1.46h.11c-.04-.31-.11-1.09-.11-1.46v-3.96h1.63v14.05h-1.31l-.24-1.33h-.07c-.59.85-1.53,1.52-3.09,1.52-2.46,0-4.07-1.72-4.07-5.12ZM157.23,12.98v-.3c0-2.46-.67-3.83-2.87-3.83-1.76,0-2.62,1.5-2.62,3.85s.87,3.73,2.64,3.73c2.09,0,2.85-1.15,2.85-3.46ZM169.13,10.85v6.75h-1.18l-.31-1.4h-.07c-.87,1.09-1.66,1.59-3.33,1.59-1.79,0-3.12-.92-3.12-2.94s1.52-3.07,4.75-3.16l1.68-.06v-.59c0-1.65-.76-2.2-2.05-2.2-1.04,0-1.98.37-2.79.76l-.5-1.22c.87-.46,2.09-.85,3.38-.85,2.4,0,3.55,1.02,3.55,3.33h0ZM166.06,12.81c-2.48.09-3.27.79-3.27,2.05,0,1.11.74,1.61,1.81,1.61,1.66,0,2.92-.91,2.92-2.83v-.89l-1.46.06ZM176.82,16.25v1.24c-.35.17-1.07.3-1.66.3-1.55,0-2.9-.67-2.9-3.07v-5.77h-1.4v-.78l1.42-.65.65-2.11h.96v2.27h2.86v1.26h-2.86v5.73c0,1.2.65,1.77,1.55,1.77.48,0,1.07-.09,1.39-.2ZM180.57,5.01c0,.7-.44,1.04-.94,1.04-.54,0-.96-.33-.96-1.04s.43-1.04.96-1.04c.5,0,.94.31.94,1.04ZM180.42,17.6h-1.63V7.69h1.63v9.91ZM187.25,17.78c-2.62,0-4.55-1.87-4.55-5.16s1.76-5.12,4.6-5.12c2.68,0,4.57,1.85,4.57,5.12s-1.79,5.16-4.62,5.16ZM187.29,16.44c2,0,2.9-1.46,2.9-3.81s-.9-3.75-2.92-3.75-2.88,1.4-2.88,3.75.89,3.81,2.9,3.81ZM202.56,11.15v6.45h-1.61v-6.34c0-1.59-.7-2.39-2.22-2.39-2.2,0-2.9,1.28-2.9,3.59v5.14h-1.63V7.69h1.31l.24,1.35h.09c.65-1.04,1.87-1.53,3.12-1.53,2.37,0,3.59,1.09,3.59,3.64ZM65.16,4.4h1.66v13.2h-1.66V4.4ZM78.12,17.6l-4.68-6.3-1.35,1.18v5.12h-1.66V4.4h1.66v6.51c.74-.83,1.52-1.66,2.27-2.51l3.57-3.99h1.94l-5.23,5.75,5.44,7.45h-1.96ZM81.87,17.6V4.4h7.38v1.46h-5.71v4.12h5.38v1.44h-5.38v4.71h5.71v1.46h-7.38ZM98.84,13.51h-5.23l-1.57,4.09h-1.68l5.16-13.26h1.5l5.14,13.26h-1.72l-1.59-4.09ZM96.86,8.04c-.11-.3-.5-1.52-.63-1.98-.18.76-.43,1.55-.57,1.98l-1.5,3.99h4.18l-1.48-3.99Z"/>
</svg>
</svg>

BIN
app/assets/images/wase.png View File

Before After
Width: 1024  |  Height: 1024  |  Size: 897 KiB

BIN
app/assets/images/windmill.png View File

Before After
Width: 630  |  Height: 963  |  Size: 196 KiB

+ 173
- 3
app/assets/stylesheets/application.css View File

@ -111,9 +111,179 @@ body {
flex-direction: column;
gap: 0;
min-height: 100vh;
min-height: 100dvh;
min-height: 100svh;
-webkit-font-smoothing: antialiased;
touch-action: manipulation;
touch-action: manipulation;
background-color: var(--clr-white);
}
button {
cursor: pointer;
}
ul[class] {
margin: 0;
padding: 0;
list-style: none;
}
header {
background-color: var(--clr-white);
position: fixed;
inset: 16px 16px auto 16px;
padding: 14px 16px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0px 2px 2px 0px #0000000A;
z-index: 100;
> a {
flex-grow: 1;
max-width: 203px;
}
}
.language-button {
display: flex;
justify-content: center;
align-items: center;
aspect-ratio: 1;
background-color: var(--clr-sand);
border: none;
border-radius: 50%;
width: 36px;
cursor: pointer;
svg {
display: none;
&:nth-child(1) {
display: block;
}
}
&[aria-expanded="true"] {
svg:nth-child(1) {
display: none;
}
svg:nth-child(2) {
display: block;
}
}
}
#language-menu {
position: absolute;
inset: 0 0 auto 0;
z-index: 90;
background-color: var(--clr-sand);
padding: calc(16px + 64px + 32px) 16px 16px 16px;
& h2 {
font: var(--td-xl);
font-weight: 400;
margin: 0 0 16px 0;
}
& ul {
font: var(--td-base);
border-top: 1px solid var(--clr-black);
}
& li {
border-bottom: 1px solid var(--clr-black);
padding: 12px 0;
position: relative;
min-height: 56px;
}
& a {
text-decoration: none;
color: var(--clr-black);
display: flex;
align-items: center;
min-height: 32px;
&.current {
font-weight: 700;
}
&::after {
content: "";
position: absolute;
right: 0;
width: 32px;
aspect-ratio: 1;
border-radius: 50%;
background-color: var(--clr-green);
background-image: url("ico-arrow-right.svg");
background-repeat: no-repeat;
background-position: 50% 50%;
}
}
}
.intro-container {
display: flex;
flex-direction: column;
gap: 0;
min-height: 100vh;
min-height: 100svh;
> img {
flex: 1 1 0;
min-height: 0;
width: 100%;
object-fit: cover;
}
> article {
background-color: var(--clr-white);
flex-shrink: 0;
text-align: center;
padding: 24px 24px 16px 24px;
}
& h1 {
font: var(--td-xl);
font-weight: 400;
margin: 0 auto 8px auto;
}
& p {
max-width: 32ch;
margin: 0 auto 24px auto;
}
& .cta + * {
margin-top: 12px;
}
}
.cta {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--clr-green);
background-image: url("ico-arrow-right.svg");
background-repeat: no-repeat;
background-position: 16px 50%;
max-width: 280px;
border-radius: 100vw;
height: 3em;
margin: 0 auto;
font-weight: 700;
color: var(--clr-black);
text-decoration: none;
}
.play-time {
font: var(--td-s);
display: inline-flex;
gap: 0.4em;
align-items: center;
}

+ 1
- 40
app/controllers/languages_controller.rb View File

@ -1,44 +1,10 @@
class LanguagesController < ApplicationController
before_action :set_locale_to_default, only: :index
before_action :set_locale, only: :show
before_action :set_node
helper_method :accept_language
def index
not_found unless @node
redirect_to locale_root_path(locale: first_matching_language)
end
def show
@accept_language = params[:locale]
not_found unless @node
render :index
end
def update
set_locale
respond_to do |format|
format.turbo_stream
end
end
private
def set_node
@node = Node.roots.viewable.first
end
def accept_language
@accept_language ||= first_matching_language
end
def first_matching_language
accept_languages = request.env["HTTP_ACCEPT_LANGUAGE"].to_s.split(",").map { |l|
lang, q_factor = l.split(";q=")
@ -60,9 +26,4 @@ private
end
"en"
end
def set_locale_to_default
I18n.locale = I18n.default_locale
end
end

+ 19
- 25
app/helpers/site_helper.rb View File

@ -19,33 +19,27 @@ module SiteHelper
end
def random_value(from, to)
range = rand(2) == 0 ? (-to..-from) : (from..to)
rand(range)
def render_responsive_picture(asset = nil, alt: "", lazy: false, fetchpriority: nil)
return unless asset
widths = [ 800, 1600, 2400 ]
metadata = asset.file.metadata
image_tag(rails_storage_proxy_path(asset.file.variant(resize_to_limit: [ 1600, nil ], format: "webp")),
alt: alt,
srcset: responsive_srcset(asset, widths),
sizes: "100vw",
width: metadata[:width],
height: metadata[:height],
loading: ("lazy" if lazy),
fetchpriority: fetchpriority,
decoding: "async")
end
def parse_article_html(html)
result = {}
return result if html.blank?
doc = Nokogiri::HTML.fragment(html, "utf-8")
first_link = doc.at_css("a")
first_link.remove if first_link
result[:title] = doc.at_css("h1")&.text
result[:description] = doc.at_css("div")&.text
result[:link] = first_link&.to_html
doc.search("*").each do |node|
while node.children.last and node.children.last.name == "br"
node.children.last.remove
end
end
result[:html] = doc.to_html
result
def responsive_srcset(asset, widths)
widths.map { |w|
"#{rails_storage_proxy_path(asset.file.variant(resize_to_limit: [ w, nil ], format: "webp"))} #{w}w"
}.join(", ")
end
end

+ 2
- 567
app/javascript/application.js View File

@ -1,17 +1,11 @@
import "@hotwired/turbo-rails"
import { Application } from "@hotwired/stimulus"
import LocaleController from "locale_controller"
import ImageController from "image_controller"
import PlausibleController from "plausible_controller"
import QuizImagePreloader from "quiz_preloader"
import LanguageMenuController from "language_menu_controller"
const application = Application.start()
application.register("locale", LocaleController)
application.register("image", ImageController)
application.register("plausible", PlausibleController)
application.register("language-menu", LanguageMenuController)
// Configure Stimulus development experience
application.debug = false
@ -19,562 +13,3 @@ 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')
let errorElements = document.querySelectorAll('.field_with_errors')
if (errorElements.length > 0) {
animationElements = document.querySelectorAll('.animation-element')
gsap.set(animationElements, {
opacity: 1
})
}
})
// document.addEventListener("turbo:before-stream-render", animateElements)
document.addEventListener("turbo:load", animateElements)
document.addEventListener("turbo:load", validateFormInput)
function validateFormInput(event) {
const form = document.getElementById('questionForm');
const submitBtn = document.getElementById('questionSubmitButton');
if (form) {
form.addEventListener('submit', e => {
// If no radio in the group is checked…
if (!form.querySelector('input[type=radio]:checked')) {
e.preventDefault(); // stop the form from submitting
submitBtn.classList.remove('wiggle'); // reset so the animation can retrigger
void submitBtn.offsetWidth; // force reflow (quick trick)
submitBtn.classList.add('wiggle'); // start the wiggle
}
});
// Tidy up the class once the animation ends
submitBtn.addEventListener('animationend', () => {
submitBtn.classList.remove('wiggle');
});
}
}
function animateElements(event) {
// console.info(event.type)
// Copy link to clipboard
document.querySelectorAll('[data-share-title]').forEach((share_btn) => {
share_btn.addEventListener('click', (e) => {
if (navigator.share) {
navigator.share({
title: e.currentTarget.getAttribute('data-share-title'),
text: e.currentTarget.getAttribute('data-share-text'),
url: e.currentTarget.getAttribute('data-share-url'),
}).then(() => {
console.log('Thanks for sharing!')
})
.catch(console.error)
} else {
navigator.clipboard.writeText(window.location.href)
alert('URL copied to clipboard!')
}
})
})
originalHeights = []
animationElements = document.querySelectorAll('.animation-element')
animationElements.forEach((animationElement, index) => {
const height = animationElement.offsetHeight
originalHeights.push(height)
// console.log(`Element ${index + 1} height: ${height}px`)
})
gsap.set(animationElements, {
height: 0,
opacity: 0
})
let typewriterElements = document.querySelectorAll('.typewriter-text')
if (typewriterElements.length > 0) {
gsap.set(typewriterElements, {
opacity: 0
})
}
// Start the sequential animation
animateElementsSequentially()
}
function animateElementsSequentially(index = 0) {
if (index >= animationElements.length) {
console.log('All animations complete')
return
}
// 'margin-top': (index == (animationElements.length-1) ? "12px" : "8px"),
gsap.to(animationElements[index], {
height: originalHeights[index],
opacity: 1,
duration: (0.004 * originalHeights[index]),
delay: (index > 0 ? 0.4 : 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: 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)
}
})
typewriter.start()
return
}
// % Answer Distributions
const answerDistributions = animationElements[index].querySelectorAll('.answer-distribution')
if (answerDistributions.length > 0 ) {
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 = []
answerDistributions.forEach((bar, i) => {
const percentage = parseInt(bar.dataset.value)
const relativePercentage = (percentage / maxPercentage) * 100
const calculatedHeight = (relativePercentage / 100) * containerHeight
const minHeight = getNaturalHeight(bar)
let finalMinHeight = minHeight
// if (bar.querySelector('.explanation')) {
// finalMinHeight = Math.max(minHeight, 160)
// }
targetHeights[i] = Math.max(calculatedHeight, finalMinHeight)
})
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 + ((Math.random() - 0.5) * 0.2))
} else if (i === lowestBar.index) {
fakeHeight = containerHeight * (0.8 + ((Math.random() - 0.5) * 0.2))
} else {
fakeHeight = containerHeight * (0.5 + ((Math.random() - 0.5) * 0.2))
}
finalHeight = targetHeights[i]
gsap.to(bar, {
keyframes: [
{ height: fakeHeight, duration: 0.3, ease: "power2.inOut" },
{ height: finalHeight, duration: 0.4, ease: "power2.in" }
],
onComplete: () => {
if (bar.dataset.answered != undefined) {
bar.classList.add('answered')
}
gsap.to(bar.querySelectorAll('.percentage'), {
opacity: 1,
duration: 0.4,
onComplete: function () {
const explanationElement = this.targets()[0].nextElementSibling
if (explanationElement) {
explanationElement.style.display = 'block'
}
}
})
if (i == (answerDistributions.length - 1)) {
setTimeout(() => animateElementsSequentially(index + 1), 1000)
}
}
})
})
return
}
animateElementsSequentially(index + 1)
}
})
}
function typeWriterConsoleSequential(container, options = {}) {
const {
selector = '.typewriter-text',
baseSpeed = 40,
divDelay = 200,
showCursor = true,
onDivComplete = null,
onAllComplete = null
} = options
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
// Cleanup function for Turbo navigation
function cleanup() {
isActive = false
if (animationId) {
cancelAnimationFrame(animationId)
animationId = null
}
// Clear all timeouts
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']
turboEvents.forEach(eventName => {
document.addEventListener(eventName, cleanup, { once: false })
})
// Store cleanup function on container for manual 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
}
// Store original text and prepare divs
function initializeDivs() {
// Set container height to prevent layout shifts
const containerHeight = container.offsetHeight
container.style.minHeight = `${containerHeight}px`
textDivs.forEach(div => {
if (!div.dataset.originalText) {
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'
})
// Force reflow to ensure styles are applied
container.offsetHeight
}
function createCursorSpan() {
// No longer needed - cursor is now handled via CSS
return null
}
function updateDivContent(div, text, withCursor = false) {
// Use textContent for better performance
div.textContent = text
// Toggle cursor class instead of adding DOM element
if (withCursor && showCursor) {
div.classList.add('typewriter-cursor')
} else {
div.classList.remove('typewriter-cursor')
}
}
function animateNextDiv() {
if (currentDivIndex >= textDivs.length || !isActive) {
isActive = false
if (onAllComplete) {
// Use setTimeout to avoid potential timing issues
requestAnimationFrame(() => {
safeSetTimeout(() => onAllComplete(container), 200)
})
}
return
}
const currentDiv = textDivs[currentDivIndex]
const originalText = currentDiv.dataset.originalText
const isLastDiv = currentDivIndex === textDivs.length - 1
const isFirstDiv = currentDivIndex === 0
// Make div visible and prepare for typing
currentDiv.style.visibility = 'visible'
updateDivContent(currentDiv, '', showCursor)
currentDiv.classList.add('typing-active')
let charIndex = 0
// Add 1 second delay for first div to let cursor blink twice
let nextCharTime = performance.now() + (isFirstDiv ? 20 : 20)
function typeFrame(currentTime) {
if (!isActive) {
if (animationId) {
cancelAnimationFrame(animationId)
animationId = null
}
return
}
if (currentTime >= nextCharTime && charIndex < originalText.length) {
charIndex++
const currentText = originalText.substring(0, charIndex)
updateDivContent(currentDiv, currentText, true)
// Calculate delay for next character
// let delay = baseSpeed + Math.random() * 15
let delay = baseSpeed
const char = originalText.charAt(charIndex - 1)
if (char === '.' || char === '!' || char === '?') {
delay += 250
} else if (char === ',' || char === '') {
delay += 120
} else if (char === ' ') {
delay += 60
}
nextCharTime = currentTime + delay
}
// Check if current div is complete
if (charIndex >= originalText.length) {
currentDiv.classList.remove('typing-active')
// Final content update
updateDivContent(currentDiv, originalText, showCursor)
if (onDivComplete) {
onDivComplete(currentDiv, currentDivIndex, container)
}
currentDivIndex++
if (isActive && currentDivIndex < textDivs.length) {
// Move to next div after delay
safeSetTimeout(() => {
if (isActive) {
// Remove cursor from previous div (except last one)
if (!isLastDiv) {
updateDivContent(currentDiv, originalText, false)
}
animateNextDiv()
}
}, divDelay)
} else if (isActive) {
// This was the last div
safeSetTimeout(() => {
if (onAllComplete) {
onAllComplete(container)
}
}, 200)
}
return
}
// Continue animation
if (isActive) {
animationId = requestAnimationFrame(typeFrame)
}
}
// Start the animation
animationId = requestAnimationFrame(typeFrame)
}
// Initialize Turbo listeners
setupTurboListeners()
// Add required CSS for cursor pseudo-element
function addCursorStyles() {
const styleId = 'typewriter-cursor-styles'
if (!document.getElementById(styleId)) {
const style = document.createElement('style')
style.id = styleId
style.textContent = `
.typewriter-cursor::after {
content: '|'
animation: typewriter-blink 1s infinite
color: currentColor
}
@keyframes typewriter-blink {
0%, 50% { opacity: 1 }
51%, 100% { opacity: 0 }
}
`
document.head.appendChild(style)
}
}
// Initialize styles
addCursorStyles()
return {
start: () => {
if (isActive) return // Prevent multiple starts
initializeDivs()
isActive = true
currentDivIndex = 0
// Use requestAnimationFrame for smoother start
requestAnimationFrame(() => {
if (isActive) {
animateNextDiv()
}
})
},
stop: () => {
cleanup()
},
reset: () => {
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')
}
})
currentDivIndex = 0
},
// Manual cleanup method for explicit cleanup
destroy: () => {
cleanup()
// Remove Turbo event listeners
const turboEvents = ['turbo:before-visit', 'turbo:before-cache', 'beforeunload']
turboEvents.forEach(eventName => {
document.removeEventListener(eventName, cleanup)
})
// Remove cleanup reference
delete container._typewriterCleanup
},
get isActive() { return isActive }
}
}
function getNaturalHeight(el) {
// Save original inline styles
const prevParent = {
display: el.style.display
};
// Apply temporary styles to parent
el.style.display = "flex";
// Handle hidden children
const childPrev = [];
for (let child of el.children) {
childPrev.push({
display: child.style.display
});
child.style.display = "block";
}
// Measure
const height = el.scrollHeight;
// Restore child styles
Array.from(el.children).forEach((child, i) => {
Object.assign(child.style, childPrev[i]);
});
// Restore parent styles
Object.assign(el.style, prevParent);
return height;
}

+ 53
- 0
app/javascript/language_menu_controller.js View File

@ -0,0 +1,53 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["button", "menu"]
connect() {
this.boundClickOutside = this.clickOutside.bind(this)
this.boundKeydown = this.keydown.bind(this)
}
disconnect() {
this.stopListening()
}
toggle(event) {
event.preventDefault()
this.isOpen ? this.close() : this.open()
}
open() {
this.buttonTarget.setAttribute("aria-expanded", "true")
this.menuTarget.hidden = false
document.addEventListener("mousedown", this.boundClickOutside)
document.addEventListener("keydown", this.boundKeydown)
}
close() {
this.buttonTarget.setAttribute("aria-expanded", "false")
this.menuTarget.hidden = true
this.stopListening()
}
stopListening() {
document.removeEventListener("mousedown", this.boundClickOutside)
document.removeEventListener("keydown", this.boundKeydown)
}
get isOpen() {
return this.buttonTarget.getAttribute("aria-expanded") === "true"
}
clickOutside(event) {
if (this.element.contains(event.target)) return
this.close()
}
keydown(event) {
if (event.key === "Escape") {
this.close()
this.buttonTarget.focus()
}
}
}

+ 0
- 56
app/javascript/locale_controller.js View File

@ -1,56 +0,0 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["select", "current"]
static values = { url: String }
connect() {
}
disconnect() {
}
changeLocale() {
this.currentTarget.textContent = this.selectTarget.options[this.selectTarget.selectedIndex].textContent
// Create form data to send
const formData = new FormData()
formData.append("locale", this.selectTarget.value)
// Make the PUT request using fetch
fetch(this.urlValue, {
method: "PUT",
headers: {
'Accept': "text/vnd.turbo-stream.html",
"X-CSRF-Token": this.getMetaValue("csrf-token")
},
body: formData
})
.then (response => response.text())
.then(html => {
Turbo.renderStreamMessage(html)
document.documentElement.setAttribute('lang', this.selectTarget.value)
requestAnimationFrame(() => {
document.querySelectorAll('.animation-element').forEach((animationElement) => {
animationElement.style.opacity = 1
})
})
})
.catch((err) => {
console.info('rejected', err)
})
}
// Helper method to get CSRF token
getMetaValue(name) {
const element = document.head.querySelector(`meta[name="${name}"]`)
return element.getAttribute("content")
}
}

+ 0
- 15
app/javascript/plausible_controller.js View File

@ -1,15 +0,0 @@
import { Controller } from "@hotwired/stimulus"
let lastTrackedUrl = null;
export default class extends Controller {
connect() {
document.addEventListener("turbo:load", () => {
const currentUrl = window.location.pathname + window.location.search;
if (window.plausible && currentUrl !== lastTrackedUrl) {
window.plausible("pageview");
lastTrackedUrl = currentUrl;
}
});
}
}

+ 2
- 2
app/models/concerns/has_attachments.rb View File

@ -26,9 +26,9 @@ module HasAttachments
end
def icon
self.assets.map{ |asset| return asset if asset.file and asset.file.content_type.starts_with?('image/') }
nil
assets.find { |a| a.file.attached? && a.file.content_type&.start_with?("image/") }
end


+ 1
- 10
app/models/node.rb View File

@ -26,16 +26,7 @@ class Node < ApplicationRecord
enum :status, { status_published: 0, status_draft: 1, status_archived: 2 }
enum :template, {
start: 0,
stage: 1,
choice: 2,
chance: 3,
bonus: 4,
best: 5,
bad: 6,
good: 7,
game_over: 8,
score: 9
start: 0
}
def available_templates


+ 38
- 6
app/views/layouts/application.html.erb View File

@ -1,11 +1,12 @@
<!doctype html>
<html lang="<%= controller_name == 'languages' ? accept_language : I18n.locale %>">
<html lang="<%= I18n.locale %>">
<head>
<title><%= content_for?(:title) ? yield(:title) : t(:project_name) %></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="theme-color" content="#F0B902">
<meta name="turbo-prefetch" content="false">
<meta name="turbo-cache-control" content="no-cache">
<%= tag.meta name: "description", content: content_for?(:meta_description) ? yield(:meta_description) : (@node.present?? @node.page_description : "") %>
<%= csrf_meta_tags %>
@ -16,16 +17,47 @@
<link rel="icon" sizes="192x192" href="/ikea-favicon-300x300.png">
<%= stylesheet_link_tag "reset", "application" %>
<%= frontend_javascript_importmap_tags %w[application @hotwired/turbo-rails @hotwired/stimulus language_menu_controller] %>
</head>
<body>
<header>
<%= link_to svg("ikea-foundation-203x22"), url_for(controller: "languages", action: (I18n.default_locale == I18n.locale ? "index" : "show")) %>
</header>
<div data-controller="language-menu">
<header>
<%= link_to svg("ikea-foundation-203x22"), url_for(controller: "site", action: "index") %>
<button type="button"
class="language-button"
data-language-menu-target="button"
data-action="click->language-menu#toggle"
aria-controls="language-menu"
aria-expanded="false"
aria-label="<%= strip_tags t("game.please_choose_a_language") %>">
<%= svg "ico-globe" %>
<%= svg "ico-close" %>
</button>
</header>
<nav id="language-menu"
data-language-menu-target="menu"
aria-labelledby="language-title"
hidden>
<h2 id="language-title"><%= t("game.please_choose_a_language").html_safe %></h2>
<ul class="languages">
<% t("language_names").each do |k, v| %>
<% current = (k == I18n.locale) %>
<li><%= link_to v,
url_for(locale: k),
class: ("current" if current),
"aria-current": ("page" if current) %></li>
<% end%>
</ul>
</nav>
</div>
<main>
<%= yield %>
</main>
</body>
</html>

+ 17
- 0
app/views/site/start.html.erb View File

@ -0,0 +1,17 @@
<%- content_for :title, node_title(@node) %>
<div class="intro-container">
<%= render_responsive_picture(@node.icon, alt: "Hero", fetchpriority: "high") %>
<article>
<%= tag.h1 t("game.intro_title").html_safe %>
<%= tag.p t("game.intro_description") %>
<%= link_to tag.span(t("game.let_me_try")), "#", class: "cta" %>
<%= tag.div class: "play-time" do %>
<%= svg "ico-clock" %>
<%= tag.span t("game.play_time") %>
<% end %>
</article>
</div>

+ 0
- 5
app/views/site/tmpl_chance.html.erb View File

@ -1,5 +0,0 @@
<%-
content_for :title, node_title(@node)
cards = @node.children.viewable.ordered
%>

+ 0
- 2
app/views/site/tmpl_choise.html.erb View File

@ -1,2 +0,0 @@
<%- content_for :title, node_title(@node) %>

+ 1
- 2
config/importmap.rb View File

@ -14,5 +14,4 @@ pin "tom-select", to: "tom-select--dist--js--tom-select.base.min.js.js" # @2.4.3
# site_helper
pin "application", preload: false
pin "locale_controller", preload: false
pin "plausible_controller", preload: false
pin "language_menu_controller", preload: false

+ 12
- 14
config/locales/da.yml View File

@ -1,16 +1,14 @@
# Danish (da_DK)
da:
get_started: Lad os komme i gang!
what_is_your_name: Hvad hedder du?
write_your_name: Skriv dit navn.
submit: Send
next_question: Næste spørgsmål
of_people_worldwide_think_just_lik_you: af mennesker verden over, der tænker som dig.
see_results: Se resultater
results: Resultater
read_more: Læs mere
share_on_story: Del
read_more_link: https://ikeafoundation.org/25
please_type_your_name_to_continue: Skriv dit navn for at fortsætte.
share_title: IKEA Foundation Week quiz result
share_text: Quiz result description
game:
please_choose_a_language: |
<strong>Vælg venligst</strong><br>
et sprog
intro_title: |
Kan du redde<br>
<strong>tomaten?</strong>
intro_description: Det lyder nemt, men det er mere udfordrende, end du tror!
let_me_try: Lad mig prøve
play_time: Spilletid 2-5 minutter

+ 12
- 10
config/locales/en.yml View File

@ -3,16 +3,18 @@ en:
project_name: IKEA Foundation Week 2026
client_name: IKEA Foundation
let_me_try: Let me try!
flip_the_card: Flip the card
next_stage: Proceed to the next stage
bonus: Bonus
go_again: Go again
use_bonus_point: Use bonus point
get_bonus_point: Get bonus point
let_my_try: Let me try
countries:
game:
please_choose_a_language: |
<strong>Please</strong><br>
choose a language
intro_title: |
Can you save<br>
<strong>the tomato?</strong>
intro_description: Sounds easy, but it’s more challenging than you might think!
let_me_try: Let me try
play_time: Play time 2-5 minutes
countries:
au: Australia
at: Austria
bh: Bahrain


+ 7
- 5
config/routes.rb View File

@ -49,10 +49,6 @@ Rails.application.routes.draw do
get "login/verification", to: "sessions#verification"
end
scope ":locale", constraints: { locale: /en|zh|hr|cs|da|nl|fi|fr|fr-CA|de|hu|it|ja|ko|nb|pl|pt|ro|sr|sk|sl|es|sv|uk/ } do
get "", to: "languages#show"
# get "*url", to: "site#page", constraints: lambda { |req| req.path.exclude?("storage") }
end
namespace :api do
namespace :v1 do
@ -60,7 +56,13 @@ Rails.application.routes.draw do
end
end
put "update_locale", to: "languages#update"
scope ":locale", constraints: { locale: /en|zh|hr|cs|da|nl|fi|fr|fr-CA|de|hu|it|ja|ko|nb|pl|pt|ro|sr|sk|sl|es|sv|uk/ } do
# get "*url", to: "site#page", constraints: lambda { |req| req.path.exclude?("storage") }
get "", to: "site#index", as: :locale_root
end
# Defines the root path route ("/")
root "languages#index"


Loading…
Cancel
Save