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
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import{a as O,p as m}from"./chunk-2T5PE5MB.js";import{A as N,B as U,a as y,b as P,c as L,d as h,e as b,f as x,g as C,h as I,i as k,j as q,k as v,l as B,m as S,n as A,o as F,p as T,q as p,r as w,s,t as i,u as H,v as z,w as D,x as G,y as J,z as K}from"./chunk-PFPHWJAA.js";import{a as R}from"./chunk-5FBI2WIM.js";import{a as Q}from"./chunk-35KYEBAC.js";var d=(o=>(o.Paged="paged",o.Infinite="infinite",o))(d||{});var f=(r=>(r.FiltersChanged="FiltersChanged",r.ResultsLoaded="ResultsLoaded",r.PointClicked="PointClicked",r.BoundsChanged="BoundsChanged",r.LayerToggled="LayerToggled",r))(f||{});var u=class{query;listeners=new Set;constructor(t={}){this.query={...t}}getQuery(){return{...this.query}}setQuery(t){this.query={...t},this.notify()}subscribe(t){return this.listeners.add(t),()=>{this.listeners.delete(t)}}notify(){for(let t of[...this.listeners])t()}};import{jsx as c}from"react/jsx-runtime";function Z({children:e}){let{Toolbar:t}=p();return c(t,{children:e})}import{useCallback as l}from"react";function tt(){let e=s(),t=i(),o=l(a=>e.loadPoints(a),[e]),n=l((a,r)=>e.selectPoint(a,r),[e]);return{bounds:t.bounds,points:t.points,loadPoints:o,selectPoint:n}}import{useCallback as g}from"react";function it(e){let t=s(),o=i(),n=g(()=>t.toggleLayer(e),[t,e]);return{visible:o.layers[e]??!0,points:o.points[e]??[],toggle:n}}export{R as BrowserHistoryPort,L as DatasetRegistry,P as FilterRegistry,m as ListingApp,T as ListingComponentsProvider,x as ListingConfig,C as ListingEngine,w as ListingEngineContext,f as ListingEventType,z as ListingFilters,G as ListingList,J as ListingMap,K as ListingPagination,U as ListingProvider,N as ListingResultHeader,y as ListingStore,Z as ListingToolbar,u as MemoryHistoryPort,d as PaginationMode,h as TypedEmitter,Q as UrlSyncController,I as composeListingProviders,b as listingDefaultConfig,s as useListing,p as useListingComponents,O as useListingEvent,H as useListingFilters,it as useListingLayer,tt as useListingMap,D as useListingResults,i as useListingState,k as withConfig,v as withDataset,B as withFilters,A as withInitialFilters,q as withMap,F as withPrimaryDataset,S as withUrlSync};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { L as LatLng } from './entity-adapter.interface-BDgbSxhq.js';
|
|
4
|
+
import { M as MapProvider } from './map-provider.interface-BxexMT3X.js';
|
|
5
|
+
import { D as DatasetDefinition, F as FilterRegistry, d as IListingComponents } from './components-provider-CjMxkxrP.js';
|
|
6
|
+
import { I as IListingConfigOptions } from './listing-config-options.interface-ZJYY_ZvR.js';
|
|
7
|
+
|
|
8
|
+
type BottomNavView = 'list' | 'map';
|
|
9
|
+
interface IBottomNavAction {
|
|
10
|
+
label: string;
|
|
11
|
+
icon?: ReactNode;
|
|
12
|
+
onClick(): void;
|
|
13
|
+
}
|
|
14
|
+
interface IBottomNavProps {
|
|
15
|
+
view: BottomNavView;
|
|
16
|
+
onViewChange(view: BottomNavView): void;
|
|
17
|
+
onFiltersClick(): void;
|
|
18
|
+
action?: IBottomNavAction;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Mobile-only bottom navigation bar (`.rle-bottom-nav`, hidden at 768px+ by
|
|
22
|
+
* CSS -- see `styles.css`): a **Filters** button that opens the mobile
|
|
23
|
+
* `BottomSheet`, an optional caller-supplied `action` button (e.g. "Add"),
|
|
24
|
+
* and a **List | Map** segmented toggle (`.rle-viewtoggle`) that drives which
|
|
25
|
+
* of `StyledListingLayout`'s two full-area panels is visible. No lucide/icon
|
|
26
|
+
* library dependency -- every icon here is a small inline SVG, matching the
|
|
27
|
+
* rest of `/styled`'s zero-extra-dependency policy.
|
|
28
|
+
*/
|
|
29
|
+
declare function BottomNav({ view, onViewChange, onFiltersClick, action }: IBottomNavProps): react.JSX.Element;
|
|
30
|
+
|
|
31
|
+
interface IStyledListingLayoutProps {
|
|
32
|
+
/** Optional search box -- same shape/reasoning as `/shadcn`'s `ListingLayout.search`; omitted entirely when not passed. */
|
|
33
|
+
search?: {
|
|
34
|
+
value: string;
|
|
35
|
+
onChange: (value: string) => void;
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
};
|
|
38
|
+
/** Extra content rendered at the end of the desktop filter bar, alongside `ListingResultHeader` (e.g. a sort control). */
|
|
39
|
+
toolbarEnd?: ReactNode;
|
|
40
|
+
/** Optional bottom-nav action button (e.g. "Add"), forwarded verbatim to `<BottomNav action={...} />`. Omit to render just Filters + the List|Map toggle. */
|
|
41
|
+
mobileAction?: IBottomNavAction;
|
|
42
|
+
/** Whether the layout fetches the first page itself on mount (`engine.applyFilters({})`). Defaults to `true`. */
|
|
43
|
+
autoFetch?: boolean;
|
|
44
|
+
/** Forwarded verbatim to `<ListingMap center={mapCenter} />` -- see that component's "Auto-fit" doc comment. */
|
|
45
|
+
mapCenter?: LatLng;
|
|
46
|
+
/** Forwarded verbatim to `<ListingMap zoom={mapZoom} />`. */
|
|
47
|
+
mapZoom?: number;
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Full, responsive, Tailwind-free listing experience -- the `/styled`
|
|
52
|
+
* counterpart to `/shadcn`'s `ListingLayout`, built from the same
|
|
53
|
+
* structure-only compound components (`~/react`) plus the injected
|
|
54
|
+
* `Styled*` slot components, but with a richer mobile experience: a bottom
|
|
55
|
+
* nav (`BottomNav`) and a bottom sheet (`BottomSheet`) for filters, instead
|
|
56
|
+
* of `/shadcn`'s inline mobile toggle bar. Every class used here is one of
|
|
57
|
+
* the `.rle-*` layout classes added to `styles.css` alongside this file --
|
|
58
|
+
* no Tailwind, no inline styles, no other stylesheet required.
|
|
59
|
+
*
|
|
60
|
+
* STRUCTURE (`.rle-app`):
|
|
61
|
+
* - `.rle-filter-bar` (desktop only, hidden below 768px by CSS): the
|
|
62
|
+
* optional `Search` slot, `<ListingFilters>` laid out as a wrapping row
|
|
63
|
+
* (`className="rle-filters-row"`, `groupClassName="rle-filter-group"`),
|
|
64
|
+
* and a trailing `.rle-filter-bar__end` cluster with `<ListingResultHeader>`
|
|
65
|
+
* + `toolbarEnd`.
|
|
66
|
+
* - `.rle-body.rle-split`: a CSS grid from 768px up (list column floors at
|
|
67
|
+
* 340px, caps at 42%; map takes the rest); below that, a single full-area
|
|
68
|
+
* panel with exactly one of `.rle-list`/`.rle-map` visible at a time via
|
|
69
|
+
* `data-mobile-view` (see `styles.css`'s mobile media query). Both regions
|
|
70
|
+
* stay mounted at ALL times regardless of viewport/toggle state -- only
|
|
71
|
+
* their visibility flips -- so neither `ListingList` nor `ListingMap`
|
|
72
|
+
* remounts (and re-triggers its own mount effects) on toggle or resize.
|
|
73
|
+
* - `<BottomNav>`: mobile-only (CSS-hidden at 768px+). Wired to the same
|
|
74
|
+
* `mobileView` state as the CSS toggle, `mobileAction`, and opens the
|
|
75
|
+
* filters `<BottomSheet>`.
|
|
76
|
+
* - `<BottomSheet title="Filters">`: the SAME `<ListingFilters>` component,
|
|
77
|
+
* stacked vertically (`className="rle-filter-stack"`) for the sheet's
|
|
78
|
+
* narrower body, plus a footer with "Clear all" (resets every registered
|
|
79
|
+
* filter to `undefined` via `engine.filters.list()`'s keys, routed through
|
|
80
|
+
* `engine.applyFilters` -- the one bulk mutator, same as `ListingFilters`
|
|
81
|
+
* itself uses) and "Show N results" (just closes the sheet -- every filter
|
|
82
|
+
* control already applies live via its own `onChange`, so there is nothing
|
|
83
|
+
* left to commit).
|
|
84
|
+
* - Fetches the first page itself on mount (`engine.applyFilters({})`) by
|
|
85
|
+
* default, exactly like `/shadcn`'s `ListingLayout` -- pass `autoFetch={false}`
|
|
86
|
+
* to opt out and drive the first fetch yourself.
|
|
87
|
+
*/
|
|
88
|
+
declare function StyledListingLayout({ search, toolbarEnd, mobileAction, autoFetch, mapCenter, mapZoom, className, }: IStyledListingLayoutProps): react.JSX.Element;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Same two-shape `map` prop as `/shadcn`'s `ListingApp` (see that file's
|
|
92
|
+
* `useResolvedMap` doc comment for why the `{ apiKey }` shorthand is resolved
|
|
93
|
+
* via a dynamic `import()` rather than a static one -- the reasoning is
|
|
94
|
+
* identical here). Re-declared rather than imported from `~/shadcn`: the two
|
|
95
|
+
* styled adapters are SIBLINGS, not a hierarchy -- `/styled` never depends on
|
|
96
|
+
* `/shadcn` (or vice versa), each is self-contained against `~/core`/`~/react`.
|
|
97
|
+
*/
|
|
98
|
+
type ListingAppMapProp = {
|
|
99
|
+
provider: MapProvider;
|
|
100
|
+
center?: LatLng;
|
|
101
|
+
zoom?: number;
|
|
102
|
+
} | {
|
|
103
|
+
apiKey: string;
|
|
104
|
+
mapId?: string;
|
|
105
|
+
center?: LatLng;
|
|
106
|
+
zoom?: number;
|
|
107
|
+
};
|
|
108
|
+
interface ListingAppProps<TFilters> {
|
|
109
|
+
/**
|
|
110
|
+
* One or more marker-layer datasets; the FIRST entry is the primary
|
|
111
|
+
* dataset (drives the results list + pagination) -- same "insertion
|
|
112
|
+
* order" rule `composeListingProviders`/`withDataset` already follow.
|
|
113
|
+
* Entity-erased (`any`, not `TEntity`) for the same reason
|
|
114
|
+
* `DatasetRegistry<unknown, TFilters>` is: one array can legitimately hold
|
|
115
|
+
* heterogeneous layers (a properties dataset and a businesses dataset have
|
|
116
|
+
* different entity types) -- see `compose-listing-providers.ts`'s
|
|
117
|
+
* `withDataset` doc comment.
|
|
118
|
+
*/
|
|
119
|
+
datasets: DatasetDefinition<any, TFilters>[];
|
|
120
|
+
/** e.g. `withRentalFilters()`, or any `(reg) => { reg.add(...); }` callback -- forwarded verbatim to `withFilters`. */
|
|
121
|
+
filters?: (reg: FilterRegistry<TFilters>) => void;
|
|
122
|
+
/** A ready `MapProvider`, or `{ apiKey, mapId? }` to build a `googleProvider` internally. Omit for no map (the styled layout shows a "Map unavailable" fallback). */
|
|
123
|
+
map?: ListingAppMapProp;
|
|
124
|
+
/** Slot overrides, merged OVER the `/styled` defaults -- see this file's doc comment for how the merge works. */
|
|
125
|
+
components?: Partial<IListingComponents>;
|
|
126
|
+
/**
|
|
127
|
+
* Hydrates the engine's initial filters on mount -- typically read from
|
|
128
|
+
* the CONSUMER's own URL (e.g. `rentalFiltersFromQuery(new
|
|
129
|
+
* URLSearchParams(window.location.search))`), but any source works. Half
|
|
130
|
+
* of the event-based URL API this component exposes: `ListingApp` never
|
|
131
|
+
* reads `window.location` itself, it only accepts filters as a prop.
|
|
132
|
+
*/
|
|
133
|
+
initialFilters?: TFilters;
|
|
134
|
+
/**
|
|
135
|
+
* EVENT OUT: fires with the engine's current filters (`TFilters`, not the
|
|
136
|
+
* store's `DeepReadonly` wrapper) every time `ListingEventType.FiltersChanged`
|
|
137
|
+
* fires on the underlying engine -- i.e. on every `engine.applyFilters(...)`
|
|
138
|
+
* call, INCLUDING the one `StyledListingLayout` makes on mount when
|
|
139
|
+
* `autoFetch` is on (a `{}` patch that still round-trips through
|
|
140
|
+
* `currentFilters()` and re-emits the very `initialFilters` this component
|
|
141
|
+
* was given). That first call is intentionally NOT suppressed: a
|
|
142
|
+
* consumer's handler is expected to write filters back to a URL via
|
|
143
|
+
* `history.replaceState` (or a router's equivalent), and replacing the URL
|
|
144
|
+
* with the SAME query string it already hydrated from is a no-op, not a
|
|
145
|
+
* visible echo -- so "just always emit" is simpler than tracking a
|
|
146
|
+
* first-call flag for no behavioral gain. This is the OTHER half of the
|
|
147
|
+
* event-based URL API: `ListingApp` (and the engine underneath it) never
|
|
148
|
+
* touches `window.history` -- the CONSUMER owns routing, driven by this
|
|
149
|
+
* callback plus `initialFilters` above. `UrlSyncController`/`BrowserHistoryPort`
|
|
150
|
+
* (`~/core`, `~/react`) remain exported as optional, lower-level helpers
|
|
151
|
+
* for consumers who want the library to own `window.history` directly, but
|
|
152
|
+
* they are no longer the primary/recommended path for new integrations.
|
|
153
|
+
*/
|
|
154
|
+
onFiltersChange?: (filters: TFilters) => void;
|
|
155
|
+
/** The mobile bottom-nav "Add" action -- forwarded verbatim to `StyledListingLayout`/`BottomNav`. */
|
|
156
|
+
mobileAction?: IBottomNavAction;
|
|
157
|
+
search?: IStyledListingLayoutProps['search'];
|
|
158
|
+
toolbarEnd?: IStyledListingLayoutProps['toolbarEnd'];
|
|
159
|
+
config?: Partial<IListingConfigOptions>;
|
|
160
|
+
autoFetch?: boolean;
|
|
161
|
+
className?: string;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Turnkey, batteries-included, Tailwind-free entry point -- the `/styled`
|
|
165
|
+
* counterpart to `/shadcn`'s `ListingApp`, and (per `src/index.ts`) the
|
|
166
|
+
* package's MAIN-ENTRY default: `import { ListingApp } from
|
|
167
|
+
* 'react-listing-engine'` gives you this component. Pass datasets + filters +
|
|
168
|
+
* a map + component overrides, and `ListingApp` composes
|
|
169
|
+
* `composeListingProviders(...)`, `<ListingProvider>`, the `/styled` defaults,
|
|
170
|
+
* and `<StyledListingLayout>` for you.
|
|
171
|
+
*
|
|
172
|
+
* `TEntity` still can't be inferred from these props (`datasets` narrows it,
|
|
173
|
+
* but a multi-dataset array widens back to the union/`unknown` in practice)
|
|
174
|
+
* -- annotate the call site when entity-level typing matters, exactly like
|
|
175
|
+
* `<ListingProvider<TEntity, TFilters>>` itself.
|
|
176
|
+
*
|
|
177
|
+
* COMPONENTS MERGE: `components` overrides are applied ON TOP of the
|
|
178
|
+
* `/styled` defaults via ONE explicit `{ ...styledDefaultComponents,
|
|
179
|
+
* ...components }` object, passed to a single `<ListingComponentsProvider>`
|
|
180
|
+
* -- never as a nested `<ListingComponentsProvider>` inside
|
|
181
|
+
* `StyledComponentsProviderWithDefaults`. `ListingComponentsProvider` merges
|
|
182
|
+
* `provided ?? ITS OWN private, unstyled fallbacks` per slot (it does not
|
|
183
|
+
* read the parent context -- see `src/react/components-provider.tsx`), so
|
|
184
|
+
* nesting would silently discard every un-overridden `/styled` default
|
|
185
|
+
* instead of keeping it.
|
|
186
|
+
*
|
|
187
|
+
* URL SYNC IS EVENT-BASED, NOT INTERNAL: unlike `/shadcn`'s `ListingApp`,
|
|
188
|
+
* this component takes no `urlSync` prop and never constructs/starts a
|
|
189
|
+
* `UrlSyncController` -- it never touches `window.history`. Instead it
|
|
190
|
+
* accepts `initialFilters` (hydrate FROM the consumer's URL) and emits
|
|
191
|
+
* `onFiltersChange` (write TO the consumer's URL/router) -- see both props'
|
|
192
|
+
* doc comments for the full contract. This keeps the library's surface
|
|
193
|
+
* router-agnostic (a plain `history.replaceState`, a Next.js `router.replace`,
|
|
194
|
+
* a React Router `setSearchParams`, etc. all work identically from the
|
|
195
|
+
* consumer's side) instead of assuming `window.history` is the right target.
|
|
196
|
+
*/
|
|
197
|
+
declare function ListingApp<TEntity, TFilters>(props: ListingAppProps<TFilters>): react.JSX.Element | null;
|
|
198
|
+
|
|
199
|
+
export { BottomNav as B, type IBottomNavAction as I, ListingApp as L, StyledListingLayout as S, type ListingAppProps as a, type BottomNavView as b, type IBottomNavProps as c, type IStyledListingLayoutProps as d, type ListingAppMapProp as e };
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { L as LatLng } from './entity-adapter.interface-BDgbSxhq.cjs';
|
|
4
|
+
import { M as MapProvider } from './map-provider.interface-BMnwW3ob.cjs';
|
|
5
|
+
import { D as DatasetDefinition, F as FilterRegistry, d as IListingComponents } from './components-provider-CF0Mo0B8.cjs';
|
|
6
|
+
import { I as IListingConfigOptions } from './listing-config-options.interface-ZJYY_ZvR.cjs';
|
|
7
|
+
|
|
8
|
+
type BottomNavView = 'list' | 'map';
|
|
9
|
+
interface IBottomNavAction {
|
|
10
|
+
label: string;
|
|
11
|
+
icon?: ReactNode;
|
|
12
|
+
onClick(): void;
|
|
13
|
+
}
|
|
14
|
+
interface IBottomNavProps {
|
|
15
|
+
view: BottomNavView;
|
|
16
|
+
onViewChange(view: BottomNavView): void;
|
|
17
|
+
onFiltersClick(): void;
|
|
18
|
+
action?: IBottomNavAction;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Mobile-only bottom navigation bar (`.rle-bottom-nav`, hidden at 768px+ by
|
|
22
|
+
* CSS -- see `styles.css`): a **Filters** button that opens the mobile
|
|
23
|
+
* `BottomSheet`, an optional caller-supplied `action` button (e.g. "Add"),
|
|
24
|
+
* and a **List | Map** segmented toggle (`.rle-viewtoggle`) that drives which
|
|
25
|
+
* of `StyledListingLayout`'s two full-area panels is visible. No lucide/icon
|
|
26
|
+
* library dependency -- every icon here is a small inline SVG, matching the
|
|
27
|
+
* rest of `/styled`'s zero-extra-dependency policy.
|
|
28
|
+
*/
|
|
29
|
+
declare function BottomNav({ view, onViewChange, onFiltersClick, action }: IBottomNavProps): react.JSX.Element;
|
|
30
|
+
|
|
31
|
+
interface IStyledListingLayoutProps {
|
|
32
|
+
/** Optional search box -- same shape/reasoning as `/shadcn`'s `ListingLayout.search`; omitted entirely when not passed. */
|
|
33
|
+
search?: {
|
|
34
|
+
value: string;
|
|
35
|
+
onChange: (value: string) => void;
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
};
|
|
38
|
+
/** Extra content rendered at the end of the desktop filter bar, alongside `ListingResultHeader` (e.g. a sort control). */
|
|
39
|
+
toolbarEnd?: ReactNode;
|
|
40
|
+
/** Optional bottom-nav action button (e.g. "Add"), forwarded verbatim to `<BottomNav action={...} />`. Omit to render just Filters + the List|Map toggle. */
|
|
41
|
+
mobileAction?: IBottomNavAction;
|
|
42
|
+
/** Whether the layout fetches the first page itself on mount (`engine.applyFilters({})`). Defaults to `true`. */
|
|
43
|
+
autoFetch?: boolean;
|
|
44
|
+
/** Forwarded verbatim to `<ListingMap center={mapCenter} />` -- see that component's "Auto-fit" doc comment. */
|
|
45
|
+
mapCenter?: LatLng;
|
|
46
|
+
/** Forwarded verbatim to `<ListingMap zoom={mapZoom} />`. */
|
|
47
|
+
mapZoom?: number;
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Full, responsive, Tailwind-free listing experience -- the `/styled`
|
|
52
|
+
* counterpart to `/shadcn`'s `ListingLayout`, built from the same
|
|
53
|
+
* structure-only compound components (`~/react`) plus the injected
|
|
54
|
+
* `Styled*` slot components, but with a richer mobile experience: a bottom
|
|
55
|
+
* nav (`BottomNav`) and a bottom sheet (`BottomSheet`) for filters, instead
|
|
56
|
+
* of `/shadcn`'s inline mobile toggle bar. Every class used here is one of
|
|
57
|
+
* the `.rle-*` layout classes added to `styles.css` alongside this file --
|
|
58
|
+
* no Tailwind, no inline styles, no other stylesheet required.
|
|
59
|
+
*
|
|
60
|
+
* STRUCTURE (`.rle-app`):
|
|
61
|
+
* - `.rle-filter-bar` (desktop only, hidden below 768px by CSS): the
|
|
62
|
+
* optional `Search` slot, `<ListingFilters>` laid out as a wrapping row
|
|
63
|
+
* (`className="rle-filters-row"`, `groupClassName="rle-filter-group"`),
|
|
64
|
+
* and a trailing `.rle-filter-bar__end` cluster with `<ListingResultHeader>`
|
|
65
|
+
* + `toolbarEnd`.
|
|
66
|
+
* - `.rle-body.rle-split`: a CSS grid from 768px up (list column floors at
|
|
67
|
+
* 340px, caps at 42%; map takes the rest); below that, a single full-area
|
|
68
|
+
* panel with exactly one of `.rle-list`/`.rle-map` visible at a time via
|
|
69
|
+
* `data-mobile-view` (see `styles.css`'s mobile media query). Both regions
|
|
70
|
+
* stay mounted at ALL times regardless of viewport/toggle state -- only
|
|
71
|
+
* their visibility flips -- so neither `ListingList` nor `ListingMap`
|
|
72
|
+
* remounts (and re-triggers its own mount effects) on toggle or resize.
|
|
73
|
+
* - `<BottomNav>`: mobile-only (CSS-hidden at 768px+). Wired to the same
|
|
74
|
+
* `mobileView` state as the CSS toggle, `mobileAction`, and opens the
|
|
75
|
+
* filters `<BottomSheet>`.
|
|
76
|
+
* - `<BottomSheet title="Filters">`: the SAME `<ListingFilters>` component,
|
|
77
|
+
* stacked vertically (`className="rle-filter-stack"`) for the sheet's
|
|
78
|
+
* narrower body, plus a footer with "Clear all" (resets every registered
|
|
79
|
+
* filter to `undefined` via `engine.filters.list()`'s keys, routed through
|
|
80
|
+
* `engine.applyFilters` -- the one bulk mutator, same as `ListingFilters`
|
|
81
|
+
* itself uses) and "Show N results" (just closes the sheet -- every filter
|
|
82
|
+
* control already applies live via its own `onChange`, so there is nothing
|
|
83
|
+
* left to commit).
|
|
84
|
+
* - Fetches the first page itself on mount (`engine.applyFilters({})`) by
|
|
85
|
+
* default, exactly like `/shadcn`'s `ListingLayout` -- pass `autoFetch={false}`
|
|
86
|
+
* to opt out and drive the first fetch yourself.
|
|
87
|
+
*/
|
|
88
|
+
declare function StyledListingLayout({ search, toolbarEnd, mobileAction, autoFetch, mapCenter, mapZoom, className, }: IStyledListingLayoutProps): react.JSX.Element;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Same two-shape `map` prop as `/shadcn`'s `ListingApp` (see that file's
|
|
92
|
+
* `useResolvedMap` doc comment for why the `{ apiKey }` shorthand is resolved
|
|
93
|
+
* via a dynamic `import()` rather than a static one -- the reasoning is
|
|
94
|
+
* identical here). Re-declared rather than imported from `~/shadcn`: the two
|
|
95
|
+
* styled adapters are SIBLINGS, not a hierarchy -- `/styled` never depends on
|
|
96
|
+
* `/shadcn` (or vice versa), each is self-contained against `~/core`/`~/react`.
|
|
97
|
+
*/
|
|
98
|
+
type ListingAppMapProp = {
|
|
99
|
+
provider: MapProvider;
|
|
100
|
+
center?: LatLng;
|
|
101
|
+
zoom?: number;
|
|
102
|
+
} | {
|
|
103
|
+
apiKey: string;
|
|
104
|
+
mapId?: string;
|
|
105
|
+
center?: LatLng;
|
|
106
|
+
zoom?: number;
|
|
107
|
+
};
|
|
108
|
+
interface ListingAppProps<TFilters> {
|
|
109
|
+
/**
|
|
110
|
+
* One or more marker-layer datasets; the FIRST entry is the primary
|
|
111
|
+
* dataset (drives the results list + pagination) -- same "insertion
|
|
112
|
+
* order" rule `composeListingProviders`/`withDataset` already follow.
|
|
113
|
+
* Entity-erased (`any`, not `TEntity`) for the same reason
|
|
114
|
+
* `DatasetRegistry<unknown, TFilters>` is: one array can legitimately hold
|
|
115
|
+
* heterogeneous layers (a properties dataset and a businesses dataset have
|
|
116
|
+
* different entity types) -- see `compose-listing-providers.ts`'s
|
|
117
|
+
* `withDataset` doc comment.
|
|
118
|
+
*/
|
|
119
|
+
datasets: DatasetDefinition<any, TFilters>[];
|
|
120
|
+
/** e.g. `withRentalFilters()`, or any `(reg) => { reg.add(...); }` callback -- forwarded verbatim to `withFilters`. */
|
|
121
|
+
filters?: (reg: FilterRegistry<TFilters>) => void;
|
|
122
|
+
/** A ready `MapProvider`, or `{ apiKey, mapId? }` to build a `googleProvider` internally. Omit for no map (the styled layout shows a "Map unavailable" fallback). */
|
|
123
|
+
map?: ListingAppMapProp;
|
|
124
|
+
/** Slot overrides, merged OVER the `/styled` defaults -- see this file's doc comment for how the merge works. */
|
|
125
|
+
components?: Partial<IListingComponents>;
|
|
126
|
+
/**
|
|
127
|
+
* Hydrates the engine's initial filters on mount -- typically read from
|
|
128
|
+
* the CONSUMER's own URL (e.g. `rentalFiltersFromQuery(new
|
|
129
|
+
* URLSearchParams(window.location.search))`), but any source works. Half
|
|
130
|
+
* of the event-based URL API this component exposes: `ListingApp` never
|
|
131
|
+
* reads `window.location` itself, it only accepts filters as a prop.
|
|
132
|
+
*/
|
|
133
|
+
initialFilters?: TFilters;
|
|
134
|
+
/**
|
|
135
|
+
* EVENT OUT: fires with the engine's current filters (`TFilters`, not the
|
|
136
|
+
* store's `DeepReadonly` wrapper) every time `ListingEventType.FiltersChanged`
|
|
137
|
+
* fires on the underlying engine -- i.e. on every `engine.applyFilters(...)`
|
|
138
|
+
* call, INCLUDING the one `StyledListingLayout` makes on mount when
|
|
139
|
+
* `autoFetch` is on (a `{}` patch that still round-trips through
|
|
140
|
+
* `currentFilters()` and re-emits the very `initialFilters` this component
|
|
141
|
+
* was given). That first call is intentionally NOT suppressed: a
|
|
142
|
+
* consumer's handler is expected to write filters back to a URL via
|
|
143
|
+
* `history.replaceState` (or a router's equivalent), and replacing the URL
|
|
144
|
+
* with the SAME query string it already hydrated from is a no-op, not a
|
|
145
|
+
* visible echo -- so "just always emit" is simpler than tracking a
|
|
146
|
+
* first-call flag for no behavioral gain. This is the OTHER half of the
|
|
147
|
+
* event-based URL API: `ListingApp` (and the engine underneath it) never
|
|
148
|
+
* touches `window.history` -- the CONSUMER owns routing, driven by this
|
|
149
|
+
* callback plus `initialFilters` above. `UrlSyncController`/`BrowserHistoryPort`
|
|
150
|
+
* (`~/core`, `~/react`) remain exported as optional, lower-level helpers
|
|
151
|
+
* for consumers who want the library to own `window.history` directly, but
|
|
152
|
+
* they are no longer the primary/recommended path for new integrations.
|
|
153
|
+
*/
|
|
154
|
+
onFiltersChange?: (filters: TFilters) => void;
|
|
155
|
+
/** The mobile bottom-nav "Add" action -- forwarded verbatim to `StyledListingLayout`/`BottomNav`. */
|
|
156
|
+
mobileAction?: IBottomNavAction;
|
|
157
|
+
search?: IStyledListingLayoutProps['search'];
|
|
158
|
+
toolbarEnd?: IStyledListingLayoutProps['toolbarEnd'];
|
|
159
|
+
config?: Partial<IListingConfigOptions>;
|
|
160
|
+
autoFetch?: boolean;
|
|
161
|
+
className?: string;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Turnkey, batteries-included, Tailwind-free entry point -- the `/styled`
|
|
165
|
+
* counterpart to `/shadcn`'s `ListingApp`, and (per `src/index.ts`) the
|
|
166
|
+
* package's MAIN-ENTRY default: `import { ListingApp } from
|
|
167
|
+
* 'react-listing-engine'` gives you this component. Pass datasets + filters +
|
|
168
|
+
* a map + component overrides, and `ListingApp` composes
|
|
169
|
+
* `composeListingProviders(...)`, `<ListingProvider>`, the `/styled` defaults,
|
|
170
|
+
* and `<StyledListingLayout>` for you.
|
|
171
|
+
*
|
|
172
|
+
* `TEntity` still can't be inferred from these props (`datasets` narrows it,
|
|
173
|
+
* but a multi-dataset array widens back to the union/`unknown` in practice)
|
|
174
|
+
* -- annotate the call site when entity-level typing matters, exactly like
|
|
175
|
+
* `<ListingProvider<TEntity, TFilters>>` itself.
|
|
176
|
+
*
|
|
177
|
+
* COMPONENTS MERGE: `components` overrides are applied ON TOP of the
|
|
178
|
+
* `/styled` defaults via ONE explicit `{ ...styledDefaultComponents,
|
|
179
|
+
* ...components }` object, passed to a single `<ListingComponentsProvider>`
|
|
180
|
+
* -- never as a nested `<ListingComponentsProvider>` inside
|
|
181
|
+
* `StyledComponentsProviderWithDefaults`. `ListingComponentsProvider` merges
|
|
182
|
+
* `provided ?? ITS OWN private, unstyled fallbacks` per slot (it does not
|
|
183
|
+
* read the parent context -- see `src/react/components-provider.tsx`), so
|
|
184
|
+
* nesting would silently discard every un-overridden `/styled` default
|
|
185
|
+
* instead of keeping it.
|
|
186
|
+
*
|
|
187
|
+
* URL SYNC IS EVENT-BASED, NOT INTERNAL: unlike `/shadcn`'s `ListingApp`,
|
|
188
|
+
* this component takes no `urlSync` prop and never constructs/starts a
|
|
189
|
+
* `UrlSyncController` -- it never touches `window.history`. Instead it
|
|
190
|
+
* accepts `initialFilters` (hydrate FROM the consumer's URL) and emits
|
|
191
|
+
* `onFiltersChange` (write TO the consumer's URL/router) -- see both props'
|
|
192
|
+
* doc comments for the full contract. This keeps the library's surface
|
|
193
|
+
* router-agnostic (a plain `history.replaceState`, a Next.js `router.replace`,
|
|
194
|
+
* a React Router `setSearchParams`, etc. all work identically from the
|
|
195
|
+
* consumer's side) instead of assuming `window.history` is the right target.
|
|
196
|
+
*/
|
|
197
|
+
declare function ListingApp<TEntity, TFilters>(props: ListingAppProps<TFilters>): react.JSX.Element | null;
|
|
198
|
+
|
|
199
|
+
export { BottomNav as B, type IBottomNavAction as I, ListingApp as L, StyledListingLayout as S, type ListingAppProps as a, type BottomNavView as b, type IBottomNavProps as c, type IStyledListingLayoutProps as d, type ListingAppMapProp as e };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare enum PaginationMode {
|
|
2
|
+
Paged = "paged",
|
|
3
|
+
Infinite = "infinite"
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface IListingConfigOptions {
|
|
7
|
+
pagination: PaginationMode;
|
|
8
|
+
pageSize: number;
|
|
9
|
+
debounceMs: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { type IListingConfigOptions as I, PaginationMode as P };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare enum PaginationMode {
|
|
2
|
+
Paged = "paged",
|
|
3
|
+
Infinite = "infinite"
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface IListingConfigOptions {
|
|
7
|
+
pagination: PaginationMode;
|
|
8
|
+
pageSize: number;
|
|
9
|
+
debounceMs: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { type IListingConfigOptions as I, PaginationMode as P };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { L as LatLng, B as Bounds } from './entity-adapter.interface-BDgbSxhq.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Providers that need an API key take it via their factory (e.g. `googleProvider({ apiKey })`),
|
|
5
|
+
* not per-mount.
|
|
6
|
+
*/
|
|
7
|
+
interface MapInitOptions {
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
center?: LatLng;
|
|
10
|
+
zoom?: number;
|
|
11
|
+
}
|
|
12
|
+
interface MapHandle {
|
|
13
|
+
readonly raw: unknown;
|
|
14
|
+
}
|
|
15
|
+
interface RenderedLayer {
|
|
16
|
+
id: string;
|
|
17
|
+
markers: Array<{
|
|
18
|
+
id: string | number;
|
|
19
|
+
position: LatLng;
|
|
20
|
+
iconUrl?: string;
|
|
21
|
+
element?: HTMLElement;
|
|
22
|
+
}>;
|
|
23
|
+
clustering?: {
|
|
24
|
+
maxZoom?: number;
|
|
25
|
+
} | false;
|
|
26
|
+
onMarkerClick?(id: string | number): void;
|
|
27
|
+
}
|
|
28
|
+
type Unsubscribe = () => void;
|
|
29
|
+
interface MapProvider {
|
|
30
|
+
mount(el: HTMLElement, opts: MapInitOptions): Promise<MapHandle> | MapHandle;
|
|
31
|
+
renderLayer(handle: MapHandle, layer: RenderedLayer): Unsubscribe;
|
|
32
|
+
onBoundsChange(handle: MapHandle, cb: (b: Bounds) => void): Unsubscribe;
|
|
33
|
+
fitBounds(handle: MapHandle, b: Bounds): void;
|
|
34
|
+
destroy(handle: MapHandle): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type { MapProvider as M, RenderedLayer as R, Unsubscribe as U, MapHandle as a, MapInitOptions as b };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { L as LatLng, B as Bounds } from './entity-adapter.interface-BDgbSxhq.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Providers that need an API key take it via their factory (e.g. `googleProvider({ apiKey })`),
|
|
5
|
+
* not per-mount.
|
|
6
|
+
*/
|
|
7
|
+
interface MapInitOptions {
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
center?: LatLng;
|
|
10
|
+
zoom?: number;
|
|
11
|
+
}
|
|
12
|
+
interface MapHandle {
|
|
13
|
+
readonly raw: unknown;
|
|
14
|
+
}
|
|
15
|
+
interface RenderedLayer {
|
|
16
|
+
id: string;
|
|
17
|
+
markers: Array<{
|
|
18
|
+
id: string | number;
|
|
19
|
+
position: LatLng;
|
|
20
|
+
iconUrl?: string;
|
|
21
|
+
element?: HTMLElement;
|
|
22
|
+
}>;
|
|
23
|
+
clustering?: {
|
|
24
|
+
maxZoom?: number;
|
|
25
|
+
} | false;
|
|
26
|
+
onMarkerClick?(id: string | number): void;
|
|
27
|
+
}
|
|
28
|
+
type Unsubscribe = () => void;
|
|
29
|
+
interface MapProvider {
|
|
30
|
+
mount(el: HTMLElement, opts: MapInitOptions): Promise<MapHandle> | MapHandle;
|
|
31
|
+
renderLayer(handle: MapHandle, layer: RenderedLayer): Unsubscribe;
|
|
32
|
+
onBoundsChange(handle: MapHandle, cb: (b: Bounds) => void): Unsubscribe;
|
|
33
|
+
fitBounds(handle: MapHandle, b: Bounds): void;
|
|
34
|
+
destroy(handle: MapHandle): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type { MapProvider as M, RenderedLayer as R, Unsubscribe as U, MapHandle as a, MapInitOptions as b };
|
|
@@ -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 _jsapiloader = require('@googlemaps/js-api-loader');var y="DEMO_MAP_ID";function u(r){return r.raw}function v(r,n){if(r.element)return r.element;if(r.iconUrl)return new n.PinElement({glyphSrc:r.iconUrl})}function p(r){for(let n of r)n.map=null}var L="#b3261e",b=40,C=64,w=14;function E(r,n){let s=n.count,o=Math.min(C,b+Math.round(Math.log10(s+1)*w)),e=document.createElement("div");return e.textContent=String(s),e.style.display="flex",e.style.alignItems="center",e.style.justifyContent="center",e.style.width=`${o}px`,e.style.height=`${o}px`,e.style.borderRadius="50%",e.style.backgroundColor=L,e.style.border="2px solid #ffffff",e.style.boxShadow="0 1px 4px rgba(0, 0, 0, 0.4)",e.style.color="#ffffff",e.style.fontFamily="inherit",e.style.fontWeight="700",e.style.fontSize=o>=52?"15px":"13px",new r.AdvancedMarkerElement({position:n.position,content:e})}var f=!1;async function h(r,n,s,o){let e;try{e=await Promise.resolve().then(() => _interopRequireWildcard(require("@googlemaps/markerclusterer")))}catch(a){f||(f=!0,console.warn('[react-listing-engine] "@googlemaps/markerclusterer" is not installed -- rendering plain (unclustered) markers instead. Install "@googlemaps/markerclusterer" to enable clustering.',a));return}if(r.layers.get(n)!==s)return;let t=new e.MarkerClusterer({map:r.map,markers:s,algorithmOptions:{maxZoom:o.maxZoom},renderer:{render:a=>E(r.markerLib,a)}});r.clusterers.set(n,t)}function g(r,n){let s=r.clusterers.get(n);s&&(s.setMap(null),r.clusterers.delete(n))}function R(r){let n=r.getSouthWest(),s=r.getNorthEast();return{west:n.lng(),south:n.lat(),east:s.lng(),north:s.lat()}}function O(r,n,s){if(typeof ResizeObserver>"u")return;let o=!1,e=()=>{let{width:a,height:i}=r.getBoundingClientRect();a===0||i===0||(google.maps.event.trigger(n,"resize"),o||(o=!0,n.setCenter(s)))},t=new ResizeObserver(e);return t.observe(r),typeof requestAnimationFrame<"u"?requestAnimationFrame(e):queueMicrotask(e),t}function P(r){if(!r.apiKey)throw new Error("googleProvider: config.apiKey is required (no hardcoded fallback key is ever used).");let n=!1,s=()=>{n||(_jsapiloader.setOptions.call(void 0, {...r.loaderOptions,key:r.apiKey}),n=!0)};return{async mount(o,e){s();let t=await _jsapiloader.importLibrary.call(void 0, "maps"),a=await _jsapiloader.importLibrary.call(void 0, "marker"),i=_nullishCoalesce(e.center, () => ({lat:0,lng:0})),l=new t.Map(o,{center:i,zoom:_nullishCoalesce(e.zoom, () => (10)),mapId:_nullishCoalesce(r.mapId, () => (y))}),d=O(o,l,i);return{raw:{map:l,markerLib:a,layers:new Map,clusterers:new Map,boundsListeners:new Set,resizeObserver:d}}},renderLayer(o,e){let t=u(o),a=t.layers.get(e.id);a&&(p(a),t.layers.delete(e.id)),g(t,e.id);let i=e.markers.map(d=>{let c=new t.markerLib.AdvancedMarkerElement({map:t.map,position:d.position,content:v(d,t.markerLib),gmpClickable:!!e.onMarkerClick});if(e.onMarkerClick){let M=e.onMarkerClick;c.addListener("click",()=>M(d.id))}return c});t.layers.set(e.id,i),e.clustering&&h(t,e.id,i,e.clustering);let l=!1;return()=>{l||(l=!0,t.layers.get(e.id)===i&&(p(i),t.layers.delete(e.id),g(t,e.id)))}},onBoundsChange(o,e){let t=u(o),a=t.map.addListener("idle",()=>{let l=t.map.getBounds();l&&e(R(l))});t.boundsListeners.add(a);let i=!1;return()=>{i||(i=!0,a.remove(),t.boundsListeners.delete(a))}},fitBounds(o,e){u(o).map.fitBounds(e)},destroy(o){let e=u(o);_optionalChain([e, 'access', _ => _.resizeObserver, 'optionalAccess', _2 => _2.disconnect, 'call', _3 => _3()]);for(let t of e.boundsListeners)t.remove();e.boundsListeners.clear();for(let t of[...e.clusterers.keys()])g(e,t);for(let t of e.layers.values())p(t);e.layers.clear()}}}exports.googleProvider = P;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { APIOptions } from '@googlemaps/js-api-loader';
|
|
2
|
+
import { M as MapProvider } from '../../map-provider.interface-BMnwW3ob.cjs';
|
|
3
|
+
import '../../entity-adapter.interface-BDgbSxhq.cjs';
|
|
4
|
+
|
|
5
|
+
interface GoogleMapsProviderConfig {
|
|
6
|
+
/** Google Maps JavaScript API key. Required -- there is no hardcoded fallback. */
|
|
7
|
+
apiKey: string;
|
|
8
|
+
/**
|
|
9
|
+
* Forwarded to every `google.maps.Map`. Required by Google for `AdvancedMarkerElement` to
|
|
10
|
+
* render at all -- a map created without a Map ID silently drops advanced markers (see
|
|
11
|
+
* https://developers.google.com/maps/documentation/javascript/advanced-markers/migration).
|
|
12
|
+
* Defaults to Google's documented zero-config dev Map ID (`DEFAULT_MAP_ID`,
|
|
13
|
+
* `'DEMO_MAP_ID'`) when omitted, so advanced markers work out of the box with no setup.
|
|
14
|
+
* `'DEMO_MAP_ID'` carries no custom styling and is not intended for production traffic --
|
|
15
|
+
* consumers should supply their own Cloud Console-configured Map ID for production use or
|
|
16
|
+
* custom map styling.
|
|
17
|
+
*/
|
|
18
|
+
mapId?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Extra `setOptions()` config (language, region, preloaded libraries, ...). `key` always
|
|
21
|
+
* comes from `apiKey` and cannot be overridden here.
|
|
22
|
+
*/
|
|
23
|
+
loaderOptions?: Partial<Omit<APIOptions, 'key'>>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* `MapProvider` backed by the Google Maps JavaScript API.
|
|
27
|
+
*
|
|
28
|
+
* The installed `@googlemaps/js-api-loader` (v2) dropped the class-based `new Loader(...)` API
|
|
29
|
+
* in favor of module-level `setOptions()` + `importLibrary()` functions -- the `Loader` class
|
|
30
|
+
* that remains exported is a deprecated stub whose constructor throws synchronously (see that
|
|
31
|
+
* package's MIGRATION.md and `dist/index.js`). This implementation targets the functional API
|
|
32
|
+
* since that is the real, installed shape.
|
|
33
|
+
*
|
|
34
|
+
* Throws immediately if `config.apiKey` is falsy -- callers must always supply a real key via
|
|
35
|
+
* the factory; there is never a hardcoded fallback.
|
|
36
|
+
*/
|
|
37
|
+
declare function googleProvider(config: GoogleMapsProviderConfig): MapProvider;
|
|
38
|
+
|
|
39
|
+
export { type GoogleMapsProviderConfig, googleProvider };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { APIOptions } from '@googlemaps/js-api-loader';
|
|
2
|
+
import { M as MapProvider } from '../../map-provider.interface-BxexMT3X.js';
|
|
3
|
+
import '../../entity-adapter.interface-BDgbSxhq.js';
|
|
4
|
+
|
|
5
|
+
interface GoogleMapsProviderConfig {
|
|
6
|
+
/** Google Maps JavaScript API key. Required -- there is no hardcoded fallback. */
|
|
7
|
+
apiKey: string;
|
|
8
|
+
/**
|
|
9
|
+
* Forwarded to every `google.maps.Map`. Required by Google for `AdvancedMarkerElement` to
|
|
10
|
+
* render at all -- a map created without a Map ID silently drops advanced markers (see
|
|
11
|
+
* https://developers.google.com/maps/documentation/javascript/advanced-markers/migration).
|
|
12
|
+
* Defaults to Google's documented zero-config dev Map ID (`DEFAULT_MAP_ID`,
|
|
13
|
+
* `'DEMO_MAP_ID'`) when omitted, so advanced markers work out of the box with no setup.
|
|
14
|
+
* `'DEMO_MAP_ID'` carries no custom styling and is not intended for production traffic --
|
|
15
|
+
* consumers should supply their own Cloud Console-configured Map ID for production use or
|
|
16
|
+
* custom map styling.
|
|
17
|
+
*/
|
|
18
|
+
mapId?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Extra `setOptions()` config (language, region, preloaded libraries, ...). `key` always
|
|
21
|
+
* comes from `apiKey` and cannot be overridden here.
|
|
22
|
+
*/
|
|
23
|
+
loaderOptions?: Partial<Omit<APIOptions, 'key'>>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* `MapProvider` backed by the Google Maps JavaScript API.
|
|
27
|
+
*
|
|
28
|
+
* The installed `@googlemaps/js-api-loader` (v2) dropped the class-based `new Loader(...)` API
|
|
29
|
+
* in favor of module-level `setOptions()` + `importLibrary()` functions -- the `Loader` class
|
|
30
|
+
* that remains exported is a deprecated stub whose constructor throws synchronously (see that
|
|
31
|
+
* package's MIGRATION.md and `dist/index.js`). This implementation targets the functional API
|
|
32
|
+
* since that is the real, installed shape.
|
|
33
|
+
*
|
|
34
|
+
* Throws immediately if `config.apiKey` is falsy -- callers must always supply a real key via
|
|
35
|
+
* the factory; there is never a hardcoded fallback.
|
|
36
|
+
*/
|
|
37
|
+
declare function googleProvider(config: GoogleMapsProviderConfig): MapProvider;
|
|
38
|
+
|
|
39
|
+
export { type GoogleMapsProviderConfig, googleProvider };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import{importLibrary as m,setOptions as k}from"@googlemaps/js-api-loader";var y="DEMO_MAP_ID";function u(r){return r.raw}function v(r,n){if(r.element)return r.element;if(r.iconUrl)return new n.PinElement({glyphSrc:r.iconUrl})}function p(r){for(let n of r)n.map=null}var L="#b3261e",b=40,C=64,w=14;function E(r,n){let s=n.count,o=Math.min(C,b+Math.round(Math.log10(s+1)*w)),e=document.createElement("div");return e.textContent=String(s),e.style.display="flex",e.style.alignItems="center",e.style.justifyContent="center",e.style.width=`${o}px`,e.style.height=`${o}px`,e.style.borderRadius="50%",e.style.backgroundColor=L,e.style.border="2px solid #ffffff",e.style.boxShadow="0 1px 4px rgba(0, 0, 0, 0.4)",e.style.color="#ffffff",e.style.fontFamily="inherit",e.style.fontWeight="700",e.style.fontSize=o>=52?"15px":"13px",new r.AdvancedMarkerElement({position:n.position,content:e})}var f=!1;async function h(r,n,s,o){let e;try{e=await import("@googlemaps/markerclusterer")}catch(a){f||(f=!0,console.warn('[react-listing-engine] "@googlemaps/markerclusterer" is not installed -- rendering plain (unclustered) markers instead. Install "@googlemaps/markerclusterer" to enable clustering.',a));return}if(r.layers.get(n)!==s)return;let t=new e.MarkerClusterer({map:r.map,markers:s,algorithmOptions:{maxZoom:o.maxZoom},renderer:{render:a=>E(r.markerLib,a)}});r.clusterers.set(n,t)}function g(r,n){let s=r.clusterers.get(n);s&&(s.setMap(null),r.clusterers.delete(n))}function R(r){let n=r.getSouthWest(),s=r.getNorthEast();return{west:n.lng(),south:n.lat(),east:s.lng(),north:s.lat()}}function O(r,n,s){if(typeof ResizeObserver>"u")return;let o=!1,e=()=>{let{width:a,height:i}=r.getBoundingClientRect();a===0||i===0||(google.maps.event.trigger(n,"resize"),o||(o=!0,n.setCenter(s)))},t=new ResizeObserver(e);return t.observe(r),typeof requestAnimationFrame<"u"?requestAnimationFrame(e):queueMicrotask(e),t}function P(r){if(!r.apiKey)throw new Error("googleProvider: config.apiKey is required (no hardcoded fallback key is ever used).");let n=!1,s=()=>{n||(k({...r.loaderOptions,key:r.apiKey}),n=!0)};return{async mount(o,e){s();let t=await m("maps"),a=await m("marker"),i=e.center??{lat:0,lng:0},l=new t.Map(o,{center:i,zoom:e.zoom??10,mapId:r.mapId??y}),d=O(o,l,i);return{raw:{map:l,markerLib:a,layers:new Map,clusterers:new Map,boundsListeners:new Set,resizeObserver:d}}},renderLayer(o,e){let t=u(o),a=t.layers.get(e.id);a&&(p(a),t.layers.delete(e.id)),g(t,e.id);let i=e.markers.map(d=>{let c=new t.markerLib.AdvancedMarkerElement({map:t.map,position:d.position,content:v(d,t.markerLib),gmpClickable:!!e.onMarkerClick});if(e.onMarkerClick){let M=e.onMarkerClick;c.addListener("click",()=>M(d.id))}return c});t.layers.set(e.id,i),e.clustering&&h(t,e.id,i,e.clustering);let l=!1;return()=>{l||(l=!0,t.layers.get(e.id)===i&&(p(i),t.layers.delete(e.id),g(t,e.id)))}},onBoundsChange(o,e){let t=u(o),a=t.map.addListener("idle",()=>{let l=t.map.getBounds();l&&e(R(l))});t.boundsListeners.add(a);let i=!1;return()=>{i||(i=!0,a.remove(),t.boundsListeners.delete(a))}},fitBounds(o,e){u(o).map.fitBounds(e)},destroy(o){let e=u(o);e.resizeObserver?.disconnect();for(let t of e.boundsListeners)t.remove();e.boundsListeners.clear();for(let t of[...e.clusterers.keys()])g(e,t);for(let t of e.layers.values())p(t);e.layers.clear()}}}export{P as googleProvider};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); 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 _chunkCHNUE46Kcjs = require('../../chunk-CHNUE46K.cjs');var _chunkLCQZIWBOcjs = require('../../chunk-LCQZIWBO.cjs');var _chunk2VUHLHHXcjs = require('../../chunk-2VUHLHHX.cjs');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=>!_optionalChain([t, 'optionalAccess', _2 => _2.categories])||t.categories.includes(i.category))).then(s=>s.map(i=>({id:i.id,position:i.coordinates,entity:i})))},marker:{iconUrl:r=>_nullishCoalesce(_optionalChain([t, 'optionalAccess', _3 => _3.icons, 'optionalAccess', _4 => _4[r.category]]), () => ("")),onClick:_optionalChain([t, 'optionalAccess', _5 => _5.onClick])},clustering:{maxZoom:15},visible:()=>!0}}var _jsxruntime = require('react/jsx-runtime');function b({value:e,onChange:t}){return _jsxruntime.jsx.call(void 0, "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"})}var _react = require('react');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]=_react.useState.call(void 0, !1),o=i=>l=>{t(l.target.checked?[...e,i]:e.filter(c=>c!==i))};return _jsxruntime.jsxs.call(void 0, "details",{open:n,className:"group relative",children:[_jsxruntime.jsxs.call(void 0, "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&&_jsxruntime.jsx.call(void 0, "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}),_jsxruntime.jsx.call(void 0, "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:_jsxruntime.jsx.call(void 0, "path",{d:"m6 9 6 6 6-6"})})]}),_jsxruntime.jsxs.call(void 0, "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:[_jsxruntime.jsx.call(void 0, "legend",{className:"sr-only",children:"Property type"}),k.map(i=>_jsxruntime.jsxs.call(void 0, "label",{className:"flex items-center gap-2 text-sm",children:[_jsxruntime.jsx.call(void 0, "input",{type:"checkbox",checked:e.includes(i),onChange:o(i),className:"size-4 rounded border-border accent-primary"}),N[i]]},i))]})]})}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 _jsxruntime.jsxs.call(void 0, "div",{className:"flex items-center gap-2",children:[_jsxruntime.jsx.call(void 0, "input",{type:"number",inputMode:"numeric","aria-label":"Minimum",placeholder:"Min",value:_nullishCoalesce(e.min, () => ("")),onChange:n,className:o}),_jsxruntime.jsx.call(void 0, "span",{"aria-hidden":"true",className:"text-muted-foreground",children:"-"}),_jsxruntime.jsx.call(void 0, "input",{type:"number",inputMode:"numeric","aria-label":"Maximum",placeholder:"Max",value:_nullishCoalesce(e.max, () => ("")),onChange:r,className:o})]})}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=_chunkLCQZIWBOcjs.a.call(void 0, "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=_jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment,{children:[_optionalChain([r, 'optionalAccess', _6 => _6.imageUrl])?_jsxruntime.jsx.call(void 0, "img",{src:r.imageUrl,alt:_nullishCoalesce(r.title, () => ("")),className:"aspect-[4/3] w-full rounded-t-[10px] object-cover"}):_jsxruntime.jsx.call(void 0, "div",{className:"aspect-[4/3] w-full rounded-t-[10px] bg-muted","aria-hidden":"true"}),_jsxruntime.jsxs.call(void 0, "div",{className:"flex flex-1 flex-col gap-1 p-3",children:[_optionalChain([r, 'optionalAccess', _7 => _7.title])&&_jsxruntime.jsx.call(void 0, "span",{className:"text-base font-semibold",children:r.title}),_optionalChain([r, 'optionalAccess', _8 => _8.address])&&_jsxruntime.jsx.call(void 0, "span",{className:"text-sm text-muted-foreground",children:r.address}),_optionalChain([r, 'optionalAccess', _9 => _9.propertyType])!=null&&r.bedrooms!=null&&r.bathrooms!=null&&_jsxruntime.jsxs.call(void 0, "span",{className:"text-sm text-muted-foreground",children:[v(r.propertyType)," \xB7 ",r.bedrooms," bd \xB7 ",r.bathrooms," ba"]}),_optionalChain([r, 'optionalAccess', _10 => _10.price])!=null&&_jsxruntime.jsxs.call(void 0, "span",{className:"mt-1 font-bold tabular-nums",children:[f(r.price)," ",_jsxruntime.jsx.call(void 0, "span",{className:"text-sm font-normal text-muted-foreground",children:"/mo"})]})]})]});return n?_jsxruntime.jsx.call(void 0, "button",{type:"button",onClick:n,"aria-pressed":_nullishCoalesce(t, () => (!1)),className:o,children:s}):_jsxruntime.jsx.call(void 0, "article",{className:o,children:s})}function ee({point:e}){let t=e.entity;return _jsxruntime.jsxs.call(void 0, "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:[_optionalChain([t, 'optionalAccess', _11 => _11.price])!=null?f(t.price):"",_jsxruntime.jsx.call(void 0, "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:_optionalChain([t, 'optionalAccess', _12 => _12.iconUrl]),element:_nullishCoalesce(_optionalChain([t, 'optionalAccess', _13 => _13.element]), () => (S)),onClick:_optionalChain([t, 'optionalAccess', _14 => _14.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=>_nullishCoalesce(e.propertyTypes, () => ([])),isActive:e=>(_nullishCoalesce(_optionalChain([e, 'access', _15 => _15.propertyTypes, 'optionalAccess', _16 => _16.length]), () => (0)))>0},O={key:"keyword",order:50,label:"Keyword",render:b,toParams:e=>({keyword:e||void 0}),fromParams:e=>_nullishCoalesce(e.keyword, () => ("")),isActive:e=>!!e.keyword},U= exports.rentalFilters =[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)),_optionalChain([e, 'access', _17 => _17.propertyTypes, 'optionalAccess', _18 => _18.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=_optionalChain([e, 'access', _19 => _19.type, 'optionalAccess', _20 => _20.split, 'call', _21 => _21(","), 'access', _22 => _22.map, 'call', _23 => _23(B=>B.trim()), 'access', _24 => _24.filter, 'call', _25 => _25(V)]);return _optionalChain([c, 'optionalAccess', _26 => _26.length])&&(t.propertyTypes=c),e.q&&(t.keyword=e.q),t}function ue(e={}){let t=_nullishCoalesce(e.history, () => (new (0, _chunkCHNUE46Kcjs.a)({mode:e.mode})));return new (0, _chunk2VUHLHHXcjs.a)({history:t,toQuery:H,toFilters:Q,hydrateOnStart:e.hydrateOnStart})}exports.KNOWN_BUSINESS_CATEGORIES = Y; exports.KeywordFilterControl = b; exports.PropertyCard = J; exports.PropertyMarker = ee; exports.PropertyTypeFilterControl = F; exports.RangeFilterControl = y; exports.defaultPriceMarkerElement = S; exports.formatRentalPrice = f; exports.nearbyBusinessesDataset = _; exports.propertiesDataset = ie; exports.rentalFilters = U; exports.rentalFiltersFromQuery = Q; exports.rentalFiltersToQuery = H; exports.rentalUrlSync = ue; exports.withRentalFilters = pe;
|