Skip to content

Commit 252174d

Browse files
etewiahclaude
andcommitted
Fix widget form routing and eager loading issues
- Fix form routing error by explicitly specifying URL instead of relying on Rails route inference from Pwb::WidgetConfig model name - Change listing_type validation to allow_blank for "All Properties" option - Convert allowed_domains textarea string to array in controller - Fix undefined method 'primary_host' by using custom_domain.presence - Add with_photos_only scope for lighter widget loading - Add widget-test.html for testing embed functionality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ec2cb25 commit 252174d

File tree

8 files changed

+222
-9
lines changed

8 files changed

+222
-9
lines changed

.claude/settings.local.json

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,39 @@
121121
"Skill(theme-creation)",
122122
"Bash(ACTIVE_STORAGE_SERVICE=cloudflare_r2 RAILS_ENV=development bundle exec rails runner:*)",
123123
"Bash(R2_USE_CDN=false RAILS_ENV=development bundle exec rails runner:*)",
124-
"Bash(gzip:*)"
124+
"Bash(gzip:*)",
125+
"Bash(for file in db/yml_seeds/content_translations/{de,fr,it,nl}.yml)",
126+
"Bash(done)",
127+
"Bash(db/migrate/20251227154344_add_dark_mode_setting_to_websites.rb )",
128+
"Bash(app/models/concerns/pwb/website_styleable.rb )",
129+
"Bash(app/controllers/site_admin/website/settings_controller.rb )",
130+
"Bash(app/views/site_admin/website/settings/_appearance_tab.html.erb )",
131+
"Bash(app/views/pwb/custom_css/_base_variables.css.erb )",
132+
"Bash(app/themes/default/views/layouts/pwb/application.html.erb )",
133+
"Bash(app/themes/brisbane/views/layouts/pwb/application.html.erb )",
134+
"Bash(app/themes/bologna/views/layouts/pwb/application.html.erb )",
135+
"Bash(app/themes/biarritz/views/layouts/pwb/application.html.erb )",
136+
"Bash(app/themes/barcelona/views/layouts/pwb/application.html.erb )",
137+
"Bash(db/schema.rb )",
138+
"Bash(app/models/pwb/website.rb )",
139+
"Bash(spec/factories/pwb_websites.rb )",
140+
"Bash(spec/models/pwb/website_spec.rb)",
141+
"Bash(wc:*)",
142+
"mcp__chrome-devtools__select_page",
143+
"Bash(app/views/pwb/custom_css/_barcelona.css.erb )",
144+
"Bash(app/views/pwb/custom_css/_biarritz.css.erb )",
145+
"Bash(app/views/pwb/custom_css/_bologna.css.erb )",
146+
"Bash(app/views/pwb/custom_css/_brisbane.css.erb )",
147+
"Bash(app/views/pwb/custom_css/_default.css.erb )",
148+
"Bash(spec/views/themes/theme_components_spec.rb )",
149+
"Bash(db/seeds/packs/netherlands_urban/images/team_*.jpg)",
150+
"Bash(db/seeds/packs/netherlands_urban/images/team_*.webp)",
151+
"Bash(RAILS_ENV=test bundle exec rake:*)",
152+
"Bash(for theme in default brisbane bologna barcelona biarritz)",
153+
"Bash(wc -l echo Files: ls -1 /Users/etewiah/dev/sites-older/property_web_builder/app/themes/$theme/palettes/*.json)",
154+
"Bash(RAILS_ENV=test bundle exec rails db:migrate:*)",
155+
"mcp__chrome-devtools__fill",
156+
"WebFetch(domain:propertywebbuilder.com)"
125157
],
126158
"deny": [],
127159
"ask": []

app/controllers/site_admin/widgets_controller.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,29 @@ def set_widget
6363
end
6464

