Compare commits
8 commits
77ff55353c
...
edcb21558a
Author | SHA1 | Date | |
---|---|---|---|
edcb21558a | |||
176ab20fd1 | |||
0305817cec | |||
4d5b583432 | |||
2e48376c5a | |||
2ea8f16e43 | |||
de99e0236b | |||
6dd8e585a3 |
14 changed files with 154 additions and 38 deletions
|
@ -3,17 +3,36 @@ document.addEventListener("change", (e) => {
|
||||||
if (!e.target.matches("species-face-picker")) return;
|
if (!e.target.matches("species-face-picker")) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mainPicker = document.querySelector("#item-preview .species-color-picker");
|
const mainPickerForm = document.querySelector(
|
||||||
|
"#item-preview species-color-picker form");
|
||||||
const mainSpeciesField =
|
const mainSpeciesField =
|
||||||
mainPicker.querySelector("[name='preview[species_id]']");
|
mainPickerForm.querySelector("[name='preview[species_id]']");
|
||||||
mainSpeciesField.value = e.target.value;
|
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) {
|
} catch (error) {
|
||||||
e.preventDefault();
|
|
||||||
console.error("Couldn't update species picker: ", error);
|
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 {
|
class SpeciesFacePicker extends HTMLElement {
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.addEventListener("click", this.#handleClick);
|
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", SpeciesFacePicker);
|
||||||
customElements.define("species-face-picker-options", SpeciesFacePickerOptions);
|
customElements.define("species-face-picker-options", SpeciesFacePickerOptions);
|
||||||
|
|
|
@ -165,9 +165,12 @@ class OutfitLayer extends HTMLElement {
|
||||||
|
|
||||||
#sendMessageToIframe(message) {
|
#sendMessageToIframe(message) {
|
||||||
// If we have no frame or it hasn't loaded, ignore this 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(
|
console.debug(
|
||||||
`Ignoring message, frame not loaded: `,
|
`Ignoring message, frame not loaded yet: `,
|
||||||
this.iframe,
|
this.iframe,
|
||||||
message,
|
message,
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,9 +5,15 @@ body.items-index, body.items-show, body.items-needed, body.item_trades
|
||||||
|
|
||||||
text-align: center
|
text-align: center
|
||||||
|
|
||||||
input[type=text]
|
.item-search-form
|
||||||
font-size: 125%
|
display: flex
|
||||||
width: 15em
|
gap: .5em
|
||||||
|
justify-content: center
|
||||||
|
|
||||||
|
input[type=text]
|
||||||
|
font-size: 125%
|
||||||
|
width: 15em
|
||||||
|
flex: 0 1 auto
|
||||||
|
|
||||||
h1
|
h1
|
||||||
margin-bottom: 1em
|
margin-bottom: 1em
|
||||||
|
|
12
app/assets/stylesheets/_responsive.sass
Normal file
12
app/assets/stylesheets/_responsive.sass
Normal 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
|
|
@ -4,6 +4,7 @@
|
||||||
@import partials/clean/mixins
|
@import partials/clean/mixins
|
||||||
|
|
||||||
@import layout
|
@import layout
|
||||||
|
@import responsive
|
||||||
|
|
||||||
@import partials/jquery.jgrowl
|
@import partials/jquery.jgrowl
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ body.items-show
|
||||||
border: 1px solid $module-border-color
|
border: 1px solid $module-border-color
|
||||||
border-radius: 1em
|
border-radius: 1em
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
margin: 0 auto .75em
|
margin: 0 auto
|
||||||
|
|
||||||
// There's no useful text in here, but double-clicking the play/pause
|
// There's no useful text in here, but double-clicking the play/pause
|
||||||
// button can cause a weird selection state. Disable text selection.
|
// button can cause a weird selection state. Disable text selection.
|
||||||
|
@ -151,22 +151,42 @@ body.items-show
|
||||||
.error-indicator
|
.error-indicator
|
||||||
display: block
|
display: block
|
||||||
|
|
||||||
.species-color-picker
|
species-color-picker
|
||||||
.error-icon
|
.error-icon
|
||||||
cursor: help
|
cursor: help
|
||||||
margin-right: .25em
|
margin-right: .25em
|
||||||
|
|
||||||
&[data-is-valid="false"]
|
form[data-is-valid="false"]
|
||||||
select
|
select
|
||||||
border-color: $error-border-color
|
border-color: $error-border-color
|
||||||
color: $error-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
|
species-face-picker
|
||||||
display: block
|
display: block
|
||||||
position: relative
|
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
|
species-face-picker-options
|
||||||
display: flex
|
display: flex
|
||||||
|
justify-content: center
|
||||||
flex-wrap: wrap
|
flex-wrap: wrap
|
||||||
|
|
||||||
img
|
img
|
||||||
|
@ -236,8 +256,6 @@ body.items-show
|
||||||
cursor: wait
|
cursor: wait
|
||||||
|
|
||||||
.item-zones-info
|
.item-zones-info
|
||||||
margin-top: .5em
|
|
||||||
|
|
||||||
h3
|
h3
|
||||||
display: inline
|
display: inline
|
||||||
font: inherit
|
font: inherit
|
||||||
|
@ -261,3 +279,35 @@ body.items-show
|
||||||
.zone-species-info
|
.zone-species-info
|
||||||
font-style: italic
|
font-style: italic
|
||||||
text-decoration: underline dotted
|
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
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
text-align: left
|
text-align: left
|
||||||
display: flex
|
display: flex
|
||||||
align-items: center
|
align-items: center
|
||||||
|
flex-wrap: wrap
|
||||||
gap: 1em
|
gap: 1em
|
||||||
|
|
||||||
abbr
|
abbr
|
||||||
|
@ -127,6 +128,7 @@
|
||||||
.item-subpages-nav
|
.item-subpages-nav
|
||||||
display: flex
|
display: flex
|
||||||
align-items: flex-end
|
align-items: flex-end
|
||||||
|
gap: 1em
|
||||||
|
|
||||||
.preview-link
|
.preview-link
|
||||||
margin-right: auto
|
margin-right: auto
|
||||||
|
@ -167,4 +169,4 @@
|
||||||
background: $background-color
|
background: $background-color
|
||||||
padding-bottom: calc(.5em + 1px)
|
padding-bottom: calc(.5em + 1px)
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
margin-bottom: -1px
|
margin-bottom: -1px
|
||||||
|
|
|
@ -95,7 +95,7 @@ class ItemsController < ApplicationController
|
||||||
sort_by { |z, a| z.label }
|
sort_by { |z, a| z.label }
|
||||||
|
|
||||||
@preview_pet_type_options = PetType.where(color: @preview_outfit.color).
|
@preview_pet_type_options = PetType.where(color: @preview_outfit.color).
|
||||||
joins(:species).merge(Species.alphabetical)
|
includes(:species).merge(Species.alphabetical)
|
||||||
end
|
end
|
||||||
|
|
||||||
format.gif do
|
format.gif do
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
module ApplicationHelper
|
module ApplicationHelper
|
||||||
include FragmentLocalization
|
include FragmentLocalization
|
||||||
|
|
||||||
def absolute_url(path_or_url)
|
def absolute_url(path_or_url)
|
||||||
if path_or_url.include?('://') # already an absolute URL
|
if path_or_url.include?('://') # already an absolute URL
|
||||||
path_or_url
|
path_or_url
|
||||||
|
@ -231,6 +231,15 @@ module ApplicationHelper
|
||||||
@hide_title_header = true
|
@hide_title_header = true
|
||||||
end
|
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
|
def signed_in_meta_tag
|
||||||
%(<meta name="user-signed-in" content="#{user_signed_in?}">).html_safe
|
%(<meta name="user-signed-in" content="#{user_signed_in?}">).html_safe
|
||||||
end
|
end
|
||||||
|
|
|
@ -533,6 +533,10 @@ class Item < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def appearances
|
def appearances
|
||||||
|
@appearances ||= build_appearances
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_appearances
|
||||||
all_swf_assets = swf_assets.to_a
|
all_swf_assets = swf_assets.to_a
|
||||||
|
|
||||||
# If there are no assets yet, there are no appearances.
|
# 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
|
# 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,
|
# 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?
|
# 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_by_body_id.map do |body_id, body_specific_assets|
|
||||||
swf_assets_for_body = body_specific_assets + swf_assets_for_all_bodies
|
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_by_body_id[body_id])
|
||||||
body = Appearance::Body.new(body_id, species)
|
|
||||||
Appearance.new(self, body, swf_assets_for_body)
|
Appearance.new(self, body, swf_assets_for_body)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,11 +3,6 @@ class Species < ApplicationRecord
|
||||||
has_many :alt_styles
|
has_many :alt_styles
|
||||||
|
|
||||||
scope :alphabetical, -> { order(:name) }
|
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={})
|
def as_json(options={})
|
||||||
super({only: [:id, :name], methods: [:human_name]}.merge(options))
|
super({only: [:id, :name], methods: [:human_name]}.merge(options))
|
||||||
|
@ -20,4 +15,15 @@ class Species < ApplicationRecord
|
||||||
I18n.translate('species.default_human_name')
|
I18n.translate('species.default_human_name')
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
- title @item.name
|
- title @item.name
|
||||||
- canonical_path @item
|
- canonical_path @item
|
||||||
|
- use_responsive_design
|
||||||
|
|
||||||
= render partial: "item_header",
|
= render partial: "item_header",
|
||||||
locals: {item: @item, trades: @trades, current_subpage: "preview",
|
locals: {item: @item, trades: @trades, current_subpage: "preview",
|
||||||
|
@ -18,20 +19,20 @@
|
||||||
.error-indicator
|
.error-indicator
|
||||||
💥 We couldn't load all of this outfit. Try again?
|
💥 We couldn't load all of this outfit. Try again?
|
||||||
|
|
||||||
= form_for item_path(@item), method: :get, class: "species-color-picker",
|
%species-color-picker
|
||||||
data: {"is-valid": @preview_error.nil?} do |f|
|
= form_for item_path(@item), method: :get, data: {"is-valid": @preview_error.nil?} do |f|
|
||||||
- if @preview_error == :pet_type_does_not_exist
|
- if @preview_error == :pet_type_does_not_exist
|
||||||
%span.error-icon{title: "We haven't seen this kind of pet before."} ⚠️
|
%span.error-icon{title: "We haven't seen this kind of pet before."} ⚠️
|
||||||
- elsif @preview_error == :no_item_data
|
- elsif @preview_error == :no_item_data
|
||||||
%span.error-icon{title: "We haven't seen this item on this pet before."} ⚠️
|
%span.error-icon{title: "We haven't seen this item on this pet before."} ⚠️
|
||||||
|
|
||||||
= select_tag "preview[color_id]",
|
= select_tag "preview[color_id]",
|
||||||
options_from_collection_for_select(Color.funny.alphabetical,
|
options_from_collection_for_select(Color.funny.alphabetical,
|
||||||
"id", "human_name", @selected_preview_pet_type.color_id)
|
"id", "human_name", @selected_preview_pet_type.color_id)
|
||||||
= select_tag "preview[species_id]",
|
= select_tag "preview[species_id]",
|
||||||
options_from_collection_for_select(Species.alphabetical,
|
options_from_collection_for_select(Species.alphabetical,
|
||||||
"id", "human_name", @selected_preview_pet_type.species_id)
|
"id", "human_name", @selected_preview_pet_type.species_id)
|
||||||
= submit_tag "Go", name: nil
|
= submit_tag "Go", name: nil
|
||||||
|
|
||||||
%species-face-picker
|
%species-face-picker
|
||||||
%noscript
|
%noscript
|
||||||
|
|
|
@ -13,6 +13,8 @@
|
||||||
%link{href: image_path('favicon.png'), rel: 'icon'}
|
%link{href: image_path('favicon.png'), rel: 'icon'}
|
||||||
= yield :stylesheets
|
= yield :stylesheets
|
||||||
= stylesheet_link_tag "application"
|
= stylesheet_link_tag "application"
|
||||||
|
- if use_responsive_design?
|
||||||
|
%meta{name: "viewport", content: "width=device-width, initial-scale=1"}
|
||||||
= yield :meta
|
= yield :meta
|
||||||
= open_graph_tags
|
= open_graph_tags
|
||||||
= csrf_meta_tag
|
= csrf_meta_tag
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
= image_tag 'https://images.neopets.com/items/mall_floatingneggfaerie.gif'
|
= image_tag 'https://images.neopets.com/items/mall_floatingneggfaerie.gif'
|
||||||
%span= t 'infinite_closet'
|
%span= t 'infinite_closet'
|
||||||
- content_for :content do
|
- 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
|
= text_field_tag :q, @query.to_s
|
||||||
= submit_tag t('.search'), :name => nil
|
= submit_tag t('.search'), :name => nil
|
||||||
= yield
|
= yield
|
||||||
|
|
Loading…
Reference in a new issue