@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.
Files changed (53) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/webapplications/propertyrentalapp/eslint.config.js +13 -2
  3. package/dist/force-app/main/default/webapplications/propertyrentalapp/package.json +3 -3
  4. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/graphql-operations-types.ts +24594 -7234
  5. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/maintenanceRequests/maintenanceRequestApi.ts +21 -157
  6. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/maintenanceRequests/query/maintenanceRequests.graphql +60 -0
  7. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/propertyDetailGraphQL.ts +45 -444
  8. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/propertyNodeUtils.ts +29 -0
  9. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/propertySearchService.ts +56 -0
  10. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/distinctPropertyStatus.graphql +19 -0
  11. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/distinctPropertyType.graphql +19 -0
  12. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/listingById.graphql +29 -0
  13. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/propertyAddressesByIds.graphql +17 -0
  14. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/propertyDetailById.graphql +124 -0
  15. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/searchProperties.graphql +85 -0
  16. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/appLayout.tsx +1 -1
  17. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/SkeletonPrimitives.tsx +9 -6
  18. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/dashboard/WeatherWidget.tsx +35 -19
  19. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/maintenanceRequests/MaintenanceRequestList.tsx +7 -5
  20. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/maintenanceRequests/MaintenanceRequestListItem.tsx +11 -10
  21. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/maintenanceRequests/MaintenanceSummaryDetailsModal.tsx +20 -15
  22. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/properties/PropertyListingCard.tsx +11 -24
  23. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/properties/PropertyMap.tsx +2 -1
  24. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/context/AuthContext.tsx +1 -1
  25. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/hooks/useCountdownTimer.ts +1 -1
  26. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/pages/Profile.tsx +3 -3
  27. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/pages/Register.tsx +1 -1
  28. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/authentication/sessionTimeout/SessionTimeoutValidator.tsx +12 -18
  29. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/object-search/components/FilterContext.tsx +1 -1
  30. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/features/object-search/hooks/useObjectSearchParams.ts +10 -5
  31. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useGeocode.ts +23 -38
  32. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useMaintenanceRequests.ts +29 -25
  33. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyDetail.ts +42 -78
  34. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyMapMarkers.ts +34 -41
  35. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useWeather.ts +14 -30
  36. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Application.tsx +41 -74
  37. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Contact.tsx +44 -55
  38. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Dashboard.tsx +1 -0
  39. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Home.tsx +63 -32
  40. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Maintenance.tsx +97 -8
  41. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/PropertyDetails.tsx +67 -45
  42. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/PropertySearch.tsx +299 -191
  43. package/dist/package-lock.json +2 -2
  44. package/dist/package.json +1 -1
  45. package/package.json +4 -1
  46. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/propertyListingGraphQL.ts +0 -380
  47. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/properties/PropertyListingSearchPagination.tsx +0 -136
  48. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/constants/propertyListing.ts +0 -4
  49. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyAddresses.ts +0 -45
  50. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyListingAmenities.ts +0 -57
  51. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyListingSearch.ts +0 -84
  52. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyPrimaryImages.ts +0 -53
  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,4 +0,0 @@
1
- /**
2
- * Defaults for consumer property listing search (map + list), independent of object-search admin pages.
3
- */
4
- export const DEFAULT_PAGE_SIZE = 20;
@@ -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
- }