[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
|
||||
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
|
||||
@outfit = Outfit.find(params[:id])
|
||||
|
|
@ -176,51 +129,5 @@ class OutfitsController < ApplicationController
|
|||
:status => :bad_request
|
||||
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
|
||||
|
||||
|
|
|
|||
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
|
||||
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(...)
|
||||
render partial: "outfit_viewer",
|
||||
locals: parse_outfit_viewer_options(...)
|
||||
|
|
@ -99,133 +75,8 @@ module OutfitsHelper
|
|||
locals: parse_outfit_viewer_options(...)
|
||||
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
|
||||
|
||||
def parse_outfit_viewer_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'}
|
||||
= stylesheet_link_tag "application/hanger-spinner"
|
||||
= 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 "idiomorph", async: true
|
||||
= javascript_include_tag "outfit-viewer", 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
|
||||
%meta{name: 'outfit-viewer-morph-mode', value: 'full-page'}
|
||||
%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.
|
||||
# Should we refactor the controller/view structure here?
|
||||
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/v2', to: 'wardrobe#show', as: :wardrobe_v2
|
||||
get '/start/:color_name/:species_name' => 'outfits#start'
|
||||
|
||||
# The outfits users have created!
|
||||
|
|
|
|||
|
|
@ -13,34 +13,34 @@ Replace the complex React outfit editor (`app/javascript/wardrobe-2020/`) with a
|
|||
|
||||
## 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
|
||||
|
||||
#### Core Infrastructure
|
||||
|
||||
**Route & Controller** ([outfits_controller.rb:80-115](app/controllers/outfits_controller.rb#L80-L115))
|
||||
- `GET /outfits/new/v2` - Main wardrobe endpoint
|
||||
**Route & Controller** ([wardrobe_controller.rb](app/controllers/wardrobe_controller.rb))
|
||||
- `GET /wardrobe/v2` - Main wardrobe endpoint
|
||||
- Takes URL params: `species`, `color`, `objects[]` (item IDs)
|
||||
- Returns full HTML page (no layout, designed to work standalone)
|
||||
- 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)
|
||||
- Responsive: stacks vertically on mobile (< 800px)
|
||||
- Uses existing `outfit_viewer` partial for rendering
|
||||
- Custom CSS in [outfits/new_v2.css](app/assets/stylesheets/outfits/new_v2.css)
|
||||
- Minimal JavaScript in [outfits/new_v2.js](app/assets/javascripts/outfits/new_v2.js)
|
||||
- Custom CSS in [wardrobe/show.css](app/assets/stylesheets/wardrobe/show.css)
|
||||
- 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
|
||||
- Floats over preview area (bottom), reveals on hover/focus
|
||||
- Progressive enhancement: submit button appears if JS slow/disabled
|
||||
- Auto-submits form on change when JS loaded
|
||||
- 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.)
|
||||
- Smart multi-zone simplification: hides redundant single-item zones
|
||||
- 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)
|
||||
- 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
|
||||
- Auto-filters to items that fit current pet (species + color + alt_style)
|
||||
- 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.)
|
||||
- "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
|
||||
- Adds item to outfit via GET request with updated `objects[]` params
|
||||
- Button hidden by default, appears on hover/focus
|
||||
- Preserves search state when adding items
|
||||
- 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
|
||||
- Removes item from outfit via GET request with updated `objects[]` params
|
||||
- Button hidden by default, appears on hover/focus
|
||||
- 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)
|
||||
- `outfit_state_params` helper generates hidden fields for outfit state
|
||||
- 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
|
||||
|
||||
**`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
|
||||
- Matches wardrobe-2020's `getZonesAndItems` behavior
|
||||
- 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))
|
||||
- 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
|
||||
|
||||
**Wardrobe V2 Files:**
|
||||
- Controller: [app/controllers/outfits_controller.rb](../app/controllers/outfits_controller.rb)
|
||||
- View: [app/views/outfits/new_v2.html.haml](../app/views/outfits/new_v2.html.haml)
|
||||
- Helpers: [app/helpers/outfits_helper.rb](../app/helpers/outfits_helper.rb)
|
||||
- Tests: [spec/helpers/outfits_helper_spec.rb](../spec/helpers/outfits_helper_spec.rb)
|
||||
- Styles: [app/assets/stylesheets/outfits/new_v2.css](../app/assets/stylesheets/outfits/new_v2.css)
|
||||
- JavaScript: [app/assets/javascripts/outfits/new_v2.js](../app/assets/javascripts/outfits/new_v2.js)
|
||||
- Controller: [app/controllers/wardrobe_controller.rb](../app/controllers/wardrobe_controller.rb)
|
||||
- View: [app/views/wardrobe/show.html.haml](../app/views/wardrobe/show.html.haml)
|
||||
- Search results partial: [app/views/wardrobe/_search_results.html.haml](../app/views/wardrobe/_search_results.html.haml)
|
||||
- Helpers: [app/helpers/wardrobe_helper.rb](../app/helpers/wardrobe_helper.rb)
|
||||
- Tests: [spec/helpers/wardrobe_helper_spec.rb](../spec/helpers/wardrobe_helper_spec.rb)
|
||||
- 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:
|
||||
- [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)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
require_relative '../rails_helper'
|
||||
|
||||
RSpec.describe OutfitsHelper, type: :helper do
|
||||
RSpec.describe WardrobeHelper, type: :helper do
|
||||
fixtures :zones, :colors, :species, :pet_types
|
||||
|
||||
# Use the Blue Acara's body_id throughout tests
|
||||
Loading…
Reference in a new issue