@salesforce/webapp-template-app-react-sample-b2x-experimental 1.68.0 → 1.69.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.
Files changed (131) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/data/Lease__c.json +13 -0
  3. package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +13 -8
  4. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/applicationApi.ts +78 -0
  5. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +17 -0
  6. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/index.ts +19 -0
  7. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/leadApi.ts +69 -0
  8. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/maintenanceRequestApi.ts +177 -0
  9. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectDetailService.ts +125 -0
  10. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoGraphQLService.ts +194 -0
  11. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoService.ts +199 -0
  12. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyDetailGraphQL.ts +497 -0
  13. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyListingGraphQL.ts +190 -0
  14. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/recordListGraphQLService.ts +365 -0
  15. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +20 -30
  16. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/FiltersPanel.tsx +375 -0
  17. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/LoadingFallback.tsx +61 -0
  18. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyListingCard.tsx +164 -0
  19. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyMap.tsx +113 -0
  20. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/SearchResultCard.tsx +131 -0
  21. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/alerts/status-alert.tsx +45 -0
  22. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailFields.tsx +55 -0
  23. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailForm.tsx +146 -0
  24. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailHeader.tsx +34 -0
  25. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailLayoutSections.tsx +80 -0
  26. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/Section.tsx +108 -0
  27. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/SectionRow.tsx +20 -0
  28. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/UiApiDetailForm.tsx +140 -0
  29. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FieldValueDisplay.tsx +73 -0
  30. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedAddress.tsx +29 -0
  31. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedEmail.tsx +17 -0
  32. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedPhone.tsx +24 -0
  33. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedText.tsx +11 -0
  34. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedUrl.tsx +29 -0
  35. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/index.ts +6 -0
  36. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterField.tsx +54 -0
  37. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterInput.tsx +55 -0
  38. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterSelect.tsx +72 -0
  39. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/filters-form.tsx +114 -0
  40. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/submit-button.tsx +47 -0
  41. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/layout/card-layout.tsx +19 -0
  42. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/ResultCardFields.tsx +71 -0
  43. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchHeader.tsx +31 -0
  44. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchPagination.tsx +144 -0
  45. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchResultsPanel.tsx +197 -0
  46. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/shared/GlobalSearchInput.tsx +114 -0
  47. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/constants.ts +39 -0
  48. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/index.ts +33 -0
  49. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/form.tsx +204 -0
  50. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/index.ts +22 -0
  51. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useGeocode.ts +35 -0
  52. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useMaintenanceRequests.ts +39 -0
  53. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectInfoBatch.ts +65 -0
  54. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectSearchData.ts +395 -0
  55. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +36 -0
  56. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyDetail.ts +99 -0
  57. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +75 -0
  58. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +100 -0
  59. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +51 -0
  60. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordDetailLayout.ts +156 -0
  61. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordListGraphQL.ts +135 -0
  62. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useWeather.ts +173 -0
  63. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +263 -76
  64. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Contact.tsx +158 -0
  65. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +137 -65
  66. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/DetailPage.tsx +109 -0
  67. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/GlobalSearch.tsx +229 -0
  68. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +469 -21
  69. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +244 -95
  70. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +211 -39
  71. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +26 -10
  72. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +165 -0
  73. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearchPlaceholder.tsx +49 -0
  74. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-01.jpg +0 -0
  75. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-02.jpg +0 -0
  76. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-03.jpg +0 -0
  77. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-04.jpg +0 -0
  78. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-05.jpg +0 -0
  79. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-06.jpg +0 -0
  80. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-07.jpg +0 -0
  81. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-08.jpg +0 -0
  82. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-09.jpg +0 -0
  83. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-10.jpg +0 -0
  84. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-11.jpg +0 -0
  85. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-12.jpg +0 -0
  86. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-13.jpg +0 -0
  87. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-14.jpg +0 -0
  88. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-15.jpg +0 -0
  89. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-16.jpg +0 -0
  90. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-17.jpg +0 -0
  91. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-18.jpg +0 -0
  92. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-19.jpg +0 -0
  93. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-20.jpg +0 -0
  94. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-21.jpg +0 -0
  95. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-22.jpg +0 -0
  96. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-23.jpg +0 -0
  97. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-24.jpg +0 -0
  98. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-25.jpg +0 -0
  99. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +32 -6
  100. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/styles/global.css +23 -63
  101. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/filters.ts +120 -0
  102. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/picklist.ts +32 -0
  103. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/index.ts +4 -0
  104. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/leaflet.d.ts +17 -0
  105. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/objectInfo/objectInfo.ts +166 -0
  106. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/recordDetail/recordDetail.ts +61 -0
  107. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/search/searchResults.ts +229 -0
  108. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/apiUtils.ts +125 -0
  109. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/cacheUtils.ts +76 -0
  110. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/debounce.ts +89 -0
  111. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldUtils.ts +354 -0
  112. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldValueExtractor.ts +67 -0
  113. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/filterUtils.ts +32 -0
  114. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formDataTransformUtils.ts +260 -0
  115. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formUtils.ts +142 -0
  116. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/geocode.ts +65 -0
  117. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLNodeFieldUtils.ts +186 -0
  118. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLObjectInfoAdapter.ts +319 -0
  119. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLRecordAdapter.ts +90 -0
  120. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/index.ts +59 -0
  121. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/layoutTransformUtils.ts +236 -0
  122. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/linkUtils.ts +14 -0
  123. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/paginationUtils.ts +49 -0
  124. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/recordUtils.ts +159 -0
  125. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/sanitizationUtils.ts +49 -0
  126. package/dist/package.json +1 -1
  127. package/package.json +2 -2
  128. package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls +0 -111
  129. package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls-meta.xml +0 -6
  130. package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls +0 -93
  131. package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls-meta.xml +0 -6
