Compare commits

..

8 commits

Author SHA1 Message Date
edcb21558a Drastically reduce queries for item page preview
Oh right okay, I made a sloppy perf hack long ago, and now let's
actually clean it up!
2024-09-05 17:52:35 -07:00
176ab20fd1 Cache the Item#appearances field
We call it enough times on this page, and it *does* have a SQL query,
that I want to cache it! (Also I want to make it fewer species queries
if I can tbh…)
2024-09-05 17:41:04 -07:00
0305817cec Use fewer SQL queries to get species for species face picker 2024-09-05 17:39:47 -07:00
4d5b583432 Remove some unnecessary console messages for new outfit viewer
For static image layers, this was *always* logging that we failed to
send the frame a "pause" message. Which, like, of course!

It makes sense to log the notable circumstance where we send a message
we *expect* to arrive, but the frame isn't loaded yet. But if there's
just no frame, ignore it and don't bother to say so.
2024-09-05 17:37:16 -07:00
2e48376c5a Auto-submit the species color picker on change, for new item previews 2024-09-05 17:34:54 -07:00
2ea8f16e43 Style the face picker on item page nicely for desktop! 2024-09-05 16:51:06 -07:00
de99e0236b Style the face picker on item page nicely for mobile
The desktop view isn't built yet, but this is nice!
2024-09-05 16:28:17 -07:00
6dd8e585a3 Add responsive layout for item page
We add a new `use_responsive_design` helper, for pages to opt into this
new CSS—mostly just because like… it's *worse* to apply these styles
for pages that don't expect it 😅

And then, I fix up a couple things on the item page (including in the
general items layout) to match!

I'm doing this because the species face picker layout is going to want
some responsive awareness, and I want to be doing that from the start!
2024-09-05 16:18:48 -07:00
14 changed files with 154 additions and 38 deletions

View file

