react-listing-engine 0.1.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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +161 -0
- package/dist/chunk-2T5PE5MB.js +2 -0
- package/dist/chunk-2VUHLHHX.cjs +2 -0
- package/dist/chunk-35KYEBAC.js +2 -0
- package/dist/chunk-5FBI2WIM.js +2 -0
- package/dist/chunk-5O36WI7H.cjs +2 -0
- package/dist/chunk-6PZHSHU3.js +2 -0
- package/dist/chunk-CHNUE46K.cjs +2 -0
- package/dist/chunk-LCQZIWBO.cjs +2 -0
- package/dist/chunk-N3OZ2HK2.cjs +2 -0
- package/dist/chunk-PFPHWJAA.js +2 -0
- package/dist/components-provider-CF0Mo0B8.d.cts +120 -0
- package/dist/components-provider-CjMxkxrP.d.ts +120 -0
- package/dist/entity-adapter.interface-BDgbSxhq.d.cts +33 -0
- package/dist/entity-adapter.interface-BDgbSxhq.d.ts +33 -0
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +674 -0
- package/dist/index.d.ts +674 -0
- package/dist/index.js +2 -0
- package/dist/listing-app-De6OqpUC.d.ts +199 -0
- package/dist/listing-app-pKMQBuQI.d.cts +199 -0
- package/dist/listing-config-options.interface-ZJYY_ZvR.d.cts +12 -0
- package/dist/listing-config-options.interface-ZJYY_ZvR.d.ts +12 -0
- package/dist/map-provider.interface-BMnwW3ob.d.cts +37 -0
- package/dist/map-provider.interface-BxexMT3X.d.ts +37 -0
- package/dist/maps/google/index.cjs +2 -0
- package/dist/maps/google/index.d.cts +39 -0
- package/dist/maps/google/index.d.ts +39 -0
- package/dist/maps/google/index.js +2 -0
- package/dist/presets/rental/index.cjs +2 -0
- package/dist/presets/rental/index.d.cts +266 -0
- package/dist/presets/rental/index.d.ts +266 -0
- package/dist/presets/rental/index.js +2 -0
- package/dist/shadcn/index.cjs +2 -0
- package/dist/shadcn/index.d.cts +261 -0
- package/dist/shadcn/index.d.ts +261 -0
- package/dist/shadcn/index.js +2 -0
- package/dist/styled/index.cjs +2 -0
- package/dist/styled/index.d.cts +123 -0
- package/dist/styled/index.d.ts +123 -0
- package/dist/styled/index.js +2 -0
- package/dist/styles.css +972 -0
- package/dist/testing/index.cjs +2 -0
- package/dist/testing/index.d.cts +51 -0
- package/dist/testing/index.d.ts +51 -0
- package/dist/testing/index.js +2 -0
- package/dist/url-sync.controller-CsU_QxoS.d.cts +89 -0
- package/dist/url-sync.controller-DX67JivW.d.ts +89 -0
- package/package.json +143 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { E as EntityId, L as LatLng, B as Bounds, b as PageRequest, P as Page, Q as QueryParams } from '../../entity-adapter.interface-BDgbSxhq.cjs';
|
|
2
|
+
import { D as DatasetDefinition, a as FilterControlProps, c as IListingCardProps, h as IListingMarkerProps, b as FilterDefinition, F as FilterRegistry } from '../../components-provider-CF0Mo0B8.cjs';
|
|
3
|
+
import * as react from 'react';
|
|
4
|
+
import { H as HistoryPort, U as UrlSyncController } from '../../url-sync.controller-CsU_QxoS.cjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Deliberately an open string type, not a union -- new business categories
|
|
8
|
+
* (a new row of DATA, e.g. `'pharmacy'`) need zero library/core change to
|
|
9
|
+
* flow through `nearbyBusinessesDataset`. `KNOWN_BUSINESS_CATEGORIES` below
|
|
10
|
+
* is a seeded starter list for consumers building category pickers, not an
|
|
11
|
+
* exhaustive/closed taxonomy.
|
|
12
|
+
*/
|
|
13
|
+
type BusinessCategory = string;
|
|
14
|
+
declare const KNOWN_BUSINESS_CATEGORIES: readonly ["grocery", "shopping", "restaurants", "schools", "hospitals", "parks", "gyms", "cafes"];
|
|
15
|
+
interface BusinessEntity {
|
|
16
|
+
id: EntityId;
|
|
17
|
+
name: string;
|
|
18
|
+
category: BusinessCategory;
|
|
19
|
+
coordinates: LatLng;
|
|
20
|
+
address?: string;
|
|
21
|
+
}
|
|
22
|
+
interface BusinessFilters {
|
|
23
|
+
categories?: BusinessCategory[];
|
|
24
|
+
bounds?: Bounds;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The "port" the CONSUMER implements against their real API -- same shape of
|
|
28
|
+
* seam as `PropertiesApiPort`. `list` is optional: many consumers only ever
|
|
29
|
+
* render businesses as a map layer (`search`) and never page through them in
|
|
30
|
+
* a results list.
|
|
31
|
+
*/
|
|
32
|
+
interface BusinessesApiPort {
|
|
33
|
+
search(filters: BusinessFilters, bounds: Bounds): Promise<BusinessEntity[]>;
|
|
34
|
+
list?(filters: BusinessFilters, page: PageRequest): Promise<Page<BusinessEntity>>;
|
|
35
|
+
}
|
|
36
|
+
interface NearbyBusinessesOptions {
|
|
37
|
+
/** Restrict which categories render; default (omitted) shows everything `search` returns. */
|
|
38
|
+
categories?: BusinessCategory[];
|
|
39
|
+
/** Per-category icon URL -- DATA, not code. New category = new map entry. */
|
|
40
|
+
icons?: Record<string, string>;
|
|
41
|
+
onClick?(business: BusinessEntity): void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Wraps a `BusinessesApiPort` into a `DatasetDefinition` -- the nearby
|
|
45
|
+
* businesses marker layer, proving req #6: this is just a SECOND
|
|
46
|
+
* dataset/marker layer sitting next to `propertiesDataset`, and the category
|
|
47
|
+
* taxonomy is DATA (`opts.categories` / `opts.icons`), not core code.
|
|
48
|
+
*/
|
|
49
|
+
declare function nearbyBusinessesDataset(api: BusinessesApiPort, opts?: NearbyBusinessesOptions): DatasetDefinition<BusinessEntity, BusinessFilters>;
|
|
50
|
+
|
|
51
|
+
/** Unstyled free-text keyword input. */
|
|
52
|
+
declare function KeywordFilterControl({ value, onChange }: FilterControlProps<string>): react.JSX.Element;
|
|
53
|
+
|
|
54
|
+
type PropertyType = 'house' | 'apartment' | 'condo' | 'townhouse' | 'land';
|
|
55
|
+
/**
|
|
56
|
+
* Mapped from the app's `@libs/core-base` `Listing` shape (nested
|
|
57
|
+
* `property.coordinates`) -- not the old flat `/find` DTO. See design doc
|
|
58
|
+
* D6/#11.
|
|
59
|
+
*/
|
|
60
|
+
interface PropertyEntity {
|
|
61
|
+
id: EntityId;
|
|
62
|
+
title: string;
|
|
63
|
+
address?: string;
|
|
64
|
+
coordinates: LatLng;
|
|
65
|
+
price: number;
|
|
66
|
+
bedrooms: number;
|
|
67
|
+
bathrooms: number;
|
|
68
|
+
propertyType: PropertyType;
|
|
69
|
+
imageUrl?: string;
|
|
70
|
+
}
|
|
71
|
+
interface RentalFilters {
|
|
72
|
+
minPrice?: number;
|
|
73
|
+
maxPrice?: number;
|
|
74
|
+
minBeds?: number;
|
|
75
|
+
maxBeds?: number;
|
|
76
|
+
minBaths?: number;
|
|
77
|
+
maxBaths?: number;
|
|
78
|
+
propertyTypes?: PropertyType[];
|
|
79
|
+
keyword?: string;
|
|
80
|
+
bounds?: Bounds;
|
|
81
|
+
}
|
|
82
|
+
/** Generic min/max shape shared by every range-style filter control (price, beds, baths). */
|
|
83
|
+
interface RangeValue {
|
|
84
|
+
min?: number;
|
|
85
|
+
max?: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Compact dropdown for the property-type multi-select -- a native
|
|
90
|
+
* `<details>`/`<summary>` disclosure (no Radix dependency, so this preset
|
|
91
|
+
* package doesn't have to pull one in) instead of the previous
|
|
92
|
+
* always-expanded checkbox list, so the top filter bar (`ListingLayout`'s
|
|
93
|
+
* horizontal row) stays compact regardless of how many property types exist.
|
|
94
|
+
* `<summary>` is the button-like trigger: "Property type" plus a count badge
|
|
95
|
+
* once 1+ types are selected. The panel (absolutely positioned so it doesn't
|
|
96
|
+
* push sibling filter groups around) holds one checkbox per `PropertyType`,
|
|
97
|
+
* unchanged from the previous always-expanded version.
|
|
98
|
+
*
|
|
99
|
+
* `open` is fully CONTROLLED local state, not left to `<details>`'s own
|
|
100
|
+
* built-in toggle -- the trigger's `onClick` calls `event.preventDefault()`
|
|
101
|
+
* (which, per spec, suppresses `<summary>`'s native open/close activation for
|
|
102
|
+
* that click) and flips `open` itself instead. This makes `aria-expanded`
|
|
103
|
+
* always exactly match what's rendered, and makes the open/close behavior
|
|
104
|
+
* independent of a given DOM engine's level of native `<details>` support --
|
|
105
|
+
* it doesn't rely on a `toggle` event firing. Keyboard access is unaffected:
|
|
106
|
+
* `preventDefault()` only suppresses the native toggle side effect, not the
|
|
107
|
+
* click event itself, so Enter/Space on a focused `<summary>` (which the
|
|
108
|
+
* browser turns into a `click`) still reaches this handler. Outside click is
|
|
109
|
+
* NOT handled (native `<details>` doesn't require it either) -- clicking
|
|
110
|
+
* elsewhere leaves the panel open until the trigger (or a checkbox) is
|
|
111
|
+
* clicked again; not fixed here, matching the design brief.
|
|
112
|
+
*/
|
|
113
|
+
declare function PropertyTypeFilterControl({ value, onChange }: FilterControlProps<PropertyType[]>): react.JSX.Element;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Unstyled min/max number inputs for any `RangeValue`-shaped filter (price,
|
|
117
|
+
* beds, baths). Deliberately generic -- no per-domain label prop -- consumers
|
|
118
|
+
* restyle/relabel via a shadcn wrapper or their own `render` override.
|
|
119
|
+
*/
|
|
120
|
+
declare function RangeFilterControl({ value, onChange }: FilterControlProps<RangeValue>): react.JSX.Element;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The "port" the CONSUMER implements against their real API (e.g. the app's
|
|
124
|
+
* `LocaListingApi`) -- this preset never imports an HTTP client itself. `list`
|
|
125
|
+
* feeds the paginated results list, `search` feeds the map (bounds-scoped,
|
|
126
|
+
* no pagination), `getById` is optional (deep-link / selection lookups).
|
|
127
|
+
*/
|
|
128
|
+
interface PropertiesApiPort {
|
|
129
|
+
list(filters: RentalFilters, page: PageRequest): Promise<Page<PropertyEntity>>;
|
|
130
|
+
search(filters: RentalFilters, bounds: Bounds): Promise<PropertyEntity[]>;
|
|
131
|
+
getById?(id: EntityId): Promise<PropertyEntity>;
|
|
132
|
+
}
|
|
133
|
+
interface PropertiesDatasetOptions {
|
|
134
|
+
onClick?(property: PropertyEntity): void;
|
|
135
|
+
iconUrl?(property: PropertyEntity): string;
|
|
136
|
+
/**
|
|
137
|
+
* Builds the `AdvancedMarkerElement` content for a property's map pin.
|
|
138
|
+
* Defaults to `defaultPriceMarkerElement` (a green teardrop price pin
|
|
139
|
+
* showing `formatRentalPrice(property.price)`) when omitted -- pass this
|
|
140
|
+
* to replace the pin entirely with a custom marker.
|
|
141
|
+
*/
|
|
142
|
+
element?(property: PropertyEntity): HTMLElement;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Default `marker.element` builder: a raw DOM teardrop price PIN
|
|
146
|
+
* (`formatRentalPrice(entity.price)` in a rounded pill, plus a small
|
|
147
|
+
* rotated-square pointer tail so the marker's visual tip lands on the exact
|
|
148
|
+
* coordinate -- Rentler-style) for the map's `AdvancedMarkerElement` content
|
|
149
|
+
* -- `GoogleMapsProvider` prefers `element` over `iconUrl` (see
|
|
150
|
+
* `google-maps.provider.ts`) when both are set.
|
|
151
|
+
*
|
|
152
|
+
* Built with `document.createElement` rather than React because map markers
|
|
153
|
+
* are raw DOM nodes, not React elements (rendering the injected `Marker`
|
|
154
|
+
* React component INTO a real marker via a portal is a documented future
|
|
155
|
+
* enhancement -- see `listing-map.tsx`'s doc comment). Since this element is
|
|
156
|
+
* constructed at runtime, Tailwind's build-time content scanner can't see
|
|
157
|
+
* the `className` strings below to generate their CSS, so the classes are
|
|
158
|
+
* kept (any consumer app that DOES happen to scan this file's literal
|
|
159
|
+
* strings, e.g. via a broad `content` glob, gets real utility classes) but
|
|
160
|
+
* are backed by the same values set directly as inline styles on both the
|
|
161
|
+
* pill and its pointer child, so the pin renders correctly with zero
|
|
162
|
+
* build-time cooperation from the consumer's Tailwind config. Mirrors
|
|
163
|
+
* `.rle-pin`/`.rle-pin::after` in `src/styled/styles.css` (the `/styled`
|
|
164
|
+
* equivalent for `StyledMarker`) -- a pseudo-element isn't an option here
|
|
165
|
+
* since inline styles can't target `::after` on a JS-constructed node, so
|
|
166
|
+
* the pointer is a real child element instead.
|
|
167
|
+
*/
|
|
168
|
+
declare function defaultPriceMarkerElement(property: PropertyEntity): HTMLElement;
|
|
169
|
+
/**
|
|
170
|
+
* Wraps a `PropertiesApiPort` into a `DatasetDefinition` -- the properties
|
|
171
|
+
* marker layer. Row -> `MapPoint` mapping reads `entity.coordinates`
|
|
172
|
+
* directly; no other core/library change is needed to add this layer to a
|
|
173
|
+
* listing.
|
|
174
|
+
*/
|
|
175
|
+
declare function propertiesDataset(api: PropertiesApiPort, opts?: PropertiesDatasetOptions): DatasetDefinition<PropertyEntity, RentalFilters>;
|
|
176
|
+
|
|
177
|
+
/** Formats a whole-dollar rental price as USD, e.g. `2800` -> `"$2,800"`. */
|
|
178
|
+
declare function formatRentalPrice(price: number): string;
|
|
179
|
+
/**
|
|
180
|
+
* Real-estate `Card` slot matching the production `/find` widget's property
|
|
181
|
+
* card layout: image -> title -> address -> "Type · N bd · N ba" -> price.
|
|
182
|
+
* `item` is `unknown` at this layer (the engine is entity-erased, same
|
|
183
|
+
* reasoning as `DefaultCard` -- see `src/shadcn/default-card.tsx`), so it's
|
|
184
|
+
* read DEFENSIVELY via a cast to `PropertyEntity`, never assumed to be the
|
|
185
|
+
* caller's real `TEntity`. Every field is optional and simply omitted from
|
|
186
|
+
* the render when absent.
|
|
187
|
+
*
|
|
188
|
+
* Mirrors `DefaultCard`'s interactive/non-interactive split: the whole card
|
|
189
|
+
* is a `button` (keyboard operable, `aria-pressed`) when `onSelect` is
|
|
190
|
+
* given, otherwise a non-interactive `<article>` with the same visual
|
|
191
|
+
* classes minus the button/focus/hover-shadow semantics.
|
|
192
|
+
*/
|
|
193
|
+
declare function PropertyCard({ item, selected, onSelect }: IListingCardProps): react.JSX.Element;
|
|
194
|
+
/**
|
|
195
|
+
* Real-estate `Marker` slot: a green teardrop price pin (Rentler-style) for
|
|
196
|
+
* a map point -- a rounded pill plus a small rotated-square tail so the
|
|
197
|
+
* marker's visual tip lands on the exact coordinate. Same defensive
|
|
198
|
+
* view-model reasoning as `DefaultMarker` -- `point.entity` is the raw row
|
|
199
|
+
* (`unknown` at this layer), read defensively via a cast. Tailwind-styled by
|
|
200
|
+
* design (not `.rle-*`) -- see `properties-dataset.ts`'s `defaultPriceMarkerElement`
|
|
201
|
+
* for the raw-DOM equivalent used by the map's own marker content, and
|
|
202
|
+
* `StyledMarker` for the `.rle-pin` (Tailwind-free `/styled`) equivalent.
|
|
203
|
+
*/
|
|
204
|
+
declare function PropertyMarker({ point }: IListingMarkerProps): react.JSX.Element;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Porting today's `/find` widget's fixed filter set (price, beds/baths,
|
|
208
|
+
* property-type, keyword) to `FilterDefinition`s. Each `render` is an
|
|
209
|
+
* unstyled control from `./controls/*`; consumers restyle/replace via
|
|
210
|
+
* `FilterRegistry.replace()`.
|
|
211
|
+
*
|
|
212
|
+
* `render`'s `ComponentType<FilterControlProps<TValue>>` is (correctly)
|
|
213
|
+
* contravariant in `TValue`, so this heterogeneous array of concretely-typed
|
|
214
|
+
* defs can't assign element-by-element into `FilterDefinition<RentalFilters>[]`
|
|
215
|
+
* (TValue defaults to `unknown`) without a cast -- `FilterRegistry` hits the
|
|
216
|
+
* identical shape and resolves it by storing defs as `..., any>` internally
|
|
217
|
+
* (see `core/registries/filter-registry.ts`); same fix here, scoped to this
|
|
218
|
+
* one array literal. The exported type stays `FilterDefinition<RentalFilters>[]`,
|
|
219
|
+
* `any`-free.
|
|
220
|
+
*/
|
|
221
|
+
declare const rentalFilters: FilterDefinition<RentalFilters>[];
|
|
222
|
+
/** `withFilters(withRentalFilters())` registers all 5 rental filters, in order, on a fresh or existing `FilterRegistry`. */
|
|
223
|
+
declare function withRentalFilters(): (registry: FilterRegistry<RentalFilters>) => void;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* `RentalFilters` -> `QueryParams` for URL sync. Short, stable query keys
|
|
227
|
+
* (`minPrice`, `maxPrice`, `minBeds`, `maxBeds`, `minBaths`, `maxBaths`,
|
|
228
|
+
* `type`, `q`) -- `type` is a comma-joined `PropertyType[]`, `keyword` maps
|
|
229
|
+
* to `q`. `bounds` is intentionally NOT serialized: it's map-driven,
|
|
230
|
+
* high-frequency, transient viewport state, not a shareable filter.
|
|
231
|
+
*/
|
|
232
|
+
declare function rentalFiltersToQuery(filters: RentalFilters): QueryParams;
|
|
233
|
+
/**
|
|
234
|
+
* Inverse of `rentalFiltersToQuery`. Numbers are parsed NaN-safely (an
|
|
235
|
+
* unparsable value is omitted, not coerced to `NaN`); `type` is split on
|
|
236
|
+
* `,` and filtered down to known `PropertyType` values (an unrecognized
|
|
237
|
+
* value in the URL -- hand-edited or stale -- is silently dropped rather
|
|
238
|
+
* than propagated into `RentalFilters`). Missing/empty query keys produce
|
|
239
|
+
* an omitted (not `undefined`-valued) filter field.
|
|
240
|
+
*/
|
|
241
|
+
declare function rentalFiltersFromQuery(query: QueryParams): RentalFilters;
|
|
242
|
+
interface RentalUrlSyncOptions {
|
|
243
|
+
history?: HistoryPort;
|
|
244
|
+
mode?: 'replace' | 'push';
|
|
245
|
+
hydrateOnStart?: boolean;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Convenience factory wiring `rentalFiltersToQuery`/`rentalFiltersFromQuery`
|
|
249
|
+
* onto a `UrlSyncController<RentalFilters>`, ready to pass as
|
|
250
|
+
* `composeListingProviders`' `urlSync` option (or straight into
|
|
251
|
+
* `UrlSyncController.start(engine)`).
|
|
252
|
+
*
|
|
253
|
+
* OPTIONAL helper, not the primary URL-sync path for the main-entry
|
|
254
|
+
* `ListingApp` (`~/styled/listing-app`) — that component takes
|
|
255
|
+
* `initialFilters`/`onFiltersChange` instead and never touches
|
|
256
|
+
* `window.history` itself (see `UrlSyncController`'s doc comment). For that
|
|
257
|
+
* event-based API, use `rentalFiltersToQuery`/`rentalFiltersFromQuery`
|
|
258
|
+
* directly: `initialFilters={rentalFiltersFromQuery(query)}` and
|
|
259
|
+
* `onFiltersChange={f => history.replaceState(null, '', '?' +
|
|
260
|
+
* new URLSearchParams(rentalFiltersToQuery(f)))}`. `rentalUrlSync()` remains
|
|
261
|
+
* the right choice when wiring `/shadcn`'s `ListingApp.urlSync` or a
|
|
262
|
+
* hand-built engine where the library owning `window.history` is desired.
|
|
263
|
+
*/
|
|
264
|
+
declare function rentalUrlSync(opts?: RentalUrlSyncOptions): UrlSyncController<RentalFilters>;
|
|
265
|
+
|
|
266
|
+
export { type BusinessCategory, type BusinessEntity, type BusinessFilters, type BusinessesApiPort, KNOWN_BUSINESS_CATEGORIES, KeywordFilterControl, type NearbyBusinessesOptions, type PropertiesApiPort, type PropertiesDatasetOptions, PropertyCard, type PropertyEntity, PropertyMarker, type PropertyType, PropertyTypeFilterControl, RangeFilterControl, type RangeValue, type RentalFilters, type RentalUrlSyncOptions, defaultPriceMarkerElement, formatRentalPrice, nearbyBusinessesDataset, propertiesDataset, rentalFilters, rentalFiltersFromQuery, rentalFiltersToQuery, rentalUrlSync, withRentalFilters };
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { E as EntityId, L as LatLng, B as Bounds, b as PageRequest, P as Page, Q as QueryParams } from '../../entity-adapter.interface-BDgbSxhq.js';
|
|
2
|
+
import { D as DatasetDefinition, a as FilterControlProps, c as IListingCardProps, h as IListingMarkerProps, b as FilterDefinition, F as FilterRegistry } from '../../components-provider-CjMxkxrP.js';
|
|
3
|
+
import * as react from 'react';
|
|
4
|
+
import { H as HistoryPort, U as UrlSyncController } from '../../url-sync.controller-DX67JivW.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Deliberately an open string type, not a union -- new business categories
|
|
8
|
+
* (a new row of DATA, e.g. `'pharmacy'`) need zero library/core change to
|
|
9
|
+
* flow through `nearbyBusinessesDataset`. `KNOWN_BUSINESS_CATEGORIES` below
|
|
10
|
+
* is a seeded starter list for consumers building category pickers, not an
|
|
11
|
+
* exhaustive/closed taxonomy.
|
|
12
|
+
*/
|
|
13
|
+
type BusinessCategory = string;
|
|
14
|
+
declare const KNOWN_BUSINESS_CATEGORIES: readonly ["grocery", "shopping", "restaurants", "schools", "hospitals", "parks", "gyms", "cafes"];
|
|
15
|
+
interface BusinessEntity {
|
|
16
|
+
id: EntityId;
|
|
17
|
+
name: string;
|
|
18
|
+
category: BusinessCategory;
|
|
19
|
+
coordinates: LatLng;
|
|
20
|
+
address?: string;
|
|
21
|
+
}
|
|
22
|
+
interface BusinessFilters {
|
|
23
|
+
categories?: BusinessCategory[];
|
|
24
|
+
bounds?: Bounds;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The "port" the CONSUMER implements against their real API -- same shape of
|
|
28
|
+
* seam as `PropertiesApiPort`. `list` is optional: many consumers only ever
|
|
29
|
+
* render businesses as a map layer (`search`) and never page through them in
|
|
30
|
+
* a results list.
|
|
31
|
+
*/
|
|
32
|
+
interface BusinessesApiPort {
|
|
33
|
+
search(filters: BusinessFilters, bounds: Bounds): Promise<BusinessEntity[]>;
|
|
34
|
+
list?(filters: BusinessFilters, page: PageRequest): Promise<Page<BusinessEntity>>;
|
|
35
|
+
}
|
|
36
|
+
interface NearbyBusinessesOptions {
|
|
37
|
+
/** Restrict which categories render; default (omitted) shows everything `search` returns. */
|
|
38
|
+
categories?: BusinessCategory[];
|
|
39
|
+
/** Per-category icon URL -- DATA, not code. New category = new map entry. */
|
|
40
|
+
icons?: Record<string, string>;
|
|
41
|
+
onClick?(business: BusinessEntity): void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Wraps a `BusinessesApiPort` into a `DatasetDefinition` -- the nearby
|
|
45
|
+
* businesses marker layer, proving req #6: this is just a SECOND
|
|
46
|
+
* dataset/marker layer sitting next to `propertiesDataset`, and the category
|
|
47
|
+
* taxonomy is DATA (`opts.categories` / `opts.icons`), not core code.
|
|
48
|
+
*/
|
|
49
|
+
declare function nearbyBusinessesDataset(api: BusinessesApiPort, opts?: NearbyBusinessesOptions): DatasetDefinition<BusinessEntity, BusinessFilters>;
|
|
50
|
+
|
|
51
|
+
/** Unstyled free-text keyword input. */
|
|
52
|
+
declare function KeywordFilterControl({ value, onChange }: FilterControlProps<string>): react.JSX.Element;
|
|
53
|
+
|
|
54
|
+
type PropertyType = 'house' | 'apartment' | 'condo' | 'townhouse' | 'land';
|
|
55
|
+
/**
|
|
56
|
+
* Mapped from the app's `@libs/core-base` `Listing` shape (nested
|
|
57
|
+
* `property.coordinates`) -- not the old flat `/find` DTO. See design doc
|
|
58
|
+
* D6/#11.
|
|
59
|
+
*/
|
|
60
|
+
interface PropertyEntity {
|
|
61
|
+
id: EntityId;
|
|
62
|
+
title: string;
|
|
63
|
+
address?: string;
|
|
64
|
+
coordinates: LatLng;
|
|
65
|
+
price: number;
|
|
66
|
+
bedrooms: number;
|
|
67
|
+
bathrooms: number;
|
|
68
|
+
propertyType: PropertyType;
|
|
69
|
+
imageUrl?: string;
|
|
70
|
+
}
|
|
71
|
+
interface RentalFilters {
|
|
72
|
+
minPrice?: number;
|
|
73
|
+
maxPrice?: number;
|
|
74
|
+
minBeds?: number;
|
|
75
|
+
maxBeds?: number;
|
|
76
|
+
minBaths?: number;
|
|
77
|
+
maxBaths?: number;
|
|
78
|
+
propertyTypes?: PropertyType[];
|
|
79
|
+
keyword?: string;
|
|
80
|
+
bounds?: Bounds;
|
|
81
|
+
}
|
|
82
|
+
/** Generic min/max shape shared by every range-style filter control (price, beds, baths). */
|
|
83
|
+
interface RangeValue {
|
|
84
|
+
min?: number;
|
|
85
|
+
max?: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Compact dropdown for the property-type multi-select -- a native
|
|
90
|
+
* `<details>`/`<summary>` disclosure (no Radix dependency, so this preset
|
|
91
|
+
* package doesn't have to pull one in) instead of the previous
|
|
92
|
+
* always-expanded checkbox list, so the top filter bar (`ListingLayout`'s
|
|
93
|
+
* horizontal row) stays compact regardless of how many property types exist.
|
|
94
|
+
* `<summary>` is the button-like trigger: "Property type" plus a count badge
|
|
95
|
+
* once 1+ types are selected. The panel (absolutely positioned so it doesn't
|
|
96
|
+
* push sibling filter groups around) holds one checkbox per `PropertyType`,
|
|
97
|
+
* unchanged from the previous always-expanded version.
|
|
98
|
+
*
|
|
99
|
+
* `open` is fully CONTROLLED local state, not left to `<details>`'s own
|
|
100
|
+
* built-in toggle -- the trigger's `onClick` calls `event.preventDefault()`
|
|
101
|
+
* (which, per spec, suppresses `<summary>`'s native open/close activation for
|
|
102
|
+
* that click) and flips `open` itself instead. This makes `aria-expanded`
|
|
103
|
+
* always exactly match what's rendered, and makes the open/close behavior
|
|
104
|
+
* independent of a given DOM engine's level of native `<details>` support --
|
|
105
|
+
* it doesn't rely on a `toggle` event firing. Keyboard access is unaffected:
|
|
106
|
+
* `preventDefault()` only suppresses the native toggle side effect, not the
|
|
107
|
+
* click event itself, so Enter/Space on a focused `<summary>` (which the
|
|
108
|
+
* browser turns into a `click`) still reaches this handler. Outside click is
|
|
109
|
+
* NOT handled (native `<details>` doesn't require it either) -- clicking
|
|
110
|
+
* elsewhere leaves the panel open until the trigger (or a checkbox) is
|
|
111
|
+
* clicked again; not fixed here, matching the design brief.
|
|
112
|
+
*/
|
|
113
|
+
declare function PropertyTypeFilterControl({ value, onChange }: FilterControlProps<PropertyType[]>): react.JSX.Element;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Unstyled min/max number inputs for any `RangeValue`-shaped filter (price,
|
|
117
|
+
* beds, baths). Deliberately generic -- no per-domain label prop -- consumers
|
|
118
|
+
* restyle/relabel via a shadcn wrapper or their own `render` override.
|
|
119
|
+
*/
|
|
120
|
+
declare function RangeFilterControl({ value, onChange }: FilterControlProps<RangeValue>): react.JSX.Element;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The "port" the CONSUMER implements against their real API (e.g. the app's
|
|
124
|
+
* `LocaListingApi`) -- this preset never imports an HTTP client itself. `list`
|
|
125
|
+
* feeds the paginated results list, `search` feeds the map (bounds-scoped,
|
|
126
|
+
* no pagination), `getById` is optional (deep-link / selection lookups).
|
|
127
|
+
*/
|
|
128
|
+
interface PropertiesApiPort {
|
|
129
|
+
list(filters: RentalFilters, page: PageRequest): Promise<Page<PropertyEntity>>;
|
|
130
|
+
search(filters: RentalFilters, bounds: Bounds): Promise<PropertyEntity[]>;
|
|
131
|
+
getById?(id: EntityId): Promise<PropertyEntity>;
|
|
132
|
+
}
|
|
133
|
+
interface PropertiesDatasetOptions {
|
|
134
|
+
onClick?(property: PropertyEntity): void;
|
|
135
|
+
iconUrl?(property: PropertyEntity): string;
|
|
136
|
+
/**
|
|
137
|
+
* Builds the `AdvancedMarkerElement` content for a property's map pin.
|
|
138
|
+
* Defaults to `defaultPriceMarkerElement` (a green teardrop price pin
|
|
139
|
+
* showing `formatRentalPrice(property.price)`) when omitted -- pass this
|
|
140
|
+
* to replace the pin entirely with a custom marker.
|
|
141
|
+
*/
|
|
142
|
+
element?(property: PropertyEntity): HTMLElement;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Default `marker.element` builder: a raw DOM teardrop price PIN
|
|
146
|
+
* (`formatRentalPrice(entity.price)` in a rounded pill, plus a small
|
|
147
|
+
* rotated-square pointer tail so the marker's visual tip lands on the exact
|
|
148
|
+
* coordinate -- Rentler-style) for the map's `AdvancedMarkerElement` content
|
|
149
|
+
* -- `GoogleMapsProvider` prefers `element` over `iconUrl` (see
|
|
150
|
+
* `google-maps.provider.ts`) when both are set.
|
|
151
|
+
*
|
|
152
|
+
* Built with `document.createElement` rather than React because map markers
|
|
153
|
+
* are raw DOM nodes, not React elements (rendering the injected `Marker`
|
|
154
|
+
* React component INTO a real marker via a portal is a documented future
|
|
155
|
+
* enhancement -- see `listing-map.tsx`'s doc comment). Since this element is
|
|
156
|
+
* constructed at runtime, Tailwind's build-time content scanner can't see
|
|
157
|
+
* the `className` strings below to generate their CSS, so the classes are
|
|
158
|
+
* kept (any consumer app that DOES happen to scan this file's literal
|
|
159
|
+
* strings, e.g. via a broad `content` glob, gets real utility classes) but
|
|
160
|
+
* are backed by the same values set directly as inline styles on both the
|
|
161
|
+
* pill and its pointer child, so the pin renders correctly with zero
|
|
162
|
+
* build-time cooperation from the consumer's Tailwind config. Mirrors
|
|
163
|
+
* `.rle-pin`/`.rle-pin::after` in `src/styled/styles.css` (the `/styled`
|
|
164
|
+
* equivalent for `StyledMarker`) -- a pseudo-element isn't an option here
|
|
165
|
+
* since inline styles can't target `::after` on a JS-constructed node, so
|
|
166
|
+
* the pointer is a real child element instead.
|
|
167
|
+
*/
|
|
168
|
+
declare function defaultPriceMarkerElement(property: PropertyEntity): HTMLElement;
|
|
169
|
+
/**
|
|
170
|
+
* Wraps a `PropertiesApiPort` into a `DatasetDefinition` -- the properties
|
|
171
|
+
* marker layer. Row -> `MapPoint` mapping reads `entity.coordinates`
|
|
172
|
+
* directly; no other core/library change is needed to add this layer to a
|
|
173
|
+
* listing.
|
|
174
|
+
*/
|
|
175
|
+
declare function propertiesDataset(api: PropertiesApiPort, opts?: PropertiesDatasetOptions): DatasetDefinition<PropertyEntity, RentalFilters>;
|
|
176
|
+
|
|
177
|
+
/** Formats a whole-dollar rental price as USD, e.g. `2800` -> `"$2,800"`. */
|
|
178
|
+
declare function formatRentalPrice(price: number): string;
|
|
179
|
+
/**
|
|
180
|
+
* Real-estate `Card` slot matching the production `/find` widget's property
|
|
181
|
+
* card layout: image -> title -> address -> "Type · N bd · N ba" -> price.
|
|
182
|
+
* `item` is `unknown` at this layer (the engine is entity-erased, same
|
|
183
|
+
* reasoning as `DefaultCard` -- see `src/shadcn/default-card.tsx`), so it's
|
|
184
|
+
* read DEFENSIVELY via a cast to `PropertyEntity`, never assumed to be the
|
|
185
|
+
* caller's real `TEntity`. Every field is optional and simply omitted from
|
|
186
|
+
* the render when absent.
|
|
187
|
+
*
|
|
188
|
+
* Mirrors `DefaultCard`'s interactive/non-interactive split: the whole card
|
|
189
|
+
* is a `button` (keyboard operable, `aria-pressed`) when `onSelect` is
|
|
190
|
+
* given, otherwise a non-interactive `<article>` with the same visual
|
|
191
|
+
* classes minus the button/focus/hover-shadow semantics.
|
|
192
|
+
*/
|
|
193
|
+
declare function PropertyCard({ item, selected, onSelect }: IListingCardProps): react.JSX.Element;
|
|
194
|
+
/**
|
|
195
|
+
* Real-estate `Marker` slot: a green teardrop price pin (Rentler-style) for
|
|
196
|
+
* a map point -- a rounded pill plus a small rotated-square tail so the
|
|
197
|
+
* marker's visual tip lands on the exact coordinate. Same defensive
|
|
198
|
+
* view-model reasoning as `DefaultMarker` -- `point.entity` is the raw row
|
|
199
|
+
* (`unknown` at this layer), read defensively via a cast. Tailwind-styled by
|
|
200
|
+
* design (not `.rle-*`) -- see `properties-dataset.ts`'s `defaultPriceMarkerElement`
|
|
201
|
+
* for the raw-DOM equivalent used by the map's own marker content, and
|
|
202
|
+
* `StyledMarker` for the `.rle-pin` (Tailwind-free `/styled`) equivalent.
|
|
203
|
+
*/
|
|
204
|
+
declare function PropertyMarker({ point }: IListingMarkerProps): react.JSX.Element;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Porting today's `/find` widget's fixed filter set (price, beds/baths,
|
|
208
|
+
* property-type, keyword) to `FilterDefinition`s. Each `render` is an
|
|
209
|
+
* unstyled control from `./controls/*`; consumers restyle/replace via
|
|
210
|
+
* `FilterRegistry.replace()`.
|
|
211
|
+
*
|
|
212
|
+
* `render`'s `ComponentType<FilterControlProps<TValue>>` is (correctly)
|
|
213
|
+
* contravariant in `TValue`, so this heterogeneous array of concretely-typed
|
|
214
|
+
* defs can't assign element-by-element into `FilterDefinition<RentalFilters>[]`
|
|
215
|
+
* (TValue defaults to `unknown`) without a cast -- `FilterRegistry` hits the
|
|
216
|
+
* identical shape and resolves it by storing defs as `..., any>` internally
|
|
217
|
+
* (see `core/registries/filter-registry.ts`); same fix here, scoped to this
|
|
218
|
+
* one array literal. The exported type stays `FilterDefinition<RentalFilters>[]`,
|
|
219
|
+
* `any`-free.
|
|
220
|
+
*/
|
|
221
|
+
declare const rentalFilters: FilterDefinition<RentalFilters>[];
|
|
222
|
+
/** `withFilters(withRentalFilters())` registers all 5 rental filters, in order, on a fresh or existing `FilterRegistry`. */
|
|
223
|
+
declare function withRentalFilters(): (registry: FilterRegistry<RentalFilters>) => void;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* `RentalFilters` -> `QueryParams` for URL sync. Short, stable query keys
|
|
227
|
+
* (`minPrice`, `maxPrice`, `minBeds`, `maxBeds`, `minBaths`, `maxBaths`,
|
|
228
|
+
* `type`, `q`) -- `type` is a comma-joined `PropertyType[]`, `keyword` maps
|
|
229
|
+
* to `q`. `bounds` is intentionally NOT serialized: it's map-driven,
|
|
230
|
+
* high-frequency, transient viewport state, not a shareable filter.
|
|
231
|
+
*/
|
|
232
|
+
declare function rentalFiltersToQuery(filters: RentalFilters): QueryParams;
|
|
233
|
+
/**
|
|
234
|
+
* Inverse of `rentalFiltersToQuery`. Numbers are parsed NaN-safely (an
|
|
235
|
+
* unparsable value is omitted, not coerced to `NaN`); `type` is split on
|
|
236
|
+
* `,` and filtered down to known `PropertyType` values (an unrecognized
|
|
237
|
+
* value in the URL -- hand-edited or stale -- is silently dropped rather
|
|
238
|
+
* than propagated into `RentalFilters`). Missing/empty query keys produce
|
|
239
|
+
* an omitted (not `undefined`-valued) filter field.
|
|
240
|
+
*/
|
|
241
|
+
declare function rentalFiltersFromQuery(query: QueryParams): RentalFilters;
|
|
242
|
+
interface RentalUrlSyncOptions {
|
|
243
|
+
history?: HistoryPort;
|
|
244
|
+
mode?: 'replace' | 'push';
|
|
245
|
+
hydrateOnStart?: boolean;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Convenience factory wiring `rentalFiltersToQuery`/`rentalFiltersFromQuery`
|
|
249
|
+
* onto a `UrlSyncController<RentalFilters>`, ready to pass as
|
|
250
|
+
* `composeListingProviders`' `urlSync` option (or straight into
|
|
251
|
+
* `UrlSyncController.start(engine)`).
|
|
252
|
+
*
|
|
253
|
+
* OPTIONAL helper, not the primary URL-sync path for the main-entry
|
|
254
|
+
* `ListingApp` (`~/styled/listing-app`) — that component takes
|
|
255
|
+
* `initialFilters`/`onFiltersChange` instead and never touches
|
|
256
|
+
* `window.history` itself (see `UrlSyncController`'s doc comment). For that
|
|
257
|
+
* event-based API, use `rentalFiltersToQuery`/`rentalFiltersFromQuery`
|
|
258
|
+
* directly: `initialFilters={rentalFiltersFromQuery(query)}` and
|
|
259
|
+
* `onFiltersChange={f => history.replaceState(null, '', '?' +
|
|
260
|
+
* new URLSearchParams(rentalFiltersToQuery(f)))}`. `rentalUrlSync()` remains
|
|
261
|
+
* the right choice when wiring `/shadcn`'s `ListingApp.urlSync` or a
|
|
262
|
+
* hand-built engine where the library owning `window.history` is desired.
|
|
263
|
+
*/
|
|
264
|
+
declare function rentalUrlSync(opts?: RentalUrlSyncOptions): UrlSyncController<RentalFilters>;
|
|
265
|
+
|
|
266
|
+
export { type BusinessCategory, type BusinessEntity, type BusinessFilters, type BusinessesApiPort, KNOWN_BUSINESS_CATEGORIES, KeywordFilterControl, type NearbyBusinessesOptions, type PropertiesApiPort, type PropertiesDatasetOptions, PropertyCard, type PropertyEntity, PropertyMarker, type PropertyType, PropertyTypeFilterControl, RangeFilterControl, type RangeValue, type RentalFilters, type RentalUrlSyncOptions, defaultPriceMarkerElement, formatRentalPrice, nearbyBusinessesDataset, propertiesDataset, rentalFilters, rentalFiltersFromQuery, rentalFiltersToQuery, rentalUrlSync, withRentalFilters };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import{a as P}from"../../chunk-5FBI2WIM.js";import{a as h}from"../../chunk-6PZHSHU3.js";import{a as x}from"../../chunk-35KYEBAC.js";var Y=["grocery","shopping","restaurants","schools","hospitals","parks","gyms","cafes"];function _(e,t){let n=e.list;return{id:"businesses",adapter:{list:n?(r,o)=>n(r,o):async()=>({items:[],nextCursor:null}),getPoints:(r,o)=>e.search(r,o).then(s=>s.filter(i=>!t?.categories||t.categories.includes(i.category))).then(s=>s.map(i=>({id:i.id,position:i.coordinates,entity:i})))},marker:{iconUrl:r=>t?.icons?.[r.category]??"",onClick:t?.onClick},clustering:{maxZoom:15},visible:()=>!0}}import{jsx as C}from"react/jsx-runtime";function b({value:e,onChange:t}){return C("input",{type:"search",value:e,onChange:r=>{t(r.target.value)},"aria-label":"Keyword",placeholder:"Search keyword",className:"h-9 w-full rounded-md border border-border bg-background px-2 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"})}import{useState as E}from"react";import{jsx as m,jsxs as u}from"react/jsx-runtime";var k=["house","apartment","condo","townhouse","land"],N={house:"House",apartment:"Apartment",condo:"Condo",townhouse:"Townhouse",land:"Land"};function F({value:e,onChange:t}){let[n,r]=E(!1),o=i=>l=>{t(l.target.checked?[...e,i]:e.filter(c=>c!==i))};return u("details",{open:n,className:"group relative",children:[u("summary",{onClick:i=>{i.preventDefault(),r(l=>!l)},"aria-expanded":n,className:"flex h-9 w-fit cursor-pointer list-none items-center gap-1.5 rounded-md border border-border bg-background px-3 text-sm text-foreground motion-safe:transition-colors hover:bg-foreground/[0.06] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&::-webkit-details-marker]:hidden",children:["Property type",e.length>0&&m("span",{className:"inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1 text-xs font-semibold tabular-nums text-primary-foreground",children:e.length}),m("svg",{"aria-hidden":"true",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round",className:"size-3.5 shrink-0 text-muted-foreground motion-safe:transition-transform group-open:rotate-180",children:m("path",{d:"m6 9 6 6 6-6"})})]}),u("fieldset",{className:"absolute z-10 mt-1.5 flex w-48 flex-col gap-1.5 rounded-md border border-border bg-popover p-3 text-popover-foreground shadow-md",children:[m("legend",{className:"sr-only",children:"Property type"}),k.map(i=>u("label",{className:"flex items-center gap-2 text-sm",children:[m("input",{type:"checkbox",checked:e.includes(i),onChange:o(i),className:"size-4 rounded border-border accent-primary"}),N[i]]},i))]})]})}import{jsx as g,jsxs as T}from"react/jsx-runtime";function R(e){if(e==="")return;let t=Number(e);return Number.isNaN(t)?void 0:t}function y({value:e,onChange:t}){let n=s=>{t({...e,min:R(s.target.value)})},r=s=>{t({...e,max:R(s.target.value)})},o="h-9 w-full rounded-md border border-border bg-background px-2 text-sm tabular-nums text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";return T("div",{className:"flex items-center gap-2",children:[g("input",{type:"number",inputMode:"numeric","aria-label":"Minimum",placeholder:"Min",value:e.min??"",onChange:n,className:o}),g("span",{"aria-hidden":"true",className:"text-muted-foreground",children:"-"}),g("input",{type:"number",inputMode:"numeric","aria-label":"Maximum",placeholder:"Max",value:e.max??"",onChange:r,className:o})]})}import{Fragment as w,jsx as a,jsxs as d}from"react/jsx-runtime";function f(e){return new Intl.NumberFormat("en-US",{style:"currency",currency:"USD",maximumFractionDigits:0}).format(e)}function v(e){return e.length?`${e[0].toUpperCase()}${e.slice(1)}`:e}function J({item:e,selected:t,onSelect:n}){let r=e,o=h("flex w-full flex-col overflow-hidden rounded-[10px] border border-border bg-card text-left text-card-foreground","motion-safe:transition-shadow",n&&"hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",t&&"ring-2 ring-ring"),s=d(w,{children:[r?.imageUrl?a("img",{src:r.imageUrl,alt:r.title??"",className:"aspect-[4/3] w-full rounded-t-[10px] object-cover"}):a("div",{className:"aspect-[4/3] w-full rounded-t-[10px] bg-muted","aria-hidden":"true"}),d("div",{className:"flex flex-1 flex-col gap-1 p-3",children:[r?.title&&a("span",{className:"text-base font-semibold",children:r.title}),r?.address&&a("span",{className:"text-sm text-muted-foreground",children:r.address}),r?.propertyType!=null&&r.bedrooms!=null&&r.bathrooms!=null&&d("span",{className:"text-sm text-muted-foreground",children:[v(r.propertyType)," \xB7 ",r.bedrooms," bd \xB7 ",r.bathrooms," ba"]}),r?.price!=null&&d("span",{className:"mt-1 font-bold tabular-nums",children:[f(r.price)," ",a("span",{className:"text-sm font-normal text-muted-foreground",children:"/mo"})]})]})]});return n?a("button",{type:"button",onClick:n,"aria-pressed":t??!1,className:o,children:s}):a("article",{className:o,children:s})}function ee({point:e}){let t=e.entity;return d("span",{className:"relative inline-flex items-center whitespace-nowrap rounded-full bg-primary px-2.5 py-1 text-xs font-bold tabular-nums text-primary-foreground shadow",children:[t?.price!=null?f(t.price):"",a("span",{"aria-hidden":"true",className:"absolute -bottom-[3px] left-1/2 h-2 w-2 -translate-x-1/2 rotate-45 rounded-br-[2px] bg-primary"})]})}function S(e){let t=document.createElement("div");t.className="relative inline-flex items-center whitespace-nowrap rounded-full bg-primary px-2.5 py-1 text-xs font-bold text-primary-foreground shadow",t.textContent=f(e.price),t.style.position="relative",t.style.display="inline-flex",t.style.alignItems="center",t.style.whiteSpace="nowrap",t.style.borderRadius="9999px",t.style.backgroundColor="var(--primary, #16a34a)",t.style.color="var(--primary-foreground, #ffffff)",t.style.padding="4px 10px",t.style.fontSize="12px",t.style.fontWeight="700",t.style.fontFamily="inherit",t.style.boxShadow="0 1px 2px rgba(0, 0, 0, 0.3)";let n=document.createElement("span");return n.className="absolute -bottom-[3px] left-1/2 h-2 w-2 -translate-x-1/2 rotate-45 rounded-br-[2px] bg-primary",n.style.position="absolute",n.style.left="50%",n.style.bottom="-3px",n.style.width="8px",n.style.height="8px",n.style.borderRadius="0 0 2px 0",n.style.backgroundColor="var(--primary, #16a34a)",n.style.transform="translateX(-50%) rotate(45deg)",t.appendChild(n),t}function ie(e,t){let n=e.getById;return{id:"properties",adapter:{list:(r,o)=>e.list(r,o),getPoints:(r,o)=>e.search(r,o).then(s=>s.map(i=>({id:i.id,position:i.coordinates,entity:i}))),getById:n?r=>n(r):void 0},marker:{iconUrl:t?.iconUrl,element:t?.element??S,onClick:t?.onClick},clustering:{maxZoom:14}}}var D={key:"price",order:10,label:"Price",render:y,toParams:e=>({minPrice:e.min,maxPrice:e.max}),fromParams:e=>({min:e.minPrice,max:e.maxPrice}),isActive:e=>e.minPrice!=null||e.maxPrice!=null},I={key:"beds",order:20,label:"Bedrooms",render:y,toParams:e=>({minBeds:e.min,maxBeds:e.max}),fromParams:e=>({min:e.minBeds,max:e.maxBeds}),isActive:e=>e.minBeds!=null||e.maxBeds!=null},M={key:"baths",order:30,label:"Bathrooms",render:y,toParams:e=>({minBaths:e.min,maxBaths:e.max}),fromParams:e=>({min:e.minBaths,max:e.maxBaths}),isActive:e=>e.minBaths!=null||e.maxBaths!=null},L={key:"propertyType",order:40,label:"Property type",render:F,toParams:e=>({propertyTypes:e.length?e:void 0}),fromParams:e=>e.propertyTypes??[],isActive:e=>(e.propertyTypes?.length??0)>0},O={key:"keyword",order:50,label:"Keyword",render:b,toParams:e=>({keyword:e||void 0}),fromParams:e=>e.keyword??"",isActive:e=>!!e.keyword},U=[D,I,M,L,O];function pe(){return e=>{U.forEach(t=>e.add(t))}}var A=["house","apartment","condo","townhouse","land"];function V(e){return A.includes(e)}function p(e){if(e===void 0||e==="")return;let t=Number(e);return Number.isNaN(t)?void 0:t}function H(e){let t={};return e.minPrice!=null&&(t.minPrice=String(e.minPrice)),e.maxPrice!=null&&(t.maxPrice=String(e.maxPrice)),e.minBeds!=null&&(t.minBeds=String(e.minBeds)),e.maxBeds!=null&&(t.maxBeds=String(e.maxBeds)),e.minBaths!=null&&(t.minBaths=String(e.minBaths)),e.maxBaths!=null&&(t.maxBaths=String(e.maxBaths)),e.propertyTypes?.length&&(t.type=e.propertyTypes.join(",")),e.keyword&&(t.q=e.keyword),t}function Q(e){let t={},n=p(e.minPrice);n!==void 0&&(t.minPrice=n);let r=p(e.maxPrice);r!==void 0&&(t.maxPrice=r);let o=p(e.minBeds);o!==void 0&&(t.minBeds=o);let s=p(e.maxBeds);s!==void 0&&(t.maxBeds=s);let i=p(e.minBaths);i!==void 0&&(t.minBaths=i);let l=p(e.maxBaths);l!==void 0&&(t.maxBaths=l);let c=e.type?.split(",").map(B=>B.trim()).filter(V);return c?.length&&(t.propertyTypes=c),e.q&&(t.keyword=e.q),t}function ue(e={}){let t=e.history??new P({mode:e.mode});return new x({history:t,toQuery:H,toFilters:Q,hydrateOnStart:e.hydrateOnStart})}export{Y as KNOWN_BUSINESS_CATEGORIES,b as KeywordFilterControl,J as PropertyCard,ee as PropertyMarker,F as PropertyTypeFilterControl,y as RangeFilterControl,S as defaultPriceMarkerElement,f as formatRentalPrice,_ as nearbyBusinessesDataset,ie as propertiesDataset,U as rentalFilters,Q as rentalFiltersFromQuery,H as rentalFiltersToQuery,ue as rentalUrlSync,pe as withRentalFilters};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }"use client";
|
|
2
|
+
var _chunk5O36WI7Hcjs = require('../chunk-5O36WI7H.cjs');var _chunkLCQZIWBOcjs = require('../chunk-LCQZIWBO.cjs');var _chunk2VUHLHHXcjs = require('../chunk-2VUHLHHX.cjs');var _jsxruntime = require('react/jsx-runtime');function oe(e){return typeof e!="number"?e:new Intl.NumberFormat("en-US",{style:"currency",currency:"USD",maximumFractionDigits:0}).format(e)}function w({item:e,selected:i,onSelect:t}){let r=_nullishCoalesce(e, () => ({})),l=_chunkLCQZIWBOcjs.a.call(void 0, "flex w-full flex-col overflow-hidden rounded-lg border bg-card text-left text-card-foreground shadow-sm","motion-safe:transition-shadow",t&&"hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",i&&"ring-2 ring-ring"),a=_jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment,{children:[r.imageUrl&&_jsxruntime.jsx.call(void 0, "img",{src:r.imageUrl,alt:_nullishCoalesce(r.title, () => ("")),className:"aspect-video w-full rounded-t-lg object-cover"}),_jsxruntime.jsxs.call(void 0, "div",{className:"flex flex-1 flex-col gap-1 p-3",children:[_jsxruntime.jsxs.call(void 0, "div",{className:"flex items-start justify-between gap-2",children:[r.title&&_jsxruntime.jsx.call(void 0, "span",{className:"font-medium",children:r.title}),r.badge&&_jsxruntime.jsx.call(void 0, "span",{className:"shrink-0 rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground",children:r.badge})]}),r.subtitle&&_jsxruntime.jsx.call(void 0, "span",{className:"text-sm text-muted-foreground",children:r.subtitle}),r.price!=null&&_jsxruntime.jsx.call(void 0, "span",{className:"mt-1 font-semibold tabular-nums",children:oe(r.price)})]})]});return t?_jsxruntime.jsx.call(void 0, "button",{type:"button",onClick:t,"aria-pressed":_nullishCoalesce(i, () => (!1)),className:l,children:a}):_jsxruntime.jsx.call(void 0, "article",{className:l,children:a})}function C(e){return _jsxruntime.jsxs.call(void 0, "div",{role:"status",className:"flex flex-col items-center justify-center gap-2 px-4 py-12 text-center text-muted-foreground",children:[_jsxruntime.jsxs.call(void 0, "svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:1.5,strokeLinecap:"round",strokeLinejoin:"round",className:"h-8 w-8","aria-hidden":"true",children:[_jsxruntime.jsx.call(void 0, "circle",{cx:"11",cy:"11",r:"7"}),_jsxruntime.jsx.call(void 0, "path",{d:"m21 21-4.3-4.3"})]}),_jsxruntime.jsx.call(void 0, "p",{className:"text-sm font-medium text-foreground",children:"No results"}),_jsxruntime.jsx.call(void 0, "p",{className:"text-xs text-muted-foreground",children:"Try adjusting your filters or search terms."})]})}function I({children:e}){return _jsxruntime.jsx.call(void 0, "div",{className:"flex flex-col gap-3",children:e})}function D(e){return _jsxruntime.jsx.call(void 0, "div",{className:"flex flex-col gap-3 p-3",role:"status","aria-busy":"true","aria-label":"Loading results",children:Array.from({length:3}).map((i,t)=>_jsxruntime.jsxs.call(void 0, "div",{className:"flex flex-col gap-2 motion-safe:animate-pulse",children:[_jsxruntime.jsx.call(void 0, "div",{className:"aspect-video w-full rounded-md bg-muted"}),_jsxruntime.jsx.call(void 0, "div",{className:"h-4 w-2/3 rounded-md bg-muted"}),_jsxruntime.jsx.call(void 0, "div",{className:"h-3 w-1/3 rounded-md bg-muted"})]},t))})}function M({point:e}){let i=_nullishCoalesce(e.entity, () => ({}));return _jsxruntime.jsx.call(void 0, "span",{className:"rounded-full border bg-background px-2 py-0.5 text-xs font-medium tabular-nums shadow-sm",children:_nullishCoalesce(i.price, () => (""))})}function F({entity:e,onClose:i}){let t=_nullishCoalesce(e, () => ({}));return _jsxruntime.jsxs.call(void 0, "div",{className:"relative w-64 rounded-lg border bg-popover p-3 text-popover-foreground shadow-md",role:"group","aria-label":"Location details",children:[_jsxruntime.jsx.call(void 0, "button",{type:"button",onClick:i,"aria-label":"Close",className:"absolute right-2 top-2 rounded-md p-1 text-muted-foreground motion-safe:transition-colors hover:bg-foreground/[0.06] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",children:_jsxruntime.jsxs.call(void 0, "svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round",className:"h-4 w-4","aria-hidden":"true",children:[_jsxruntime.jsx.call(void 0, "path",{d:"M18 6 6 18"}),_jsxruntime.jsx.call(void 0, "path",{d:"m6 6 12 12"})]})}),_jsxruntime.jsxs.call(void 0, "div",{className:"pr-6",children:[t.title&&_jsxruntime.jsx.call(void 0, "div",{className:"font-medium",children:t.title}),t.subtitle&&_jsxruntime.jsx.call(void 0, "div",{className:"text-sm text-muted-foreground",children:t.subtitle}),t.price!=null&&_jsxruntime.jsx.call(void 0, "div",{className:"mt-1 font-semibold tabular-nums",children:t.price})]})]})}function S({count:e,total:i}){let t=i!=null&&i!==e?`${e} of ${i} results`:`${e} results`;return _jsxruntime.jsx.call(void 0, "div",{className:"text-sm tabular-nums text-muted-foreground",children:t})}function T({value:e,onChange:i,placeholder:t}){return _jsxruntime.jsx.call(void 0, "input",{type:"search",value:e,placeholder:t,onChange:r=>i(r.target.value),"aria-label":_nullishCoalesce(t, () => ("Search")),className:_chunkLCQZIWBOcjs.a.call(void 0, "h-9 w-full rounded-md border bg-background px-3 text-sm text-foreground placeholder:text-muted-foreground","focus-visible:border-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring")})}function R({children:e}){return _jsxruntime.jsx.call(void 0, "aside",{className:"flex w-full flex-col gap-4 p-4 md:border-r md:border-border",children:e})}function A({children:e}){return _jsxruntime.jsx.call(void 0, "div",{className:"flex items-center gap-2 border-b p-2",children:e})}var g={Card:w,Marker:M,Popup:F,Sidebar:R,FilterPanel:I,Search:T,Empty:C,Loading:D,ResultHeader:S,Toolbar:A};function fe({children:e}){return _jsxruntime.jsx.call(void 0, _chunk5O36WI7Hcjs.p,{...g,children:e})}var _react = require('react');var ve=_jsxruntime.jsx.call(void 0, "p",{className:"px-4 text-center text-sm text-muted-foreground",children:"Map unavailable - provide a Google Maps API key"});function E({search:e,className:i,toolbarEnd:t,autoFetch:r=!0,mapCenter:l,mapZoom:a}){let p=_chunk5O36WI7Hcjs.s.call(void 0, ),{Search:u}=_chunk5O36WI7Hcjs.q.call(void 0, ),[f,y]=_react.useState.call(void 0, "list");_react.useEffect.call(void 0, ()=>{r!==!1&&p.applyFilters({})},[p,r]);let h=f==="map"?"hidden md:block":"block",P=f==="list"?"hidden md:block":"block";return _jsxruntime.jsxs.call(void 0, "div",{className:_chunkLCQZIWBOcjs.a.call(void 0, "flex h-full min-h-0 w-full flex-col bg-background text-foreground",i),"data-slot":"listing-layout",children:[_jsxruntime.jsxs.call(void 0, "div",{className:"sticky top-0 z-10 flex flex-wrap items-end gap-3 border-b border-border bg-background p-3","data-slot":"listing-layout-filter-bar",children:[e&&_jsxruntime.jsx.call(void 0, "div",{className:"w-full min-w-0 sm:w-auto sm:max-w-xs sm:flex-1",children:_jsxruntime.jsx.call(void 0, u,{value:e.value,onChange:e.onChange,placeholder:e.placeholder})}),_jsxruntime.jsx.call(void 0, _chunk5O36WI7Hcjs.v,{className:"flex flex-wrap items-end gap-3",groupClassName:"min-w-0"}),_jsxruntime.jsxs.call(void 0, "div",{className:"ml-auto flex items-center gap-3",children:[_jsxruntime.jsx.call(void 0, _chunk5O36WI7Hcjs.A,{}),t]})]}),_jsxruntime.jsx.call(void 0, "div",{className:"flex items-center justify-center gap-1 border-b border-border bg-background p-2 md:hidden","data-slot":"listing-layout-mobile-toggle",children:_jsxruntime.jsx.call(void 0, "div",{role:"group","aria-label":"View",className:"inline-flex rounded-md border border-border p-0.5",children:[{view:"list",label:"List"},{view:"map",label:"Map"}].map(({view:d,label:n})=>_jsxruntime.jsx.call(void 0, "button",{type:"button","aria-pressed":f===d,onClick:()=>y(d),className:_chunkLCQZIWBOcjs.a.call(void 0, "rounded-[5px] px-3 py-1 text-sm font-medium motion-safe:transition-colors",f===d?"bg-primary text-primary-foreground":"text-muted-foreground hover:bg-foreground/[0.06] hover:text-foreground"),children:n},d))})}),_jsxruntime.jsxs.call(void 0, "div",{className:"grid min-h-0 flex-1 md:grid-cols-[minmax(340px,42%)_1fr]","data-slot":"listing-layout-split",children:[_jsxruntime.jsxs.call(void 0, "div",{className:_chunkLCQZIWBOcjs.a.call(void 0, "min-h-0 overflow-y-auto p-3",h),"data-slot":"listing-layout-list",children:[_jsxruntime.jsx.call(void 0, _chunk5O36WI7Hcjs.x,{className:"grid content-start gap-3 [grid-template-columns:repeat(auto-fill,minmax(200px,1fr))]"}),_jsxruntime.jsx.call(void 0, "div",{className:"mt-4 flex justify-center",children:_jsxruntime.jsx.call(void 0, _chunk5O36WI7Hcjs.z,{})})]}),_jsxruntime.jsx.call(void 0, "div",{className:_chunkLCQZIWBOcjs.a.call(void 0, "min-h-0 md:sticky md:top-0",P),"data-slot":"listing-layout-map",children:_jsxruntime.jsx.call(void 0, _chunk5O36WI7Hcjs.y,{center:l,zoom:a,fallback:ve})})]})]})}function ee(e){return"provider"in e}function ye(e){let i=e!=null&&!ee(e),t=i?e.apiKey:void 0,r=i?e.mapId:void 0,[l,a]=_react.useState.call(void 0, ()=>!e||ee(e)?{ready:!0,provider:_optionalChain([e, 'optionalAccess', _2 => _2.provider])}:{ready:!1});return _react.useEffect.call(void 0, ()=>{if(!t)return;let p=!1;return Promise.resolve().then(() => _interopRequireWildcard(require("../maps/google/index.cjs"))).then(({googleProvider:u})=>{p||a({ready:!0,provider:u({apiKey:t,mapId:r})})}),()=>{p=!0}},[t,r]),l}function he(e){let{datasets:i,filters:t,map:r,components:l,urlSync:a,initialFilters:p,config:u,className:f,search:y,autoFetch:h}=e,{ready:P,provider:d}=ye(r);if(!P)return null;let n=[];for(let ie of i)n.push(_chunk5O36WI7Hcjs.k.call(void 0, ie));t&&n.push(_chunk5O36WI7Hcjs.l.call(void 0, t)),d&&n.push(_chunk5O36WI7Hcjs.j.call(void 0, d)),a instanceof _chunk2VUHLHHXcjs.a&&n.push(_chunk5O36WI7Hcjs.m.call(void 0, a)),p&&n.push(_chunk5O36WI7Hcjs.n.call(void 0, p)),u&&n.push(_chunk5O36WI7Hcjs.i.call(void 0, u));let te=_chunk5O36WI7Hcjs.h.call(void 0, ...n),re={...g,...l};return _jsxruntime.jsx.call(void 0, _chunk5O36WI7Hcjs.B,{...te,children:_jsxruntime.jsx.call(void 0, _chunk5O36WI7Hcjs.p,{...re,children:_jsxruntime.jsx.call(void 0, E,{className:f,search:y,autoFetch:h,mapCenter:_optionalChain([r, 'optionalAccess', _3 => _3.center]),mapZoom:_optionalChain([r, 'optionalAccess', _4 => _4.zoom])})})})}exports.DefaultCard = w; exports.DefaultEmpty = C; exports.DefaultFilterPanel = I; exports.DefaultLoading = D; exports.DefaultMarker = M; exports.DefaultPopup = F; exports.DefaultResultHeader = S; exports.DefaultSearch = T; exports.DefaultSidebar = R; exports.DefaultToolbar = A; exports.ListingApp = he; exports.ListingComponentsProviderWithDefaults = fe; exports.ListingLayout = E; exports.cn = _chunkLCQZIWBOcjs.a; exports.shadcnDefaultComponents = g;
|