Compare commits
7 commits
ecde507b60
...
d34bebc336
Author | SHA1 | Date | |
---|---|---|---|
d34bebc336 | |||
4e11ee4da7 | |||
1a76923ce6 | |||
a8648fc9c1 | |||
1b03c2caed | |||
1bd5598b64 | |||
189cb6a132 |
10 changed files with 162 additions and 28 deletions
|
@ -4,6 +4,9 @@
|
||||||
.item-list
|
.item-list
|
||||||
border-collapse: collapse
|
border-collapse: collapse
|
||||||
border: 1px solid $soft-border-color
|
border: 1px solid $soft-border-color
|
||||||
|
width: 60%
|
||||||
|
table-layout: auto
|
||||||
|
margin-bottom: 2em
|
||||||
|
|
||||||
td, th
|
td, th
|
||||||
border-top: 1px solid $soft-border-color
|
border-top: 1px solid $soft-border-color
|
||||||
|
@ -17,20 +20,29 @@
|
||||||
&:last-child
|
&:last-child
|
||||||
padding-right: .5em
|
padding-right: .5em
|
||||||
|
|
||||||
.thumbnail-cell img
|
.thumbnail-cell
|
||||||
display: block
|
|
||||||
width: 2.5em
|
width: 2.5em
|
||||||
height: 2.5em
|
height: 2.5em
|
||||||
|
|
||||||
|
img
|
||||||
|
display: block
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
|
||||||
|
.name-cell a
|
||||||
|
text-decoration: none
|
||||||
|
&:hover, &:focus
|
||||||
|
text-decoration: underline
|
||||||
|
|
||||||
.actions-cell
|
.actions-cell
|
||||||
text-align: right
|
text-align: right
|
||||||
padding-left: 1em
|
padding-left: 1em
|
||||||
font-size: 85%
|
font-size: 85%
|
||||||
|
|
||||||
.name-cell a
|
tbody
|
||||||
text-decoration: none
|
tr
|
||||||
&:hover
|
&:hover, &:focus-within
|
||||||
text-decoration: underline
|
background: rgba($module-bg-color, 0.5)
|
||||||
|
|
||||||
thead
|
thead
|
||||||
background: $module-bg-color
|
background: $module-bg-color
|
||||||
|
@ -38,6 +50,23 @@
|
||||||
th
|
th
|
||||||
text-align: left
|
text-align: left
|
||||||
|
|
||||||
|
.thumbnail-cell img
|
||||||
|
outline: 1px solid $soft-border-color
|
||||||
|
|
||||||
.actions-cell button
|
.actions-cell button
|
||||||
/* Bootstrap's Purple 600 */
|
/* Bootstrap's Purple 600 */
|
||||||
+awesome-button-color(#59359a)
|
+awesome-button-color(#59359a)
|
||||||
|
|
||||||
|
.special-color-explanation
|
||||||
|
text-wrap: balance
|
||||||
|
font-style: italic
|
||||||
|
|
||||||
|
/* For wearable items that belong to a specific set that all come together,
|
||||||
|
* like a Paint Brush. */
|
||||||
|
&[data-group-type="bundle"]
|
||||||
|
tbody
|
||||||
|
.thumbnail-cell
|
||||||
|
opacity: 0.65
|
||||||
|
|
||||||
|
tr:hover .thumbnail-cell
|
||||||
|
opacity: 0.85
|
||||||
|
|
|
@ -114,18 +114,36 @@ class ItemsController < ApplicationController
|
||||||
|
|
||||||
def sources
|
def sources
|
||||||
item_ids = params[:ids].split(",")
|
item_ids = params[:ids].split(",")
|
||||||
@items = Item.where(id: item_ids).includes(:nc_mall_record).order(:name)
|
@items = Item.where(id: item_ids).includes(:nc_mall_record).order(:name).
|
||||||
|
limit(50)
|
||||||
|
|
||||||
if @items.empty?
|
if @items.empty?
|
||||||
render file: "public/404.html", status: :not_found, layout: nil
|
render file: "public/404.html", status: :not_found, layout: nil
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Group the items by category!
|
||||||
@nc_mall_items = @items.select(&:currently_in_mall?)
|
@nc_mall_items = @items.select(&:currently_in_mall?)
|
||||||
@other_nc_items = @items.select(&:nc?).reject(&:currently_in_mall?)
|
@other_nc_items = @items.select(&:nc?).reject(&:currently_in_mall?)
|
||||||
@np_items = @items.select(&:np?)
|
@np_items = @items.select(&:np?)
|
||||||
@pb_items = @items.select(&:pb?)
|
@pb_items = @items.select(&:pb?)
|
||||||
|
|
||||||
|
# Also, PB items have some special handling: we group them by color, then
|
||||||
|
# load example pet types for the colors that don't have paint brushes.
|
||||||
|
@pb_items_by_color = @pb_items.group_by(&:pb_color).
|
||||||
|
sort_by { |color, items| color.name }.to_h
|
||||||
|
|
||||||
|
colors_without_thumbnails =
|
||||||
|
@pb_items_by_color.keys.reject(&:pb_item_thumbnail_url?)
|
||||||
|
|
||||||
|
@pb_color_pet_types = colors_without_thumbnails.map do |color|
|
||||||
|
# Infer the ideal species from the first item we can, then try to find a
|
||||||
|
# matching pet type to use as the thumbnail, if needed.
|
||||||
|
species = @pb_items_by_color[color].map(&:pb_species).select(&:present?)
|
||||||
|
.first
|
||||||
|
[color, color.example_pet_type(preferred_species: species)]
|
||||||
|
end.to_h
|
||||||
|
|
||||||
render layout: "application"
|
render layout: "application"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,23 +2,34 @@ require "addressable/template"
|
||||||
|
|
||||||
module ItemsHelper
|
module ItemsHelper
|
||||||
module PetTypeImage
|
module PetTypeImage
|
||||||
Format = 'https://pets.neopets.com/cp/%s/%i/%i.png'
|
Template = Addressable::Template.new(
|
||||||
|
"https://pets.neopets.com/cp/{hash}/{emotion}/{size}.png"
|
||||||
|
)
|
||||||
|
|
||||||
Emotions = {
|
Emotions = {
|
||||||
:happy => 1,
|
happy: 1,
|
||||||
:sad => 2,
|
sad: 2,
|
||||||
:angry => 3,
|
angry: 3,
|
||||||
:ill => 4
|
ill: 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
Sizes = {
|
Sizes = {
|
||||||
:face => 1,
|
face: 1,
|
||||||
:thumb => 2,
|
thumb: 2,
|
||||||
:zoom => 3,
|
zoom: 3,
|
||||||
:full => 4
|
full: 4,
|
||||||
|
face_2x: 6,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def pet_type_image_url(pet_type, emotion: :happy, size: :face)
|
||||||
|
PetTypeImage::Template.expand(
|
||||||
|
hash: pet_type.basic_image_hash || pet_type.image_hash,
|
||||||
|
emotion: PetTypeImage::Emotions[emotion],
|
||||||
|
size: PetTypeImage::Sizes[size],
|
||||||
|
).to_s
|
||||||
|
end
|
||||||
|
|
||||||
def standard_species_search_links
|
def standard_species_search_links
|
||||||
build_on_pet_types(Species.alphabetical) do |pet_type|
|
build_on_pet_types(Species.alphabetical) do |pet_type|
|
||||||
image = pet_type_image(pet_type, :happy, :zoom)
|
image = pet_type_image(pet_type, :happy, :zoom)
|
||||||
|
@ -97,8 +108,9 @@ module ItemsHelper
|
||||||
SHOP_WIZARD_URL_TEMPLATE = Addressable::Template.new(
|
SHOP_WIZARD_URL_TEMPLATE = Addressable::Template.new(
|
||||||
"https://www.neopets.com/shops/wizard.phtml{?string}"
|
"https://www.neopets.com/shops/wizard.phtml{?string}"
|
||||||
)
|
)
|
||||||
def shop_wizard_url_for(item)
|
def shop_wizard_url_for(item_or_name)
|
||||||
SHOP_WIZARD_URL_TEMPLATE.expand(string: item.name).to_s
|
item_or_name = item_or_name.name if item_or_name.is_a? Item
|
||||||
|
SHOP_WIZARD_URL_TEMPLATE.expand(string: item_or_name).to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
SUPER_SHOP_WIZARD_URL_TEMPLATE = Addressable::Template.new(
|
SUPER_SHOP_WIZARD_URL_TEMPLATE = Addressable::Template.new(
|
||||||
|
@ -111,8 +123,9 @@ module ItemsHelper
|
||||||
TRADING_POST_URL_TEMPLATE = Addressable::Template.new(
|
TRADING_POST_URL_TEMPLATE = Addressable::Template.new(
|
||||||
"https://www.neopets.com/island/tradingpost.phtml?type=browse&criteria=item_exact{&search_string}"
|
"https://www.neopets.com/island/tradingpost.phtml?type=browse&criteria=item_exact{&search_string}"
|
||||||
)
|
)
|
||||||
def trading_post_url_for(item)
|
def trading_post_url_for(item_or_name)
|
||||||
TRADING_POST_URL_TEMPLATE.expand(search_string: item.name).to_s
|
item_or_name = item_or_name.name if item_or_name.is_a? Item
|
||||||
|
TRADING_POST_URL_TEMPLATE.expand(search_string: item_or_name).to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
AUCTION_GENIE_URL_TEMPLATE = Addressable::Template.new(
|
AUCTION_GENIE_URL_TEMPLATE = Addressable::Template.new(
|
||||||
|
@ -155,9 +168,7 @@ module ItemsHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def pet_type_image(pet_type, emotion, size)
|
def pet_type_image(pet_type, emotion, size)
|
||||||
emotion_id = PetTypeImage::Emotions[emotion]
|
src = pet_type_image_url(pet_type, emotion:, size:)
|
||||||
size_id = PetTypeImage::Sizes[size]
|
|
||||||
src = sprintf(PetTypeImage::Format, pet_type.basic_image_hash, emotion_id, size_id)
|
|
||||||
human_name = pet_type.species.name.humanize
|
human_name = pet_type.species.name.humanize
|
||||||
image_tag(src, :alt => human_name, :title => human_name)
|
image_tag(src, :alt => human_name, :title => human_name)
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,6 +21,11 @@ class Color < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def example_pet_type(preferred_species: Species.first)
|
||||||
|
pet_types.order([Arel.sql("species_id = ? DESC"), preferred_species.id],
|
||||||
|
"species_id ASC").first
|
||||||
|
end
|
||||||
|
|
||||||
def unfunny_human_name
|
def unfunny_human_name
|
||||||
if name
|
if name
|
||||||
name.split(' ').map { |word| word.capitalize }.join(' ')
|
name.split(' ').map { |word| word.capitalize }.join(' ')
|
||||||
|
|
|
@ -179,6 +179,36 @@ class Item < ApplicationRecord
|
||||||
nc_mall_record&.current_price
|
nc_mall_record&.current_price
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# If this is a PB item, return the corresponding Color, inferred from the
|
||||||
|
# item name. If it's not a PB item, or we fail to infer, return nil.
|
||||||
|
def pb_color
|
||||||
|
return nil unless pb?
|
||||||
|
|
||||||
|
# NOTE: To handle colors like "Royalboy", where the items aren't consistent
|
||||||
|
# with the color name regarding whether or not there's spaces, we remove
|
||||||
|
# all spaces from the item name and color name when matching. We also
|
||||||
|
# hackily handle the fact that "Elderlyboy" color has items named "Elderly
|
||||||
|
# Male" (and same for Girl/Female) by replacing those words, too. These
|
||||||
|
# hacks could cause false matches in theory, but I'm not aware of any rn!
|
||||||
|
normalized_name = name.downcase.gsub("female", "girl").gsub("male", "boy").
|
||||||
|
gsub(/\s/, "")
|
||||||
|
|
||||||
|
Color.order(:name).
|
||||||
|
find { |c| normalized_name.include?(c.name.downcase.gsub(/\s/, "")) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# If this is a PB item, return the corresponding Species, inferred from the
|
||||||
|
# item name. If it's not a PB item, or we fail to infer, return nil.
|
||||||
|
def pb_species
|
||||||
|
return nil unless pb?
|
||||||
|
normalized_name = name.downcase
|
||||||
|
Species.order(:name).find { |s| normalized_name.include?(s.name.downcase) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def pb_item_name
|
||||||
|
pb_color&.pb_item_name
|
||||||
|
end
|
||||||
|
|
||||||
def restricted_zones(options={})
|
def restricted_zones(options={})
|
||||||
options[:scope] ||= Zone.all
|
options[:scope] ||= Zone.all
|
||||||
options[:scope].find(restricted_zone_ids)
|
options[:scope].find(restricted_zone_ids)
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
%tr
|
%tr
|
||||||
%td.thumbnail-cell= link_to item_thumbnail_for(item), item, target: "_blank"
|
%td.thumbnail-cell
|
||||||
|
= link_to item_thumbnail_for(item), item, target: "_blank",
|
||||||
|
tabindex: "-1"
|
||||||
%td.name-cell= link_to item.name, item, target: "_blank"
|
%td.name-cell= link_to item.name, item, target: "_blank"
|
||||||
%td.actions-cell= yield
|
%td.actions-cell= yield
|
||||||
|
|
|
@ -54,13 +54,40 @@
|
||||||
target: "_blank", icon: search_icon
|
target: "_blank", icon: search_icon
|
||||||
|
|
||||||
- if @pb_items.present?
|
- if @pb_items.present?
|
||||||
%h2 Paintbrush items
|
%h2 Paint Brush items
|
||||||
:markdown
|
:markdown
|
||||||
These items are part of a paintbrush set. Once you paint your pet,
|
These items are part of a paint brush set. Once you paint your pet,
|
||||||
these items will be semi-permanently added to your Closet, even if your
|
these items will be semi-permanently added to your Closet, even if your
|
||||||
pet changes color again! You can use this to mix-and-match styles for
|
pet changes color again! You can use this to mix-and-match styles for
|
||||||
"cross-paint" outfits.
|
"cross-paint" outfits.
|
||||||
= render @pb_items
|
- @pb_items_by_color.each do |color, items|
|
||||||
|
%table.item-list{"data-group-type": "bundle"}
|
||||||
|
%thead
|
||||||
|
%td.thumbnail-cell
|
||||||
|
- if color.pb_item_thumbnail_url?
|
||||||
|
= image_tag color.pb_item_thumbnail_url,
|
||||||
|
alt: "Item thumbnail for #{color.pb_item_name}"
|
||||||
|
- else
|
||||||
|
= image_tag pet_type_image_url(@pb_color_pet_types[color], size: :face),
|
||||||
|
srcset: ["#{pet_type_image_url(@pb_color_pet_types[color], size: :face_2x)} 2x"],
|
||||||
|
alt: @pb_color_pet_types[color].human_name
|
||||||
|
%th
|
||||||
|
#{color.pb_item_name || color.name.humanize}
|
||||||
|
(#{pluralize items.size, "item"})
|
||||||
|
%td.actions-cell
|
||||||
|
- if color.pb_item_name?
|
||||||
|
= button_link_to "Shops",
|
||||||
|
shop_wizard_url_for(color.pb_item_name),
|
||||||
|
target: "_blank", icon: search_icon
|
||||||
|
= button_link_to "Trades",
|
||||||
|
trading_post_url_for(color.pb_item_name),
|
||||||
|
target: "_blank", icon: search_icon
|
||||||
|
- else
|
||||||
|
.special-color-explanation
|
||||||
|
Get via Lab Ray, morphing potions, etc.
|
||||||
|
%tbody
|
||||||
|
- items.each do |item|
|
||||||
|
= render "item_list_row", item:
|
||||||
|
|
||||||
- if @other_nc_items.present?
|
- if @other_nc_items.present?
|
||||||
%h2 Neocash items (Capsules, Dyeworks, events, retired, etc.)
|
%h2 Neocash items (Capsules, Dyeworks, events, retired, etc.)
|
||||||
|
|
5
db/migrate/20240522222040_add_pb_item_name_to_colors.rb
Normal file
5
db/migrate/20240522222040_add_pb_item_name_to_colors.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
class AddPbItemNameToColors < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
add_column :colors, :pb_item_name, :string
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddPbItemThumbnailUrlToColors < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
add_column :colors, :pb_item_thumbnail_url, :string
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2024_05_11_003019) do
|
ActiveRecord::Schema[7.1].define(version: 2024_05_22_233638) do
|
||||||
create_table "alt_styles", charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "alt_styles", charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.integer "species_id", null: false
|
t.integer "species_id", null: false
|
||||||
t.integer "color_id", null: false
|
t.integer "color_id", null: false
|
||||||
|
@ -76,6 +76,8 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_11_003019) do
|
||||||
t.boolean "standard"
|
t.boolean "standard"
|
||||||
t.boolean "prank", default: false, null: false
|
t.boolean "prank", default: false, null: false
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
|
t.string "pb_item_name"
|
||||||
|
t.string "pb_item_thumbnail_url"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "contributions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
create_table "contributions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
|
|
Loading…
Reference in a new issue