@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
|
@@ -496,8 +496,18 @@ export default function SessionTimeoutValidator({
|
|
|
496
496
|
// Get current location from React Router
|
|
497
497
|
const location = useLocation();
|
|
498
498
|
|
|
499
|
-
//
|
|
500
|
-
|
|
499
|
+
// Session expired alert — checked once at mount via lazy initializer.
|
|
500
|
+
// The session timeout handler triggers a hard navigation (window.location.replace),
|
|
501
|
+
// so the component always mounts fresh on the login page after expiry.
|
|
502
|
+
const [showExpiredAlert, setShowExpiredAlert] = useState(() => {
|
|
503
|
+
const isLoginPage = location.pathname === ROUTES.LOGIN.PATH;
|
|
504
|
+
const shouldShow =
|
|
505
|
+
isLoginPage && sessionStorage.getItem(STORAGE_KEYS.SHOW_SESSION_MESSAGE) === "true";
|
|
506
|
+
if (shouldShow) {
|
|
507
|
+
sessionStorage.removeItem(STORAGE_KEYS.SHOW_SESSION_MESSAGE);
|
|
508
|
+
}
|
|
509
|
+
return shouldShow;
|
|
510
|
+
});
|
|
501
511
|
|
|
502
512
|
// Session timeout monitoring hook
|
|
503
513
|
const sessionTimeout = useSessionTimeout({
|
|
@@ -505,22 +515,6 @@ export default function SessionTimeoutValidator({
|
|
|
505
515
|
isGuest,
|
|
506
516
|
});
|
|
507
517
|
|
|
508
|
-
/**
|
|
509
|
-
* Check if we should show expired session message
|
|
510
|
-
* Called on mount and whenever pathname changes
|
|
511
|
-
*/
|
|
512
|
-
useEffect(() => {
|
|
513
|
-
// Check if we're on the login page and should show expired message
|
|
514
|
-
const isLoginPage = location.pathname === ROUTES.LOGIN.PATH;
|
|
515
|
-
const shouldShowMessage = sessionStorage.getItem(STORAGE_KEYS.SHOW_SESSION_MESSAGE) === "true";
|
|
516
|
-
|
|
517
|
-
if (isLoginPage && shouldShowMessage) {
|
|
518
|
-
setShowExpiredAlert(true);
|
|
519
|
-
// Clear the flag immediately after reading
|
|
520
|
-
sessionStorage.removeItem(STORAGE_KEYS.SHOW_SESSION_MESSAGE);
|
|
521
|
-
}
|
|
522
|
-
}, [location.pathname]);
|
|
523
|
-
|
|
524
518
|
/**
|
|
525
519
|
* Handle session extension
|
|
526
520
|
* Called when user clicks "Continue Working" in warning modal
|
|
@@ -60,7 +60,7 @@ export function useFilterPanel() {
|
|
|
60
60
|
return { hasActiveFilters: filters.length > 0, resetAll: onReset };
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
type FilterResetButtonProps = Omit<React.ComponentProps<typeof Button>, "onClick">;
|
|
64
64
|
|
|
65
65
|
export function FilterResetButton({ children, ...props }: FilterResetButtonProps) {
|
|
66
66
|
const { hasActiveFilters, resetAll } = useFilterPanel();
|
|
@@ -61,7 +61,10 @@ export function useObjectSearchParams<TFilter, TOrderBy>(
|
|
|
61
61
|
paginationConfig?: PaginationConfig,
|
|
62
62
|
) {
|
|
63
63
|
const defaultPageSize = paginationConfig?.defaultPageSize ?? 10;
|
|
64
|
-
const validPageSizes =
|
|
64
|
+
const validPageSizes = useMemo(
|
|
65
|
+
() => paginationConfig?.validPageSizes ?? [defaultPageSize],
|
|
66
|
+
[paginationConfig?.validPageSizes, defaultPageSize],
|
|
67
|
+
);
|
|
65
68
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
66
69
|
|
|
67
70
|
// Seed local state from URL on initial load
|
|
@@ -76,8 +79,10 @@ export function useObjectSearchParams<TFilter, TOrderBy>(
|
|
|
76
79
|
const [sort, setLocalSort] = useState<SortState | null>(initial.sort);
|
|
77
80
|
|
|
78
81
|
// Pagination — cursor-based with a stack to support "previous page" navigation.
|
|
79
|
-
const getValidPageSize = (
|
|
80
|
-
validPageSizes.includes(size) ? size : defaultPageSize
|
|
82
|
+
const getValidPageSize = useCallback(
|
|
83
|
+
(size: number) => (validPageSizes.includes(size) ? size : defaultPageSize),
|
|
84
|
+
[validPageSizes, defaultPageSize],
|
|
85
|
+
);
|
|
81
86
|
|
|
82
87
|
const [pageSize, setPageSizeState] = useState<number>(
|
|
83
88
|
getValidPageSize(initial.pageSize ?? defaultPageSize),
|
|
@@ -166,7 +171,7 @@ export function useObjectSearchParams<TFilter, TOrderBy>(
|
|
|
166
171
|
resetPagination();
|
|
167
172
|
syncToUrl([], null, defaultPageSize, 0);
|
|
168
173
|
setPageSizeState(defaultPageSize);
|
|
169
|
-
}, [syncToUrl, resetPagination]);
|
|
174
|
+
}, [syncToUrl, resetPagination, defaultPageSize]);
|
|
170
175
|
|
|
171
176
|
// -- Pagination callbacks ---------------------------------------------------
|
|
172
177
|
// Uses a cursor stack to track visited pages. "Next" pushes the current
|
|
@@ -204,7 +209,7 @@ export function useObjectSearchParams<TFilter, TOrderBy>(
|
|
|
204
209
|
resetPagination();
|
|
205
210
|
debouncedSyncRef.current(f, s, validated);
|
|
206
211
|
},
|
|
207
|
-
[resetPagination],
|
|
212
|
+
[resetPagination, getValidPageSize],
|
|
208
213
|
);
|
|
209
214
|
|
|
210
215
|
// -- Derived query objects ---------------------------------------------------
|
package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useGeocode.ts
CHANGED
|
@@ -1,48 +1,33 @@
|
|
|
1
|
-
import { useState, useEffect } from "react";
|
|
2
1
|
import { geocodeAddress, getStateZipFromAddress, type GeocodeResult } from "@/utils/geocode";
|
|
2
|
+
import { useCachedAsyncData } from "@/features/object-search/hooks/useCachedAsyncData";
|
|
3
|
+
|
|
4
|
+
async function geocodeWithFallback(address: string): Promise<GeocodeResult | null> {
|
|
5
|
+
const normalized = address.replace(/\n/g, ", ").trim();
|
|
6
|
+
const result = await geocodeAddress(normalized);
|
|
7
|
+
if (result != null) return result;
|
|
8
|
+
|
|
9
|
+
// Fallback: try state + zip if full address failed
|
|
10
|
+
const stateZip = getStateZipFromAddress(normalized);
|
|
11
|
+
if (stateZip !== normalized) {
|
|
12
|
+
return geocodeAddress(stateZip);
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
3
16
|
|
|
4
17
|
export function useGeocode(address: string | null | undefined): {
|
|
5
18
|
coords: GeocodeResult | null;
|
|
6
19
|
loading: boolean;
|
|
7
20
|
} {
|
|
8
|
-
const
|
|
9
|
-
const [loading, setLoading] = useState(false);
|
|
10
|
-
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
if (!address?.trim()) {
|
|
13
|
-
setCoords(null);
|
|
14
|
-
setLoading(false);
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
let cancelled = false;
|
|
18
|
-
setLoading(true);
|
|
19
|
-
const normalized = address.replace(/\n/g, ", ").trim();
|
|
20
|
-
|
|
21
|
-
(async () => {
|
|
22
|
-
try {
|
|
23
|
-
let result = await geocodeAddress(normalized);
|
|
24
|
-
if (cancelled) return;
|
|
25
|
-
if (result != null) {
|
|
26
|
-
setCoords(result);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
// Fallback: same as property search – try state + zip if full address failed
|
|
30
|
-
const stateZip = getStateZipFromAddress(normalized);
|
|
31
|
-
if (stateZip !== normalized) {
|
|
32
|
-
result = await geocodeAddress(stateZip);
|
|
33
|
-
if (!cancelled) setCoords(result);
|
|
34
|
-
}
|
|
35
|
-
} catch {
|
|
36
|
-
if (!cancelled) setCoords(null);
|
|
37
|
-
} finally {
|
|
38
|
-
if (!cancelled) setLoading(false);
|
|
39
|
-
}
|
|
40
|
-
})();
|
|
21
|
+
const trimmed = address?.trim() ?? "";
|
|
41
22
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
23
|
+
const { data: coords, loading } = useCachedAsyncData(
|
|
24
|
+
() => {
|
|
25
|
+
if (!trimmed) return Promise.resolve(null);
|
|
26
|
+
return geocodeWithFallback(trimmed);
|
|
27
|
+
},
|
|
28
|
+
[trimmed],
|
|
29
|
+
{ key: `geocode:${trimmed}`, ttl: 600_000 },
|
|
30
|
+
);
|
|
46
31
|
|
|
47
32
|
return { coords, loading };
|
|
48
33
|
}
|
|
@@ -1,39 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fetches Maintenance_Request__c list and exposes refetch for after create.
|
|
3
3
|
*/
|
|
4
|
-
import { useState,
|
|
4
|
+
import { useState, useCallback } from "react";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
type
|
|
6
|
+
searchMaintenanceRequests,
|
|
7
|
+
type MaintenanceRequestNode,
|
|
8
8
|
} from "@/api/maintenanceRequests/maintenanceRequestApi";
|
|
9
|
+
import { ResultOrder } from "@/api/graphql-operations-types";
|
|
10
|
+
import {
|
|
11
|
+
useCachedAsyncData,
|
|
12
|
+
clearCacheEntry,
|
|
13
|
+
} from "@/features/object-search/hooks/useCachedAsyncData";
|
|
14
|
+
|
|
15
|
+
const CACHE_KEY = "maintenance-requests";
|
|
16
|
+
|
|
17
|
+
async function fetchMaintenanceNodes(): Promise<MaintenanceRequestNode[]> {
|
|
18
|
+
const result = await searchMaintenanceRequests({
|
|
19
|
+
first: 50,
|
|
20
|
+
orderBy: { Scheduled__c: { order: ResultOrder.Desc } },
|
|
21
|
+
});
|
|
22
|
+
return (result.edges ?? []).flatMap((e) => (e?.node ? [e.node] : []));
|
|
23
|
+
}
|
|
9
24
|
|
|
10
25
|
export function useMaintenanceRequests(): {
|
|
11
|
-
requests:
|
|
26
|
+
requests: MaintenanceRequestNode[];
|
|
12
27
|
loading: boolean;
|
|
13
28
|
error: string | null;
|
|
14
|
-
refetch: () =>
|
|
29
|
+
refetch: () => void;
|
|
15
30
|
} {
|
|
16
|
-
const [
|
|
17
|
-
const [loading, setLoading] = useState(true);
|
|
18
|
-
const [error, setError] = useState<string | null>(null);
|
|
31
|
+
const [generation, setGeneration] = useState(0);
|
|
19
32
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
const list = await queryMaintenanceRequests(50, null);
|
|
25
|
-
setRequests(list);
|
|
26
|
-
} catch (e) {
|
|
27
|
-
setError(e instanceof Error ? e.message : "Failed to load maintenance requests");
|
|
28
|
-
setRequests([]);
|
|
29
|
-
} finally {
|
|
30
|
-
setLoading(false);
|
|
31
|
-
}
|
|
32
|
-
}, []);
|
|
33
|
+
const { data, loading, error } = useCachedAsyncData(fetchMaintenanceNodes, [generation], {
|
|
34
|
+
key: `${CACHE_KEY}:${generation}`,
|
|
35
|
+
});
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
const refetch = useCallback(() => {
|
|
38
|
+
clearCacheEntry(`${CACHE_KEY}:${generation}`);
|
|
39
|
+
setGeneration((g) => g + 1);
|
|
40
|
+
}, [generation]);
|
|
37
41
|
|
|
38
|
-
return { requests, loading, error, refetch
|
|
42
|
+
return { requests: data ?? [], loading, error, refetch };
|
|
39
43
|
}
|
package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyDetail.ts
CHANGED
|
@@ -1,99 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Fetches
|
|
2
|
+
* Fetches Property__c by id with all related data (images, costs, features, listings)
|
|
3
|
+
* in a single GraphQL query.
|
|
3
4
|
*/
|
|
4
|
-
import { useState,
|
|
5
|
+
import { useState, useCallback } from "react";
|
|
5
6
|
import {
|
|
7
|
+
fetchPropertyDetailById,
|
|
6
8
|
fetchListingById,
|
|
7
|
-
|
|
8
|
-
fetchImagesByPropertyId,
|
|
9
|
-
fetchCostsByPropertyId,
|
|
10
|
-
fetchFeaturesByPropertyId,
|
|
11
|
-
type ListingDetail,
|
|
12
|
-
type PropertyDetail,
|
|
13
|
-
type PropertyImageRecord,
|
|
14
|
-
type PropertyCostRecord,
|
|
15
|
-
type PropertyFeatureRecord,
|
|
9
|
+
type PropertyDetailNode,
|
|
16
10
|
} from "@/api/properties/propertyDetailGraphQL";
|
|
11
|
+
import {
|
|
12
|
+
useCachedAsyncData,
|
|
13
|
+
clearCacheEntry,
|
|
14
|
+
} from "@/features/object-search/hooks/useCachedAsyncData";
|
|
17
15
|
|
|
18
16
|
export interface PropertyDetailState {
|
|
19
|
-
|
|
20
|
-
property: PropertyDetail | null;
|
|
21
|
-
images: PropertyImageRecord[];
|
|
22
|
-
costs: PropertyCostRecord[];
|
|
23
|
-
features: PropertyFeatureRecord[];
|
|
17
|
+
property: PropertyDetailNode | null;
|
|
24
18
|
loading: boolean;
|
|
25
19
|
error: string | null;
|
|
26
20
|
}
|
|
27
21
|
|
|
22
|
+
const CACHE_KEY_PREFIX = "property-detail";
|
|
23
|
+
|
|
24
|
+
async function fetchDetail(id: string): Promise<PropertyDetailNode | null> {
|
|
25
|
+
// First try directly as a Property__c ID (common path).
|
|
26
|
+
const detail = await fetchPropertyDetailById(id);
|
|
27
|
+
if (detail) return detail;
|
|
28
|
+
|
|
29
|
+
// Fall back: treat as a Property_Listing__c ID and resolve to its Property__c.
|
|
30
|
+
const listing = await fetchListingById(id);
|
|
31
|
+
const propertyId = listing?.Property__c?.value ?? null;
|
|
32
|
+
if (!propertyId) return null;
|
|
33
|
+
return fetchPropertyDetailById(propertyId);
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
export function usePropertyDetail(
|
|
29
|
-
|
|
37
|
+
id: string | undefined,
|
|
30
38
|
): PropertyDetailState & { refetch: () => void } {
|
|
31
|
-
const [
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const [costs, setCosts] = useState<PropertyCostRecord[]>([]);
|
|
35
|
-
const [features, setFeatures] = useState<PropertyFeatureRecord[]>([]);
|
|
36
|
-
const [loading, setLoading] = useState(true);
|
|
37
|
-
const [error, setError] = useState<string | null>(null);
|
|
39
|
+
const [generation, setGeneration] = useState(0);
|
|
40
|
+
const trimmedId = id?.trim() ?? "";
|
|
41
|
+
const cacheKey = `${CACHE_KEY_PREFIX}:${trimmedId}:${generation}`;
|
|
38
42
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
setError(null);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
setLoading(true);
|
|
51
|
-
setError(null);
|
|
52
|
-
try {
|
|
53
|
-
const listingData = await fetchListingById(listingId);
|
|
54
|
-
setListing(listingData ?? null);
|
|
55
|
-
if (!listingData?.propertyId) {
|
|
56
|
-
setProperty(null);
|
|
57
|
-
setImages([]);
|
|
58
|
-
setCosts([]);
|
|
59
|
-
setFeatures([]);
|
|
60
|
-
setLoading(false);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const [propertyData, imagesData, costsData, featuresData] = await Promise.all([
|
|
64
|
-
fetchPropertyById(listingData.propertyId),
|
|
65
|
-
fetchImagesByPropertyId(listingData.propertyId),
|
|
66
|
-
fetchCostsByPropertyId(listingData.propertyId),
|
|
67
|
-
fetchFeaturesByPropertyId(listingData.propertyId),
|
|
68
|
-
]);
|
|
69
|
-
setProperty(propertyData ?? null);
|
|
70
|
-
setImages(imagesData ?? []);
|
|
71
|
-
setCosts(costsData ?? []);
|
|
72
|
-
setFeatures(featuresData ?? []);
|
|
73
|
-
} catch (e) {
|
|
74
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
75
|
-
setListing(null);
|
|
76
|
-
setProperty(null);
|
|
77
|
-
setImages([]);
|
|
78
|
-
setCosts([]);
|
|
79
|
-
setFeatures([]);
|
|
80
|
-
} finally {
|
|
81
|
-
setLoading(false);
|
|
82
|
-
}
|
|
83
|
-
}, [listingId]);
|
|
43
|
+
const { data, loading, error } = useCachedAsyncData(
|
|
44
|
+
() => {
|
|
45
|
+
if (!trimmedId) return Promise.resolve(null);
|
|
46
|
+
return fetchDetail(trimmedId);
|
|
47
|
+
},
|
|
48
|
+
[trimmedId, generation],
|
|
49
|
+
{ key: cacheKey },
|
|
50
|
+
);
|
|
84
51
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
52
|
+
const refetch = useCallback(() => {
|
|
53
|
+
clearCacheEntry(cacheKey);
|
|
54
|
+
setGeneration((g) => g + 1);
|
|
55
|
+
}, [cacheKey]);
|
|
88
56
|
|
|
89
57
|
return {
|
|
90
|
-
|
|
91
|
-
property,
|
|
92
|
-
images,
|
|
93
|
-
costs,
|
|
94
|
-
features,
|
|
58
|
+
property: data ?? null,
|
|
95
59
|
loading,
|
|
96
60
|
error,
|
|
97
|
-
refetch
|
|
61
|
+
refetch,
|
|
98
62
|
};
|
|
99
63
|
}
|
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Builds map markers from search result nodes. Uses coordinates when available,
|
|
3
|
+
* falls back to geocoding addresses for properties missing coordinates.
|
|
4
4
|
*/
|
|
5
|
-
import { useState, useEffect } from "react";
|
|
5
|
+
import { useState, useEffect, useRef } from "react";
|
|
6
6
|
import { fetchPropertyAddresses } from "@/api/properties/propertyDetailGraphQL";
|
|
7
7
|
import { geocodeAddress, getStateZipFromAddress } from "@/utils/geocode";
|
|
8
|
-
import {
|
|
9
|
-
import type { SearchResultRecord } from "@/types/searchResults.js";
|
|
8
|
+
import type { PropertySearchNode } from "@/api/properties/propertySearchService";
|
|
10
9
|
import type { MapMarker } from "@/components/properties/PropertyMap";
|
|
11
10
|
|
|
12
|
-
function getListingName(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (!f || typeof f !== "object") return "Property";
|
|
17
|
-
if (f.displayValue != null && f.displayValue !== "") return String(f.displayValue);
|
|
18
|
-
if (f.value != null && typeof f.value === "string") return f.value;
|
|
11
|
+
function getListingName(node: PropertySearchNode): string {
|
|
12
|
+
if (node.Name?.displayValue != null && node.Name.displayValue !== "")
|
|
13
|
+
return node.Name.displayValue;
|
|
14
|
+
if (node.Name?.value != null && node.Name.value !== "") return node.Name.value;
|
|
19
15
|
return "Property";
|
|
20
16
|
}
|
|
21
17
|
|
|
@@ -28,13 +24,9 @@ function toFiniteNumber(value: unknown): number | null {
|
|
|
28
24
|
return null;
|
|
29
25
|
}
|
|
30
26
|
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const latRaw = record.fields?.["Property__r.Coordinates__Latitude__s"]?.value;
|
|
35
|
-
const lngRaw = record.fields?.["Property__r.Coordinates__Longitude__s"]?.value;
|
|
36
|
-
const lat = toFiniteNumber(latRaw);
|
|
37
|
-
const lng = toFiniteNumber(lngRaw);
|
|
27
|
+
function getCoordinatesFromNode(node: PropertySearchNode): { lat: number; lng: number } | null {
|
|
28
|
+
const lat = toFiniteNumber(node.Coordinates__Latitude__s?.value);
|
|
29
|
+
const lng = toFiniteNumber(node.Coordinates__Longitude__s?.value);
|
|
38
30
|
if (lat == null || lng == null) return null;
|
|
39
31
|
return { lat, lng };
|
|
40
32
|
}
|
|
@@ -75,48 +67,50 @@ function spreadDuplicateMarkers(markers: MapMarker[]): MapMarker[] {
|
|
|
75
67
|
return result;
|
|
76
68
|
}
|
|
77
69
|
|
|
78
|
-
export function usePropertyMapMarkers(results:
|
|
70
|
+
export function usePropertyMapMarkers(results: PropertySearchNode[]): {
|
|
79
71
|
markers: MapMarker[];
|
|
80
72
|
loading: boolean;
|
|
81
73
|
} {
|
|
82
74
|
const [markers, setMarkers] = useState<MapMarker[]>([]);
|
|
83
75
|
const [loading, setLoading] = useState(false);
|
|
84
76
|
|
|
85
|
-
|
|
86
|
-
const propertyIds = results
|
|
87
|
-
.map((r) => r?.record && getPropertyIdFromRecord(r.record))
|
|
88
|
-
.filter((id): id is string => Boolean(id));
|
|
77
|
+
const propertyIds = results.map((r) => r.Id).filter(Boolean);
|
|
89
78
|
const propertyIdToLabel = new Map<string, string>();
|
|
90
|
-
for (const
|
|
91
|
-
if (!
|
|
92
|
-
|
|
93
|
-
if (id && !propertyIdToLabel.has(id)) {
|
|
94
|
-
propertyIdToLabel.set(id, getListingName(r.record));
|
|
79
|
+
for (const node of results) {
|
|
80
|
+
if (!propertyIdToLabel.has(node.Id)) {
|
|
81
|
+
propertyIdToLabel.set(node.Id, getListingName(node));
|
|
95
82
|
}
|
|
96
83
|
}
|
|
84
|
+
const idsKey = [...new Set(propertyIds)].join(",");
|
|
97
85
|
|
|
86
|
+
const resultsRef = useRef(results);
|
|
87
|
+
const labelMapRef = useRef(propertyIdToLabel);
|
|
98
88
|
useEffect(() => {
|
|
99
|
-
|
|
89
|
+
resultsRef.current = results;
|
|
90
|
+
labelMapRef.current = propertyIdToLabel;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
const uniqIds = idsKey === "" ? [] : idsKey.split(",");
|
|
95
|
+
if (uniqIds.length === 0) {
|
|
100
96
|
setMarkers([]);
|
|
101
97
|
setLoading(false);
|
|
102
98
|
return;
|
|
103
99
|
}
|
|
104
100
|
let cancelled = false;
|
|
105
101
|
setLoading(true);
|
|
106
|
-
const
|
|
102
|
+
const currentLabels = labelMapRef.current;
|
|
107
103
|
const directMarkers: MapMarker[] = [];
|
|
108
104
|
const missingIds: string[] = [];
|
|
109
|
-
for (const
|
|
110
|
-
if (!
|
|
111
|
-
const
|
|
112
|
-
if (!id || !uniqIds.includes(id)) continue;
|
|
113
|
-
const coords = getCoordinatesFromRecord(r.record);
|
|
105
|
+
for (const node of results) {
|
|
106
|
+
if (!uniqIds.includes(node.Id)) continue;
|
|
107
|
+
const coords = getCoordinatesFromNode(node);
|
|
114
108
|
if (coords) {
|
|
115
109
|
directMarkers.push({
|
|
116
110
|
lat: coords.lat,
|
|
117
111
|
lng: coords.lng,
|
|
118
|
-
label: propertyIdToLabel.get(
|
|
119
|
-
propertyId:
|
|
112
|
+
label: propertyIdToLabel.get(node.Id) ?? "Property",
|
|
113
|
+
propertyId: node.Id,
|
|
120
114
|
});
|
|
121
115
|
}
|
|
122
116
|
}
|
|
@@ -140,7 +134,6 @@ export function usePropertyMapMarkers(results: SearchResultRecord[]): {
|
|
|
140
134
|
setLoading(false);
|
|
141
135
|
return;
|
|
142
136
|
}
|
|
143
|
-
// Geocode all addresses in parallel; fallback to City, State Zip if full address fails
|
|
144
137
|
Promise.all(
|
|
145
138
|
toGeocode.map(async ([id, address]) => {
|
|
146
139
|
const normalized = address.replace(/\n/g, ", ").trim();
|
|
@@ -161,7 +154,7 @@ export function usePropertyMapMarkers(results: SearchResultRecord[]): {
|
|
|
161
154
|
.map(({ id, coords }) => ({
|
|
162
155
|
lat: coords.lat,
|
|
163
156
|
lng: coords.lng,
|
|
164
|
-
label:
|
|
157
|
+
label: currentLabels.get(id) ?? "Property",
|
|
165
158
|
propertyId: id,
|
|
166
159
|
}));
|
|
167
160
|
setMarkers(spreadDuplicateMarkers([...directMarkers, ...geocoded]));
|
|
@@ -182,7 +175,7 @@ export function usePropertyMapMarkers(results: SearchResultRecord[]): {
|
|
|
182
175
|
return () => {
|
|
183
176
|
cancelled = true;
|
|
184
177
|
};
|
|
185
|
-
}, [
|
|
178
|
+
}, [idsKey]);
|
|
186
179
|
|
|
187
180
|
return { markers, loading };
|
|
188
181
|
}
|
package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useWeather.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Detects user location via Geolocation API; falls back to San Francisco.
|
|
4
4
|
*/
|
|
5
5
|
import { useState, useEffect } from "react";
|
|
6
|
+
import { useCachedAsyncData } from "@/features/object-search/hooks/useCachedAsyncData";
|
|
6
7
|
|
|
7
8
|
const FALLBACK = {
|
|
8
9
|
LAT: 37.7749,
|
|
@@ -268,17 +269,14 @@ interface GeoPosition {
|
|
|
268
269
|
}
|
|
269
270
|
|
|
270
271
|
function useGeolocation(): GeoPosition {
|
|
271
|
-
const [position, setPosition] = useState<GeoPosition>({
|
|
272
|
+
const [position, setPosition] = useState<GeoPosition>(() => ({
|
|
272
273
|
latitude: FALLBACK.LAT,
|
|
273
274
|
longitude: FALLBACK.LNG,
|
|
274
|
-
resolved:
|
|
275
|
-
});
|
|
275
|
+
resolved: typeof navigator === "undefined" || !navigator.geolocation,
|
|
276
|
+
}));
|
|
276
277
|
|
|
277
278
|
useEffect(() => {
|
|
278
|
-
if (!navigator.geolocation)
|
|
279
|
-
setPosition((prev) => ({ ...prev, resolved: true }));
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
279
|
+
if (!navigator.geolocation) return;
|
|
282
280
|
navigator.geolocation.getCurrentPosition(
|
|
283
281
|
(pos) =>
|
|
284
282
|
setPosition({
|
|
@@ -299,29 +297,15 @@ export function useWeather(lat?: number | null, lng?: number | null) {
|
|
|
299
297
|
const longitude = lng ?? geo.longitude;
|
|
300
298
|
const canFetch = lat != null || geo.resolved;
|
|
301
299
|
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
300
|
+
const cached = useCachedAsyncData(
|
|
301
|
+
() => fetchWeather(latitude, longitude),
|
|
302
|
+
[latitude, longitude, canFetch],
|
|
303
|
+
{ key: `weather:${latitude},${longitude}:${canFetch}`, ttl: 300_000 },
|
|
304
|
+
);
|
|
305
305
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
setLoading(true);
|
|
310
|
-
setError(null);
|
|
311
|
-
fetchWeather(latitude, longitude)
|
|
312
|
-
.then((result) => {
|
|
313
|
-
if (!cancelled) setData(result);
|
|
314
|
-
})
|
|
315
|
-
.catch((err) => {
|
|
316
|
-
if (!cancelled) setError(err instanceof Error ? err.message : "Failed to load weather");
|
|
317
|
-
})
|
|
318
|
-
.finally(() => {
|
|
319
|
-
if (!cancelled) setLoading(false);
|
|
320
|
-
});
|
|
321
|
-
return () => {
|
|
322
|
-
cancelled = true;
|
|
323
|
-
};
|
|
324
|
-
}, [latitude, longitude, canFetch]);
|
|
306
|
+
if (!canFetch) {
|
|
307
|
+
return { data: null, loading: true, error: null };
|
|
308
|
+
}
|
|
325
309
|
|
|
326
|
-
return { data, loading, error };
|
|
310
|
+
return { data: cached.data, loading: cached.loading, error: cached.error };
|
|
327
311
|
}
|