dugway 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.bundle/config +2 -0
  3. data/.github/workflows/main.yml +1 -1
  4. data/.gitignore +1 -0
  5. data/README.md +32 -7
  6. data/dugway.gemspec +11 -9
  7. data/lib/dugway/application.rb +5 -3
  8. data/lib/dugway/assets/big_cartel_logo.svg +4 -0
  9. data/lib/dugway/cli/build.rb +7 -1
  10. data/lib/dugway/cli/server.rb +69 -9
  11. data/lib/dugway/cli/templates/source/settings.json +8 -0
  12. data/lib/dugway/cli/validate.rb +9 -2
  13. data/lib/dugway/controller.rb +5 -1
  14. data/lib/dugway/liquid/drops/account_drop.rb +4 -0
  15. data/lib/dugway/liquid/drops/artists_drop.rb +12 -0
  16. data/lib/dugway/liquid/drops/base_drop.rb +27 -2
  17. data/lib/dugway/liquid/drops/categories_drop.rb +12 -0
  18. data/lib/dugway/liquid/drops/features_drop.rb +144 -0
  19. data/lib/dugway/liquid/drops/pages_drop.rb +30 -2
  20. data/lib/dugway/liquid/drops/product_drop.rb +11 -0
  21. data/lib/dugway/liquid/drops/products_drop.rb +39 -0
  22. data/lib/dugway/liquid/drops/theme_drop.rb +52 -6
  23. data/lib/dugway/liquid/drops/translations_drop.rb +122 -0
  24. data/lib/dugway/liquid/filters/core_filters.rb +169 -7
  25. data/lib/dugway/liquid/filters/font_filters.rb +1 -0
  26. data/lib/dugway/liquid/filters/util_filters.rb +1 -10
  27. data/lib/dugway/liquid/tags/get.rb +6 -6
  28. data/lib/dugway/liquid/tags/paginate.rb +61 -11
  29. data/lib/dugway/liquifier.rb +44 -8
  30. data/lib/dugway/store.rb +46 -3
  31. data/lib/dugway/theme.rb +151 -15
  32. data/lib/dugway/version.rb +1 -1
  33. data/lib/dugway.rb +55 -2
  34. data/locales/storefront.de.yml +81 -0
  35. data/locales/storefront.en-CA.yml +81 -0
  36. data/locales/storefront.en-GB.yml +81 -0
  37. data/locales/storefront.en-US.yml +81 -0
  38. data/locales/storefront.es-ES.yml +81 -0
  39. data/locales/storefront.es-MX.yml +81 -0
  40. data/locales/storefront.fr-CA.yml +81 -0
  41. data/locales/storefront.fr-FR.yml +81 -0
  42. data/locales/storefront.id.yml +81 -0
  43. data/locales/storefront.it.yml +81 -0
  44. data/locales/storefront.ja.yml +81 -0
  45. data/locales/storefront.ko.yml +81 -0
  46. data/locales/storefront.nl.yml +81 -0
  47. data/locales/storefront.pl.yml +81 -0
  48. data/locales/storefront.pt-BR.yml +81 -0
  49. data/locales/storefront.pt-PT.yml +81 -0
  50. data/locales/storefront.ro.yml +81 -0
  51. data/locales/storefront.sv.yml +81 -0
  52. data/locales/storefront.tr.yml +81 -0
  53. data/locales/storefront.zh-CN.yml +81 -0
  54. data/locales/storefront.zh-TW.yml +81 -0
  55. data/log/dugway.log +1 -0
  56. data/mise.toml +2 -0
  57. data/spec/features/page_rendering_spec.rb +4 -4
  58. data/spec/fixtures/theme/layout.html +2 -0
  59. data/spec/fixtures/theme/settings.json +6 -0
  60. data/spec/spec_helper.rb +7 -0
  61. data/spec/units/dugway/liquid/drops/features_drop_spec.rb +182 -0
  62. data/spec/units/dugway/liquid/drops/pages_drop_spec.rb +186 -7
  63. data/spec/units/dugway/liquid/drops/product_drop_spec.rb +17 -0
  64. data/spec/units/dugway/liquid/drops/theme_drop_spec.rb +45 -0
  65. data/spec/units/dugway/liquid/drops/translations_drop_spec.rb +292 -0
  66. data/spec/units/dugway/liquid/filters/core_filters_spec.rb +301 -3
  67. data/spec/units/dugway/store_spec.rb +55 -0
  68. data/spec/units/dugway/theme_spec.rb +543 -1
  69. metadata +84 -25