@@ -31,11 +31,23 @@ export default function PropertyListings() {
31
31
  <Input
32
32
  type="text"
33
33
  defaultValue="San Francisco, CA"
34
- className="min-w-[200px] flex-1 flex-[1_1_200px]"
34
+ className="min-w-[200px] flex-1 flex-[1_1_200px] rounded-xl"
35
35
  />
36
- <Button variant="outline">Price</Button>
37
- <Button variant="outline">Beds/Bath</Button>
38
- <Button>All Filters</Button>
36
+ <Button
37
+ variant="outline"
38
+ className="cursor-pointer rounded-xl transition-colors duration-200"
39
+ >
40
+ Price
41
+ </Button>
42
+ <Button
43
+ variant="outline"
44
+ className="cursor-pointer rounded-xl transition-colors duration-200"
45
+ >
46
+ Beds/Bath
47
+ </Button>
48
+ <Button className="cursor-pointer rounded-xl transition-colors duration-200">
49
+ All Filters
50
+ </Button>
39
51
  </div>
40
52
  <h2 className="mb-1 text-lg font-semibold text-foreground">
41
53
  2 Bedroom Apartments for Rent in San Francisco CA
@@ -43,13 +55,13 @@ export default function PropertyListings() {
43
55
  <p className="mb-4 text-sm text-muted-foreground">1,181 Rentals Available</p>
44
56
  <div className="space-y-4">
45
57
  {listings.map((p) => (
46
- <Card key={p.name}>
58
+ <Card key={p.name} className="rounded-2xl shadow-md">
47
59
  <CardContent className="flex gap-4 p-4">
48
60
  <Link
49
61
  to={`/property/${p.id}`}
50
- className="relative block size-[200px] shrink-0 rounded-xl bg-muted"
62
+ className="relative block size-[200px] shrink-0 cursor-pointer rounded-xl bg-muted transition-opacity duration-200 hover:opacity-95"
51
63
  >
52
- <span className="absolute left-2 top-2 rounded-md bg-primary px-2 py-1 text-xs text-primary-foreground">
64
+ <span className="absolute left-2 top-2 rounded-full bg-violet-600 px-2 py-0.5 text-xs font-medium text-white">
53
65
  Virtual Tours
54
66
  </span>
55
67
  </Link>
@@ -57,7 +69,7 @@ export default function PropertyListings() {
57
69
  <h3 className="mb-1 text-base font-semibold">
58
70
  <Link
59
71
  to={`/property/${p.id}`}
60
- className="text-primary no-underline hover:underline"
72
+ className="cursor-pointer text-primary no-underline transition-colors duration-200 hover:underline"
61
73
  >
62
74
  {p.name}
63
75
  </Link>
@@ -70,8 +82,12 @@ export default function PropertyListings() {
70
82
  In Unit Washer & Dryer, Pets Allowed, Fitness Center
71
83
  </p>
72
84
  <p className="mb-2 text-sm text-primary">{p.phone}</p>
73
- <Button asChild size="sm">
74
- <Link to={`/property/${p.id}`}>Email Property</Link>
85
+ <Button
86
+ asChild
87
+ size="sm"
88
+ className="cursor-pointer rounded-xl transition-colors duration-200"
89
+ >
90
+ <Link to={`/application?listingId=${encodeURIComponent(p.id)}`}>Apply</Link>
75
91
  </Button>
76
92
  </div>
77
93
  </CardContent>
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Property Search page – ZENLEASE-style layout.
3
+ * Map ~2/3 left, scrollable listings ~1/3 right; search/filter bar above.
4
+ */
5
+ import { useState, useCallback, useMemo, useEffect } from "react";
6
+ import { useSearchParams } from "react-router";
7
+ import { DEFAULT_PAGE_SIZE } from "@/constants";
8
+ import { usePropertyListingSearch } from "@/hooks/usePropertyListingSearch";
9
+ import {
10
+ usePropertyPrimaryImages,
11
+ getPropertyIdFromRecord,
12
+ } from "@/hooks/usePropertyPrimaryImages";
13
+ import { usePropertyAddresses } from "@/hooks/usePropertyAddresses";
14
+ import { usePropertyMapMarkers } from "@/hooks/usePropertyMapMarkers";
15
+ import { Input } from "@/components/ui/input";
16
+ import SearchPagination from "@/components/search/SearchPagination";
17
+ import PropertyListingCard from "@/components/PropertyListingCard";
18
+ import PropertyMap from "@/components/PropertyMap";
19
+ import PropertySearchPlaceholder from "./PropertySearchPlaceholder";
20
+
21
+ const MAP_CENTER_SF: [number, number] = [37.7749, -122.4194];
22
+
23
+ export default function PropertySearch() {
24
+ const [searchParams] = useSearchParams();
25
+ const initialSearch = searchParams.get("search") ?? "";
26
+ const [searchQuery, setSearchQuery] = useState(initialSearch);
27
+ const [searchPageSize, setSearchPageSize] = useState(DEFAULT_PAGE_SIZE);
28
+ const [searchPageToken, setSearchPageToken] = useState("0");
29
+
30
+ // Sync from URL when navigating with ?search=... (e.g. from Home "Find Home")
31
+ useEffect(() => {
32
+ const q = searchParams.get("search") ?? "";
33
+ setSearchQuery(q);
34
+ setSearchPageToken("0");
35
+ }, [searchParams]);
36
+
37
+ const {
38
+ results,
39
+ nextPageToken,
40
+ previousPageToken,
41
+ currentPageToken,
42
+ resultsLoading,
43
+ resultsError,
44
+ } = usePropertyListingSearch(searchQuery, searchPageSize, searchPageToken);
45
+
46
+ const primaryImagesMap = usePropertyPrimaryImages(results);
47
+ const propertyAddressMap = usePropertyAddresses(results);
48
+ const { markers: mapMarkers } = usePropertyMapMarkers(results);
49
+ const apiUnavailable = Boolean(resultsError);
50
+
51
+ const validResults = useMemo(() => results.filter((r) => r?.record?.id), [results]);
52
+
53
+ const handlePageChange = useCallback((newPageToken: string) => {
54
+ setSearchPageToken(newPageToken);
55
+ }, []);
56
+
57
+ const handlePageSizeChange = useCallback((newPageSize: number) => {
58
+ setSearchPageSize(newPageSize);
59
+ setSearchPageToken("0");
60
+ }, []);
61
+
62
+ const handleSearchSubmit = useCallback(() => {
63
+ setSearchPageToken("0");
64
+ }, []);
65
+
66
+ return (
67
+ <div className="flex h-[calc(100vh-4rem)] min-h-[500px] flex-col">
68
+ {/* Search bar */}
69
+ <div className="flex shrink-0 flex-wrap items-center gap-2 border-b bg-background px-4 py-3">
70
+ <Input
71
+ type="text"
72
+ value={searchQuery}
73
+ onChange={(e) => setSearchQuery(e.target.value)}
74
+ onKeyDown={(e) => e.key === "Enter" && handleSearchSubmit()}
75
+ placeholder="Add area or search"
76
+ className="h-9 w-48 max-w-[200px]"
77
+ aria-label="Search property listings"
78
+ />
79
+ </div>
80
+
81
+ {/* Main: map left (2/3), list right (1/3) */}
82
+ <div className="flex min-h-0 flex-1 flex-col lg:flex-row">
83
+ {/* Map – takes ~2/3 on desktop */}
84
+ <div className="h-64 shrink-0 lg:h-full lg:min-h-0 lg:w-2/3" aria-label="Map">
85
+ <PropertyMap
86
+ center={MAP_CENTER_SF}
87
+ zoom={mapMarkers.length > 0 ? 12 : 11}
88
+ markers={mapMarkers}
89
+ className="h-full w-full"
90
+ />
91
+ </div>
92
+
93
+ {/* Listings – scrollable, ~1/3 */}
94
+ <aside className="flex w-full flex-col border-t border-border bg-background lg:w-1/3 lg:border-l lg:border-t-0">
95
+ <div className="shrink-0 border-b bg-muted/30 px-4 py-3">
96
+ <h2 className="text-base font-semibold text-foreground">
97
+ Property Listings
98
+ {searchQuery.trim() ? ` matching "${searchQuery.trim()}"` : ""}
99
+ </h2>
100
+ <p className="text-sm text-muted-foreground">
101
+ {apiUnavailable
102
+ ? "Placeholder (API unavailable)"
103
+ : resultsLoading
104
+ ? "Loading…"
105
+ : `${results.length} result(s)`}
106
+ </p>
107
+ </div>
108
+ <div className="flex-1 overflow-y-auto p-4">
109
+ {apiUnavailable ? (
110
+ <PropertySearchPlaceholder
111
+ message={resultsError ?? "Search is temporarily unavailable."}
112
+ />
113
+ ) : resultsLoading ? (
114
+ <div className="space-y-4" role="status">
115
+ {[1, 2, 3].map((i) => (
116
+ <div key={i} className="overflow-hidden rounded-lg border">
117
+ <div className="aspect-[16/10] animate-pulse bg-muted" />
118
+ <div className="space-y-2 p-3">
119
+ <div className="h-4 w-3/4 animate-pulse rounded bg-muted" />
120
+ <div className="h-4 w-1/2 animate-pulse rounded bg-muted" />
121
+ </div>
122
+ </div>
123
+ ))}
124
+ </div>
125
+ ) : results.length === 0 ? (
126
+ <div className="py-12 text-center">
127
+ <p className="mb-2 font-medium">No results found</p>
128
+ <p className="text-sm text-muted-foreground">Try adjusting search or filters</p>
129
+ </div>
130
+ ) : (
131
+ <>
132
+ <ul className="space-y-4" role="list" aria-label="Search results">
133
+ {validResults.map((record, index) => {
134
+ const propertyId = getPropertyIdFromRecord(record.record);
135
+ const imageUrl = propertyId ? (primaryImagesMap[propertyId] ?? null) : null;
136
+ const address = propertyId ? (propertyAddressMap[propertyId] ?? null) : null;
137
+ return (
138
+ <li key={record.record.id ?? index}>
139
+ <PropertyListingCard
140
+ record={record.record}
141
+ imageUrl={imageUrl}
142
+ address={address}
143
+ />
144
+ </li>
145
+ );
146
+ })}
147
+ </ul>
148
+ <div className="mt-4">
149
+ <SearchPagination
150
+ currentPageToken={currentPageToken}
151
+ nextPageToken={nextPageToken}
152
+ previousPageToken={previousPageToken}
153
+ pageSize={searchPageSize}
154
+ onPageChange={handlePageChange}
155
+ onPageSizeChange={handlePageSizeChange}
156
+ />
157
+ </div>
158
+ </>
159
+ )}
160
+ </div>
161
+ </aside>
162
+ </div>
163
+ </div>
164
+ );
165
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Placeholder content when Property List search API is unavailable.
3
+ * Shows skeleton cards and a friendly message so the layout still feels like the search experience.
4
+ */
5
+ import { Card, CardContent } from "@/components/ui/card";
6
+ import { Skeleton } from "@/components/ui/skeleton";
7
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
8
+ import { AlertCircle } from "lucide-react";
9
+
10
+ const SKELETON_CARD_COUNT = 3;
11
+
12
+ export default function PropertySearchPlaceholder({
13
+ message = "Search is temporarily unavailable. Please try again later.",
14
+ }: {
15
+ message?: string;
16
+ }) {
17
+ return (
18
+ <div className="space-y-4" role="status" aria-live="polite">
19
+ <Alert
20
+ variant="default"
21
+ className="border-amber-200 bg-amber-50 dark:border-amber-900 dark:bg-amber-950/30"
22
+ >
23
+ <AlertCircle className="h-4 w-4" aria-hidden="true" />
24
+ <AlertTitle>Service temporarily unavailable</AlertTitle>
25
+ <AlertDescription>{message}</AlertDescription>
26
+ </Alert>
27
+ <p className="text-sm text-muted-foreground">Showing placeholder results</p>
28
+ <div className="space-y-4" aria-hidden="true">
29
+ {[...Array(SKELETON_CARD_COUNT)].map((_, i) => (
30
+ <Card key={i}>
31
+ <CardContent className="flex gap-4 p-4">
32
+ <Skeleton className="size-[200px] shrink-0 rounded-xl" />
33
+ <div className="min-w-0 flex-1 space-y-3">
34
+ <Skeleton className="h-5 w-3/4" />
35
+ <Skeleton className="h-4 w-full" />
36
+ <Skeleton className="h-4 w-1/2" />
37
+ <Skeleton className="h-4 w-2/3" />
38
+ <div className="flex gap-2 pt-2">
39
+ <Skeleton className="h-9 w-24" />
40
+ <Skeleton className="h-9 w-28" />
41
+ </div>
42
+ </div>
43
+ </CardContent>
44
+ </Card>
45
+ ))}
46
+ </div>
47
+ </div>
48
+ );
49
+ }
@@ -2,12 +2,16 @@ import type { RouteObject } from 'react-router';
2
2
  import AppLayout from './appLayout';
3
3
  import Home from '@/pages/Home';
4
4
  import NotFound from '@/pages/NotFound';
5
+ import GlobalSearch from "./pages/GlobalSearch";
6
+ import DetailPage from "./pages/DetailPage";
7
+ import { Suspense } from "react";
8
+ import LoadingFallback from "./components/LoadingFallback";
5
9
  import Dashboard from "@/pages/Dashboard";
6
10
  import Maintenance from "@/pages/Maintenance";
7
- import PropertyListings from "@/pages/PropertyListings";
11
+ import PropertySearch from "@/pages/PropertySearch";
8
12
  import PropertyDetails from "@/pages/PropertyDetails";
9
13
  import Application from "@/pages/Application";
10
- import HelpCenter from "@/pages/HelpCenter";
14
+ import Contact from "@/pages/Contact";
11
15
 
12
16
  export const routes: RouteObject[] = [
13
17
  {
@@ -23,6 +27,24 @@ export const routes: RouteObject[] = [
23
27
  path: "*",
24
28
  element: <NotFound />
25
29
  },
30
+ {
31
+ path: "global-search/:query",
32
+ element: (
33
+ <Suspense fallback={<LoadingFallback />}>
34
+ <GlobalSearch />
35
+ </Suspense>
36
+ ),
37
+ handle: { showInNavigation: false }
38
+ },
39
+ {
40
+ path: "object/:objectApiName/:recordId",
41
+ element: (
42
+ <Suspense fallback={<LoadingFallback />}>
43
+ <DetailPage />
44
+ </Suspense>
45
+ ),
46
+ handle: { showInNavigation: false }
47
+ },
26
48
  {
27
49
  path: "dashboard",
28
50
  element: <Dashboard />,
@@ -30,13 +52,17 @@ export const routes: RouteObject[] = [
30
52
  },
31
53
  {
32
54
  path: "properties",
33
- element: <PropertyListings />,
55
+ element: <PropertySearch />,
34
56
  handle: { showInNavigation: true, label: "Property Search" }
35
57
  },
36
58
  {
37
59
  path: "property/:id",
38
60
  element: <PropertyDetails />
39
61
  },
62
+ {
63
+ path: "object/Property_Listing__c/:id",
64
+ element: <PropertyDetails />
65
+ },
40
66
  {
41
67
  path: "maintenance",
42
68
  element: <Maintenance />,
@@ -47,9 +73,9 @@ export const routes: RouteObject[] = [
47
73
  element: <Application />
48
74
  },
49
75
  {
50
- path: "help",
51
- element: <HelpCenter />,
52
- handle: { showInNavigation: true, label: "Help Center" }
76
+ path: "contact",
77
+ element: <Contact />,
78
+ handle: { showInNavigation: true, label: "Contact" }
53
79
  }
54
80
  ]
55
81
  }
@@ -134,69 +134,29 @@
134
134
  }
135
135
  }
136
136
 
137
- @layer base {
138
- :root {
139
- color-scheme: light;
140
- --background: oklch(0.985 0.002 247.858);
141
- --foreground: oklch(0.145 0 0);
142
- --card: oklch(1 0 0);
143
- --card-foreground: oklch(0.145 0 0);
144
- --popover: oklch(1 0 0);
145
- --popover-foreground: oklch(0.145 0 0);
146
- --primary: oklch(0.488 0.09 195.75);
147
- --primary-foreground: oklch(0.985 0 0);
148
- --secondary: oklch(0.97 0.01 195);
149
- --secondary-foreground: oklch(0.205 0 0);
150
- --muted: oklch(0.97 0.005 195);
151
- --muted-foreground: oklch(0.556 0 0);
152
- --accent: oklch(0.97 0.01 195);
153
- --accent-foreground: oklch(0.205 0 0);
154
- --destructive: oklch(0.577 0.245 27.325);
155
- --destructive-foreground: oklch(0.985 0 0);
156
- --border: oklch(0.922 0 0);
157
- --input: oklch(0.922 0 0);
158
- --ring: oklch(0.488 0.09 195.75);
159
- --radius: 0.625rem;
160
- --spacing: 0.25rem;
161
- }
162
- @media (prefers-reduced-motion: reduce) {
163
- * {
164
- animation-duration: 0.01ms !important;
165
- animation-iteration-count: 1 !important;
166
- transition-duration: 0.01ms !important;
167
- scroll-behavior: auto !important;
168
- }
169
- }
170
- * {
171
- @apply border-[var(--border)];
172
- }
173
- body {
174
- @apply bg-[var(--background)] text-[var(--foreground)] antialiased;
175
- }
137
+ /* ZENLEASE design language: teal primary, cream background, soft rounded corners */
138
+
139
+ :root {
140
+ /* Primary: deep teal (#20888F / #2c8a8d) */
141
+ --primary: oklch(0.52 0.09 192);
142
+ --primary-foreground: oklch(0.985 0 0);
143
+ /* Background: warm cream (#F8F6F1) */
144
+ --background: oklch(0.97 0.01 85);
145
+ --foreground: oklch(0.21 0.02 260);
146
+ /* Card: white with subtle warmth */
147
+ --card: oklch(1 0 0);
148
+ --card-foreground: oklch(0.21 0.02 260);
149
+ /* Muted: light cream for secondary surfaces */
150
+ --muted: oklch(0.955 0.01 85);
151
+ --muted-foreground: oklch(0.48 0.02 260);
152
+ /* Border / input: soft teal-tinted gray */
153
+ --border: oklch(0.91 0.02 192);
154
+ --input: oklch(0.91 0.02 192);
155
+ --ring: oklch(0.52 0.09 192);
156
+ /* Slightly larger radius for soft, friendly feel (screenshots use 16–24px) */
157
+ --radius: 0.75rem;
176
158
  }
177
159
 
178
- @theme inline {
179
- --color-background: var(--background);
180
- --color-foreground: var(--foreground);
181
- --color-card: var(--card);
182
- --color-card-foreground: var(--card-foreground);
183
- --color-popover: var(--popover);
184
- --color-popover-foreground: var(--popover-foreground);
185
- --color-primary: var(--primary);
186
- --color-primary-foreground: var(--primary-foreground);
187
- --color-secondary: var(--secondary);
188
- --color-secondary-foreground: var(--secondary-foreground);
189
- --color-muted: var(--muted);
190
- --color-muted-foreground: var(--muted-foreground);
191
- --color-accent: var(--accent);
192
- --color-accent-foreground: var(--accent-foreground);
193
- --color-destructive: var(--destructive);
194
- --color-border: var(--border);
195
- --color-input: var(--input);
196
- --color-ring: var(--ring);
197
- --radius-sm: calc(var(--radius) - 4px);
198
- --radius-md: calc(var(--radius) - 2px);
199
- --radius-lg: var(--radius);
200
- --radius-xl: calc(var(--radius) + 4px);
201
- --spacing: var(--spacing);
160
+ body {
161
+ @apply bg-background text-foreground;
202
162
  }
@@ -0,0 +1,120 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Type definitions for filter structures
5
+ * All types are derived from Zod schemas using z.infer for type safety
6
+ */
7
+
8
+ /**
9
+ * Allowed filter operators for Salesforce search queries
10
+ */
11
+ export const FILTER_OPERATORS = [
12
+ "eq", // Equals
13
+ "ne", // Not equals
14
+ "like", // Pattern matching (contains)
15
+ "gt", // Greater than
16
+ "gte", // Greater than or equal
17
+ "lt", // Less than
18
+ "lte", // Less than or equal
19
+ ] as const;
20
+
21
+ /**
22
+ * Filter operator type
23
+ */
24
+ export type FilterOperator = (typeof FILTER_OPERATORS)[number];
25
+
26
+ /**
27
+ * Salesforce field path validation regex
28
+ * Validates field paths like:
29
+ * - Simple fields: "Name", "FieldName__c"
30
+ * - Relationship fields: "Account__r.Name", "Owner__r.FieldName__c"
31
+ * - Nested relationships: "Account__r.Owner__r.Name"
32
+ *
33
+ * Pattern explanation:
34
+ * - ^[A-Za-z][A-Za-z0-9_]* - Starts with letter, followed by letters/numbers/underscores
35
+ * - (__[cr])? - Optional relationship suffix (__r or __c)
36
+ * - (\.[A-Za-z][A-Za-z0-9_]*(__[cr])?)* - Optional relationship traversal (dot notation)
37
+ */
38
+ const SALESFORCE_FIELD_PATH_REGEX =
39
+ /^[A-Za-z][A-Za-z0-9_]*(__[cr])?(\.[A-Za-z][A-Za-z0-9_]*(__[cr])?)*$/;
40
+
41
+ /**
42
+ * Validates Salesforce field path format
43
+ * @param fieldPath - The field path to validate
44
+ * @returns true if valid, false otherwise
45
+ */
46
+ function isValidSalesforceFieldPath(fieldPath: string): boolean {
47
+ if (!fieldPath || fieldPath.trim().length === 0) {
48
+ return false;
49
+ }
50
+ return SALESFORCE_FIELD_PATH_REGEX.test(fieldPath);
51
+ }
52
+
53
+ // Zod Schema for Filter Attributes
54
+ const FilterAttributesSchema = z.object({
55
+ affordance: z.string().optional(),
56
+ placeholder: z.string().optional(),
57
+ });
58
+
59
+ /**
60
+ * Filter attributes containing input-specific properties
61
+ */
62
+ export type FilterAttributes = z.infer<typeof FilterAttributesSchema>;
63
+
64
+ // Zod Schema for Filter
65
+ const FilterSchema = z.object({
66
+ affordance: z.string(),
67
+ attributes: FilterAttributesSchema.optional(),
68
+ defaultValues: z.array(z.string()).optional(),
69
+ helpMessage: z.string().nullable().optional(),
70
+ label: z.string(),
71
+ targetFieldPath: z.string().refine((value) => isValidSalesforceFieldPath(value), {
72
+ message:
73
+ "Invalid Salesforce field path format. Field paths must start with a letter and can contain letters, numbers, underscores, and relationship notation (__r or __c). Use dot notation for relationships (e.g., 'Account__r.Name').",
74
+ }),
75
+ type: z.string(),
76
+ });
77
+
78
+ /**
79
+ * Single filter definition from getObjectListFilters API
80
+ */
81
+ export type Filter = z.infer<typeof FilterSchema>;
82
+
83
+ // Export schema for validation
84
+ export const FilterArraySchema = z.array(FilterSchema);
85
+
86
+ // Zod Schema for Filter Criteria with operator and field path validation
87
+ const FilterCriteriaSchema = z.object({
88
+ objectApiName: z.string().min(1, "Object API name is required"),
89
+ fieldPath: z
90
+ .string()
91
+ .min(1, "Field path is required")
92
+ .refine((value) => isValidSalesforceFieldPath(value), {
93
+ message:
94
+ "Invalid Salesforce field path format. Field paths must start with a letter and can contain letters, numbers, underscores, and relationship notation (__r or __c). Use dot notation for relationships (e.g., 'Account__r.Name').",
95
+ }),
96
+ operator: z.enum(FILTER_OPERATORS, {
97
+ message: `Operator must be one of: ${FILTER_OPERATORS.join(", ")}`,
98
+ }),
99
+ values: z.array(z.union([z.string(), z.number()])).min(1, "At least one value is required"),
100
+ });
101
+
102
+ /**
103
+ * Filter criteria structure for filtering search results
104
+ */
105
+ export type FilterCriteria = z.infer<typeof FilterCriteriaSchema>;
106
+
107
+ // Export schema for validation
108
+ export const FilterCriteriaArraySchema = z.array(FilterCriteriaSchema);
109
+
110
+ // Zod Schema for Filters Response
111
+ const FiltersResponseSchema = z.record(z.string(), z.unknown()).and(
112
+ z.object({
113
+ filters: FilterArraySchema.optional(),
114
+ }),
115
+ );
116
+
117
+ /**
118
+ * Filters response structure
119
+ */
120
+ export type FiltersResponse = z.infer<typeof FiltersResponseSchema>;