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' => %w[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' }.freeze
- STYLE_LABELS =
Human-readable labels for style options
{ 'round-bars' => 'Round Bars', 'square-bars' => 'Square 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' }, # 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', # 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
260 261 262 263 264 265 266 267 268 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 |
# File 'app/models/concerns/towel_warmer_filter_slugs.rb', line 260 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 if filter_type == :size return size_filter_counts(scope_without_this_filter) end # 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 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 end |
.build_filter_path(ransack_params) ⇒ String
Build URL path from Ransack parameters
175 176 177 178 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 |
# File 'app/models/concerns/towel_warmer_filter_slugs.rb', line 175 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
214 215 216 217 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 |
# File 'app/models/concerns/towel_warmer_filter_slugs.rb', line 214 def (filter_type, available_counts: nil) = case filter_type when :finish FINISH_SLUGS.map { |slug, db_value| [db_value, slug] } 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
154 155 156 157 158 159 160 161 162 163 |
# File 'app/models/concerns/towel_warmer_filter_slugs.rb', line 154 def parse_filter_segments(filter_path) return {} if filter_path.blank? segments = filter_path.to_s.split('/').map(&:strip).reject(&: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
322 323 324 325 326 327 328 329 330 331 |
# File 'app/models/concerns/towel_warmer_filter_slugs.rb', line 322 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.each_with_object({}) do |slug, counts| counts[slug] = raw_counts[slug] || 0 end end |