[WV2] Move to a new WardrobeController
This commit is contained in:
parent
811bb3e036
commit
78931ddb47
11 changed files with 277 additions and 268 deletions
|
|
@ -77,53 +77,6 @@ class OutfitsController < ApplicationController
|
||||||
@campaign = Fundraising::Campaign.current rescue nil
|
@campaign = Fundraising::Campaign.current rescue nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def new_v2
|
|
||||||
# Get selected species and color from params, or default to Blue Acara
|
|
||||||
@selected_species = params[:species] ? Species.find_by_id(params[:species]) : Species.find_by_name("Acara")
|
|
||||||
@selected_color = params[:color] ? Color.find_by_id(params[:color]) : Color.find_by_name("Blue")
|
|
||||||
|
|
||||||
# Load valid colors for the selected species (colors that have existing pet types)
|
|
||||||
@species = Species.alphabetical
|
|
||||||
@colors = @selected_species.compatible_colors
|
|
||||||
|
|
||||||
# Find the best pet type for this species+color combo
|
|
||||||
# If the exact combo doesn't exist, this will fall back to a simple color
|
|
||||||
@pet_type = PetType.for_species_and_color(
|
|
||||||
species_id: @selected_species.id,
|
|
||||||
color_id: @selected_color.id
|
|
||||||
)
|
|
||||||
|
|
||||||
# Use the pet type's actual color as the selected color
|
|
||||||
# (might differ from requested color if we fell back to a simple color)
|
|
||||||
@selected_color = @pet_type&.color
|
|
||||||
|
|
||||||
# Load items from the objects[] parameter
|
|
||||||
item_ids = params[:objects] || []
|
|
||||||
items = Item.where(id: item_ids)
|
|
||||||
|
|
||||||
# Build the outfit
|
|
||||||
@outfit = Outfit.new(
|
|
||||||
pet_state: @pet_type&.canonical_pet_state,
|
|
||||||
worn_items: items,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Preload the manifests for all visible layers, so they load efficiently
|
|
||||||
# in parallel rather than sequentially when rendering
|
|
||||||
SwfAsset.preload_manifests(@outfit.visible_layers)
|
|
||||||
|
|
||||||
# Handle search mode
|
|
||||||
@search_mode = params[:q].present?
|
|
||||||
if @search_mode
|
|
||||||
search_filters = build_search_filters(params[:q], @outfit)
|
|
||||||
query_params = ActionController::Parameters.new(
|
|
||||||
search_filters.each_with_index.map { |filter, i| [i.to_s, filter] }.to_h
|
|
||||||
)
|
|
||||||
@query = Item::Search::Query.from_params(query_params, current_user)
|
|
||||||
@search_results = @query.results.paginate(page: params.dig(:q, :page), per_page: 30)
|
|
||||||
end
|
|
||||||
|
|
||||||
render layout: false
|
|
||||||
end
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@outfit = Outfit.find(params[:id])
|
@outfit = Outfit.find(params[:id])
|
||||||
|
|
@ -176,51 +129,5 @@ class OutfitsController < ApplicationController
|
||||||
:status => :bad_request
|
:status => :bad_request
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_search_filters(query_params, outfit)
|
|
||||||
filters = []
|
|
||||||
|
|
||||||
# Add name filter if present
|
|
||||||
if query_params[:name].present?
|
|
||||||
filters << { key: "name", value: query_params[:name] }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Add item kind filter if present
|
|
||||||
if query_params[:item_kind].present?
|
|
||||||
case query_params[:item_kind]
|
|
||||||
when "nc"
|
|
||||||
filters << { key: "is_nc", value: "true" }
|
|
||||||
when "np"
|
|
||||||
filters << { key: "is_np", value: "true" }
|
|
||||||
when "pb"
|
|
||||||
filters << { key: "is_pb", value: "true" }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Add zone filter if present
|
|
||||||
if query_params[:zone].present?
|
|
||||||
filters << { key: "occupied_zone_set_name", value: query_params[:zone] }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Always add auto-filter for items that fit the current pet
|
|
||||||
pet_type = outfit.pet_type
|
|
||||||
if pet_type
|
|
||||||
fit_filter = {
|
|
||||||
key: "fits",
|
|
||||||
value: {
|
|
||||||
species_id: pet_type.species_id.to_s,
|
|
||||||
color_id: pet_type.color_id.to_s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Include alt_style_id if present
|
|
||||||
if outfit.alt_style_id.present?
|
|
||||||
fit_filter[:value][:alt_style_id] = outfit.alt_style_id.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
filters << fit_filter
|
|
||||||
end
|
|
||||||
|
|
||||||
filters
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
98
app/controllers/wardrobe_controller.rb
Normal file
98
app/controllers/wardrobe_controller.rb
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
class WardrobeController < ApplicationController
|
||||||
|
def show
|
||||||
|
# Get selected species and color from params, or default to Blue Acara
|
||||||
|
@selected_species = params[:species] ? Species.find_by_id(params[:species]) : Species.find_by_name("Acara")
|
||||||
|
@selected_color = params[:color] ? Color.find_by_id(params[:color]) : Color.find_by_name("Blue")
|
||||||
|
|
||||||
|
# Load valid colors for the selected species (colors that have existing pet types)
|
||||||
|
@species = Species.alphabetical
|
||||||
|
@colors = @selected_species.compatible_colors
|
||||||
|
|
||||||
|
# Find the best pet type for this species+color combo
|
||||||
|
# If the exact combo doesn't exist, this will fall back to a simple color
|
||||||
|
@pet_type = PetType.for_species_and_color(
|
||||||
|
species_id: @selected_species.id,
|
||||||
|
color_id: @selected_color.id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use the pet type's actual color as the selected color
|
||||||
|
# (might differ from requested color if we fell back to a simple color)
|
||||||
|
@selected_color = @pet_type&.color
|
||||||
|
|
||||||
|
# Load items from the objects[] parameter
|
||||||
|
item_ids = params[:objects] || []
|
||||||
|
items = Item.where(id: item_ids)
|
||||||
|
|
||||||
|
# Build the outfit
|
||||||
|
@outfit = Outfit.new(
|
||||||
|
pet_state: @pet_type&.canonical_pet_state,
|
||||||
|
worn_items: items,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Preload the manifests for all visible layers, so they load efficiently
|
||||||
|
# in parallel rather than sequentially when rendering
|
||||||
|
SwfAsset.preload_manifests(@outfit.visible_layers)
|
||||||
|
|
||||||
|
# Handle search mode
|
||||||
|
@search_mode = params[:q].present?
|
||||||
|
if @search_mode
|
||||||
|
search_filters = build_search_filters(params[:q], @outfit)
|
||||||
|
query_params = ActionController::Parameters.new(
|
||||||
|
search_filters.each_with_index.map { |filter, i| [i.to_s, filter] }.to_h
|
||||||
|
)
|
||||||
|
@query = Item::Search::Query.from_params(query_params, current_user)
|
||||||
|
@search_results = @query.results.paginate(page: params.dig(:q, :page), per_page: 30)
|
||||||
|
end
|
||||||
|
|
||||||
|
render layout: false
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def build_search_filters(query_params, outfit)
|
||||||
|
filters = []
|
||||||
|
|
||||||
|
# Add name filter if present
|
||||||
|
if query_params[:name].present?
|
||||||
|
filters << { key: "name", value: query_params[:name] }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Add item kind filter if present
|
||||||
|
if query_params[:item_kind].present?
|
||||||
|
case query_params[:item_kind]
|
||||||
|
when "nc"
|
||||||
|
filters << { key: "is_nc", value: "true" }
|
||||||
|
when "np"
|
||||||
|
filters << { key: "is_np", value: "true" }
|
||||||
|
when "pb"
|
||||||
|
filters << { key: "is_pb", value: "true" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Add zone filter if present
|
||||||
|
if query_params[:zone].present?
|
||||||
|
filters << { key: "occupied_zone_set_name", value: query_params[:zone] }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Always add auto-filter for items that fit the current pet
|
||||||
|
pet_type = outfit.pet_type
|
||||||
|
if pet_type
|
||||||
|
fit_filter = {
|
||||||
|
key: "fits",
|
||||||
|
value: {
|
||||||
|
species_id: pet_type.species_id.to_s,
|
||||||
|
color_id: pet_type.color_id.to_s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Include alt_style_id if present
|
||||||
|
if outfit.alt_style_id.present?
|
||||||
|
fit_filter[:value][:alt_style_id] = outfit.alt_style_id.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
filters << fit_filter
|
||||||
|
end
|
||||||
|
|
||||||
|
filters
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -65,30 +65,6 @@ module OutfitsHelper
|
||||||
text_field_tag 'name', nil, options
|
text_field_tag 'name', nil, options
|
||||||
end
|
end
|
||||||
|
|
||||||
# Generate hidden fields to preserve outfit state in URL params.
|
|
||||||
# Use the `except` parameter to skip certain fields, e.g. to override
|
|
||||||
# them with specific values, like in the species/color picker.
|
|
||||||
def outfit_state_params(outfit = @outfit, except: [])
|
|
||||||
fields = []
|
|
||||||
|
|
||||||
fields << hidden_field_tag(:species, @outfit.species_id) unless except.include?(:species)
|
|
||||||
fields << hidden_field_tag(:color, @outfit.color_id) unless except.include?(:color)
|
|
||||||
|
|
||||||
unless except.include?(:worn_items)
|
|
||||||
outfit.worn_items.each do |item|
|
|
||||||
fields << hidden_field_tag('objects[]', item.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
unless except.include?(:q)
|
|
||||||
(params[:q] || {}).each do |key, value|
|
|
||||||
fields << hidden_field_tag("q[#{key}]", value) if value.present?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
safe_join fields
|
|
||||||
end
|
|
||||||
|
|
||||||
def outfit_viewer(...)
|
def outfit_viewer(...)
|
||||||
render partial: "outfit_viewer",
|
render partial: "outfit_viewer",
|
||||||
locals: parse_outfit_viewer_options(...)
|
locals: parse_outfit_viewer_options(...)
|
||||||
|
|
@ -99,133 +75,8 @@ module OutfitsHelper
|
||||||
locals: parse_outfit_viewer_options(...)
|
locals: parse_outfit_viewer_options(...)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Group outfit items by zone, applying smart multi-zone simplification.
|
|
||||||
# Returns an array of hashes: {zone:, items:}
|
|
||||||
# This matches the logic from wardrobe-2020's getZonesAndItems function.
|
|
||||||
def outfit_items_by_zone(outfit)
|
|
||||||
return [] if outfit.pet_type.nil?
|
|
||||||
|
|
||||||
# Get item appearances for this outfit
|
|
||||||
item_appearances = Item.appearances_for(
|
|
||||||
outfit.worn_items,
|
|
||||||
outfit.pet_type,
|
|
||||||
swf_asset_includes: [:zone]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Separate incompatible items (no layers for this pet)
|
|
||||||
compatible_items = []
|
|
||||||
incompatible_items = []
|
|
||||||
|
|
||||||
outfit.worn_items.each do |item|
|
|
||||||
appearance = item_appearances[item.id]
|
|
||||||
if appearance&.present?
|
|
||||||
compatible_items << {item: item, appearance: appearance}
|
|
||||||
else
|
|
||||||
incompatible_items << item
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Group items by zone - multi-zone items appear in each zone
|
|
||||||
items_by_zone = Hash.new { |h, k| h[k] = [] }
|
|
||||||
zones_by_id = {}
|
|
||||||
|
|
||||||
compatible_items.each do |item_with_appearance|
|
|
||||||
item = item_with_appearance[:item]
|
|
||||||
appearance = item_with_appearance[:appearance]
|
|
||||||
|
|
||||||
# Get unique zones for this item (an item may have multiple assets per zone)
|
|
||||||
appearance.swf_assets.map(&:zone).uniq.each do |zone|
|
|
||||||
zones_by_id[zone.id] = zone
|
|
||||||
items_by_zone[zone.id] << item
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Create zone groups with sorted items
|
|
||||||
zones_and_items = items_by_zone.map do |zone_id, items|
|
|
||||||
{
|
|
||||||
zone_id: zone_id,
|
|
||||||
zone_label: zones_by_id[zone_id].label,
|
|
||||||
items: items.sort_by { |item| item.name.downcase }
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Sort zone groups alphabetically by label, then by ID for tiebreaking
|
|
||||||
zones_and_items.sort_by! do |group|
|
|
||||||
[group[:zone_label].downcase, group[:zone_id]]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Apply multi-zone simplification: remove redundant single-item groups
|
|
||||||
zones_and_items = simplify_multi_zone_groups(zones_and_items)
|
|
||||||
|
|
||||||
# Add zone ID disambiguation for duplicate labels
|
|
||||||
zones_and_items = disambiguate_zone_labels(zones_and_items)
|
|
||||||
|
|
||||||
# Add incompatible items section if any
|
|
||||||
if incompatible_items.any?
|
|
||||||
zones_and_items << {
|
|
||||||
zone_id: nil,
|
|
||||||
zone_label: "Incompatible",
|
|
||||||
items: incompatible_items.sort_by { |item| item.name.downcase }
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
zones_and_items
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Simplify zone groups by removing redundant single-item groups.
|
|
||||||
# Keep groups with multiple items (conflicts). For single-item groups,
|
|
||||||
# only keep them if the item doesn't appear in a multi-item group.
|
|
||||||
def simplify_multi_zone_groups(zones_and_items)
|
|
||||||
# Find groups with conflicts (multiple items)
|
|
||||||
groups_with_conflicts = zones_and_items.select { |g| g[:items].length > 1 }
|
|
||||||
|
|
||||||
# Track which items appear in conflict groups
|
|
||||||
items_with_conflicts = Set.new(
|
|
||||||
groups_with_conflicts.flat_map { |g| g[:items].map(&:id) }
|
|
||||||
)
|
|
||||||
|
|
||||||
# Track which items we've already shown
|
|
||||||
items_we_have_seen = Set.new
|
|
||||||
|
|
||||||
# Filter groups
|
|
||||||
zones_and_items.select do |group|
|
|
||||||
# Always keep groups with multiple items
|
|
||||||
if group[:items].length > 1
|
|
||||||
group[:items].each { |item| items_we_have_seen.add(item.id) }
|
|
||||||
true
|
|
||||||
else
|
|
||||||
# For single-item groups, only keep if:
|
|
||||||
# - Item hasn't been seen yet AND
|
|
||||||
# - Item won't appear in a conflict group
|
|
||||||
item = group[:items].first
|
|
||||||
item_id = item.id
|
|
||||||
|
|
||||||
if items_we_have_seen.include?(item_id) || items_with_conflicts.include?(item_id)
|
|
||||||
false
|
|
||||||
else
|
|
||||||
items_we_have_seen.add(item_id)
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Add zone IDs to labels when there are duplicates
|
|
||||||
def disambiguate_zone_labels(zones_and_items)
|
|
||||||
label_counts = zones_and_items.group_by { |g| g[:zone_label] }
|
|
||||||
.transform_values(&:count)
|
|
||||||
|
|
||||||
zones_and_items.each do |group|
|
|
||||||
if label_counts[group[:zone_label]] > 1
|
|
||||||
group[:zone_label] = "#{group[:zone_label]} (##{group[:zone_id]})"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
zones_and_items
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse_outfit_viewer_options(
|
def parse_outfit_viewer_options(
|
||||||
outfit=nil, pet_state: nil, preferred_image_format: :png, **html_options
|
outfit=nil, pet_state: nil, preferred_image_format: :png, **html_options
|
||||||
)
|
)
|
||||||
|
|
|
||||||
152
app/helpers/wardrobe_helper.rb
Normal file
152
app/helpers/wardrobe_helper.rb
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
module WardrobeHelper
|
||||||
|
# Generate hidden fields to preserve outfit state in URL params.
|
||||||
|
# Use the `except` parameter to skip certain fields, e.g. to override
|
||||||
|
# them with specific values, like in the species/color picker.
|
||||||
|
def outfit_state_params(outfit = @outfit, except: [])
|
||||||
|
fields = []
|
||||||
|
|
||||||
|
fields << hidden_field_tag(:species, @outfit.species_id) unless except.include?(:species)
|
||||||
|
fields << hidden_field_tag(:color, @outfit.color_id) unless except.include?(:color)
|
||||||
|
|
||||||
|
unless except.include?(:worn_items)
|
||||||
|
outfit.worn_items.each do |item|
|
||||||
|
fields << hidden_field_tag('objects[]', item.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
unless except.include?(:q)
|
||||||
|
(params[:q] || {}).each do |key, value|
|
||||||
|
fields << hidden_field_tag("q[#{key}]", value) if value.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
safe_join fields
|
||||||
|
end
|
||||||
|
|
||||||
|
# Group outfit items by zone, applying smart multi-zone simplification.
|
||||||
|
# Returns an array of hashes: {zone:, items:}
|
||||||
|
# This matches the logic from wardrobe-2020's getZonesAndItems function.
|
||||||
|
def outfit_items_by_zone(outfit)
|
||||||
|
return [] if outfit.pet_type.nil?
|
||||||
|
|
||||||
|
# Get item appearances for this outfit
|
||||||
|
item_appearances = Item.appearances_for(
|
||||||
|
outfit.worn_items,
|
||||||
|
outfit.pet_type,
|
||||||
|
swf_asset_includes: [:zone]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Separate incompatible items (no layers for this pet)
|
||||||
|
compatible_items = []
|
||||||
|
incompatible_items = []
|
||||||
|
|
||||||
|
outfit.worn_items.each do |item|
|
||||||
|
appearance = item_appearances[item.id]
|
||||||
|
if appearance&.present?
|
||||||
|
compatible_items << {item: item, appearance: appearance}
|
||||||
|
else
|
||||||
|
incompatible_items << item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Group items by zone - multi-zone items appear in each zone
|
||||||
|
items_by_zone = Hash.new { |h, k| h[k] = [] }
|
||||||
|
zones_by_id = {}
|
||||||
|
|
||||||
|
compatible_items.each do |item_with_appearance|
|
||||||
|
item = item_with_appearance[:item]
|
||||||
|
appearance = item_with_appearance[:appearance]
|
||||||
|
|
||||||
|
# Get unique zones for this item (an item may have multiple assets per zone)
|
||||||
|
appearance.swf_assets.map(&:zone).uniq.each do |zone|
|
||||||
|
zones_by_id[zone.id] = zone
|
||||||
|
items_by_zone[zone.id] << item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create zone groups with sorted items
|
||||||
|
zones_and_items = items_by_zone.map do |zone_id, items|
|
||||||
|
{
|
||||||
|
zone_id: zone_id,
|
||||||
|
zone_label: zones_by_id[zone_id].label,
|
||||||
|
items: items.sort_by { |item| item.name.downcase }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sort zone groups alphabetically by label, then by ID for tiebreaking
|
||||||
|
zones_and_items.sort_by! do |group|
|
||||||
|
[group[:zone_label].downcase, group[:zone_id]]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Apply multi-zone simplification: remove redundant single-item groups
|
||||||
|
zones_and_items = simplify_multi_zone_groups(zones_and_items)
|
||||||
|
|
||||||
|
# Add zone ID disambiguation for duplicate labels
|
||||||
|
zones_and_items = disambiguate_zone_labels(zones_and_items)
|
||||||
|
|
||||||
|
# Add incompatible items section if any
|
||||||
|
if incompatible_items.any?
|
||||||
|
zones_and_items << {
|
||||||
|
zone_id: nil,
|
||||||
|
zone_label: "Incompatible",
|
||||||
|
items: incompatible_items.sort_by { |item| item.name.downcase }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
zones_and_items
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Simplify zone groups by removing redundant single-item groups.
|
||||||
|
# Keep groups with multiple items (conflicts). For single-item groups,
|
||||||
|
# only keep them if the item doesn't appear in a multi-item group.
|
||||||
|
def simplify_multi_zone_groups(zones_and_items)
|
||||||
|
# Find groups with conflicts (multiple items)
|
||||||
|
groups_with_conflicts = zones_and_items.select { |g| g[:items].length > 1 }
|
||||||
|
|
||||||
|
# Track which items appear in conflict groups
|
||||||
|
items_with_conflicts = Set.new(
|
||||||
|
groups_with_conflicts.flat_map { |g| g[:items].map(&:id) }
|
||||||
|
)
|
||||||
|
|
||||||
|
# Track which items we've already shown
|
||||||
|
items_we_have_seen = Set.new
|
||||||
|
|
||||||
|
# Filter groups
|
||||||
|
zones_and_items.select do |group|
|
||||||
|
# Always keep groups with multiple items
|
||||||
|
if group[:items].length > 1
|
||||||
|
group[:items].each { |item| items_we_have_seen.add(item.id) }
|
||||||
|
true
|
||||||
|
else
|
||||||
|
# For single-item groups, only keep if:
|
||||||
|
# - Item hasn't been seen yet AND
|
||||||
|
# - Item won't appear in a conflict group
|
||||||
|
item = group[:items].first
|
||||||
|
item_id = item.id
|
||||||
|
|
||||||
|
if items_we_have_seen.include?(item_id) || items_with_conflicts.include?(item_id)
|
||||||
|
false
|
||||||
|
else
|
||||||
|
items_we_have_seen.add(item_id)
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Add zone IDs to labels when there are duplicates
|
||||||
|
def disambiguate_zone_labels(zones_and_items)
|
||||||
|
label_counts = zones_and_items.group_by { |g| g[:zone_label] }
|
||||||
|
.transform_values(&:count)
|
||||||
|
|
||||||
|
zones_and_items.each do |group|
|
||||||
|
if label_counts[group[:zone_label]] > 1
|
||||||
|
group[:zone_label] = "#{group[:zone_label]} (##{group[:zone_id]})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
zones_and_items
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -9,12 +9,12 @@
|
||||||
%link{href: image_path('favicon.png'), rel: 'icon'}
|
%link{href: image_path('favicon.png'), rel: 'icon'}
|
||||||
= stylesheet_link_tag "application/hanger-spinner"
|
= stylesheet_link_tag "application/hanger-spinner"
|
||||||
= stylesheet_link_tag "application/outfit-viewer"
|
= stylesheet_link_tag "application/outfit-viewer"
|
||||||
= page_stylesheet_link_tag "outfits/new_v2"
|
= page_stylesheet_link_tag "wardrobe/show"
|
||||||
= javascript_include_tag "application", async: true
|
= javascript_include_tag "application", async: true
|
||||||
= javascript_include_tag "idiomorph", async: true
|
= javascript_include_tag "idiomorph", async: true
|
||||||
= javascript_include_tag "outfit-viewer", async: true
|
= javascript_include_tag "outfit-viewer", async: true
|
||||||
= javascript_include_tag "species-color-picker", async: true
|
= javascript_include_tag "species-color-picker", async: true
|
||||||
= javascript_include_tag "outfits/new_v2", async: true
|
= javascript_include_tag "wardrobe/show", async: true
|
||||||
= csrf_meta_tags
|
= csrf_meta_tags
|
||||||
%meta{name: 'outfit-viewer-morph-mode', value: 'full-page'}
|
%meta{name: 'outfit-viewer-morph-mode', value: 'full-page'}
|
||||||
%body.wardrobe-v2
|
%body.wardrobe-v2
|
||||||
|
|
@ -10,8 +10,8 @@ OpenneoImpressItems::Application.routes.draw do
|
||||||
# TODO: It's a bit silly that outfits/new points to outfits#edit.
|
# TODO: It's a bit silly that outfits/new points to outfits#edit.
|
||||||
# Should we refactor the controller/view structure here?
|
# Should we refactor the controller/view structure here?
|
||||||
get '/outfits/new', to: 'outfits#edit', as: :wardrobe
|
get '/outfits/new', to: 'outfits#edit', as: :wardrobe
|
||||||
get '/outfits/new/v2', to: 'outfits#new_v2', as: :wardrobe_v2
|
|
||||||
get '/wardrobe' => redirect('/outfits/new')
|
get '/wardrobe' => redirect('/outfits/new')
|
||||||
|
get '/wardrobe/v2', to: 'wardrobe#show', as: :wardrobe_v2
|
||||||
get '/start/:color_name/:species_name' => 'outfits#start'
|
get '/start/:color_name/:species_name' => 'outfits#start'
|
||||||
|
|
||||||
# The outfits users have created!
|
# The outfits users have created!
|
||||||
|
|
|
||||||
|
|
@ -13,34 +13,34 @@ Replace the complex React outfit editor (`app/javascript/wardrobe-2020/`) with a
|
||||||
|
|
||||||
## Current Status
|
## Current Status
|
||||||
|
|
||||||
**Wardrobe V2 is in early prototype/proof-of-concept stage.** It's accessible at `/outfits/new/v2` but is not yet linked from the main UI.
|
**Wardrobe V2 is in early prototype/proof-of-concept stage.** It's accessible at `/wardrobe/v2` but is not yet linked from the main UI.
|
||||||
|
|
||||||
### What's Implemented
|
### What's Implemented
|
||||||
|
|
||||||
#### Core Infrastructure
|
#### Core Infrastructure
|
||||||
|
|
||||||
**Route & Controller** ([outfits_controller.rb:80-115](app/controllers/outfits_controller.rb#L80-L115))
|
**Route & Controller** ([wardrobe_controller.rb](app/controllers/wardrobe_controller.rb))
|
||||||
- `GET /outfits/new/v2` - Main wardrobe endpoint
|
- `GET /wardrobe/v2` - Main wardrobe endpoint
|
||||||
- Takes URL params: `species`, `color`, `objects[]` (item IDs)
|
- Takes URL params: `species`, `color`, `objects[]` (item IDs)
|
||||||
- Returns full HTML page (no layout, designed to work standalone)
|
- Returns full HTML page (no layout, designed to work standalone)
|
||||||
- Defaults to Blue Acara if no pet specified
|
- Defaults to Blue Acara if no pet specified
|
||||||
|
|
||||||
**View Layer** ([new_v2.html.haml](app/views/outfits/new_v2.html.haml))
|
**View Layer** ([show.html.haml](app/views/wardrobe/show.html.haml))
|
||||||
- Full-page layout with preview (left) and controls (right)
|
- Full-page layout with preview (left) and controls (right)
|
||||||
- Responsive: stacks vertically on mobile (< 800px)
|
- Responsive: stacks vertically on mobile (< 800px)
|
||||||
- Uses existing `outfit_viewer` partial for rendering
|
- Uses existing `outfit_viewer` partial for rendering
|
||||||
- Custom CSS in [outfits/new_v2.css](app/assets/stylesheets/outfits/new_v2.css)
|
- Custom CSS in [wardrobe/show.css](app/assets/stylesheets/wardrobe/show.css)
|
||||||
- Minimal JavaScript in [outfits/new_v2.js](app/assets/javascripts/outfits/new_v2.js)
|
- Minimal JavaScript in [wardrobe/show.js](app/assets/javascripts/wardrobe/show.js)
|
||||||
|
|
||||||
**Pet Selection** ([new_v2.html.haml:31-42](app/views/outfits/new_v2.html.haml#L31-L42))
|
**Pet Selection** ([show.html.haml:31-42](app/views/wardrobe/show.html.haml#L31-L42))
|
||||||
- Species/color picker using `<species-color-picker>` web component
|
- Species/color picker using `<species-color-picker>` web component
|
||||||
- Floats over preview area (bottom), reveals on hover/focus
|
- Floats over preview area (bottom), reveals on hover/focus
|
||||||
- Progressive enhancement: submit button appears if JS slow/disabled
|
- Progressive enhancement: submit button appears if JS slow/disabled
|
||||||
- Auto-submits form on change when JS loaded
|
- Auto-submits form on change when JS loaded
|
||||||
- Filters colors to only those compatible with selected species
|
- Filters colors to only those compatible with selected species
|
||||||
- Advanced fallback: if species+color combo doesn't exist, falls back to simple color ([outfits_controller.rb:89-98](app/controllers/outfits_controller.rb#L89-L98))
|
- Advanced fallback: if species+color combo doesn't exist, falls back to simple color ([wardrobe_controller.rb:13-16](app/controllers/wardrobe_controller.rb#L13-L16))
|
||||||
|
|
||||||
**Item Display** ([new_v2.html.haml:47-64](app/views/outfits/new_v2.html.haml#L47-L64))
|
**Item Display** ([show.html.haml:47-64](app/views/wardrobe/show.html.haml#L47-L64))
|
||||||
- Groups worn items by zone (Hat, Jacket, Wings, etc.)
|
- Groups worn items by zone (Hat, Jacket, Wings, etc.)
|
||||||
- Smart multi-zone simplification: hides redundant single-item zones
|
- Smart multi-zone simplification: hides redundant single-item zones
|
||||||
- Shows items that occupy multiple zones in conflict zones only
|
- Shows items that occupy multiple zones in conflict zones only
|
||||||
|
|
@ -49,7 +49,7 @@ Replace the complex React outfit editor (`app/javascript/wardrobe-2020/`) with a
|
||||||
- Displays item thumbnails, names, and badges (NC/NP, first seen date)
|
- Displays item thumbnails, names, and badges (NC/NP, first seen date)
|
||||||
- Alphabetical sorting within zones
|
- Alphabetical sorting within zones
|
||||||
|
|
||||||
**Item Search** ([new_v2.html.haml:47-50](app/views/outfits/new_v2.html.haml#L47-L50))
|
**Item Search** ([show.html.haml:47-50](app/views/wardrobe/show.html.haml#L47-L50))
|
||||||
- Search form at top of controls section
|
- Search form at top of controls section
|
||||||
- Auto-filters to items that fit current pet (species + color + alt_style)
|
- Auto-filters to items that fit current pet (species + color + alt_style)
|
||||||
- Uses `Item::Search::Query.from_params` for structured search
|
- Uses `Item::Search::Query.from_params` for structured search
|
||||||
|
|
@ -58,20 +58,20 @@ Replace the complex React outfit editor (`app/javascript/wardrobe-2020/`) with a
|
||||||
- All search state scoped under `q[...]` params (name, page, etc.)
|
- All search state scoped under `q[...]` params (name, page, etc.)
|
||||||
- "Back to outfit" button to exit search
|
- "Back to outfit" button to exit search
|
||||||
|
|
||||||
**Item Addition** ([_search_results.html.haml](app/views/outfits/_search_results.html.haml))
|
**Item Addition** ([_search_results.html.haml](app/views/wardrobe/_search_results.html.haml))
|
||||||
- Add button (➕) on each search result item
|
- Add button (➕) on each search result item
|
||||||
- Adds item to outfit via GET request with updated `objects[]` params
|
- Adds item to outfit via GET request with updated `objects[]` params
|
||||||
- Button hidden by default, appears on hover/focus
|
- Button hidden by default, appears on hover/focus
|
||||||
- Preserves search state when adding items
|
- Preserves search state when adding items
|
||||||
- Uses `outfit.with_item(item)` helper to generate new state
|
- Uses `outfit.with_item(item)` helper to generate new state
|
||||||
|
|
||||||
**Item Removal** ([new_v2.html.haml:70-72](app/views/outfits/new_v2.html.haml#L70-L72))
|
**Item Removal** ([show.html.haml:70-72](app/views/wardrobe/show.html.haml#L70-L72))
|
||||||
- Remove button (❌) on each worn item
|
- Remove button (❌) on each worn item
|
||||||
- Removes item from outfit via GET request with updated `objects[]` params
|
- Removes item from outfit via GET request with updated `objects[]` params
|
||||||
- Button hidden by default, appears on hover/focus
|
- Button hidden by default, appears on hover/focus
|
||||||
- Uses `outfit.without_item(item)` helper to generate new state
|
- Uses `outfit.without_item(item)` helper to generate new state
|
||||||
|
|
||||||
**State Management** ([outfits_helper.rb:68-90](app/helpers/outfits_helper.rb#L68-L90))
|
**State Management** ([wardrobe_helper.rb:68-90](app/helpers/wardrobe_helper.rb#L68-L90))
|
||||||
- All state lives in URL params (no client-side state)
|
- All state lives in URL params (no client-side state)
|
||||||
- `outfit_state_params` helper generates hidden fields for outfit state
|
- `outfit_state_params` helper generates hidden fields for outfit state
|
||||||
- Preserves: species, color, worn items (`objects[]`), search query (`q[...]`)
|
- Preserves: species, color, worn items (`objects[]`), search query (`q[...]`)
|
||||||
|
|
@ -80,11 +80,11 @@ Replace the complex React outfit editor (`app/javascript/wardrobe-2020/`) with a
|
||||||
|
|
||||||
#### Supporting Helpers
|
#### Supporting Helpers
|
||||||
|
|
||||||
**`outfit_items_by_zone`** ([outfits_helper.rb:96-167](app/helpers/outfits_helper.rb#L96-L167))
|
**`outfit_items_by_zone`** ([wardrobe_helper.rb:96-167](app/helpers/wardrobe_helper.rb#L96-L167))
|
||||||
- Core grouping logic for items by zone
|
- Core grouping logic for items by zone
|
||||||
- Matches wardrobe-2020's `getZonesAndItems` behavior
|
- Matches wardrobe-2020's `getZonesAndItems` behavior
|
||||||
- Returns array of `{zone_id:, zone_label:, items:}` hashes
|
- Returns array of `{zone_id:, zone_label:, items:}` hashes
|
||||||
- Extensively tested in [outfits_helper_spec.rb](spec/helpers/outfits_helper_spec.rb)
|
- Extensively tested in [wardrobe_helper_spec.rb](spec/helpers/wardrobe_helper_spec.rb)
|
||||||
|
|
||||||
**`outfit_viewer`** ([outfits_helper.rb:86-89](app/helpers/outfits_helper.rb#L86-L89))
|
**`outfit_viewer`** ([outfits_helper.rb:86-89](app/helpers/outfits_helper.rb#L86-L89))
|
||||||
- Renders `<outfit-viewer>` web component
|
- Renders `<outfit-viewer>` web component
|
||||||
|
|
@ -969,12 +969,13 @@ This section documents ALL features in the React-based Wardrobe 2020 for referen
|
||||||
- [Customization Architecture](./customization-architecture.md) - Data model deep dive
|
- [Customization Architecture](./customization-architecture.md) - Data model deep dive
|
||||||
|
|
||||||
**Wardrobe V2 Files:**
|
**Wardrobe V2 Files:**
|
||||||
- Controller: [app/controllers/outfits_controller.rb](../app/controllers/outfits_controller.rb)
|
- Controller: [app/controllers/wardrobe_controller.rb](../app/controllers/wardrobe_controller.rb)
|
||||||
- View: [app/views/outfits/new_v2.html.haml](../app/views/outfits/new_v2.html.haml)
|
- View: [app/views/wardrobe/show.html.haml](../app/views/wardrobe/show.html.haml)
|
||||||
- Helpers: [app/helpers/outfits_helper.rb](../app/helpers/outfits_helper.rb)
|
- Search results partial: [app/views/wardrobe/_search_results.html.haml](../app/views/wardrobe/_search_results.html.haml)
|
||||||
- Tests: [spec/helpers/outfits_helper_spec.rb](../spec/helpers/outfits_helper_spec.rb)
|
- Helpers: [app/helpers/wardrobe_helper.rb](../app/helpers/wardrobe_helper.rb)
|
||||||
- Styles: [app/assets/stylesheets/outfits/new_v2.css](../app/assets/stylesheets/outfits/new_v2.css)
|
- Tests: [spec/helpers/wardrobe_helper_spec.rb](../spec/helpers/wardrobe_helper_spec.rb)
|
||||||
- JavaScript: [app/assets/javascripts/outfits/new_v2.js](../app/assets/javascripts/outfits/new_v2.js)
|
- Styles: [app/assets/stylesheets/wardrobe/show.css](../app/assets/stylesheets/wardrobe/show.css)
|
||||||
|
- JavaScript: [app/assets/javascripts/wardrobe/show.js](../app/assets/javascripts/wardrobe/show.js)
|
||||||
- Web Components:
|
- Web Components:
|
||||||
- [app/assets/javascripts/species-color-picker.js](../app/assets/javascripts/species-color-picker.js)
|
- [app/assets/javascripts/species-color-picker.js](../app/assets/javascripts/species-color-picker.js)
|
||||||
- [app/assets/javascripts/outfit-viewer.js](../app/assets/javascripts/outfit-viewer.js)
|
- [app/assets/javascripts/outfit-viewer.js](../app/assets/javascripts/outfit-viewer.js)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
require_relative '../rails_helper'
|
require_relative '../rails_helper'
|
||||||
|
|
||||||
RSpec.describe OutfitsHelper, type: :helper do
|
RSpec.describe WardrobeHelper, type: :helper do
|
||||||
fixtures :zones, :colors, :species, :pet_types
|
fixtures :zones, :colors, :species, :pet_types
|
||||||
|
|
||||||
# Use the Blue Acara's body_id throughout tests
|
# Use the Blue Acara's body_id throughout tests
|
||||||
Loading…
Reference in a new issue