@@ -40,21 +40,89 @@ module Dugway
40
40
  when 'sign' then money_with_sign(amount)
41
41
  when 'code' then money_with_code(amount)
42
42
  when 'sign_and_code' then money_with_sign_and_code(amount)
43
- else number_to_currency(amount, :unit => '').strip
43
+ when 'rounded' then
44
+ begin
45
+ number_to_currency(amount.ceil, { :unit => '', :format => '%n', :precision => 0 }).to_s.strip
46
+ rescue FrozenError => e
47
+ sprintf('%.0f', amount.ceil.to_f)
48
+ end
49
+ when 'sign_rounded' then money_with_sign(amount.ceil, 0)
50
+ when 'code_rounded' then money_with_code(amount.ceil, 0)
51
+ else
52
+ begin
53
+ number_to_currency(amount, { :unit => '', :format => '%n' }).to_s.strip
54
+ rescue FrozenError => e
55
+ # Fallback for frozen hash issues in newer Ruby versions
56
+ sprintf('%.2f', amount.to_f)
57
+ end
58
+ end
59
+ end
60
+
61
+ def product_price(product, money_format = nil, range_format = nil)
62
+ return unless product
63
+
64
+ if product['variable_pricing']
65
+ case range_format
66
+ when 'min_only'
67
+ money(product['min_price'], money_format)
68
+ when 'max_only'
69
+ money(product['max_price'], money_format)
70
+ else # default, nil, or any other value
71
+ format_price_range(product['min_price'], product['max_price'], money_format)
72
+ end
73
+ else
74
+ money(product['default_price'] || product['price'], money_format)
44
75
  end
45
76
  end
46
77
 
