Module: TowelWarmerFilterSlugs
- Extended by:
- ActiveSupport::Concern
- Included in:
- Www::TowelWarmerFiltersComponent, Www::TowelWarmersController
- Defined in:
- app/models/concerns/towel_warmer_filter_slugs.rb
Overview
TowelWarmerFilterSlugs
Provides URL slug mappings for SEO-friendly towel warmer filter URLs.
Maps human-readable slugs to database values for Ransack filtering.
URL Structure: /towel-warmer/finish/style/connection/mounting/size
Examples:
/towel-warmer/brushed-gold
/towel-warmer/brushed-gold/round-bars
/towel-warmer/brushed-gold/round-bars/hardwired
/towel-warmer/matte-black/square-bars/plug-in/wall-mounted
/towel-warmer/compact
/towel-warmer/large
Constant Summary collapse
- FINISH_SLUGS =
Finish slugs -> database values
{ 'brushed-stainless-steel' => 'Brushed Stainless Steel', 'polished-stainless-steel' => 'Polished Stainless Steel', 'matte-black' => 'Matte Black', 'brushed-gold' => 'Brushed Gold', 'polished-gold' => 'Polished Gold' }.freeze
- COMPOSITE_FINISH_SLUGS =
Composite finish slugs that match multiple finishes (OR logic).
These appear in SLUG_TO_RANSACK but not in FINISH_SLUGS so individual
finish dropdowns/counts still work on a per-finish basis. { 'gold' => ['Brushed Gold', 'Polished Gold'] }.freeze
- STYLE_SLUGS =
Style (bar shape) slugs -> database values
Keys are URL slugs, values are database values { 'round-bars' => 'Round', 'square-bars' => 'Square', 'flat-bars' => 'Flat' }.freeze
- STYLE_LABELS =
Human-readable labels for style options
{ 'round-bars' => 'Round Bars', 'square-bars' => 'Square Bars', 'flat-bars' => 'Flat Bars' }.freeze
- CONNECTION_SLUGS =
Connection method slugs -> database values
{ 'hardwired' => 'Hardwired', 'plug-in' => 'Plug-in', 'plug-in-or-hardwired' => 'Plug-in or Hardwired' }.freeze
- CONNECTION_LABELS =
Human-readable labels for connection options
{ 'hardwired' => 'Hardwired', 'plug-in' => 'Plug-in', 'plug-in-or-hardwired' => 'Plug-in or Hardwired' }.freeze
- MOUNTING_SLUGS =
Mounting method slugs -> database values
Note: Database uses "Wall Mounted" (no hyphen) and "Free standing" (space) { 'wall-mounted' => 'Wall Mounted', 'freestanding' => 'Freestanding' }.freeze
- MOUNTING_LABELS =
Human-readable labels for mounting options
{ 'wall-mounted' => 'Wall Mounted', 'freestanding' => 'Freestanding' }.freeze
- SIZE_SLUGS =
Size tier slugs — map to size_classification spec values (set by towel_warmer_size_classification.rb)
Classification is based on physical height: Compact ≤ 30", Standard 30–40", Large > 40" %w[compact standard large].freeze
- SIZE_LABELS =
Slug -> size_classification spec value (stored in product_specifications JSONB)
{ 'compact' => 'Compact', 'standard' => 'Standard', 'large' => 'Large' }.freeze
- SLUG_TO_RANSACK =
Combined mapping of all slugs to their Ransack parameters
Used by the controller to parse URL segments { # Finishes 'brushed-stainless-steel' => { spec_finish_eq: 'Brushed Stainless Steel' }, 'polished-stainless-steel' => { spec_finish_eq: 'Polished Stainless Steel' }, 'matte-black' => { spec_finish_eq: 'Matte Black' }, 'brushed-gold' => { spec_finish_eq: 'Brushed Gold' }, 'polished-gold' => { spec_finish_eq: 'Polished Gold' }, # Composite finish: matches both Brushed Gold and Polished Gold 'gold' => { spec_finish_in: COMPOSITE_FINISH_SLUGS['gold'] }, # Styles 'round-bars' => { spec_bar_shape_eq: 'Round' }, 'square-bars' => { spec_bar_shape_eq: 'Square' }, 'flat-bars' => { spec_bar_shape_eq: 'Flat' }, # Connection 'hardwired' => { spec_connection_method_eq: 'Hardwired' }, 'plug-in' => { spec_connection_method_eq: 'Plug-in' }, 'plug-in-or-hardwired' => { spec_connection_method_eq: 'Plug-in or Hardwired' }, # Mounting - Note: database uses "Wall Mounted" (no hyphen) 'wall-mounted' => { spec_mounting_method_eq: 'Wall Mounted' }, 'freestanding' => { spec_mounting_method_eq: 'Freestanding' }, # Size tiers - range-based (uses custom ransacker) 'compact' => { spec_size_eq: 'compact' }, 'standard' => { spec_size_eq: 'standard' }, 'large' => { spec_size_eq: 'large' } }.freeze
- RANSACK_TO_SLUG =
Reverse mapping: database values to slugs (for URL generation)
{ # Finishes 'Brushed Stainless Steel' => 'brushed-stainless-steel', 'Polished Stainless Steel' => 'polished-stainless-steel', 'Matte Black' => 'matte-black', 'Brushed Gold' => 'brushed-gold', 'Polished Gold' => 'polished-gold', # Styles 'Round' => 'round-bars', 'Square' => 'square-bars', 'Flat' => 'flat-bars', # Connection 'Hardwired' => 'hardwired', 'Plug-in' => 'plug-in', 'Plug-in or Hardwired' => 'plug-in-or-hardwired', # Mounting 'Wall Mounted' => 'wall-mounted', 'Freestanding' => 'freestanding', # Size tiers (slugs map to themselves) 'compact' => 'compact', 'standard' => 'standard', 'large' => 'large' }.freeze
Class Method Summary collapse
-
.available_filter_counts(base_scope, filter_type, current_filters = {}) ⇒ Hash
Calculate available filter counts for a given filter type based on current results Uses a single GROUP BY query per filter type to avoid N+1 queries.
-
.build_filter_path(ransack_params) ⇒ String
Build URL path from Ransack parameters.
-
.filter_options_with_slugs(filter_type, available_counts: nil) ⇒ Array<Array>
Generate select options with slug values for the filter component.
-
.parse_filter_segments(filter_path) ⇒ Hash
Parse URL filter segments into Ransack parameters.
-
.size_filter_counts(scope) ⇒ Hash
Calculate counts for each size tier using a single GROUP BY on size_classification spec.
Class Method Details
.available_filter_counts(base_scope, filter_type, current_filters = {}) ⇒ Hash
Calculate available filter counts for a given filter type based on current results
Uses a single GROUP BY query per filter type to avoid N+1 queries
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 |
# File 'app/models/concerns/towel_warmer_filter_slugs.rb', line 269 def available_filter_counts(base_scope, filter_type, current_filters = {}) # Build a scope without the current filter type applied keys_to_strip = filter_keys_for_type(filter_type) other_filters = current_filters.reject { |k, _| keys_to_strip.include?(k) } scope_without_this_filter = base_scope.ransack(other_filters).result # Handle size specially since it's range-based return size_filter_counts(scope_without_this_filter) if filter_type == :size # Get the JSONB field path for this filter type jsonb_field = case filter_type when :finish then "product_specifications -> 'finish' ->> 'raw'" when :style then "product_specifications -> 'bar_shape' ->> 'raw'" when :connection then "product_specifications -> 'connection_method' ->> 'raw'" when :mounting then "product_specifications -> 'mounting_method' ->> 'raw'" else return {} end # Get the slug-to-value mapping for this filter type slug_hash = case filter_type when :finish then FINISH_SLUGS when :style then STYLE_SLUGS when :connection then CONNECTION_SLUGS when :mounting then MOUNTING_SLUGS else {} end # Single GROUP BY query to get all counts raw_counts = scope_without_this_filter .group(Arel.sql(jsonb_field)) .count # Map database values to slugs counts = slug_hash.each_with_object({}) do |(slug, db_value), counts| # Direct match for the value count = raw_counts[db_value] || 0 # For connection type, also add dual-connection products to Plug-in and Hardwired counts if filter_type == :connection && %w[Plug-in Hardwired].include?(db_value) dual_count = raw_counts['Plug-in or Hardwired'] || 0 count += dual_count end # For mounting, handle partial matches (e.g., "Wall Mounted (20\" x 20 1/2\")" matches "Wall Mounted") if filter_type == :mounting raw_counts.each do |raw_value, raw_count| next if raw_value == db_value # Already counted count += raw_count if raw_value.to_s.start_with?(db_value) end end counts[slug] = count end # Composite "gold" combines both gold finishes so its filter option # reflects a non-zero count (and isn't disabled). counts['gold'] = counts.values_at('brushed-gold', 'polished-gold').compact.sum if filter_type == :finish counts end |
.build_filter_path(ransack_params) ⇒ String
Build URL path from Ransack parameters
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'app/models/concerns/towel_warmer_filter_slugs.rb', line 179 def build_filter_path(ransack_params) return '' if ransack_params.blank? segments = [] # Order matters for consistent URLs: finish, style, connection, mounting, size if (finishes = ransack_params[:spec_finish_in]) composite_slug = COMPOSITE_FINISH_SLUGS.find { |_slug, vals| vals.sort == Array(finishes).sort }&.first segments << composite_slug if composite_slug elsif (finish = ransack_params[:spec_finish_eq]) segments << RANSACK_TO_SLUG[finish] if RANSACK_TO_SLUG[finish] end if (style = ransack_params[:spec_bar_shape_eq]) segments << RANSACK_TO_SLUG[style] if RANSACK_TO_SLUG[style] end if (connection = ransack_params[:spec_connection_method_eq]) segments << RANSACK_TO_SLUG[connection] if RANSACK_TO_SLUG[connection] end if (mounting = ransack_params[:spec_mounting_method_eq]) segments << RANSACK_TO_SLUG[mounting] if RANSACK_TO_SLUG[mounting] end if (size = ransack_params[:spec_size_eq]) segments << RANSACK_TO_SLUG[size] if RANSACK_TO_SLUG[size] end segments.join('/') end |
.filter_options_with_slugs(filter_type, available_counts: nil) ⇒ Array<Array>
Generate select options with slug values for the filter component
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 |
# File 'app/models/concerns/towel_warmer_filter_slugs.rb', line 218 def (filter_type, available_counts: nil) = case filter_type when :finish finish_opts = FINISH_SLUGS.map { |slug, db_value| [db_value, slug] } # Surface the composite "Gold" filter (matches both gold finishes) # just ahead of the individual gold options. gold_at = finish_opts.index { |_label, slug| slug == 'brushed-gold' } finish_opts.insert(gold_at, ['Gold', 'gold']) if gold_at finish_opts when :style STYLE_LABELS.map { |slug, label| [label, slug] } when :connection CONNECTION_LABELS.map { |slug, label| [label, slug] } when :mounting MOUNTING_LABELS.map { |slug, label| [label, slug] } when :size SIZE_LABELS.map { |slug, label| [label, slug] } else [] end # Add disabled status if counts are provided if available_counts = .map do |label, slug| count = available_counts[slug] || 0 [label, slug, { disabled: count.zero? }] end end # Add "All" option at the beginning all_label = case filter_type when :finish then 'All Finishes' when :style then 'All Styles' when :connection then 'All Connection Types' when :mounting then 'All Mounting Types' when :size then 'All Sizes' else 'All' end [[all_label, '']] + end |
.parse_filter_segments(filter_path) ⇒ Hash
Parse URL filter segments into Ransack parameters
158 159 160 161 162 163 164 165 166 167 |
# File 'app/models/concerns/towel_warmer_filter_slugs.rb', line 158 def parse_filter_segments(filter_path) return {} if filter_path.blank? segments = filter_path.to_s.split('/').map(&:strip).compact_blank segments.each_with_object({}) do |segment, params| if (ransack_params = SLUG_TO_RANSACK[segment]) params.merge!(ransack_params) end end end |
.size_filter_counts(scope) ⇒ Hash
Calculate counts for each size tier using a single GROUP BY on size_classification spec
336 337 338 339 340 341 342 343 344 345 |
# File 'app/models/concerns/towel_warmer_filter_slugs.rb', line 336 def size_filter_counts(scope) raw_counts = scope .where.not("product_specifications -> 'size_classification' ->> 'raw' IS NULL") .group(Arel.sql("lower(product_specifications -> 'size_classification' ->> 'raw')")) .count SIZE_SLUGS.index_with do |slug| raw_counts[slug] || 0 end end |