@ -3,17 +3,36 @@ document.addEventListener("change", (e) => {
if (!e.target.matches("species-face-picker")) return;
try {
const mainPicker = document.querySelector("#item-preview .species-color-picker");
const mainPickerForm = document.querySelector(
"#item-preview species-color-picker form");
const mainSpeciesField =
mainPicker.querySelector("[name='preview[species_id]']");
mainPickerForm.querySelector("[name='preview[species_id]']");
mainSpeciesField.value = e.target.value;
mainPicker.requestSubmit(); // `submit` doesn't get captured by Turbo!
mainPickerForm.requestSubmit(); // `submit` doesn't get captured by Turbo!
} catch (error) {
e.preventDefault();
console.error("Couldn't update species picker: ", error);
}
});
class SpeciesColorPicker extends HTMLElement {
#internals;
constructor() {
super();
this.#internals = this.attachInternals();
}
connectedCallback() {
// Listen for changes to auto-submit the form, then tell CSS about it!
this.addEventListener("change", this.#handleChange);
this.#internals.states.add("auto-loading");
}
#handleChange(e) {
this.querySelector("form").requestSubmit();
}
}
class SpeciesFacePicker extends HTMLElement {
connectedCallback() {
this.addEventListener("click", this.#handleClick);
@ -52,5 +71,6 @@ class SpeciesFacePickerOptions extends HTMLElement {
}
}
customElements.define("species-color-picker", SpeciesColorPicker);
customElements.define("species-face-picker", SpeciesFacePicker);
customElements.define("species-face-picker-options", SpeciesFacePickerOptions);

View file

@ -165,9 +165,12 @@ class OutfitLayer extends HTMLElement {
#sendMessageToIframe(message) {
// If we have no frame or it hasn't loaded, ignore this message.
if (this.iframe?.contentWindow == null) {
if (this.iframe == null) {
return;
}
if (this.iframe.contentWindow == null) {
console.debug(
`Ignoring message, frame not loaded: `,
`Ignoring message, frame not loaded yet: `,
this.iframe,
message,
);

View file

@ -5,9 +5,15 @@ body.items-index, body.items-show, body.items-needed, body.item_trades
text-align: center
input[type=text]
font-size: 125%
width: 15em
.item-search-form
display: flex
gap: .5em
justify-content: center
input[type=text]
font-size: 125%
width: 15em
flex: 0 1 auto
h1
margin-bottom: 1em

View file

@ -0,0 +1,12 @@
body.use-responsive-design
#container
max-width: 100%
padding-inline: 1rem
box-sizing: border-box
#home-link
margin-left: 1rem
padding-inline: 0
#userbar
margin-right: 1rem

View file

@ -4,6 +4,7 @@
@import partials/clean/mixins
@import layout
@import responsive
@import partials/jquery.jgrowl

View file

@ -46,7 +46,7 @@ body.items-show
border: 1px solid $module-border-color
border-radius: 1em
overflow: hidden
margin: 0 auto .75em
margin: 0 auto
// There's no useful text in here, but double-clicking the play/pause
// button can cause a weird selection state. Disable text selection.
@ -151,22 +151,42 @@ body.items-show
.error-indicator
display: block
.species-color-picker
species-color-picker
.error-icon
cursor: help
margin-right: .25em
&[data-is-valid="false"]
form[data-is-valid="false"]
select
border-color: $error-border-color
color: $error-color
// If JS is enabled, but auto-loading isn't ready yet (script loading or
// failed?), hide the submit button for .75sec, to give it time to load.
@media (scripting: enabled)
input[type=submit]
position: absolute
margin-left: .5em
opacity: 0
animation: fade-in .25s forwards
animation-delay: .75s
// Once the auto-loading behavior is ready, remove the submit button.
&:state(auto-loading)
input[type=submit]
display: none
species-face-picker
display: block
position: relative
max-height: 200px // 4 rows of 50px images, and padding will offer a hint of below
padding: 10px // leave enough room for the zoomed-in selected face
margin-top: -10px
overflow: auto
species-face-picker-options
display: flex
justify-content: center
flex-wrap: wrap
img
@ -236,8 +256,6 @@ body.items-show
cursor: wait
.item-zones-info
margin-top: .5em
h3
display: inline
font: inherit
@ -261,3 +279,35 @@ body.items-show
.zone-species-info
font-style: italic
text-decoration: underline dotted
#item-preview
display: flex
flex-direction: column
gap: .75em
@media (min-width: 600px)
display: grid
grid-template-areas: "viewer faces" "picker zones"
gap: .5em
outfit-viewer
grid-area: viewer
width: 350px
height: 350px
species-color-picker
grid-area: picker
species-face-picker
grid-area: faces
max-height: 350px
margin: -10px
.item-zones-info
grid-area: zones
@keyframes fade-in
from
opacity: 0
to
opacity: 1

View file

@ -35,6 +35,7 @@
text-align: left
display: flex
align-items: center
flex-wrap: wrap
gap: 1em
abbr
@ -127,6 +128,7 @@
.item-subpages-nav
display: flex
align-items: flex-end
gap: 1em
.preview-link
margin-right: auto
@ -167,4 +169,4 @@
background: $background-color
padding-bottom: calc(.5em + 1px)
font-weight: bold
margin-bottom: -1px
margin-bottom: -1px

View file

@ -95,7 +95,7 @@ class ItemsController < ApplicationController
sort_by { |z, a| z.label }
@preview_pet_type_options = PetType.where(color: @preview_outfit.color).
joins(:species).merge(Species.alphabetical)
includes(:species).merge(Species.alphabetical)
end
format.gif do

View file

@ -1,6 +1,6 @@
module ApplicationHelper
include FragmentLocalization
def absolute_url(path_or_url)
if path_or_url.include?('://') # already an absolute URL
path_or_url
@ -231,6 +231,15 @@ module ApplicationHelper
@hide_title_header = true
end
def use_responsive_design
@use_responsive_design = true
add_body_class "use-responsive-design"
end
def use_responsive_design?
@use_responsive_design || false
end
def signed_in_meta_tag
%(<meta name="user-signed-in" content="#{user_signed_in?}">).html_safe
end

View file

@ -533,6 +533,10 @@ class Item < ApplicationRecord
end
def appearances
@appearances ||= build_appearances
end
def build_appearances
all_swf_assets = swf_assets.to_a
# If there are no assets yet, there are no appearances.
@ -551,10 +555,10 @@ class Item < ApplicationRecord
# Otherwise, create an appearance for each real (nonzero) body ID. We don't
# generally expect body_id = 0 and body_id != 0 to mix, but if they do,
# uhh, let's merge the body_id = 0 ones in?
species_by_body_id = Species.with_body_ids(swf_assets_by_body_id.keys)
swf_assets_by_body_id.map do |body_id, body_specific_assets|
swf_assets_for_body = body_specific_assets + swf_assets_for_all_bodies
species = Species.with_body_id(body_id).first!
body = Appearance::Body.new(body_id, species)
body = Appearance::Body.new(body_id, species_by_body_id[body_id])
Appearance.new(self, body, swf_assets_for_body)
end
end

View file

@ -3,11 +3,6 @@ class Species < ApplicationRecord
has_many :alt_styles
scope :alphabetical, -> { order(:name) }
scope :with_body_id, -> body_id {
pt = PetType.arel_table
joins(:pet_types).where(pt[:body_id].eq(body_id)).limit(1)
}
def as_json(options={})
super({only: [:id, :name], methods: [:human_name]}.merge(options))
@ -20,4 +15,15 @@ class Species < ApplicationRecord
I18n.translate('species.default_human_name')
end
end
# Given a list of body IDs, return a hash from body ID to Species.
# (We assume that each body ID belongs to just one species; if not, which
# species we return for that body ID is undefined.)
def self.with_body_ids(body_ids)
species_ids_by_body_id = PetType.where(body_id: body_ids).distinct.
pluck(:body_id, :species_id).to_h
species_by_id = Species.where(id: species_ids_by_body_id.values).
to_h { |s| [s.id, s] }
species_ids_by_body_id.transform_values { |id| species_by_id[id] }
end
end

View file

@ -1,5 +1,6 @@
- title @item.name
- canonical_path @item
- use_responsive_design
= render partial: "item_header",
locals: {item: @item, trades: @trades, current_subpage: "preview",
@ -18,20 +19,20 @@
.error-indicator
💥 We couldn't load all of this outfit. Try again?
= form_for item_path(@item), method: :get, class: "species-color-picker",
data: {"is-valid": @preview_error.nil?} do |f|
- if @preview_error == :pet_type_does_not_exist
%span.error-icon{title: "We haven't seen this kind of pet before."} ⚠️
- elsif @preview_error == :no_item_data
%span.error-icon{title: "We haven't seen this item on this pet before."} ⚠️
%species-color-picker
= form_for item_path(@item), method: :get, data: {"is-valid": @preview_error.nil?} do |f|
- if @preview_error == :pet_type_does_not_exist
%span.error-icon{title: "We haven't seen this kind of pet before."} ⚠️
- elsif @preview_error == :no_item_data
%span.error-icon{title: "We haven't seen this item on this pet before."} ⚠️
= select_tag "preview[color_id]",
options_from_collection_for_select(Color.funny.alphabetical,
"id", "human_name", @selected_preview_pet_type.color_id)
= select_tag "preview[species_id]",
options_from_collection_for_select(Species.alphabetical,
"id", "human_name", @selected_preview_pet_type.species_id)
= submit_tag "Go", name: nil
= select_tag "preview[color_id]",
options_from_collection_for_select(Color.funny.alphabetical,
"id", "human_name", @selected_preview_pet_type.color_id)
= select_tag "preview[species_id]",
options_from_collection_for_select(Species.alphabetical,
"id", "human_name", @selected_preview_pet_type.species_id)
= submit_tag "Go", name: nil
%species-face-picker
%noscript

View file

@ -13,6 +13,8 @@
%link{href: image_path('favicon.png'), rel: 'icon'}
= yield :stylesheets
= stylesheet_link_tag "application"
- if use_responsive_design?
%meta{name: "viewport", content: "width=device-width, initial-scale=1"}
= yield :meta
= open_graph_tags
= csrf_meta_tag

View file

@ -7,7 +7,7 @@
= image_tag 'https://images.neopets.com/items/mall_floatingneggfaerie.gif'
%span= t 'infinite_closet'
- content_for :content do
= form_tag items_path, :method => :get do
= form_tag items_path, method: :get, class: "item-search-form" do
= text_field_tag :q, @query.to_s
= submit_tag t('.search'), :name => nil
= yield