@salesforce/webapp-template-app-react-sample-b2x-experimental 1.116.7 → 1.116.9
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/dist/CHANGELOG.md +16 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/eslint.config.js +13 -2
- package/dist/force-app/main/default/webapplications/propertyrentalapp/package.json +3 -3
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/graphql-operations-types.ts +24594 -7234
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/maintenanceRequests/maintenanceRequestApi.ts +21 -157
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/maintenanceRequests/query/maintenanceRequests.graphql +60 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/propertyDetailGraphQL.ts +45 -444
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/propertyNodeUtils.ts +29 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/propertySearchService.ts +56 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/distinctPropertyStatus.graphql +19 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/distinctPropertyType.graphql +19 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/listingById.graphql +29 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/propertyAddressesByIds.graphql +17 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/propertyDetailById.graphql +124 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/searchProperties.graphql +85 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/appLayout.tsx +1 -1
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/SkeletonPrimitives.tsx +9 -6
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/dashboard/WeatherWidget.tsx +35 -19
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/maintenanceRequests/MaintenanceRequestList.tsx +7 -5
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/maintenanceRequests/MaintenanceRequestListItem.tsx +11 -10
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/maintenanceRequests/MaintenanceSummaryDetailsModal.tsx +20 -15
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/properties/PropertyListingCard.tsx +11 -24
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/properties/PropertyMap.tsx +2 -1
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/context/AuthContext.tsx +1 -1
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/hooks/useCountdownTimer.ts +1 -1
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/pages/Profile.tsx +3 -3
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/pages/Register.tsx +1 -1
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/sessionTimeout/SessionTimeoutValidator.tsx +12 -18
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/object-search/components/FilterContext.tsx +1 -1
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/object-search/hooks/useObjectSearchParams.ts +10 -5
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useGeocode.ts +23 -38
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useMaintenanceRequests.ts +29 -25
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyDetail.ts +42 -78
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyMapMarkers.ts +34 -41
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useWeather.ts +14 -30
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Application.tsx +41 -74
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Contact.tsx +44 -55
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Dashboard.tsx +1 -0
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Home.tsx +63 -32
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Maintenance.tsx +97 -8
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/PropertyDetails.tsx +67 -45
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/PropertySearch.tsx +299 -191
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/package.json +4 -1
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/propertyListingGraphQL.ts +0 -380
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/properties/PropertyListingSearchPagination.tsx +0 -136
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/constants/propertyListing.ts +0 -4
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyAddresses.ts +0 -45
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyListingAmenities.ts +0 -57
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyListingSearch.ts +0 -84
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyPrimaryImages.ts +0 -53
- /package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/{leadApi.ts → leads/leadApi.ts} +0 -0
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Property Listing search via Salesforce GraphQL.
|
|
3
|
-
* Replaces REST keyword search with uiapi.query.Property_Listing__c.
|
|
4
|
-
*/
|
|
5
|
-
import { gql } from "@salesforce/sdk-data";
|
|
6
|
-
import type {
|
|
7
|
-
PropertyListingsQuery,
|
|
8
|
-
PropertyListingsQueryVariables,
|
|
9
|
-
} from "@/api/graphql-operations-types.js";
|
|
10
|
-
import { ResultOrder } from "@/api/graphql-operations-types.js";
|
|
11
|
-
import { executeGraphQL } from "@/api/graphqlClient.js";
|
|
12
|
-
import type {
|
|
13
|
-
SearchResultRecord,
|
|
14
|
-
SearchResultRecordData,
|
|
15
|
-
FieldValue,
|
|
16
|
-
} from "@/types/searchResults.js";
|
|
17
|
-
|
|
18
|
-
const OBJECT_API_NAME = "Property_Listing__c";
|
|
19
|
-
|
|
20
|
-
/** GraphQL node shape: fields are { value?, displayValue? }. Property__r for property name and location. */
|
|
21
|
-
type PropertyListingNode = {
|
|
22
|
-
Id: string;
|
|
23
|
-
ApiName?: string | null;
|
|
24
|
-
Name?: { value?: string | null; displayValue?: string | null } | null;
|
|
25
|
-
Listing_Price__c?: { value?: number | null; displayValue?: string | null } | null;
|
|
26
|
-
Listing_Status__c?: { value?: string | null; displayValue?: string | null } | null;
|
|
27
|
-
Property__c?: { value?: string | null; displayValue?: string | null } | null;
|
|
28
|
-
Property__r?: {
|
|
29
|
-
Name?: { value?: string | null; displayValue?: string | null } | null;
|
|
30
|
-
Address__c?: { value?: string | null; displayValue?: string | null } | null;
|
|
31
|
-
Coordinates__Latitude__s?: { value?: number | null; displayValue?: string | null } | null;
|
|
32
|
-
Coordinates__Longitude__s?: { value?: number | null; displayValue?: string | null } | null;
|
|
33
|
-
Bedrooms__c?: { value?: number | null; displayValue?: string | null } | null;
|
|
34
|
-
} | null;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
function nodeToFieldValue(
|
|
38
|
-
fieldObj: { value?: unknown; displayValue?: string | null } | null | undefined,
|
|
39
|
-
): FieldValue {
|
|
40
|
-
if (fieldObj == null) {
|
|
41
|
-
return { displayValue: null, value: null };
|
|
42
|
-
}
|
|
43
|
-
const value = fieldObj.value ?? null;
|
|
44
|
-
const displayValue = fieldObj.displayValue ?? (value != null ? String(value) : null);
|
|
45
|
-
return { displayValue, value };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function nodeToSearchResultRecordData(node: PropertyListingNode): SearchResultRecordData {
|
|
49
|
-
const fields: Record<string, FieldValue> = {};
|
|
50
|
-
const fieldKeys: (keyof PropertyListingNode)[] = [
|
|
51
|
-
"Name",
|
|
52
|
-
"Listing_Price__c",
|
|
53
|
-
"Listing_Status__c",
|
|
54
|
-
"Property__c",
|
|
55
|
-
];
|
|
56
|
-
for (const key of fieldKeys) {
|
|
57
|
-
if (key === "Id" || key === "ApiName") continue;
|
|
58
|
-
const raw = node[key];
|
|
59
|
-
if (raw != null && typeof raw === "object" && "value" in raw) {
|
|
60
|
-
fields[key] = nodeToFieldValue(raw as { value?: unknown; displayValue?: string | null });
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
// Flatten property name and address for display (read-only fields for search results)
|
|
64
|
-
const prop = node.Property__r;
|
|
65
|
-
if (prop?.Name != null && typeof prop.Name === "object" && "value" in prop.Name) {
|
|
66
|
-
fields["Property__r.Name"] = nodeToFieldValue(
|
|
67
|
-
prop.Name as { value?: unknown; displayValue?: string | null },
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
if (
|
|
71
|
-
prop?.Address__c != null &&
|
|
72
|
-
typeof prop.Address__c === "object" &&
|
|
73
|
-
"value" in prop.Address__c
|
|
74
|
-
) {
|
|
75
|
-
fields["Property__r.Address__c"] = nodeToFieldValue(
|
|
76
|
-
prop.Address__c as { value?: unknown; displayValue?: string | null },
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
if (
|
|
80
|
-
prop?.Bedrooms__c != null &&
|
|
81
|
-
typeof prop.Bedrooms__c === "object" &&
|
|
82
|
-
"value" in prop.Bedrooms__c
|
|
83
|
-
) {
|
|
84
|
-
fields["Property__r.Bedrooms__c"] = nodeToFieldValue(
|
|
85
|
-
prop.Bedrooms__c as { value?: unknown; displayValue?: string | null },
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
const lat = prop?.Coordinates__Latitude__s;
|
|
89
|
-
if (lat != null && typeof lat === "object" && "value" in lat) {
|
|
90
|
-
fields["Property__r.Coordinates__Latitude__s"] = nodeToFieldValue(
|
|
91
|
-
lat as { value?: unknown; displayValue?: string | null },
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
const lng = prop?.Coordinates__Longitude__s;
|
|
95
|
-
if (lng != null && typeof lng === "object" && "value" in lng) {
|
|
96
|
-
fields["Property__r.Coordinates__Longitude__s"] = nodeToFieldValue(
|
|
97
|
-
lng as { value?: unknown; displayValue?: string | null },
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
return {
|
|
101
|
-
id: node.Id,
|
|
102
|
-
apiName: typeof node.ApiName === "string" ? node.ApiName : OBJECT_API_NAME,
|
|
103
|
-
eTag: "",
|
|
104
|
-
fields,
|
|
105
|
-
childRelationships: {},
|
|
106
|
-
weakEtag: 0,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Query with optional search term (Name like), pagination, and orderBy.
|
|
111
|
-
* Uses Property_Listing__c_Filter. orderBy is optional; omit if org schema does not support it. */
|
|
112
|
-
const PROPERTY_LISTINGS_QUERY = gql`
|
|
113
|
-
query PropertyListings(
|
|
114
|
-
$where: Property_Listing__c_Filter
|
|
115
|
-
$first: Int!
|
|
116
|
-
$after: String
|
|
117
|
-
$orderBy: Property_Listing__c_OrderBy
|
|
118
|
-
) {
|
|
119
|
-
uiapi {
|
|
120
|
-
query {
|
|
121
|
-
Property_Listing__c(where: $where, first: $first, after: $after, orderBy: $orderBy) {
|
|
122
|
-
edges {
|
|
123
|
-
node {
|
|
124
|
-
Id
|
|
125
|
-
ApiName
|
|
126
|
-
Name @optional {
|
|
127
|
-
value
|
|
128
|
-
displayValue
|
|
129
|
-
}
|
|
130
|
-
Listing_Price__c @optional {
|
|
131
|
-
value
|
|
132
|
-
displayValue
|
|
133
|
-
}
|
|
134
|
-
Listing_Status__c @optional {
|
|
135
|
-
value
|
|
136
|
-
displayValue
|
|
137
|
-
}
|
|
138
|
-
Property__c @optional {
|
|
139
|
-
value
|
|
140
|
-
displayValue
|
|
141
|
-
}
|
|
142
|
-
Property__r @optional {
|
|
143
|
-
Name @optional {
|
|
144
|
-
value
|
|
145
|
-
displayValue
|
|
146
|
-
}
|
|
147
|
-
Address__c @optional {
|
|
148
|
-
value
|
|
149
|
-
displayValue
|
|
150
|
-
}
|
|
151
|
-
Coordinates__Latitude__s @optional {
|
|
152
|
-
value
|
|
153
|
-
displayValue
|
|
154
|
-
}
|
|
155
|
-
Coordinates__Longitude__s @optional {
|
|
156
|
-
value
|
|
157
|
-
displayValue
|
|
158
|
-
}
|
|
159
|
-
Bedrooms__c @optional {
|
|
160
|
-
value
|
|
161
|
-
displayValue
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
cursor
|
|
166
|
-
}
|
|
167
|
-
pageInfo {
|
|
168
|
-
hasNextPage
|
|
169
|
-
hasPreviousPage
|
|
170
|
-
startCursor
|
|
171
|
-
endCursor
|
|
172
|
-
}
|
|
173
|
-
totalCount
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
`;
|
|
179
|
-
|
|
180
|
-
export interface PropertyListingGraphQLResult {
|
|
181
|
-
records: SearchResultRecord[];
|
|
182
|
-
nextPageToken: string | null;
|
|
183
|
-
previousPageToken: string | null;
|
|
184
|
-
endCursor: string | null;
|
|
185
|
-
totalCount: number | null;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** SortBy values match PropertySearchFilters SortBy (price_asc, price_desc, beds_asc, beds_desc). */
|
|
189
|
-
export interface PropertyListingFilters {
|
|
190
|
-
priceMin?: number;
|
|
191
|
-
priceMax?: number;
|
|
192
|
-
bedroomsMin?: number;
|
|
193
|
-
bedroomsMax?: number;
|
|
194
|
-
sortBy?: "price_asc" | "price_desc" | "beds_asc" | "beds_desc" | null;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/** Where condition for Property_Listing__c_Filter (matches graphql-operations-types.ts). */
|
|
198
|
-
type PropertyListingsWhereCondition = {
|
|
199
|
-
Name?: { like: string };
|
|
200
|
-
Listing_Status__c?: { like: string };
|
|
201
|
-
Listing_Price__c?: { gte?: number; lte?: number };
|
|
202
|
-
Property__r?: {
|
|
203
|
-
Name?: { like: string };
|
|
204
|
-
Address__c?: { like: string };
|
|
205
|
-
Bedrooms__c?: { gte?: number; lte?: number };
|
|
206
|
-
};
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
/** Text search: name or Property__r name/address. Returns { or: [...] } or undefined. */
|
|
210
|
-
function buildTextWhere(
|
|
211
|
-
searchTerm: string,
|
|
212
|
-
): NonNullable<PropertyListingsQueryVariables["where"]> | undefined {
|
|
213
|
-
const term = searchTerm.trim();
|
|
214
|
-
if (term.length === 0) return undefined;
|
|
215
|
-
const pattern = `%${term}%`;
|
|
216
|
-
return {
|
|
217
|
-
or: [
|
|
218
|
-
{ Name: { like: pattern } },
|
|
219
|
-
{ Property__r: { Name: { like: pattern } } },
|
|
220
|
-
{ Property__r: { Address__c: { like: pattern } } },
|
|
221
|
-
],
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** Element type allowed inside { and: [...] } (no nested and). */
|
|
226
|
-
type AndElement = PropertyListingsWhereCondition | { or: PropertyListingsWhereCondition[] };
|
|
227
|
-
|
|
228
|
-
/** Combines text search + price range + bedroom range (min/max) into one where (and of conditions). */
|
|
229
|
-
function buildWhere(
|
|
230
|
-
searchTerm: string,
|
|
231
|
-
filters: PropertyListingFilters | undefined,
|
|
232
|
-
): PropertyListingsQueryVariables["where"] {
|
|
233
|
-
const conditions: AndElement[] = [];
|
|
234
|
-
const textWhere = buildTextWhere(searchTerm);
|
|
235
|
-
if (textWhere) conditions.push(textWhere as AndElement);
|
|
236
|
-
const priceMin =
|
|
237
|
-
filters?.priceMin != null && Number.isFinite(filters.priceMin) ? filters.priceMin : undefined;
|
|
238
|
-
const priceMax =
|
|
239
|
-
filters?.priceMax != null && Number.isFinite(filters.priceMax) ? filters.priceMax : undefined;
|
|
240
|
-
if (priceMin != null || priceMax != null) {
|
|
241
|
-
conditions.push({
|
|
242
|
-
Listing_Price__c: {
|
|
243
|
-
...(priceMin != null && { gte: priceMin }),
|
|
244
|
-
...(priceMax != null && { lte: priceMax }),
|
|
245
|
-
},
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
const bedroomsMin =
|
|
249
|
-
filters?.bedroomsMin != null && Number.isFinite(filters.bedroomsMin) && filters.bedroomsMin >= 0
|
|
250
|
-
? filters.bedroomsMin
|
|
251
|
-
: undefined;
|
|
252
|
-
const bedroomsMax =
|
|
253
|
-
filters?.bedroomsMax != null && Number.isFinite(filters.bedroomsMax) && filters.bedroomsMax >= 0
|
|
254
|
-
? filters.bedroomsMax
|
|
255
|
-
: undefined;
|
|
256
|
-
if (bedroomsMin != null || bedroomsMax != null) {
|
|
257
|
-
conditions.push({
|
|
258
|
-
Property__r: {
|
|
259
|
-
Bedrooms__c: {
|
|
260
|
-
...(bedroomsMin != null && { gte: bedroomsMin }),
|
|
261
|
-
...(bedroomsMax != null && { lte: bedroomsMax }),
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
if (conditions.length === 0) return undefined;
|
|
267
|
-
if (conditions.length === 1) return conditions[0];
|
|
268
|
-
return { and: conditions };
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/** Build orderBy for Property_Listing__c from SortBy. Returns undefined when sortBy is null or not supported. */
|
|
272
|
-
function buildOrderBy(
|
|
273
|
-
sortBy: PropertyListingFilters["sortBy"],
|
|
274
|
-
): PropertyListingsQueryVariables["orderBy"] {
|
|
275
|
-
if (sortBy == null) return undefined;
|
|
276
|
-
switch (sortBy) {
|
|
277
|
-
case "price_asc":
|
|
278
|
-
return { Listing_Price__c: { order: ResultOrder.Asc } };
|
|
279
|
-
case "price_desc":
|
|
280
|
-
return { Listing_Price__c: { order: ResultOrder.Desc } };
|
|
281
|
-
case "beds_asc":
|
|
282
|
-
return { Property__r: { Bedrooms__c: { order: ResultOrder.Asc } } };
|
|
283
|
-
case "beds_desc":
|
|
284
|
-
return { Property__r: { Bedrooms__c: { order: ResultOrder.Desc } } };
|
|
285
|
-
default:
|
|
286
|
-
return undefined;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Fetch Property_Listing__c records via GraphQL.
|
|
292
|
-
* Optional text search (name, Property__r name/address), price range (min/max), and bedroom range (min/max).
|
|
293
|
-
*/
|
|
294
|
-
export async function queryPropertyListingsGraphQL(
|
|
295
|
-
searchTerm: string,
|
|
296
|
-
pageSize: number,
|
|
297
|
-
afterCursor: string | null,
|
|
298
|
-
_signal?: AbortSignal,
|
|
299
|
-
filters?: PropertyListingFilters,
|
|
300
|
-
): Promise<PropertyListingGraphQLResult> {
|
|
301
|
-
const where = buildWhere(searchTerm, filters);
|
|
302
|
-
|
|
303
|
-
const variables: PropertyListingsQueryVariables = {
|
|
304
|
-
first: Math.min(Math.max(pageSize, 1), 200),
|
|
305
|
-
};
|
|
306
|
-
if (where) variables.where = where;
|
|
307
|
-
if (afterCursor) variables.after = afterCursor;
|
|
308
|
-
const orderBy = buildOrderBy(filters?.sortBy);
|
|
309
|
-
if (orderBy != null) variables.orderBy = orderBy;
|
|
310
|
-
|
|
311
|
-
const response = await executeGraphQL<PropertyListingsQuery, PropertyListingsQueryVariables>(
|
|
312
|
-
PROPERTY_LISTINGS_QUERY,
|
|
313
|
-
variables,
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
const conn = response.uiapi?.query?.Property_Listing__c;
|
|
317
|
-
const edges = conn?.edges ?? [];
|
|
318
|
-
const pageInfo = conn?.pageInfo;
|
|
319
|
-
|
|
320
|
-
const records: SearchResultRecord[] = edges
|
|
321
|
-
.filter((e) => e != null && e.node != null)
|
|
322
|
-
.map((e) => {
|
|
323
|
-
const node = e!.node!;
|
|
324
|
-
const data = nodeToSearchResultRecordData(node as PropertyListingNode);
|
|
325
|
-
return {
|
|
326
|
-
record: data,
|
|
327
|
-
highlightInfo: { fields: {}, snippet: null },
|
|
328
|
-
searchInfo: { isPromoted: false, isSpellCorrected: false },
|
|
329
|
-
};
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
return {
|
|
333
|
-
records,
|
|
334
|
-
nextPageToken: pageInfo?.hasNextPage ? (pageInfo.endCursor ?? null) : null,
|
|
335
|
-
previousPageToken: pageInfo?.hasPreviousPage ? (pageInfo.startCursor ?? null) : null,
|
|
336
|
-
endCursor: pageInfo?.endCursor ?? null,
|
|
337
|
-
totalCount: conn?.totalCount ?? null,
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Fetch the available price range (min/max) from the first page of listings for the current search.
|
|
343
|
-
* No price or bedroom filters applied. Used to render the filter bar with known bounds.
|
|
344
|
-
*/
|
|
345
|
-
export async function queryPropertyListingPriceRange(
|
|
346
|
-
searchTerm: string,
|
|
347
|
-
): Promise<{ priceMin: number; priceMax: number } | null> {
|
|
348
|
-
const where = buildTextWhere(searchTerm);
|
|
349
|
-
const variables: PropertyListingsQueryVariables = {
|
|
350
|
-
first: 200,
|
|
351
|
-
};
|
|
352
|
-
if (where) variables.where = where;
|
|
353
|
-
|
|
354
|
-
const response = await executeGraphQL<PropertyListingsQuery, PropertyListingsQueryVariables>(
|
|
355
|
-
PROPERTY_LISTINGS_QUERY,
|
|
356
|
-
variables,
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
const edges = response.uiapi?.query?.Property_Listing__c?.edges ?? [];
|
|
360
|
-
const prices: number[] = [];
|
|
361
|
-
for (const edge of edges) {
|
|
362
|
-
const node = edge?.node as PropertyListingNode | null | undefined;
|
|
363
|
-
const raw = node?.Listing_Price__c?.value;
|
|
364
|
-
const num = typeof raw === "number" && Number.isFinite(raw) ? raw : null;
|
|
365
|
-
if (num != null && num >= 0) prices.push(num);
|
|
366
|
-
}
|
|
367
|
-
if (prices.length === 0) return null;
|
|
368
|
-
return {
|
|
369
|
-
priceMin: Math.min(...prices),
|
|
370
|
-
priceMax: Math.max(...prices),
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/** Static columns for Property_Listing__c list (only fields exposed in the GraphQL query). */
|
|
375
|
-
export const PROPERTY_LISTING_COLUMNS = [
|
|
376
|
-
{ fieldApiName: "Name", label: "Name", searchable: true, sortable: true },
|
|
377
|
-
{ fieldApiName: "Listing_Price__c", label: "Listing Price", searchable: false, sortable: true },
|
|
378
|
-
{ fieldApiName: "Listing_Status__c", label: "Status", searchable: false, sortable: true },
|
|
379
|
-
{ fieldApiName: "Property__c", label: "Property", searchable: false, sortable: true },
|
|
380
|
-
] as const;
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pagination controls for Property_Listing__c consumer search (token-based).
|
|
3
|
-
* Replaces feature-react-global-search SearchPagination without pulling in that feature.
|
|
4
|
-
*/
|
|
5
|
-
import { Button } from "@/components/ui/button";
|
|
6
|
-
import {
|
|
7
|
-
Select,
|
|
8
|
-
SelectContent,
|
|
9
|
-
SelectItem,
|
|
10
|
-
SelectTrigger,
|
|
11
|
-
SelectValue,
|
|
12
|
-
} from "@/components/ui/select";
|
|
13
|
-
import { Label } from "@/components/ui/label";
|
|
14
|
-
import {
|
|
15
|
-
PAGE_SIZE_OPTIONS,
|
|
16
|
-
getValidPageSize,
|
|
17
|
-
isValidPageSize,
|
|
18
|
-
} from "@/utils/propertyListingPaginationUtils";
|
|
19
|
-
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
20
|
-
|
|
21
|
-
interface PropertyListingSearchPaginationProps {
|
|
22
|
-
currentPageToken: string;
|
|
23
|
-
nextPageToken: string | null;
|
|
24
|
-
previousPageToken: string | null;
|
|
25
|
-
hasNextPage?: boolean;
|
|
26
|
-
hasPreviousPage?: boolean;
|
|
27
|
-
pageSize: number;
|
|
28
|
-
onPageChange: (newPageToken: string, direction?: "next" | "prev" | "first") => void;
|
|
29
|
-
onPageSizeChange: (newPageSize: number) => void;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export default function PropertyListingSearchPagination({
|
|
33
|
-
currentPageToken,
|
|
34
|
-
nextPageToken,
|
|
35
|
-
previousPageToken,
|
|
36
|
-
hasNextPage = false,
|
|
37
|
-
hasPreviousPage = false,
|
|
38
|
-
pageSize,
|
|
39
|
-
onPageChange,
|
|
40
|
-
onPageSizeChange,
|
|
41
|
-
}: PropertyListingSearchPaginationProps) {
|
|
42
|
-
const validPageSize = getValidPageSize(pageSize);
|
|
43
|
-
|
|
44
|
-
const currentPageTokenNum = parseInt(currentPageToken, 10) || 0;
|
|
45
|
-
const currentPage = currentPageTokenNum + 1;
|
|
46
|
-
|
|
47
|
-
const canGoPrevious = Boolean(hasPreviousPage);
|
|
48
|
-
const canGoNext = Boolean(hasNextPage && nextPageToken != null);
|
|
49
|
-
|
|
50
|
-
const handlePrevious = () => {
|
|
51
|
-
if (canGoPrevious) {
|
|
52
|
-
onPageChange(previousPageToken ?? "", "prev");
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const handleNext = () => {
|
|
57
|
-
if (canGoNext && nextPageToken != null) {
|
|
58
|
-
onPageChange(nextPageToken, "next");
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const handlePageSizeChange = (newPageSize: string) => {
|
|
63
|
-
const newSize = parseInt(newPageSize, 10);
|
|
64
|
-
if (!isNaN(newSize) && isValidPageSize(newSize) && newSize !== validPageSize) {
|
|
65
|
-
onPageSizeChange(newSize);
|
|
66
|
-
onPageChange("0", "first");
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<nav
|
|
72
|
-
className="flex w-full flex-row flex-wrap items-center justify-between gap-4 py-2"
|
|
73
|
-
aria-label="Search results pagination"
|
|
74
|
-
>
|
|
75
|
-
<div
|
|
76
|
-
className="flex shrink-0 items-center gap-2"
|
|
77
|
-
role="group"
|
|
78
|
-
aria-label="Page size selector"
|
|
79
|
-
>
|
|
80
|
-
<Label htmlFor="page-size-select" className="whitespace-nowrap text-sm font-normal">
|
|
81
|
-
Results per page:
|
|
82
|
-
</Label>
|
|
83
|
-
<Select value={validPageSize.toString()} onValueChange={handlePageSizeChange}>
|
|
84
|
-
<SelectTrigger id="page-size-select" className="w-[70px]" aria-label="Results per page">
|
|
85
|
-
<SelectValue />
|
|
86
|
-
</SelectTrigger>
|
|
87
|
-
<SelectContent>
|
|
88
|
-
{PAGE_SIZE_OPTIONS.map((option) => (
|
|
89
|
-
<SelectItem key={option.value} value={option.value}>
|
|
90
|
-
{option.label}
|
|
91
|
-
</SelectItem>
|
|
92
|
-
))}
|
|
93
|
-
</SelectContent>
|
|
94
|
-
</Select>
|
|
95
|
-
</div>
|
|
96
|
-
|
|
97
|
-
<div className="flex shrink-0 items-center gap-1" role="group" aria-label="Page navigation">
|
|
98
|
-
<Button
|
|
99
|
-
type="button"
|
|
100
|
-
variant="outline"
|
|
101
|
-
size="sm"
|
|
102
|
-
disabled={!canGoPrevious}
|
|
103
|
-
onClick={handlePrevious}
|
|
104
|
-
aria-label={
|
|
105
|
-
canGoPrevious
|
|
106
|
-
? `Go to previous page (Page ${currentPage - 1})`
|
|
107
|
-
: "Previous page (disabled)"
|
|
108
|
-
}
|
|
109
|
-
>
|
|
110
|
-
<ChevronLeft className="size-4" aria-hidden />
|
|
111
|
-
Previous
|
|
112
|
-
</Button>
|
|
113
|
-
<span
|
|
114
|
-
className="min-w-[4rem] px-2 text-center text-sm text-muted-foreground"
|
|
115
|
-
aria-current="page"
|
|
116
|
-
aria-label={`Page ${currentPage}, current page`}
|
|
117
|
-
>
|
|
118
|
-
Page {currentPage}
|
|
119
|
-
</span>
|
|
120
|
-
<Button
|
|
121
|
-
type="button"
|
|
122
|
-
variant="outline"
|
|
123
|
-
size="sm"
|
|
124
|
-
disabled={!canGoNext}
|
|
125
|
-
onClick={handleNext}
|
|
126
|
-
aria-label={
|
|
127
|
-
canGoNext ? `Go to next page (Page ${currentPage + 1})` : "Next page (disabled)"
|
|
128
|
-
}
|
|
129
|
-
>
|
|
130
|
-
Next
|
|
131
|
-
<ChevronRight className="size-4" aria-hidden />
|
|
132
|
-
</Button>
|
|
133
|
-
</div>
|
|
134
|
-
</nav>
|
|
135
|
-
);
|
|
136
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fetches Address__c for each property id from search results.
|
|
3
|
-
* Returns propertyId -> address for display on listing cards.
|
|
4
|
-
*/
|
|
5
|
-
import { useState, useEffect } from "react";
|
|
6
|
-
import { fetchPropertyAddresses } from "@/api/properties/propertyDetailGraphQL";
|
|
7
|
-
import { getPropertyIdFromRecord } from "@/hooks/usePropertyPrimaryImages";
|
|
8
|
-
import type { SearchResultRecord } from "@/types/searchResults.js";
|
|
9
|
-
|
|
10
|
-
export function usePropertyAddresses(
|
|
11
|
-
results: SearchResultRecord[],
|
|
12
|
-
): Record<string, string> & { loading: boolean } {
|
|
13
|
-
const [map, setMap] = useState<Record<string, string>>({});
|
|
14
|
-
const [fetchedKey, setFetchedKey] = useState("");
|
|
15
|
-
|
|
16
|
-
const propertyIds = results
|
|
17
|
-
.map((r) => r?.record && getPropertyIdFromRecord(r.record))
|
|
18
|
-
.filter((id): id is string => Boolean(id));
|
|
19
|
-
const idsKey = [...new Set(propertyIds)].join(",");
|
|
20
|
-
const loading = idsKey !== "" && idsKey !== fetchedKey;
|
|
21
|
-
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
if (idsKey === "") {
|
|
24
|
-
setMap({});
|
|
25
|
-
setFetchedKey("");
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
let cancelled = false;
|
|
29
|
-
fetchPropertyAddresses(idsKey.split(","))
|
|
30
|
-
.then((next) => {
|
|
31
|
-
if (!cancelled) setMap(next);
|
|
32
|
-
})
|
|
33
|
-
.catch(() => {
|
|
34
|
-
if (!cancelled) setMap({});
|
|
35
|
-
})
|
|
36
|
-
.finally(() => {
|
|
37
|
-
if (!cancelled) setFetchedKey(idsKey);
|
|
38
|
-
});
|
|
39
|
-
return () => {
|
|
40
|
-
cancelled = true;
|
|
41
|
-
};
|
|
42
|
-
}, [idsKey]);
|
|
43
|
-
|
|
44
|
-
return Object.assign(map, { loading });
|
|
45
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fetches amenities (feature descriptions) for each property in search results.
|
|
3
|
-
* Returns a map of propertyId -> "Amenity 1 | Amenity 2 | ..." for use in listing cards.
|
|
4
|
-
*/
|
|
5
|
-
import { useState, useEffect } from "react";
|
|
6
|
-
import { fetchFeaturesByPropertyId } from "@/api/properties/propertyDetailGraphQL";
|
|
7
|
-
import { getPropertyIdFromRecord } from "@/hooks/usePropertyPrimaryImages";
|
|
8
|
-
import type { SearchResultRecord } from "@/types/searchResults.js";
|
|
9
|
-
|
|
10
|
-
const AMENITIES_SEPARATOR = " | ";
|
|
11
|
-
|
|
12
|
-
export function usePropertyListingAmenities(
|
|
13
|
-
results: SearchResultRecord[],
|
|
14
|
-
): Record<string, string> & { loading: boolean } {
|
|
15
|
-
const [map, setMap] = useState<Record<string, string>>({});
|
|
16
|
-
const [fetchedKey, setFetchedKey] = useState("");
|
|
17
|
-
|
|
18
|
-
const propertyIds = results
|
|
19
|
-
.map((r) => r?.record && getPropertyIdFromRecord(r.record))
|
|
20
|
-
.filter((id): id is string => Boolean(id));
|
|
21
|
-
const uniqueIds = [...new Set(propertyIds)];
|
|
22
|
-
const idsKey = uniqueIds.join(",");
|
|
23
|
-
const loading = idsKey !== "" && idsKey !== fetchedKey;
|
|
24
|
-
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
if (uniqueIds.length === 0) {
|
|
27
|
-
setMap({});
|
|
28
|
-
setFetchedKey("");
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
let cancelled = false;
|
|
32
|
-
Promise.all(uniqueIds.map((id) => fetchFeaturesByPropertyId(id)))
|
|
33
|
-
.then((featuresPerProperty) => {
|
|
34
|
-
if (cancelled) return;
|
|
35
|
-
const next: Record<string, string> = {};
|
|
36
|
-
uniqueIds.forEach((id, i) => {
|
|
37
|
-
const features = featuresPerProperty[i] ?? [];
|
|
38
|
-
const descriptions = features
|
|
39
|
-
.map((f) => f.description)
|
|
40
|
-
.filter((d): d is string => d != null && d.trim() !== "");
|
|
41
|
-
next[id] = descriptions.join(AMENITIES_SEPARATOR);
|
|
42
|
-
});
|
|
43
|
-
setMap(next);
|
|
44
|
-
})
|
|
45
|
-
.catch(() => {
|
|
46
|
-
if (!cancelled) setMap({});
|
|
47
|
-
})
|
|
48
|
-
.finally(() => {
|
|
49
|
-
if (!cancelled) setFetchedKey(idsKey);
|
|
50
|
-
});
|
|
51
|
-
return () => {
|
|
52
|
-
cancelled = true;
|
|
53
|
-
};
|
|
54
|
-
}, [idsKey]);
|
|
55
|
-
|
|
56
|
-
return Object.assign(map, { loading });
|
|
57
|
-
}
|