6565
def widget_params
66-
params.require(:pwb_widget_config).permit(
66+
permitted = params.require(:pwb_widget_config).permit(
6767
:name, :active, :layout, :columns, :max_properties,
6868
:show_search, :show_filters, :show_pagination,
6969
:listing_type, :min_price_cents, :max_price_cents,
7070
:min_bedrooms, :max_bedrooms, :highlighted_only,
71-
property_types: [], allowed_domains: [],
71+
:allowed_domains,
72+
property_types: [],
7273
theme: [:primary_color, :secondary_color, :text_color,
7374
:background_color, :card_background, :border_color,
7475
:border_radius, :font_family],
7576
visible_fields: [:price, :bedrooms, :bathrooms, :area,
7677
:location, :reference, :property_type]
7778
)
79+
80+
# Convert allowed_domains from textarea (newline-separated) to array
81+
if permitted[:allowed_domains].is_a?(String)
82+
permitted[:allowed_domains] = permitted[:allowed_domains]
83+
.split(/[\r\n]+/)
84+
.map(&:strip)
85+
.reject(&:blank?)
86+
end
87+
88+
permitted
7889
end
7990
end
8091
end

app/controllers/widgets_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def javascript
2424
# Renders the widget in an iframe-friendly format
2525
def iframe
2626
@widget_config = Pwb::WidgetConfig.active.find_by!(widget_key: params[:widget_key])
27-
@properties = @widget_config.properties_query.with_eager_loading
27+
@properties = @widget_config.properties_query.with_photos_only
2828

2929
# Record impression
3030
@widget_config.record_impression!

app/models/concerns/listed_property/searchable.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ module Searchable
1515
# Use this when you need both website and photos (e.g., cross-tenant operations)
1616
scope :with_full_eager_loading, -> { includes(:website, prop_photos: { image_attachment: :blob }) }
1717

18+
# Lighter scope for widgets - only loads photos without attachment blob data
19+
# Use when you only need first photo or just need to check has_image?
20+
scope :with_photos_only, -> { includes(:prop_photos) }
21+
1822
# Basic visibility and operation type scopes
1923
scope :visible, -> { where(visible: true) }
2024
scope :for_sale, -> { where(for_sale: true) }

app/models/pwb/widget_config.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class WidgetConfig < ApplicationRecord
5050
validates :layout, inclusion: { in: %w[grid list carousel] }
5151
validates :columns, numericality: { in: 1..6 }
5252
validates :max_properties, numericality: { greater_than: 0, less_than_or_equal_to: 100 }
53-
validates :listing_type, inclusion: { in: %w[sale rent] }, allow_nil: true
53+
validates :listing_type, inclusion: { in: %w[sale rent] }, allow_blank: true
5454

5555
# Callbacks
5656
before_validation :generate_widget_key, on: :create
@@ -152,7 +152,7 @@ def domain_allowed?(domain)
152152

153153
# Generate embed code for this widget
154154
def embed_code(host: nil)
155-
widget_host = host || website.primary_host || "#{website.subdomain}.propertywebbuilder.com"
155+
widget_host = host || website.custom_domain.presence || "#{website.subdomain}.propertywebbuilder.com"
156156

157157
<<~HTML.strip
158158
<!-- PropertyWebBuilder Widget -->
@@ -163,7 +163,7 @@ def embed_code(host: nil)
163163

164164
# Generate iframe embed code (alternative)
165165
def iframe_embed_code(host: nil)
166-
widget_host = host || website.primary_host || "#{website.subdomain}.propertywebbuilder.com"
166+
widget_host = host || website.custom_domain.presence || "#{website.subdomain}.propertywebbuilder.com"
167167

168168
<<~HTML.strip
169169
<!-- PropertyWebBuilder Widget (iframe) -->

app/views/site_admin/widgets/_form.html.erb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
<%= form_with model: [:site_admin, @widget], local: true, class: "space-y-8" do |f| %>
1+
<%= form_with model: @widget,
2+
url: @widget.new_record? ? site_admin_widgets_path : site_admin_widget_path(@widget),
3+
local: true, class: "space-y-8" do |f| %>
24
<% if @widget.errors.any? %>
35
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
46
<h3 class="text-red-800 font-medium">Please fix the following errors:</h3>

app/views/widgets/iframe.html.erb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<% visible = @widget_config.effective_visible_fields %>
2+
<% widget_host = @widget_config.website.custom_domain.presence || request.host %>
23

34
<% if @properties.any? %>
45
<div class="pwb-grid">
56
<% @properties.each do |prop| %>
67
<div class="pwb-card">
7-
<a href="<%= prop.for_sale ? prop_show_for_sale_url(id: prop.id, url_friendly_title: prop.url_friendly_title, host: @widget_config.website.primary_host || "#{@widget_config.website.subdomain}.propertywebbuilder.com", protocol: 'https') : prop_show_for_rent_url(id: prop.id, url_friendly_title: prop.url_friendly_title, host: @widget_config.website.primary_host || "#{@widget_config.website.subdomain}.propertywebbuilder.com", protocol: 'https') %>" target="_blank" rel="noopener">
8+
<a href="<%= prop.for_sale ? prop_show_for_sale_url(id: prop.id, url_friendly_title: prop.url_friendly_title, host: widget_host, protocol: request.protocol.delete(':')) : prop_show_for_rent_url(id: prop.id, url_friendly_title: prop.url_friendly_title, host: widget_host, protocol: request.protocol.delete(':')) %>" target="_blank" rel="noopener">
89
<div class="pwb-card-image">
910
<% first_photo = prop.prop_photos.first %>
1011
<% if first_photo&.has_image? %>

public/widget-test.html

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Widget Test Page</title>
7+
<style>
8+
* { box-sizing: border-box; }
9+
body {
10+
font-family: system-ui, -apple-system, sans-serif;
11+
max-width: 1200px;
12+
margin: 0 auto;
13+
padding: 40px 20px;
14+
background: #f5f5f5;
15+
}
16+
h1 { color: #1f2937; margin-bottom: 8px; }
17+
.subtitle { color: #6b7280; margin-bottom: 32px; }
18+
.test-section {
19+
background: white;
20+
border-radius: 12px;
21+
padding: 24px;
22+
margin-bottom: 24px;
23+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
24+
}
25+
.test-section h2 {
26+
font-size: 18px;
27+
color: #374151;
28+
margin-bottom: 16px;
29+
padding-bottom: 12px;
30+
border-bottom: 1px solid #e5e7eb;
31+
}
32+
.widget-container {
33+
min-height: 200px;
34+
border: 2px dashed #d1d5db;
35+
border-radius: 8px;
36+
padding: 16px;
37+
background: #fafafa;
38+
}
39+
.instructions {
40+
background: #eff6ff;
41+
border: 1px solid #bfdbfe;
42+
border-radius: 8px;
43+
padding: 16px;
44+
margin-bottom: 24px;
45+
}
46+
.instructions h3 { color: #1e40af; margin-bottom: 8px; font-size: 14px; }
47+
.instructions ol { margin: 0; padding-left: 20px; color: #1e3a8a; }
48+
.instructions li { margin-bottom: 4px; }
49+
.instructions code {
50+
background: #dbeafe;
51+
padding: 2px 6px;
52+
border-radius: 4px;
53+
font-size: 13px;
54+
}
55+
#widget-key-input {
56+
width: 200px;
57+
padding: 8px 12px;
58+
border: 1px solid #d1d5db;
59+
border-radius: 6px;
60+
font-family: monospace;
61+
}
62+
button {
63+
padding: 8px 16px;
64+
background: #3b82f6;
65+
color: white;
66+
border: none;
67+
border-radius: 6px;
68+
cursor: pointer;
69+
margin-left: 8px;
70+
}
71+
button:hover { background: #2563eb; }
72+
.input-group { margin-bottom: 16px; }
73+
label { display: block; margin-bottom: 4px; font-weight: 500; color: #374151; }
74+
</style>
75+
</head>
76+
<body>
77+
<h1>Widget Test Page</h1>
78+
<p class="subtitle">Test your PropertyWebBuilder embeddable widgets here</p>
79+
80+
<div class="instructions">
81+
<h3>How to test:</h3>
82+
<ol>
83+
<li>Create a widget at <code>/site_admin/widgets/new</code></li>
84+
<li>Copy the widget key (e.g., <code>abc123xyz</code>)</li>
85+
<li>Paste it below and click "Load Widget"</li>
86+
</ol>
87+
</div>
88+
89+
<div class="test-section">
90+
<h2>JavaScript Embed Test</h2>
91+
<div class="input-group">
92+
<label for="widget-key-input">Widget Key:</label>
93+
<input type="text" id="widget-key-input" placeholder="e.g., abc123xyz">
94+
<button onclick="loadWidget()">Load Widget</button>
95+
</div>
96+
<div id="js-widget-container" class="widget-container">
97+
<p style="color: #9ca3af; text-align: center; margin-top: 60px;">
98+
Enter a widget key above to load the widget
99+
</p>
100+
</div>
101+
</div>
102+
103+
<div class="test-section">
104+
<h2>Iframe Embed Test</h2>
105+
<div class="input-group">
106+
<label for="iframe-key-input">Widget Key:</label>
107+
<input type="text" id="iframe-key-input" placeholder="e.g., abc123xyz">
108+
<button onclick="loadIframe()">Load Iframe</button>
109+
</div>
110+
<div id="iframe-widget-container" class="widget-container">
111+
<p style="color: #9ca3af; text-align: center; margin-top: 60px;">
112+
Enter a widget key above to load the iframe widget
113+
</p>
114+
</div>
115+
</div>
116+
117+
<script>
118+
function loadWidget() {
119+
const key = document.getElementById('widget-key-input').value.trim();
120+
if (!key) {
121+
alert('Please enter a widget key');
122+
return;
123+
}
124+
125+
const container = document.getElementById('js-widget-container');
126+
container.innerHTML = '<div id="pwb-widget-' + key + '"></div>';
127+
128+
// Remove any existing script
129+
const existingScript = document.getElementById('pwb-widget-script');
130+
if (existingScript) existingScript.remove();
131+
132+
// Load the widget script
133+
const script = document.createElement('script');
134+
script.id = 'pwb-widget-script';
135+
script.src = '/widget.js';
136+
script.dataset.widgetId = key;
137+
script.async = true;
138+
container.appendChild(script);
139+
}
140+
141+
function loadIframe() {
142+
const key = document.getElementById('iframe-key-input').value.trim();
143+
if (!key) {
144+
alert('Please enter a widget key');
145+
return;
146+
}
147+
148+
const container = document.getElementById('iframe-widget-container');
149+
container.innerHTML = `
150+
<iframe
151+
src="/widget/${key}"
152+
width="100%"
153+
height="600"
154+
frameborder="0"
155+
style="border: none; width: 100%; min-height: 600px;"
156+
loading="lazy"
157+
title="Property Listings">
158+
</iframe>
159+
`;
160+
}
161+
</script>
162+
</body>
163+
</html>

0 commit comments

Comments
 (0)