@braudypedrosa/bp-listings 1.0.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.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/docs.html +640 -0
- package/index.html +257 -0
- package/listings-map.css +891 -0
- package/listings-map.js +1042 -0
- package/package.json +40 -0
package/docs.html
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
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>ListingsMap - Usage Guide</title>
|
|
7
|
+
<style>
|
|
8
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
11
|
+
color: #222;
|
|
12
|
+
background: #fff;
|
|
13
|
+
}
|
|
14
|
+
a { color: inherit; }
|
|
15
|
+
|
|
16
|
+
/* Layout */
|
|
17
|
+
.page { display: flex; min-height: 100vh; }
|
|
18
|
+
.sidebar {
|
|
19
|
+
position: sticky; top: 0; height: 100vh; width: 220px; flex-shrink: 0;
|
|
20
|
+
border-right: 1px solid #eee; padding: 32px 0; overflow-y: auto; background: #fafafa;
|
|
21
|
+
}
|
|
22
|
+
.sidebar-brand { padding: 0 20px; margin-bottom: 24px; }
|
|
23
|
+
.sidebar-brand a { text-decoration: none; color: #222; font-size: 15px; font-weight: 700; letter-spacing: -0.3px; }
|
|
24
|
+
.sidebar-version { font-size: 11px; color: #999; margin-top: 2px; }
|
|
25
|
+
.sidebar-nav { display: flex; flex-direction: column; gap: 1px; }
|
|
26
|
+
.sidebar-nav a {
|
|
27
|
+
padding: 7px 20px; font-size: 13px; color: #555; text-decoration: none;
|
|
28
|
+
border-left: 2px solid transparent; transition: background 0.1s, color 0.1s;
|
|
29
|
+
}
|
|
30
|
+
.sidebar-nav a:hover { background: #f0f0f0; color: #222; border-left-color: #ddd; }
|
|
31
|
+
.sidebar-downloads { padding: 24px 20px 0; border-top: 1px solid #eee; margin-top: 24px; }
|
|
32
|
+
.sidebar-downloads a {
|
|
33
|
+
display: block; text-align: center; padding: 8px 16px; border-radius: 8px;
|
|
34
|
+
text-decoration: none; font-size: 13px; font-weight: 600; margin-bottom: 8px;
|
|
35
|
+
}
|
|
36
|
+
.btn-primary { background: #222; color: #fff; }
|
|
37
|
+
.btn-secondary { background: #fff; color: #222; border: 1px solid #ddd; }
|
|
38
|
+
|
|
39
|
+
.content { flex: 1; max-width: 740px; padding: 48px 48px 100px; }
|
|
40
|
+
|
|
41
|
+
/* Typography */
|
|
42
|
+
.back-link { font-size: 13px; color: #717171; text-decoration: none; display: inline-flex; align-items: center; gap: 4px; margin-bottom: 8px; }
|
|
43
|
+
h1 { font-size: 36px; font-weight: 800; letter-spacing: -1px; line-height: 1.15; color: #111; }
|
|
44
|
+
.subtitle { font-size: 17px; color: #717171; margin-top: 12px; line-height: 1.6; max-width: 560px; }
|
|
45
|
+
h2 { font-size: 22px; font-weight: 700; letter-spacing: -0.3px; margin-top: 56px; margin-bottom: 12px; scroll-margin-top: 80px; color: #111; }
|
|
46
|
+
h3 { font-size: 16px; font-weight: 600; margin-top: 32px; color: #222; }
|
|
47
|
+
p { color: #555; line-height: 1.7; font-size: 15px; }
|
|
48
|
+
hr { border: none; border-top: 1px solid #eee; margin: 48px 0 0; }
|
|
49
|
+
code { padding: 2px 6px; border-radius: 4px; font-size: 13px; }
|
|
50
|
+
|
|
51
|
+
/* Code blocks */
|
|
52
|
+
.code-block { position: relative; background: #1a1a1a; border-radius: 12px; overflow: hidden; margin-top: 16px; }
|
|
53
|
+
.code-block-header {
|
|
54
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
55
|
+
padding: 10px 16px; border-bottom: 1px solid #2a2a2a; font-size: 12px; color: #888; font-family: monospace;
|
|
56
|
+
}
|
|
57
|
+
.code-block pre {
|
|
58
|
+
padding: 16px 20px; overflow: auto; font-size: 13px; line-height: 1.7;
|
|
59
|
+
color: #e0e0e0; margin: 0; font-family: 'SF Mono', 'Fira Code', 'Fira Mono', 'Roboto Mono', monospace;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Tables */
|
|
63
|
+
.table-wrap { border: 1px solid #eee; border-radius: 12px; overflow: hidden; margin-top: 16px; }
|
|
64
|
+
table { width: 100%; font-size: 13px; border-collapse: collapse; }
|
|
65
|
+
thead tr { background: #fafafa; }
|
|
66
|
+
th { padding: 10px 16px; text-align: left; font-weight: 600; border-bottom: 1px solid #eee; color: #222; }
|
|
67
|
+
td { padding: 9px 16px; }
|
|
68
|
+
td.mono { font-family: monospace; font-weight: 500; color: #222; }
|
|
69
|
+
td.mono-sm { font-family: monospace; font-size: 12px; color: #717171; }
|
|
70
|
+
tr + tr { border-top: 1px solid #f5f5f5; }
|
|
71
|
+
|
|
72
|
+
/* Badges */
|
|
73
|
+
.badge { display: inline-block; font-size: 11px; font-weight: 600; padding: 2px 7px; border-radius: 4px; vertical-align: middle; margin-left: 6px; }
|
|
74
|
+
.badge-required { background: #222; color: #fff; }
|
|
75
|
+
.badge-default { background: #f0f0f0; color: #555; }
|
|
76
|
+
|
|
77
|
+
/* Cards */
|
|
78
|
+
.card { background: #f7f7f7; border: 1px solid #eee; border-radius: 12px; padding: 20px; margin-top: 16px; }
|
|
79
|
+
.card h3 { font-family: monospace; font-size: 15px; font-weight: 600; color: #222; margin: 0; }
|
|
80
|
+
.card p { font-size: 13px; color: #717171; margin-top: 6px; line-height: 1.5; }
|
|
81
|
+
.cb-card { border: 1px solid #eee; border-radius: 12px; overflow: hidden; }
|
|
82
|
+
.cb-card-head { padding: 14px 18px; background: #fafafa; }
|
|
83
|
+
.cb-card-head code { font-size: 14px; font-weight: 600; color: #222; background: none; padding: 0; }
|
|
84
|
+
.cb-card-head p { font-size: 13px; color: #717171; margin-top: 4px; line-height: 1.5; }
|
|
85
|
+
|
|
86
|
+
.method-card { background: #fafafa; border: 1px solid #eee; border-radius: 10px; padding: 14px 18px; }
|
|
87
|
+
.method-card code { font-size: 14px; font-weight: 600; color: #222; background: none; padding: 0; }
|
|
88
|
+
.method-card p { font-size: 13px; color: #717171; margin-top: 4px; line-height: 1.5; }
|
|
89
|
+
|
|
90
|
+
/* Callouts */
|
|
91
|
+
.callout { border-radius: 10px; padding: 14px 18px; margin-top: 16px; font-size: 13px; line-height: 1.6; }
|
|
92
|
+
.callout-warn { background: #fffbeb; border: 1px solid #fde68a; color: #92400e; }
|
|
93
|
+
.callout-info { background: #f0f7ff; border: 1px solid #c8e1ff; color: #1e40af; }
|
|
94
|
+
|
|
95
|
+
/* Swatch */
|
|
96
|
+
.swatch { display: inline-block; width: 12px; height: 12px; border-radius: 3px; border: 1px solid #ddd; vertical-align: middle; margin-right: 6px; }
|
|
97
|
+
|
|
98
|
+
/* Grid */
|
|
99
|
+
.file-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 16px; }
|
|
100
|
+
.file-card { background: #f7f7f7; border: 1px solid #eee; border-radius: 10px; padding: 16px; }
|
|
101
|
+
.file-card-name { font-size: 13px; font-weight: 600; margin-bottom: 4px; color: #222; }
|
|
102
|
+
.file-card-size { font-size: 12px; color: #999; }
|
|
103
|
+
|
|
104
|
+
/* Footer */
|
|
105
|
+
.footer { margin-top: 80px; padding-top: 24px; border-top: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; font-size: 13px; color: #aaa; }
|
|
106
|
+
.footer a { color: #717171; text-decoration: none; }
|
|
107
|
+
|
|
108
|
+
/* Stack */
|
|
109
|
+
.stack { display: flex; flex-direction: column; }
|
|
110
|
+
.stack-8 { gap: 8px; }
|
|
111
|
+
.stack-12 { gap: 12px; }
|
|
112
|
+
|
|
113
|
+
/* Responsive */
|
|
114
|
+
@media (max-width: 768px) {
|
|
115
|
+
.sidebar { display: none; }
|
|
116
|
+
.content { padding: 24px 20px 80px; }
|
|
117
|
+
.file-grid { grid-template-columns: 1fr; }
|
|
118
|
+
}
|
|
119
|
+
</style>
|
|
120
|
+
</head>
|
|
121
|
+
<body>
|
|
122
|
+
<div class="page">
|
|
123
|
+
|
|
124
|
+
<!-- Sidebar -->
|
|
125
|
+
<nav class="sidebar">
|
|
126
|
+
<div class="sidebar-brand">
|
|
127
|
+
<a href="index.html">ListingsMap</a>
|
|
128
|
+
<div class="sidebar-version">v1.0.0</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="sidebar-nav">
|
|
131
|
+
<a href="#installation">Installation</a>
|
|
132
|
+
<a href="#quick-start">Quick Start</a>
|
|
133
|
+
<a href="#listing-schema">Listing Schema</a>
|
|
134
|
+
<a href="#api">API Reference</a>
|
|
135
|
+
<a href="#css-variables">CSS Variables</a>
|
|
136
|
+
<a href="#callbacks">Callbacks</a>
|
|
137
|
+
<a href="#search-slot">Search Slot</a>
|
|
138
|
+
<a href="#wordpress">WordPress</a>
|
|
139
|
+
<a href="#examples">Examples</a>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="sidebar-downloads">
|
|
142
|
+
<a href="listings-map.js" download class="btn-primary">Download JS</a>
|
|
143
|
+
<a href="listings-map.css" download class="btn-secondary">Download CSS</a>
|
|
144
|
+
</div>
|
|
145
|
+
</nav>
|
|
146
|
+
|
|
147
|
+
<!-- Main Content -->
|
|
148
|
+
<main class="content">
|
|
149
|
+
|
|
150
|
+
<a href="index.html" class="back-link">
|
|
151
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="10 13 5 8 10 3"/></svg>
|
|
152
|
+
Live Demo
|
|
153
|
+
</a>
|
|
154
|
+
<h1>Usage Guide</h1>
|
|
155
|
+
<p class="subtitle">Everything you need to integrate the ListingsMap widget into any website. Two files, zero framework dependencies, one function call.</p>
|
|
156
|
+
|
|
157
|
+
<!-- Installation -->
|
|
158
|
+
<hr />
|
|
159
|
+
<h2 id="installation">Installation</h2>
|
|
160
|
+
<p>Download the two library files and include them in your project. No build step, no npm, no bundler required.</p>
|
|
161
|
+
|
|
162
|
+
<div class="file-grid">
|
|
163
|
+
<div class="file-card">
|
|
164
|
+
<div class="file-card-name">listings-map.js</div>
|
|
165
|
+
<div class="file-card-size">Core library (~20KB)</div>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="file-card">
|
|
168
|
+
<div class="file-card-name">listings-map.css</div>
|
|
169
|
+
<div class="file-card-size">Styles (~4KB)</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div class="code-block">
|
|
174
|
+
<div class="code-block-header"><span>html</span></div>
|
|
175
|
+
<pre><code><!-- Add to your <head> -->
|
|
176
|
+
<link rel="stylesheet" href="path/to/listings-map.css">
|
|
177
|
+
|
|
178
|
+
<!-- Add before </body> -->
|
|
179
|
+
<script src="path/to/listings-map.js"></script></code></pre>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div class="callout callout-warn">
|
|
183
|
+
<strong>Note:</strong> Leaflet.js is loaded automatically if not already present on the page. No need to include it separately.
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<!-- Quick Start -->
|
|
187
|
+
<hr />
|
|
188
|
+
<h2 id="quick-start">Quick Start</h2>
|
|
189
|
+
<p>Create a container element, include the library files, and call <code>ListingsMap.init()</code> with your data.</p>
|
|
190
|
+
|
|
191
|
+
<div class="code-block">
|
|
192
|
+
<div class="code-block-header"><span>html</span></div>
|
|
193
|
+
<pre><code><!DOCTYPE html>
|
|
194
|
+
<html lang="en">
|
|
195
|
+
<head>
|
|
196
|
+
<meta charset="UTF-8">
|
|
197
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
198
|
+
<title>My Listings</title>
|
|
199
|
+
<link rel="stylesheet" href="listings-map.css">
|
|
200
|
+
</head>
|
|
201
|
+
<body>
|
|
202
|
+
|
|
203
|
+
<div id="app" style="width: 100%; height: 100vh;"></div>
|
|
204
|
+
|
|
205
|
+
<script src="listings-map.js"></script>
|
|
206
|
+
<script>
|
|
207
|
+
var widget = ListingsMap.init({
|
|
208
|
+
container: '#app',
|
|
209
|
+
currency: '₱',
|
|
210
|
+
mapOptions: {
|
|
211
|
+
center: [14.58, 121.05],
|
|
212
|
+
zoom: 12
|
|
213
|
+
},
|
|
214
|
+
listings: [
|
|
215
|
+
{
|
|
216
|
+
id: '1',
|
|
217
|
+
title: 'Apartment in Quezon City',
|
|
218
|
+
subtitle: 'Condo in Cubao | Sunset & City Lights',
|
|
219
|
+
details: '1 bedroom · 1 bed',
|
|
220
|
+
dates: 'Feb 28 – Mar 5',
|
|
221
|
+
price: 13689,
|
|
222
|
+
pricePeriod: 'for 5 nights',
|
|
223
|
+
tag: 'Free cancellation',
|
|
224
|
+
rating: 5.0,
|
|
225
|
+
reviewCount: 89,
|
|
226
|
+
badge: 'Guest favorite',
|
|
227
|
+
lat: 14.628,
|
|
228
|
+
lng: 121.055,
|
|
229
|
+
images: ['photo-1.jpg', 'photo-2.jpg']
|
|
230
|
+
}
|
|
231
|
+
]
|
|
232
|
+
});
|
|
233
|
+
</script>
|
|
234
|
+
|
|
235
|
+
</body>
|
|
236
|
+
</html></code></pre>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<!-- Listing Schema -->
|
|
240
|
+
<hr />
|
|
241
|
+
<h2 id="listing-schema">Listing Object Schema</h2>
|
|
242
|
+
<p style="margin-bottom:16px">Each listing in the array should be an object with the following properties.</p>
|
|
243
|
+
|
|
244
|
+
<div class="table-wrap">
|
|
245
|
+
<table>
|
|
246
|
+
<thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
|
|
247
|
+
<tbody>
|
|
248
|
+
<tr><td class="mono">id <span class="badge badge-required">required</span></td><td class="mono-sm">string</td><td>Unique identifier for the listing</td></tr>
|
|
249
|
+
<tr><td class="mono">title <span class="badge badge-required">required</span></td><td class="mono-sm">string</td><td>Main heading, e.g. "Apartment in Quezon City"</td></tr>
|
|
250
|
+
<tr><td class="mono">subtitle</td><td class="mono-sm">string</td><td>Secondary description line</td></tr>
|
|
251
|
+
<tr><td class="mono">details</td><td class="mono-sm">string</td><td>Room info, e.g. "1 bedroom · 1 bed"</td></tr>
|
|
252
|
+
<tr><td class="mono">dates</td><td class="mono-sm">string</td><td>Date range, e.g. "Feb 28 – Mar 5"</td></tr>
|
|
253
|
+
<tr><td class="mono">price <span class="badge badge-required">required</span></td><td class="mono-sm">number</td><td>Numeric price value (formatted with currency)</td></tr>
|
|
254
|
+
<tr><td class="mono">pricePeriod</td><td class="mono-sm">string</td><td>e.g. "for 5 nights", "per night"</td></tr>
|
|
255
|
+
<tr><td class="mono">tag</td><td class="mono-sm">string</td><td>Bottom line tag, e.g. "Free cancellation"</td></tr>
|
|
256
|
+
<tr><td class="mono">rating</td><td class="mono-sm">number</td><td>Star rating value, e.g. 4.91</td></tr>
|
|
257
|
+
<tr><td class="mono">reviewCount</td><td class="mono-sm">number</td><td>Number of reviews</td></tr>
|
|
258
|
+
<tr><td class="mono">badge</td><td class="mono-sm">string</td><td>"Guest favorite" or "Superhost"</td></tr>
|
|
259
|
+
<tr><td class="mono">lat <span class="badge badge-required">required</span></td><td class="mono-sm">number</td><td>Latitude coordinate for map marker</td></tr>
|
|
260
|
+
<tr><td class="mono">lng <span class="badge badge-required">required</span></td><td class="mono-sm">number</td><td>Longitude coordinate for map marker</td></tr>
|
|
261
|
+
<tr><td class="mono">images <span class="badge badge-required">required</span></td><td class="mono-sm">string[]</td><td>Array of image URLs for the carousel</td></tr>
|
|
262
|
+
<tr><td class="mono">favorited</td><td class="mono-sm">boolean</td><td>Pre-set the heart/favorite state</td></tr>
|
|
263
|
+
</tbody>
|
|
264
|
+
</table>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<!-- API Reference -->
|
|
268
|
+
<hr />
|
|
269
|
+
<h2 id="api">API Reference</h2>
|
|
270
|
+
|
|
271
|
+
<div class="card">
|
|
272
|
+
<h3>ListingsMap.init(config)</h3>
|
|
273
|
+
<p>Creates and mounts the widget into the target container. Returns a widget instance with methods listed below.</p>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<div class="code-block">
|
|
277
|
+
<div class="code-block-header"><span>javascript</span></div>
|
|
278
|
+
<pre><code>var widget = ListingsMap.init({
|
|
279
|
+
container: '#my-container', // CSS selector or DOM element
|
|
280
|
+
listings: [...], // Array of listing objects
|
|
281
|
+
currency: '₱', // Currency symbol (default: "$")
|
|
282
|
+
mapOptions: {
|
|
283
|
+
center: [14.58, 121.05], // [lat, lng]
|
|
284
|
+
zoom: 12 // Initial zoom level
|
|
285
|
+
},
|
|
286
|
+
tileUrl: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
287
|
+
tileAttribution: '© OpenStreetMap contributors',
|
|
288
|
+
renderSearchSlot: function(containerEl) { /* your search UI */ },
|
|
289
|
+
onFavorite: function(listing, isFavorited) { },
|
|
290
|
+
onListingClick: function(listing) { },
|
|
291
|
+
onMapMoveEnd: function(bounds) { }
|
|
292
|
+
});</code></pre>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<h3 style="margin-top:24px">Instance Methods</h3>
|
|
296
|
+
<div class="stack stack-12" style="margin-top:12px">
|
|
297
|
+
<div class="method-card">
|
|
298
|
+
<code>widget.setListings(listings)</code>
|
|
299
|
+
<p>Replace all listings and map markers with new data. Accepts an array of listing objects.</p>
|
|
300
|
+
</div>
|
|
301
|
+
<div class="method-card">
|
|
302
|
+
<code>widget.panToListing(id)</code>
|
|
303
|
+
<p>Pan the map to a specific listing, open its popup, and highlight the card.</p>
|
|
304
|
+
</div>
|
|
305
|
+
<div class="method-card">
|
|
306
|
+
<code>widget.toggleMap()</code>
|
|
307
|
+
<p>Toggle the map panel visibility on mobile.</p>
|
|
308
|
+
</div>
|
|
309
|
+
<div class="method-card">
|
|
310
|
+
<code>widget.goToPage(n)</code>
|
|
311
|
+
<p>Navigate to a specific page number (when pagination is enabled).</p>
|
|
312
|
+
</div>
|
|
313
|
+
<div class="method-card">
|
|
314
|
+
<code>widget.destroy()</code>
|
|
315
|
+
<p>Remove the widget, clean up the map, and release all event listeners.</p>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<!-- CSS Variables -->
|
|
320
|
+
<hr />
|
|
321
|
+
<h2 id="css-variables">CSS Variables</h2>
|
|
322
|
+
<p style="margin-bottom:8px">Override any variable on <code>.lm-widget</code> to customize the look and feel.</p>
|
|
323
|
+
|
|
324
|
+
<div class="code-block">
|
|
325
|
+
<div class="code-block-header"><span>css</span></div>
|
|
326
|
+
<pre><code>.lm-widget {
|
|
327
|
+
--lm-font: 'Inter', sans-serif;
|
|
328
|
+
--lm-color-text: #1a1a2e;
|
|
329
|
+
--lm-color-heart: #e94560;
|
|
330
|
+
--lm-color-superhost-bg: #0f3460;
|
|
331
|
+
--lm-radius: 16px;
|
|
332
|
+
--lm-image-height: 220px;
|
|
333
|
+
}</code></pre>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<div class="table-wrap" style="margin-top:20px">
|
|
337
|
+
<table>
|
|
338
|
+
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
|
|
339
|
+
<tbody>
|
|
340
|
+
<tr><td class="mono-sm">--lm-font</td><td><code style="font-size:12px;color:#717171">system-ui, sans-serif</code></td><td>Font family</td></tr>
|
|
341
|
+
<tr><td class="mono-sm">--lm-color-text</td><td><span class="swatch" style="background:#222222"></span><code style="font-size:12px;color:#717171">#222222</code></td><td>Primary text</td></tr>
|
|
342
|
+
<tr><td class="mono-sm">--lm-color-text-secondary</td><td><span class="swatch" style="background:#717171"></span><code style="font-size:12px;color:#717171">#717171</code></td><td>Secondary text (subtitles, details)</td></tr>
|
|
343
|
+
<tr><td class="mono-sm">--lm-color-border</td><td><span class="swatch" style="background:#dddddd"></span><code style="font-size:12px;color:#717171">#dddddd</code></td><td>Panel borders</td></tr>
|
|
344
|
+
<tr><td class="mono-sm">--lm-color-bg</td><td><span class="swatch" style="background:#ffffff"></span><code style="font-size:12px;color:#717171">#ffffff</code></td><td>Background</td></tr>
|
|
345
|
+
<tr><td class="mono-sm">--lm-color-heart</td><td><span class="swatch" style="background:#ff385c"></span><code style="font-size:12px;color:#717171">#ff385c</code></td><td>Active heart color</td></tr>
|
|
346
|
+
<tr><td class="mono-sm">--lm-color-badge-bg</td><td><span class="swatch" style="background:#ffffff"></span><code style="font-size:12px;color:#717171">#ffffff</code></td><td>Guest favorite badge background</td></tr>
|
|
347
|
+
<tr><td class="mono-sm">--lm-color-superhost-bg</td><td><span class="swatch" style="background:#222222"></span><code style="font-size:12px;color:#717171">#222222</code></td><td>Superhost badge background</td></tr>
|
|
348
|
+
<tr><td class="mono-sm">--lm-color-price-marker-bg</td><td><span class="swatch" style="background:#ffffff"></span><code style="font-size:12px;color:#717171">#ffffff</code></td><td>Map marker pill background</td></tr>
|
|
349
|
+
<tr><td class="mono-sm">--lm-color-price-marker-hover-bg</td><td><span class="swatch" style="background:#222222"></span><code style="font-size:12px;color:#717171">#222222</code></td><td>Map marker hover background</td></tr>
|
|
350
|
+
<tr><td class="mono-sm">--lm-radius</td><td><code style="font-size:12px;color:#717171">12px</code></td><td>Card border radius</td></tr>
|
|
351
|
+
<tr><td class="mono-sm">--lm-image-height</td><td><code style="font-size:12px;color:#717171">260px</code></td><td>Carousel image height</td></tr>
|
|
352
|
+
</tbody>
|
|
353
|
+
</table>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<!-- Callbacks -->
|
|
357
|
+
<hr />
|
|
358
|
+
<h2 id="callbacks">Callbacks</h2>
|
|
359
|
+
|
|
360
|
+
<div class="stack stack-12" style="margin-top:8px">
|
|
361
|
+
<div class="cb-card">
|
|
362
|
+
<div class="cb-card-head">
|
|
363
|
+
<code>onFavorite(listing, isFavorited)</code>
|
|
364
|
+
<p>Fired when a user clicks the heart button. Use this to persist favorites to your backend.</p>
|
|
365
|
+
</div>
|
|
366
|
+
<div class="code-block" style="border-radius:0;margin-top:0">
|
|
367
|
+
<div class="code-block-header"><span>javascript</span></div>
|
|
368
|
+
<pre><code>onFavorite: function(listing, isFavorited) {
|
|
369
|
+
fetch('/api/favorites', {
|
|
370
|
+
method: 'POST',
|
|
371
|
+
body: JSON.stringify({
|
|
372
|
+
listingId: listing.id,
|
|
373
|
+
favorited: isFavorited
|
|
374
|
+
})
|
|
375
|
+
});
|
|
376
|
+
}</code></pre>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<div class="cb-card">
|
|
381
|
+
<div class="cb-card-head">
|
|
382
|
+
<code>onListingClick(listing)</code>
|
|
383
|
+
<p>Fired when a user clicks on a listing card. Use this to navigate to a detail page.</p>
|
|
384
|
+
</div>
|
|
385
|
+
<div class="code-block" style="border-radius:0;margin-top:0">
|
|
386
|
+
<div class="code-block-header"><span>javascript</span></div>
|
|
387
|
+
<pre><code>onListingClick: function(listing) {
|
|
388
|
+
window.location.href = '/listing/' + listing.id;
|
|
389
|
+
}</code></pre>
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
|
|
393
|
+
<div class="cb-card">
|
|
394
|
+
<div class="cb-card-head">
|
|
395
|
+
<code>onMapMoveEnd(bounds)</code>
|
|
396
|
+
<p>Fired when the user finishes panning/zooming the map. Use this to fetch listings within the new viewport.</p>
|
|
397
|
+
</div>
|
|
398
|
+
<div class="code-block" style="border-radius:0;margin-top:0">
|
|
399
|
+
<div class="code-block-header"><span>javascript</span></div>
|
|
400
|
+
<pre><code>onMapMoveEnd: function(bounds) {
|
|
401
|
+
// bounds = { north, south, east, west, center, zoom }
|
|
402
|
+
fetch('/api/listings?n=' + bounds.north + '&s=' + bounds.south)
|
|
403
|
+
.then(function(r) { return r.json(); })
|
|
404
|
+
.then(function(data) { widget.setListings(data); });
|
|
405
|
+
}</code></pre>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
<!-- Search Slot -->
|
|
411
|
+
<hr />
|
|
412
|
+
<h2 id="search-slot">Search Slot</h2>
|
|
413
|
+
<p>The <code>renderSearchSlot</code> callback lets you render any search UI into the widget. You receive a container element and can fill it with anything: a custom form, a third-party search library, or even a WordPress shortcode output.</p>
|
|
414
|
+
|
|
415
|
+
<div class="code-block">
|
|
416
|
+
<div class="code-block-header"><span>javascript</span></div>
|
|
417
|
+
<pre><code>ListingsMap.init({
|
|
418
|
+
container: '#app',
|
|
419
|
+
listings: myListings,
|
|
420
|
+
renderSearchSlot: function(containerEl) {
|
|
421
|
+
// Option A: Plain HTML
|
|
422
|
+
containerEl.innerHTML = '<input type="text" placeholder="Search...">';
|
|
423
|
+
|
|
424
|
+
// Option B: Mount a library
|
|
425
|
+
// new MySearchWidget(containerEl);
|
|
426
|
+
|
|
427
|
+
// Option C: jQuery / shortcode output
|
|
428
|
+
// $(containerEl).append($('#my-search-template').html());
|
|
429
|
+
}
|
|
430
|
+
});</code></pre>
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
<div class="callout callout-warn">
|
|
434
|
+
<strong>Tip:</strong> The container element has the CSS class <code>lm-search-slot</code> which adds 16px bottom margin by default. Override it in your stylesheet if needed.
|
|
435
|
+
</div>
|
|
436
|
+
|
|
437
|
+
<!-- WordPress -->
|
|
438
|
+
<hr />
|
|
439
|
+
<h2 id="wordpress">WordPress Plugin</h2>
|
|
440
|
+
<p>Create a WordPress plugin in minutes. Drop the two library files into your plugin folder and use the shortcode.</p>
|
|
441
|
+
|
|
442
|
+
<div class="callout callout-info" style="margin-top:16px">
|
|
443
|
+
<strong>Plugin folder structure:</strong>
|
|
444
|
+
<pre style="margin-top:8px;font-size:13px;line-height:1.6;font-family:monospace">wp-content/plugins/listings-map/
|
|
445
|
+
listings-map.php (plugin file below)
|
|
446
|
+
assets/
|
|
447
|
+
listings-map.js (download above)
|
|
448
|
+
listings-map.css (download above)</pre>
|
|
449
|
+
</div>
|
|
450
|
+
|
|
451
|
+
<div class="code-block">
|
|
452
|
+
<div class="code-block-header"><span>php</span></div>
|
|
453
|
+
<pre><code><?php
|
|
454
|
+
/**
|
|
455
|
+
* Plugin Name: ListingsMap Widget
|
|
456
|
+
* Description: Airbnb-style property listings with interactive map
|
|
457
|
+
* Version: 1.0.0
|
|
458
|
+
*/
|
|
459
|
+
|
|
460
|
+
// 1. Enqueue the library assets
|
|
461
|
+
function lm_enqueue_assets() {
|
|
462
|
+
wp_enqueue_style(
|
|
463
|
+
'listings-map-css',
|
|
464
|
+
plugin_dir_url(__FILE__) . 'assets/listings-map.css',
|
|
465
|
+
array(), '1.0.0'
|
|
466
|
+
);
|
|
467
|
+
wp_enqueue_script(
|
|
468
|
+
'listings-map-js',
|
|
469
|
+
plugin_dir_url(__FILE__) . 'assets/listings-map.js',
|
|
470
|
+
array(), '1.0.0', true
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
add_action('wp_enqueue_scripts', 'lm_enqueue_assets');
|
|
474
|
+
|
|
475
|
+
// 2. Register [listings_map] shortcode
|
|
476
|
+
function lm_shortcode($atts) {
|
|
477
|
+
$atts = shortcode_atts(array(
|
|
478
|
+
'height' => '700px',
|
|
479
|
+
), $atts);
|
|
480
|
+
|
|
481
|
+
$id = 'lm-widget-' . uniqid();
|
|
482
|
+
ob_start();
|
|
483
|
+
?>
|
|
484
|
+
<div id="<?php echo esc_attr($id); ?>"
|
|
485
|
+
style="width:100%; height:<?php echo esc_attr($atts['height']); ?>;">
|
|
486
|
+
</div>
|
|
487
|
+
<script>
|
|
488
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
489
|
+
if (typeof ListingsMap === 'undefined') return;
|
|
490
|
+
|
|
491
|
+
fetch('/wp-json/listings-map/v1/listings')
|
|
492
|
+
.then(function(res) { return res.json(); })
|
|
493
|
+
.then(function(data) {
|
|
494
|
+
ListingsMap.init({
|
|
495
|
+
container: '#<?php echo esc_js($id); ?>',
|
|
496
|
+
listings: data,
|
|
497
|
+
currency: '\u20b1',
|
|
498
|
+
mapOptions: { center: [14.58, 121.05], zoom: 12 }
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
</script>
|
|
503
|
+
<?php
|
|
504
|
+
return ob_get_clean();
|
|
505
|
+
}
|
|
506
|
+
add_shortcode('listings_map', 'lm_shortcode');
|
|
507
|
+
|
|
508
|
+
// 3. REST API endpoint
|
|
509
|
+
function lm_register_api() {
|
|
510
|
+
register_rest_route('listings-map/v1', '/listings', array(
|
|
511
|
+
'methods' => 'GET',
|
|
512
|
+
'callback' => 'lm_get_listings',
|
|
513
|
+
'permission_callback' => '__return_true',
|
|
514
|
+
));
|
|
515
|
+
}
|
|
516
|
+
add_action('rest_api_init', 'lm_register_api');
|
|
517
|
+
|
|
518
|
+
function lm_get_listings() {
|
|
519
|
+
// Replace with your data source (custom post type, ACF, etc.)
|
|
520
|
+
return array(
|
|
521
|
+
array(
|
|
522
|
+
'id' => '1',
|
|
523
|
+
'title' => 'Apartment in Quezon City',
|
|
524
|
+
'subtitle' => 'Condo in Cubao | Sunset & City Lights',
|
|
525
|
+
'details' => '1 bedroom · 1 bed',
|
|
526
|
+
'price' => 13689,
|
|
527
|
+
'pricePeriod' => 'for 5 nights',
|
|
528
|
+
'rating' => 5.0,
|
|
529
|
+
'reviewCount' => 89,
|
|
530
|
+
'badge' => 'Guest favorite',
|
|
531
|
+
'lat' => 14.628,
|
|
532
|
+
'lng' => 121.055,
|
|
533
|
+
'images' => array(
|
|
534
|
+
'https://example.com/photo1.jpg',
|
|
535
|
+
'https://example.com/photo2.jpg',
|
|
536
|
+
),
|
|
537
|
+
),
|
|
538
|
+
);
|
|
539
|
+
}</code></pre>
|
|
540
|
+
</div>
|
|
541
|
+
|
|
542
|
+
<p style="margin-top:16px">Then add the shortcode to any page or post:</p>
|
|
543
|
+
<div class="code-block">
|
|
544
|
+
<div class="code-block-header"><span>wordpress</span></div>
|
|
545
|
+
<pre><code>[listings_map]
|
|
546
|
+
|
|
547
|
+
<!-- With custom height -->
|
|
548
|
+
[listings_map height="500px"]</code></pre>
|
|
549
|
+
</div>
|
|
550
|
+
|
|
551
|
+
<!-- Examples -->
|
|
552
|
+
<hr />
|
|
553
|
+
<h2 id="examples">More Examples</h2>
|
|
554
|
+
|
|
555
|
+
<h3>Dynamic data loading</h3>
|
|
556
|
+
<div class="code-block">
|
|
557
|
+
<div class="code-block-header"><span>javascript</span></div>
|
|
558
|
+
<pre><code>var widget = ListingsMap.init({
|
|
559
|
+
container: '#app',
|
|
560
|
+
listings: [], // Start empty
|
|
561
|
+
currency: '$',
|
|
562
|
+
mapOptions: { center: [40.7, -74.0], zoom: 13 }
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Fetch from your API and update
|
|
566
|
+
fetch('/api/listings')
|
|
567
|
+
.then(function(r) { return r.json(); })
|
|
568
|
+
.then(function(data) {
|
|
569
|
+
widget.setListings(data);
|
|
570
|
+
});</code></pre>
|
|
571
|
+
</div>
|
|
572
|
+
|
|
573
|
+
<h3>Custom map tiles (Mapbox, Stadia, etc.)</h3>
|
|
574
|
+
<div class="code-block">
|
|
575
|
+
<div class="code-block-header"><span>javascript</span></div>
|
|
576
|
+
<pre><code>ListingsMap.init({
|
|
577
|
+
container: '#app',
|
|
578
|
+
listings: myListings,
|
|
579
|
+
tileUrl: 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}',
|
|
580
|
+
tileAttribution: '© Mapbox',
|
|
581
|
+
mapOptions: { center: [51.505, -0.09], zoom: 13 }
|
|
582
|
+
});</code></pre>
|
|
583
|
+
</div>
|
|
584
|
+
|
|
585
|
+
<h3>Load-more on map pan</h3>
|
|
586
|
+
<div class="code-block">
|
|
587
|
+
<div class="code-block-header"><span>javascript</span></div>
|
|
588
|
+
<pre><code>var widget = ListingsMap.init({
|
|
589
|
+
container: '#app',
|
|
590
|
+
listings: initialListings,
|
|
591
|
+
currency: '€',
|
|
592
|
+
mapOptions: { center: [48.85, 2.35], zoom: 13 },
|
|
593
|
+
onMapMoveEnd: function(bounds) {
|
|
594
|
+
var url = '/api/listings?n=' + bounds.north
|
|
595
|
+
+ '&s=' + bounds.south
|
|
596
|
+
+ '&e=' + bounds.east
|
|
597
|
+
+ '&w=' + bounds.west;
|
|
598
|
+
|
|
599
|
+
fetch(url)
|
|
600
|
+
.then(function(r) { return r.json(); })
|
|
601
|
+
.then(function(data) {
|
|
602
|
+
widget.setListings(data);
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
});</code></pre>
|
|
606
|
+
</div>
|
|
607
|
+
|
|
608
|
+
<h3>Using renderSearchSlot with a filter form</h3>
|
|
609
|
+
<div class="code-block">
|
|
610
|
+
<div class="code-block-header"><span>javascript</span></div>
|
|
611
|
+
<pre><code>var widget = ListingsMap.init({
|
|
612
|
+
container: '#app',
|
|
613
|
+
listings: allListings,
|
|
614
|
+
renderSearchSlot: function(containerEl) {
|
|
615
|
+
var input = document.createElement('input');
|
|
616
|
+
input.type = 'text';
|
|
617
|
+
input.placeholder = 'Filter by title...';
|
|
618
|
+
input.style.cssText = 'width:100%;padding:10px 16px;border:1px solid #ddd;border-radius:8px;font-size:14px;';
|
|
619
|
+
input.addEventListener('input', function() {
|
|
620
|
+
var q = input.value.toLowerCase();
|
|
621
|
+
var filtered = allListings.filter(function(l) {
|
|
622
|
+
return l.title.toLowerCase().indexOf(q) !== -1;
|
|
623
|
+
});
|
|
624
|
+
widget.setListings(filtered);
|
|
625
|
+
});
|
|
626
|
+
containerEl.appendChild(input);
|
|
627
|
+
}
|
|
628
|
+
});</code></pre>
|
|
629
|
+
</div>
|
|
630
|
+
|
|
631
|
+
<!-- Footer -->
|
|
632
|
+
<div class="footer">
|
|
633
|
+
<span>ListingsMap v1.0.0</span>
|
|
634
|
+
<a href="index.html">View Live Demo</a>
|
|
635
|
+
</div>
|
|
636
|
+
|
|
637
|
+
</main>
|
|
638
|
+
</div>
|
|
639
|
+
</body>
|
|
640
|
+
</html>
|