diff --git a/Gemfile.lock b/Gemfile.lock index a132333..88912cb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -151,7 +151,7 @@ GEM kaminari-core (= 1.2.2) kaminari-core (1.2.2) language_server-protocol (3.17.0.5) - lexxy (0.9.15.alpha.1) + lexxy (0.9.15.alpha.2) rails (>= 8.0.2) lint_roller (1.1.0) logger (1.7.0) @@ -292,7 +292,7 @@ GEM io-console (~> 0.5) request_store (1.7.0) rack (>= 1.4) - rubocop (1.86.2) + rubocop (1.87.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -460,7 +460,7 @@ CHECKSUMS kaminari-activerecord (1.2.2) sha256=0dd3a67bab356a356f36b3b7236bcb81cef313095365befe8e98057dd2472430 kaminari-core (1.2.2) sha256=3bd26fec7370645af40ca73b9426a448d09b8a8ba7afa9ba3c3e0d39cdbb83ff language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc - lexxy (0.9.15.alpha.1) sha256=5906f921e9ec8b0b7c47853e7fb9fb1b3c97921f4edd701d94a76ba25c45fa84 + lexxy (0.9.15.alpha.2) sha256=4bcbc2d1e6d6462707fdf34ead5f4a916d226b089695c1446099a17ba7386432 lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 loofah (2.25.1) sha256=d436c73dbd0c1147b16c4a41db097942d217303e1f7728704b37e4df9f6d2e04 @@ -521,7 +521,7 @@ CHECKSUMS regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 request_store (1.7.0) sha256=e1b75d5346a315f452242a68c937ef8e48b215b9453a77a6c0acdca2934c88cb - rubocop (1.86.2) sha256=bb2e97f635eda42c448f2588f4a6ff78f221b8bdfdf65b1e9b07fbd57521b45d + rubocop (1.87.0) sha256=b9d9ddf55116a513f8ef2c7ae660662d8b49301f118d3f0df61865b33a5c188d rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035 rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 rubocop-rails (2.35.3) sha256=6edd45410866912b9b2e90ae3aeafd31d576df2bb2a9c9408f1667a50c32c7de diff --git a/app/assets/images/ico-wave.svg b/app/assets/images/ico-wave.svg new file mode 100644 index 0000000..29e6dcf --- /dev/null +++ b/app/assets/images/ico-wave.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index d618b40..52b622e 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -131,8 +131,8 @@ ul[class] { header { background-color: var(--clr-white); position: fixed; - inset: 16px 16px auto 16px; - padding: 14px 16px; + inset: 1rem 1rem auto 1rem; + padding: 0.875rem 1rem; display: flex; justify-content: space-between; align-items: center; @@ -141,7 +141,7 @@ header { > a { flex-grow: 1; - max-width: 203px; + max-width: 12.6875rem; } } @@ -153,7 +153,7 @@ header { background-color: var(--clr-sand); border: none; border-radius: 50%; - width: 36px; + width: 2.25rem; cursor: pointer; svg { @@ -179,12 +179,12 @@ header { inset: 0 0 auto 0; z-index: 90; background-color: var(--clr-sand); - padding: calc(16px + 64px + 32px) 16px 16px 16px; + padding: calc(1rem + 4rem + 2rem) 1rem 1rem 1rem; & h2 { font: var(--td-xl); font-weight: 400; - margin: 0 0 16px 0; + margin: 0 0 1rem 0; } & ul { @@ -194,10 +194,9 @@ header { & li { border-bottom: 1px solid var(--clr-black); - padding: 12px 0; + padding: 0.75rem 0; position: relative; - min-height: 56px; - + min-height: 3.5rem; } & a { @@ -205,7 +204,7 @@ header { color: var(--clr-black); display: flex; align-items: center; - min-height: 32px; + min-height: 2rem; &.current { font-weight: 700; @@ -215,7 +214,7 @@ header { content: ""; position: absolute; right: 0; - width: 32px; + width: 2rem; aspect-ratio: 1; border-radius: 50%; background-color: var(--clr-green); @@ -223,44 +222,75 @@ header { background-repeat: no-repeat; background-position: 50% 50%; } - } } -.intro-container { +main { 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); + + & article { + flex: 0 0 auto; + background-color: var(--clr-bg); flex-shrink: 0; text-align: center; - padding: 24px 24px 16px 24px; + padding: 1.5rem 1.5rem 1rem 1.5rem; + + + & p { + max-width: 32ch; + margin: 0 auto 1.5rem auto; + } + + & .cta + * { + margin-top: 0.75rem; + } } - & h1 { + & h1, h2 { font: var(--td-xl); font-weight: 400; - margin: 0 auto 8px auto; + margin: 0 auto 0.5rem auto; } - & p { - max-width: 32ch; - margin: 0 auto 24px auto; +} + +.start { + --clr-bg: var(--clr-white); + --clr-action: var(--clr-green); +} + +.facts { + --clr-bg: var(--clr-green); + --clr-action: var(--clr-white); +} + + + +.hero-container, .carousel { + flex: 1 1 0; + min-height: 0; +} + +.hero-container { + display: grid; + grid-template: 1fr / 1fr; + + & img { + grid-area: 1 / 1; + width: 100%; + height: 100%; + object-fit: cover; + min-width: 0; + min-height: 0; } - & .cta + * { - margin-top: 12px; + & svg { + grid-area: 1 / 1; + place-self: end end; + fill: var(--clr-bg, var(--clr-green)); } } @@ -268,13 +298,13 @@ header { display: flex; align-items: center; justify-content: center; - background-color: var(--clr-green); + background-color: var(--clr-action, var(--clr-sand-light)); background-image: url("ico-arrow-right.svg"); background-repeat: no-repeat; - background-position: 16px 50%; - max-width: 280px; + background-position: 1rem 50%; + max-width: 17.5rem; border-radius: 100vw; - height: 3em; + height: 3rem; margin: 0 auto; font-weight: 700; color: var(--clr-black); @@ -286,4 +316,46 @@ header { display: inline-flex; gap: 0.4em; align-items: center; +} + + +.carousel { + display: grid; + grid-auto-flow: column; + grid-auto-columns: 100%; + overflow-x: auto; + scroll-behavior: smooth; + scroll-snap-type: x mandatory; + scrollbar-width: none; + + > li { + scroll-snap-align: center; + display: flex; + flex-direction: column; + + --clr-bg: var(--clr-green); + + } +} + +.scroll-marker-group { + display: flex; + gap: 0.5rem; + justify-content: center; + background-color: var(--clr-bg); + padding: 0 0 1rem 0; + + & button { + appearance: none; + border: none; + width: 0.75rem; + aspect-ratio: 1; + border-radius: 50%; + background-color: var(--clr-white); + transition: background-color 0.1s ease; + + &.current { + background-color: var(--clr-black); + } + } } \ No newline at end of file diff --git a/app/assets/stylesheets/forms.css b/app/assets/stylesheets/forms.css index 054f254..ae70247 100644 --- a/app/assets/stylesheets/forms.css +++ b/app/assets/stylesheets/forms.css @@ -1153,4 +1153,11 @@ ul { word-break: break-all; /* Ensures long strings don't push the width */ } } + + h2, h3, h4, h5 { + font-weight: 400; + strong { + font-weight: 700; + } + } } \ No newline at end of file diff --git a/app/controllers/site_controller.rb b/app/controllers/site_controller.rb index 238dd55..51470ea 100644 --- a/app/controllers/site_controller.rb +++ b/app/controllers/site_controller.rb @@ -1,32 +1,26 @@ class SiteController < ApplicationController before_action :set_locale + helper_method :root_node def index - @node = Node.roots.viewable.first + @node = root_node not_found and return unless @node render(template: "site/#{@node.template}") end - def page - @node = Node.find_by!("url->>'#{I18n.locale}' = ?", url_from_param) - - not_found and return unless @node and @node.viewable? - - # Redirect to first child if index? - redirect_to @node.children.viewable.ordered.first.url and return if @node.index? and @node.children.viewable.any? - - # Link or render - redirect_to(@node.href, allow_other_host: true) and return if @node.href.present? - - render(template: "site/#{@node.template}") + def facts + @node = root_node&.children&.facts&.first end private + def root_node + @root_node ||= Node.roots.viewable.first + end def url_from_param return "" unless params[:url] diff --git a/app/javascript/application.js b/app/javascript/application.js index 49a7bea..21741f6 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -2,10 +2,12 @@ import "@hotwired/turbo-rails" import { Application } from "@hotwired/stimulus" import LanguageMenuController from "language_menu_controller" +import CarouselController from "carousel_controller" const application = Application.start() application.register("language-menu", LanguageMenuController) +application.register("carousel", CarouselController) // Configure Stimulus development experience application.debug = false diff --git a/app/javascript/carousel_controller.js b/app/javascript/carousel_controller.js new file mode 100644 index 0000000..355210f --- /dev/null +++ b/app/javascript/carousel_controller.js @@ -0,0 +1,55 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["carousel", "marker"] + + connect() { + this.boundUpdate = this.update.bind(this) + this.boundEqualizeHeights = this.equalizeHeights.bind(this) + this.carouselTarget.addEventListener("scroll", this.boundUpdate, { passive: true }) + window.addEventListener("resize", this.boundEqualizeHeights) + this.equalizeHeights() + this.update() + } + + disconnect() { + this.carouselTarget.removeEventListener("scroll", this.boundUpdate) + window.removeEventListener("resize", this.boundEqualizeHeights) + } + + equalizeHeights() { + const articles = this.carouselTarget.querySelectorAll(":scope > li > article") + articles.forEach((article) => (article.style.height = "auto")) + const tallest = Array.from(articles).reduce((max, article) => Math.max(max, article.offsetHeight), 0) + articles.forEach((article) => (article.style.height = `${tallest}px`)) + } + + update() { + const slides = Array.from(this.carouselTarget.children) + const center = this.carouselTarget.scrollLeft + this.carouselTarget.clientWidth / 2 + + let activeIndex = 0 + let closest = Infinity + + slides.forEach((slide, i) => { + const slideCenter = slide.offsetLeft + slide.offsetWidth / 2 + const distance = Math.abs(slideCenter - center) + if (distance < closest) { + closest = distance + activeIndex = i + } + }) + + this.markerTargets.forEach((marker, i) => { + marker.classList.toggle("current", i === activeIndex) + }) + } + + goto(event) { + const index = parseInt(event.currentTarget.dataset.index, 10) + const slide = this.carouselTarget.children[index] + if (slide) { + this.carouselTarget.scrollTo({ left: slide.offsetLeft, behavior: "smooth" }) + } + } +} diff --git a/app/models/concerns/has_attachments.rb b/app/models/concerns/has_attachments.rb index e2d719f..c545a1d 100644 --- a/app/models/concerns/has_attachments.rb +++ b/app/models/concerns/has_attachments.rb @@ -2,36 +2,29 @@ module HasAttachments extend ActiveSupport::Concern included do - has_many :attachments, -> { order :position }, dependent: :destroy, as: :attachable_for, class_name: "Attachment" has_many :assets, through: :attachments has_many :assets, - through: :attachments, - source: :asset + through: :attachments, + source: :asset - #has_many :images, + # has_many :images, # -> { with_attached_file.where('assets.content_type LIKE ?', "image/%") }, # through: :attachments, # source: :asset -# - #has_many :images_favorites_first, + # + # has_many :images_favorites_first, # -> { with_attached_file.where('assets.content_type LIKE ?', "image/%").reorder('attachments.is_favorite DESC', 'attachments.position ASC') }, # through: :attachments, # source: :asset accepts_nested_attributes_for :attachments, allow_destroy: true - end - def icon assets.find { |a| a.file.attached? && a.file.content_type&.start_with?("image/") } end - - - - end diff --git a/app/models/node.rb b/app/models/node.rb index 6420ed8..3f1a59c 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -26,7 +26,8 @@ class Node < ApplicationRecord enum :status, { status_published: 0, status_draft: 1, status_archived: 2 } enum :template, { - start: 0 + start: 0, + facts: 1 } def available_templates @@ -34,19 +35,9 @@ class Node < ApplicationRecord case depth when 1 - [ :stage, :game_over, :score ] + [ :facts ] when 2 - [ :choice, :chance, :bonus ] - when 3 - if parent&.chance? - [ :good, :bad, :game_over, :bonus ] - elsif parent&.choice? - [ :best, :good, :bad ] - elsif parent&.bonus? - [ :good, :bad ] - else - [] - end + [] else [] end diff --git a/app/views/admin/attachments/_attachment.html.erb b/app/views/admin/attachments/_attachment.html.erb index 47d5733..800d647 100644 --- a/app/views/admin/attachments/_attachment.html.erb +++ b/app/views/admin/attachments/_attachment.html.erb @@ -10,11 +10,8 @@ locale: I18n.default_locale), tmp_id: local_assigns[:tmp_id] } %> - - - <%= button_tag 'delete', type: 'button', diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 6c45b7a..186fa0b 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -17,47 +17,17 @@ <%= stylesheet_link_tag "reset", "application" %> - <%= frontend_javascript_importmap_tags %w[application @hotwired/turbo-rails @hotwired/stimulus language_menu_controller] %> + <%= frontend_javascript_importmap_tags %w[application @hotwired/turbo-rails @hotwired/stimulus language_menu_controller carousel_controller] %> -
- -