47
- def money_with_sign(amount)
48
- unit = I18n.translate('number.currency.format.unit')
49
- number_to_currency(amount).gsub(unit, %{<span class="currency_sign">#{ HTMLEntities.new.encode(unit, :named) }</span>})
78
+ def money_with_sign(amount, precision = nil)
79
+ begin
80
+ unit = I18n.translate('number.currency.format.unit')
81
+ options = { :unit => "<span class=\"currency_sign\">#{HTMLEntities.new.encode(unit, :named)}</span>" }
82
+ options[:precision] = precision if precision
83
+ number_to_currency(amount, options)
84
+ rescue FrozenError => e
85
+ # Fallback for frozen hash issues
86
+ format_string = precision == 0 ? '%.0f' : '%.2f'
87
+ "<span class=\"currency_sign\">$</span>#{sprintf(format_string, amount.to_f)}"
88
+ end
50
89
  end
51
90
 
52
- def money_with_code(amount)
53
- %{#{ money(amount) } <span class="currency_code">#{ currency['code'] }</span>}
91
+ def money_with_code(amount, precision = nil)
92
+ begin
93
+ # Format number without currency unit, respecting locale's number formatting rules
94
+ number_locale = normalize_currency_locale_for_formatting(currency['locale'])
95
+ options = { :unit => '', :format => '%n', :locale => number_locale }
96
+ options[:precision] = precision if precision
97
+ formatted_number = number_to_currency(amount, options).to_s.strip
98
+
99
+ # Use locale-specific format string to ensure proper currency code placement
100
+ # Keep original locale for bigcartel-currency-locales gem compatibility
101
+ format_string = I18n.translate('number.currency.format.format',
102
+ :locale => currency['locale'],
103
+ :default => '%u%n')
104
+
105
+ code_span = "<span class=\"currency_code\">#{currency['code']}</span>"
106
+ format_string.gsub('%n', formatted_number).gsub('%u', code_span)
107
+ rescue => e
108
+ # Fallback for frozen hash or I18n issues
109
+ format_string = precision == 0 ? '%.0f' : '%.2f'
110
+ "<span class=\"currency_code\">#{currency['code']}</span>#{sprintf(format_string, amount.to_f)}"
111
+ end
54
112
  end
55
113
 
56
114
  def money_with_sign_and_code(amount)
57
- %{#{ money_with_sign(amount) } <span class="currency_code">#{ currency['code'] }</span>}
115
+ %{#{ money_with_sign(amount).to_s } <span class="currency_code">#{ currency['code'] }</span>}
116
+ end
117
+
118
+ # Liquid 5.x compatibility: ensure proper JSON output
119
+ def to_json(input)
120
+ # Handle Liquid Drops specially - use as_json for serialization
121
+ if input.respond_to?(:as_json)
122
+ JSON.generate(input.as_json)
123
+ else
124
+ JSON.generate(input)
125
+ end
58
126
  end
59
127
 
60
128
  # Shipping Filters
@@ -137,10 +205,104 @@ module Dugway
137
205
 
138
206
  private
139
207
 
208
+ def currency
209
+ @context.registers[:currency]
210
+ end
211
+
140
212
  def contact_tab_index
141
213
  @contact_tab_index ||= 0
142
214
  @contact_tab_index += 1
143
215
  end
216
+
217
+ def format_price_range(min_price, max_price, money_format)
218
+ case money_format
219
+ when "sign"
220
+ format_range_with_sign(min_price, max_price)
221
+ when "code"
222
+ format_range_with_code(min_price, max_price)
223
+ when "sign_rounded"
224
+ format_range_with_sign(min_price.ceil, max_price.ceil, 0)
225
+ when "code_rounded"
226
+ format_range_with_code(min_price.ceil, max_price.ceil, 0)
227
+ when "rounded"
228
+ format_range_plain(min_price.ceil, max_price.ceil, 0)
229
+ else
230
+ format_range_plain(min_price, max_price)
231
+ end
232
+ end
233
+
234
+ def format_range_with_sign(min_price, max_price, precision = nil)
235
+ # Sign on both prices for clarity
236
+ # US: $10.00 - $20.00 | FR: 10,00 € - 20,00 €
237
+ min_formatted = money_with_sign(min_price, precision)
238
+ max_formatted = money_with_sign(max_price, precision)
239
+ "#{min_formatted} - #{max_formatted}"
240
+ end
241
+
242
+ def format_range_with_code(min_price, max_price, precision = nil)
243
+ # Single code in locale-appropriate position
244
+ # US: $10.00 - 20.00 USD | FR: 10,00 - 20,00 EUR
245
+ code_span = "<span class=\"currency_code\">#{currency['code']}</span>"
246
+ range_string = build_number_range(min_price, max_price, precision)
247
+ apply_currency_format(range_string, code_span)
248
+ end
249
+
250
+ def format_range_plain(min_price, max_price, precision = nil)
251
+ # No currency indicators
252
+ begin
253
+ options = { :unit => '', :format => '%n' }
254
+ options[:precision] = precision if precision
255
+ min_formatted = number_to_currency(min_price, options).to_s.strip
256
+ max_formatted = number_to_currency(max_price, options).to_s.strip
257
+ "#{min_formatted} - #{max_formatted}"
258
+ rescue FrozenError => e
259
+ # Fallback for frozen hash issues
260
+ format_string = precision == 0 ? '%.0f' : '%.2f'
261
+ "#{sprintf(format_string, min_price.to_f)} - #{sprintf(format_string, max_price.to_f)}"
262
+ end
263
+ end
264
+
265
+ def build_number_range(min_price, max_price, precision = nil)
266
+ min_formatted = format_number_plain(min_price, precision)
267
+ max_formatted = format_number_plain(max_price, precision)
268
+ "#{min_formatted} - #{max_formatted}"
269
+ end
270
+
271
+ def format_number_plain(amount, precision = nil)
272
+ begin
273
+ number_locale = normalize_currency_locale_for_formatting(currency['locale'])
274
+ options = { :unit => '', :format => '%n', :locale => number_locale }
275
+ options[:precision] = precision if precision
276
+ number_to_currency(amount, options).to_s.strip
277
+ rescue FrozenError => e
278
+ format_string = precision == 0 ? '%.0f' : '%.2f'
279
+ sprintf(format_string, amount.to_f)
280
+ end
281
+ end
282
+
283
+ def apply_currency_format(range_string, currency_span)
284
+ # Use I18n to get locale-specific format like storefront does
285
+ begin
286
+ # Keep original locale for bigcartel-currency-locales gem compatibility
287
+ format_string = I18n.translate('number.currency.format.format',
288
+ locale: currency['locale'],
289
+ default: '%u%n')
290
+ format_string.gsub('%n', range_string).gsub('%u', currency_span)
291
+ rescue
292
+ # Fallback if I18n fails
293
+ "#{range_string} #{currency_span}"
294
+ end
295
+ end
296
+
297
+ def normalize_currency_locale_for_formatting(locale)
298
+ # Handle invalid 'eu' locale from production system for number formatting only
299
+ case locale
300
+ when 'eu'
301
+ 'de-DE' # Use German locale for EUR number formatting (1.200,99 style)
302
+ else
303
+ locale
304
+ end
305
+ end
144
306
  end
145
307
  end
146
308
  end
@@ -2,6 +2,7 @@ module Dugway
2
2
  module Filters
3
3
  module FontFilters
4
4
  def font_family(font_name)
5
+ return '' if font_name.nil? || font_name.to_s.strip.empty?
5
6
  ThemeFont.find_family_by_name(font_name)
6
7
  end
7
8
  end
@@ -1,15 +1,6 @@
1
1
  module Dugway
2
2
  module Filters
3
3
  module UtilFilters
4
- private
5
-
6
- # Registered Vars
7
-
8
- def currency
9
- @context.registers[:currency]
10
- end
11
-
12
- # Mimics
13
4
 
14
5
  DEFAULT_CURRENCY_VALUES = { :format => "%u%n", :negative_format => "-%u%n", :unit => "$", :separator => ".", :delimiter => ",", :precision => 2, :significant => false, :strip_insignificant_zeros => false }
15
6
 
@@ -126,7 +117,7 @@ module Dugway
126
117
 
127
118
  def content_tag(type, content, options = {})
128
119
  result = tag(type, options, true)
129
- result += content
120
+ result += content.to_s
130
121
  result += "</#{type}>"
131
122
  end
132
123
 
@@ -5,16 +5,16 @@ module Dugway
5
5
 
6
6
  def initialize(tag_name, markup, tokens)
7
7
  if markup =~ Syntax
8
- @number_to_get = $1.present? ? $2 : nil
9
- @variable_name = $3
10
- @collection_name = $4
8
+ @number_to_get = Regexp.last_match(1).present? ? Regexp.last_match(2) : nil
9
+ @variable_name = Regexp.last_match(3)
10
+ @collection_name = Regexp.last_match(4)
11
11
 
12
12
  @attributes = {}
13
13
  markup.scan(Liquid::TagAttributes) { |key, value|
14
14
  @attributes[key] = value
15
15
  }
16
16
  else
17
- raise SyntaxError.new("Syntax Error in tag 'get' - Valid syntax: get [number] [items] from [collection] order:[order]")
17
+ raise SyntaxError, "Syntax Error in tag 'get' - Valid syntax: get [number] [items] from [collection] order:[order]"
18
18
  end
19
19
 
20
20
  super
@@ -48,9 +48,9 @@ module Dugway
48
48
 
49
49
  context[@variable_name] = context[@collection_name]
50
50
 
51
- raise ArgumentError.new("Cannot get array '#{ @collection_name }'. Not found.") if context[@variable_name].total_entries.nil?
51
+ raise ArgumentError, "Cannot get array '#{@collection_name}'. Not found." if context[@variable_name].total_entries.nil?
52
52
 
53
- render_all @nodelist, context
53
+ super(context)
54
54
  end
55
55
  end
56
56
  end
@@ -5,9 +5,9 @@ module Dugway
5
5
 
6
6
  def initialize(tag_name, markup, tokens)
7
7
  if markup =~ Syntax
8
- @variable_name = $1
9
- @collection_name = $2
10
- @per_page = $3.present? ? $4 : nil
8
+ @variable_name = Regexp.last_match(1)
9
+ @collection_name = Regexp.last_match(2)
10
+ @per_page = Regexp.last_match(3).present? ? Regexp.last_match(4) : nil
11
11
 
12
12
  @attributes = { 'inner_window' => 3, 'outer_window' => 1 }
13
13
 
@@ -17,7 +17,7 @@ module Dugway
17
17
 
18
18
  @limit = @attributes['limit']
19
19
  else
20
- raise SyntaxError.new("Syntax Error in tag 'paginate' - Valid syntax: paginate [variable] from [collection] by [number]")
20
+ raise SyntaxError, "Syntax Error in tag 'paginate' - Valid syntax: paginate [variable] from [collection] by [number]"
21
21
  end
22
22
 
23
23
  super
@@ -29,6 +29,7 @@ module Dugway
29
29
  @per_page = context[@per_page].present? ? context[@per_page] : (@per_page.present? ? @per_page.to_i : nil)
30
30
  @order = context[@attributes['order']].present? ? context[@attributes['order']] : @attributes['order']
31
31
 
32
+
32
33
  context.stack do
33
34
  context['internal'] = {
34
35
  'per_page' => @per_page,
@@ -37,21 +38,45 @@ module Dugway
37
38
  'limit' => @limit
38
39
  }
39
40
 
40
- collection = context[@collection_name]
41
+ # Handle nested property access like "products.current"
42
+ collection = resolve_collection_path(context, @collection_name)
43
+
41
44
  context[@variable_name] = collection
42
- current_page = collection.current_page
45
+
46
+ begin
47
+ current_page = collection.current_page
48
+ rescue StandardError => e
49
+ puts "Warning: Failed to get current_page: #{e.message}" if ENV['DEBUG']
50
+ current_page = 1
51
+ end
52
+
53
+ begin
54
+ page_size = collection.per_page
55
+ offset = collection.offset
56
+ rescue StandardError => e
57
+ puts "Warning: Failed to get pagination info: #{e.message}" if ENV['DEBUG']
58
+ page_size = 10
59
+ offset = 0
60
+ end
43
61
 
44
62
  pagination = {
45
- 'page_size' => collection.per_page,
63
+ 'page_size' => page_size,
46
64
  'current_page' => current_page,
47
- 'current_offset' => collection.offset
65
+ 'current_offset' => offset
48
66
  }
49
67
 
50
68
  context['paginate'] = pagination
51
69
 
52
- collection_size = collection.total_entries
70
+ begin
71
+ collection_size = collection.total_entries
72
+ rescue StandardError => e
73
+ puts "Warning: Failed to get total_entries: #{e.message}" if ENV['DEBUG']
74
+ collection_size = collection.respond_to?(:size) ? collection.size : 0
75
+ end
53
76
 
54
- raise ArgumentError.new("Cannot paginate array '#{ @collection_name }'. Not found.") if collection_size.nil?
77
+ if collection_size.nil? || collection_size == 0
78
+ return ""
79
+ end
55
80
 
56
81
  page_count = collection.total_pages
57
82
 
@@ -103,12 +128,37 @@ module Dugway
103
128
  }
104
129
  end
105
130
 
106
- render_all @nodelist, context
131
+ super(context)
107
132
  end
108
133
  end
109
134
 
110
135
  private
111
136
 
137
+ def resolve_collection_path(context, path)
138
+ return context[path] unless path.include?('.')
139
+
140
+ parts = path.split('.')
141
+ collection = context[parts.shift]
142
+
143
+ parts.each do |part|
144
+ break unless collection
145
+
146
+ if collection.respond_to?(part)
147
+ collection = collection.public_send(part)
148
+ elsif collection.respond_to?(:liquid_method_missing)
149
+ collection = collection.liquid_method_missing(part)
150
+ else
151
+ collection = nil
152
+ break
153
+ end
154
+ end
155
+
156
+ collection || []
157
+ rescue StandardError => e
158
+ puts "Warning: Failed to resolve collection '#{path}': #{e.message}" if ENV['DEBUG']
159
+ []
160
+ end
161
+
112
162
  def no_link(title)
113
163
  { 'title' => title.to_s, 'is_link' => false }
114
164
  end
@@ -16,6 +16,7 @@ module Dugway
16
16
  class Liquifier
17
17
  def initialize(request)
18
18
  @request = request
19
+ load_theme_locales
19
20
  end
20
21
 
21
22
  def render(content, variables={})
@@ -41,7 +42,7 @@ module Dugway
41
42
 
42
43
  def self.render_styles(css)
43
44
  Liquid::Template.parse(css).render!(
44
- { 'theme' => Drops::ThemeDrop.new(Dugway.theme.customization) },
45
+ { 'theme' => Drops::ThemeDrop.new(Dugway.theme.customization, Dugway.theme.settings) },
45
46
  :registers => { :settings => Dugway.theme.settings }
46
47
  )
47
48
  end
@@ -68,7 +69,8 @@ module Dugway
68
69
  {
69
70
  'store' => Drops::AccountDrop.new(store.account),
70
71
  'cart' => Drops::CartDrop.new(cart),
71
- 'theme' => Drops::ThemeDrop.new(theme.customization),
72
+ # Pass both customization and definitions to ThemeDrop
73
+ 'theme' => Drops::ThemeDrop.new(theme.customization, theme.settings),
72
74
  'pages' => Drops::PagesDrop.new(store.pages.map { |p| Drops::PageDrop.new(p) }),
73
75
  'categories' => Drops::CategoriesDrop.new(store.categories.map { |c| Drops::CategoryDrop.new(c) }),
74
76
  'artists' => Drops::ArtistsDrop.new(store.artists.map { |a| Drops::ArtistDrop.new(a) }),
@@ -77,6 +79,10 @@ module Dugway
77
79
  'head_content' => [window_bigcartel_script, head_content].join,
78
80
  'bigcartel_credit' => bigcartel_credit,
79
81
  'powered_by_big_cartel' => powered_by_big_cartel,
82
+ 'big_cartel_credit_logo' => big_cartel_credit_logo,
83
+ # Pass both customization and definitions to TranslationsDrop
84
+ 'translations' => Drops::TranslationsDrop.new(theme.customization, theme.settings),
85
+ 't' => Drops::TranslationsDrop.new(theme.customization, theme.settings),
80
86
  }
81
87
  end
82
88
 
@@ -109,17 +115,47 @@ module Dugway
109
115
  "<script>#{script}</script>"
110
116
  end
111
117
 
118
+ def read_svg_asset(filename)
119
+ File.read(File.join(File.dirname(__FILE__), 'assets', filename))
120
+ end
121
+
112
122
  def bigcartel_credit
113
123
  '<a href="http://bigcartel.com/" title="Start your own store at Big Cartel now">Online Store by Big Cartel</a>'
114
124
  end
115
125
 
116
126
  def powered_by_big_cartel
117
- %(<a class="bigcartel-credit" href="https://www.bigcartel.com/?utm_source=bigcartel&utm_medium=storefront&utm_campaign=123" title="Powered by Big Cartel">
118
- <span class="bigcartel-credit__text" aria-hidden="true">Powered by</span>
119
- <svg aria-hidden="true" class="bigcartel-credit__lockup" xmlns="http://www.w3.org/2000/svg" viewBox="1.99 2.05 124.96 24.95">
120
- <path d="M46.18 4A1.91 1.91 0 1 1 50 4a1.91 1.91 0 1 1-3.81 0Zm78.76 14.35a.81.81 0 0 1-.25-.69V2.23h-4.52v2.68h1.58V18c0 2.14 1.11 3.28 3.2 3.28a6.56 6.56 0 0 0 2-.42v-2.78c-1.28.51-1.8.38-2.01.23Zm-75.27.05h1.43V21h-5.79v-2.64h1.43v-8h-1.61V7.7h4.54Zm-11.09-11a5.81 5.81 0 0 0-4.36 1.87V2.23H29.7v2.68h1.62v12.71a.81.81 0 0 1-.25.69c-.43.33-1.5 0-2.06-.23v2.76a6.59 6.59 0 0 0 2.05.42 2.92 2.92 0 0 0 2.74-1.32 6.86 6.86 0 0 0 4.27 1.34 6.66 6.66 0 0 0 6.86-7c-.01-4.06-2.68-6.97-6.35-6.97ZM38 18.57c-2.15 0-3.72-1.75-3.72-4.17s1.55-4.32 3.78-4.32a3.75 3.75 0 0 1 3.75 4.1c.02 2.55-1.58 4.39-3.81 4.39Zm68.86-.49v2.76a7.52 7.52 0 0 1-2 .42c-2.09 0-3.2-1.14-3.2-3.28V5.36l2.93-1.92V7.7h3.81l-1.91 2.68h-1.9v7.24a.77.77 0 0 0 .26.69c.53.4 2.03-.23 2.03-.23ZM58 7.31c-3.88 0-6.69 3-6.69 7.11s2.66 6.87 6.33 6.87A6.14 6.14 0 0 0 62 19.45v1.42c0 2.72-2.4 3.45-3.83 3.45a5.22 5.22 0 0 1-3.12-1.06l-2.36 1.83A7.78 7.78 0 0 0 58 27c3.21 0 6.95-1.63 6.95-6.21v-6.38A6.84 6.84 0 0 0 58 7.31Zm.12 11.29c-2.19 0-3.72-1.74-3.72-4.23s1.6-4.21 3.8-4.21a3.94 3.94 0 0 1 3.8 4.21 4 4 0 0 1-3.85 4.23ZM120.6 15c0-5.05-3.45-7.69-6.85-7.69a6.76 6.76 0 0 0-6.58 7.06 7.13 7.13 0 0 0 12.69 4.39l-2.22-1.71a4.24 4.24 0 0 1-3.44 1.69 3.86 3.86 0 0 1-3.94-3.11h10.28a3.09 3.09 0 0 0 .06-.63Zm-10.35-2.08a3.65 3.65 0 0 1 3.56-3.11 3.77 3.77 0 0 1 3.77 3.11ZM94.92 10V7.7h-4v2.68h1.62v8h-2.35a.83.83 0 0 1-.61-.19.91.91 0 0 1-.19-.64v-5.77c0-1.31-.65-4.47-5.52-4.47a7.85 7.85 0 0 0-4.14 1.18l1.17 2.23a5 5 0 0 1 3-.78 3.26 3.26 0 0 1 1.76.49 2.08 2.08 0 0 1 .81 1.78v1.25a6.58 6.58 0 0 0-3.21-.92c-2.58 0-3.91 1.62-5.19 3.2s-2.51 3-5 2.92c-2.27-.11-3.63-1.86-3.63-4.43 0-2.39 1.45-4 3.54-4a3.75 3.75 0 0 1 3.7 3.18l2.45-1.9a6.3 6.3 0 0 0-6.3-4.18 6.72 6.72 0 0 0-6.48 7c0 3.43 2.1 7.16 6.62 7.16a7.45 7.45 0 0 0 5.87-2.73 4.38 4.38 0 0 0 4.08 2.57 5.91 5.91 0 0 0 3.93-1.66 2.87 2.87 0 0 0 2.8 1.33h7.42v-2.64h-1.61v-3.21c0-3.3 1.09-4.77 3.56-4.77a3.68 3.68 0 0 1 1.45.31V8a4.81 4.81 0 0 0-1.74-.25A4.21 4.21 0 0 0 94.92 10Zm-8.47 7.48a4.93 4.93 0 0 1-3.16 1.41 1.9 1.9 0 0 1-2.05-1.91 2 2 0 0 1 2.18-2 5 5 0 0 1 3 1.18ZM11 14.52v-.89a1.78 1.78 0 0 1 .83-1.51l7.35-4.7A1.79 1.79 0 0 0 20 5.91V2.05L11 7.8 2 2.05V14.2a8.69 8.69 0 0 0 3.88 7.58L11 25.05l5.12-3.27A8.69 8.69 0 0 0 20 14.2V8.77Z" class="a"></path>
121
- </svg>
122
- </a>)
127
+ build_powered_by_link(include_text: true)
128
+ end
129
+
130
+ def big_cartel_credit_logo
131
+ build_powered_by_link(include_text: false)
132
+ end
133
+
134
+ def build_powered_by_link(include_text: true)
135
+ svg_content = read_svg_asset('big_cartel_logo.svg')
136
+
137
+ text_span = include_text ? ' <span class="bigcartel-credit__text" aria-hidden="true">Powered by</span>' : ''
138
+ <<~HTML
139
+ <a class="bigcartel-credit" href="https://www.bigcartel.com/?utm_source=bigcartel&utm_medium=storefront&utm_campaign=123}" title="Powered by Big Cartel" data-bc-hook="attribution">
140
+ #{text_span}
141
+ #{svg_content}
142
+ </a>
143
+ HTML
144
+ end
145
+
146
+ # Load theme-specific override locales
147
+ def load_theme_locales
148
+ return unless Dugway.source_dir && Dir.exist?(File.dirname(Dugway.source_dir))
149
+
150
+ parent_dir = File.dirname(Dugway.source_dir)
151
+ theme_locales_dir = File.join(parent_dir, 'locales')
152
+
153
+ if Dir.exist?(theme_locales_dir)
154
+ theme_locales_path = File.expand_path(theme_locales_dir)
155
+ # Ensure theme locales are loaded *after* defaults to allow overrides
156
+ I18n.load_path |= Dir[File.join(theme_locales_path, 'storefront.*.yml')]
157
+ I18n.backend.reload!
158
+ end
123
159
  end
124
160
  end
125
161
  end
data/lib/dugway/store.rb CHANGED
@@ -8,8 +8,9 @@ module Dugway
8
8
  default_timeout 5
9
9
  headers 'User-Agent' => "Dugway #{ Dugway::VERSION }"
10
10
 
11
- def initialize(subdomain)
11
+ def initialize(subdomain, store_options = {})
12
12
  self.class.base_uri "https://api.bigcartel.com/#{ subdomain }"
13
+ @store_options = store_options || {}
13
14
  end
14
15
 
15
16
  def account
@@ -35,7 +36,7 @@ module Dugway
35
36
  end
36
37
 
37
38
  def pages
38
- @pages ||= theme_pages + custom_pages
39
+ @pages ||= theme_pages + custom_pages + external_pages + required_pages
39
40
  end
40
41
 
41
42
  def page(permalink)
@@ -124,13 +125,55 @@ module Dugway
124
125
  end
125
126
 
126
127
  def locale
127
- currency['locale']
128
+ @store_options[:locale] || currency['locale']
129
+ end
130
+
131
+ def website
132
+ @store_options[:website] || account['website']
128
133
  end
129
134
 
130
135
  def instant_checkout?
131
136
  Dugway.options.dig(:store, :instant_checkout) || false
132
137
  end
133
138
 
139
+ def subscribe_url
140
+ @store_options[:subscribe_url] || account['subscribe_url']
141
+ end
142
+
143
+ def price_suffix
144
+ @store_options[:price_suffix]
145
+ end
146
+
147
+ def external_pages
148
+ @external_pages ||= begin
149
+ pages = []
150
+ if subscribe_url && !subscribe_url.empty?
151
+ pages << {
152
+ 'name' => 'Subscribe',
153
+ 'permalink' => 'subscribe',
154
+ 'url' => subscribe_url,
155
+ 'category' => 'external'
156
+ }
157
+ end
158
+ pages
159
+ end
160
+ end
161
+
162
+ def required_pages
163
+ @required_pages ||= begin
164
+ required_pages_config = @store_options[:required_pages] || account['required_pages'] || []
165
+ required_pages_config.map do |page_config|
166
+ {
167
+ 'name' => page_config['name'],
168
+ 'permalink' => page_config['permalink'],
169
+ 'url' => page_config['url'],
170
+ 'category' => page_config['category'] || 'custom',
171
+ 'required' => true
172
+ }
173
+ end
174
+ end
175
+ end
176
+
134
177
  private
135
178
 
136
179
  def get(path)