@salesforce/webapp-template-app-react-sample-b2x-experimental 1.116.8 → 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 +8 -0
- 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/hooks/useGeocode.ts +23 -39
- 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 +20 -36
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useWeather.ts +10 -23
- package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Application.tsx +40 -73
- 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 +95 -6
- 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 +1 -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 -58
- 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 -54
- /package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/{leadApi.ts → leads/leadApi.ts} +0 -0
|
@@ -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
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,23 +67,18 @@ 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
|
}
|
|
97
84
|
const idsKey = [...new Set(propertyIds)].join(",");
|
|
@@ -112,21 +99,18 @@ export function usePropertyMapMarkers(results: SearchResultRecord[]): {
|
|
|
112
99
|
}
|
|
113
100
|
let cancelled = false;
|
|
114
101
|
setLoading(true);
|
|
115
|
-
const currentResults = resultsRef.current;
|
|
116
102
|
const currentLabels = labelMapRef.current;
|
|
117
103
|
const directMarkers: MapMarker[] = [];
|
|
118
104
|
const missingIds: string[] = [];
|
|
119
|
-
for (const
|
|
120
|
-
if (!
|
|
121
|
-
const
|
|
122
|
-
if (!id || !uniqIds.includes(id)) continue;
|
|
123
|
-
const coords = getCoordinatesFromRecord(r.record);
|
|
105
|
+
for (const node of results) {
|
|
106
|
+
if (!uniqIds.includes(node.Id)) continue;
|
|
107
|
+
const coords = getCoordinatesFromNode(node);
|
|
124
108
|
if (coords) {
|
|
125
109
|
directMarkers.push({
|
|
126
110
|
lat: coords.lat,
|
|
127
111
|
lng: coords.lng,
|
|
128
|
-
label:
|
|
129
|
-
propertyId:
|
|
112
|
+
label: propertyIdToLabel.get(node.Id) ?? "Property",
|
|
113
|
+
propertyId: node.Id,
|
|
130
114
|
});
|
|
131
115
|
}
|
|
132
116
|
}
|
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,
|
|
@@ -296,29 +297,15 @@ export function useWeather(lat?: number | null, lng?: number | null) {
|
|
|
296
297
|
const longitude = lng ?? geo.longitude;
|
|
297
298
|
const canFetch = lat != null || geo.resolved;
|
|
298
299
|
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
300
|
+
const cached = useCachedAsyncData(
|
|
301
|
+
() => fetchWeather(latitude, longitude),
|
|
302
|
+
[latitude, longitude, canFetch],
|
|
303
|
+
{ key: `weather:${latitude},${longitude}:${canFetch}`, ttl: 300_000 },
|
|
304
|
+
);
|
|
302
305
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
setLoading(true);
|
|
307
|
-
setError(null);
|
|
308
|
-
fetchWeather(latitude, longitude)
|
|
309
|
-
.then((result) => {
|
|
310
|
-
if (!cancelled) setData(result);
|
|
311
|
-
})
|
|
312
|
-
.catch((err) => {
|
|
313
|
-
if (!cancelled) setError(err instanceof Error ? err.message : "Failed to load weather");
|
|
314
|
-
})
|
|
315
|
-
.finally(() => {
|
|
316
|
-
if (!cancelled) setLoading(false);
|
|
317
|
-
});
|
|
318
|
-
return () => {
|
|
319
|
-
cancelled = true;
|
|
320
|
-
};
|
|
321
|
-
}, [latitude, longitude, canFetch]);
|
|
306
|
+
if (!canFetch) {
|
|
307
|
+
return { data: null, loading: true, error: null };
|
|
308
|
+
}
|
|
322
309
|
|
|
323
|
-
return { data, loading, error };
|
|
310
|
+
return { data: cached.data, loading: cached.loading, error: cached.error };
|
|
324
311
|
}
|
package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Application.tsx
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import { useSearchParams, Link } from "react-router";
|
|
2
|
-
import { useCallback,
|
|
2
|
+
import { useCallback, useState, type ChangeEvent, type SubmitEvent } from "react";
|
|
3
3
|
import { Button } from "../components/ui/button";
|
|
4
4
|
import { Input } from "../components/ui/input";
|
|
5
5
|
import { Label } from "../components/ui/label";
|
|
6
6
|
import { Card, CardContent } from "../components/ui/card";
|
|
7
7
|
import { Skeleton } from "../components/ui/skeleton";
|
|
8
8
|
import { SkeletonField } from "@/components/SkeletonPrimitives";
|
|
9
|
-
import {
|
|
10
|
-
fetchListingById,
|
|
11
|
-
fetchPropertyById,
|
|
12
|
-
fetchPrimaryImagesByPropertyIds,
|
|
13
|
-
} from "@/api/properties/propertyDetailGraphQL";
|
|
9
|
+
import { fetchPropertyDetailById } from "@/api/properties/propertyDetailGraphQL";
|
|
14
10
|
import { createApplicationRecord } from "@/api/applications/applicationApi";
|
|
15
11
|
import { useAuth } from "@/features/authentication/context/AuthContext";
|
|
16
12
|
import { fetchUserContact } from "../features/authentication/api/userProfileApi";
|
|
13
|
+
import { useCachedAsyncData } from "@/features/object-search/hooks/useCachedAsyncData";
|
|
17
14
|
|
|
18
15
|
function ApplicationSkeleton() {
|
|
19
16
|
return (
|
|
@@ -47,15 +44,40 @@ function ApplicationSkeleton() {
|
|
|
47
44
|
export default function Application() {
|
|
48
45
|
const { user } = useAuth();
|
|
49
46
|
const [searchParams] = useSearchParams();
|
|
50
|
-
const
|
|
47
|
+
const propertyId = searchParams.get("propertyId") ?? "";
|
|
51
48
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
const { data: contactData } = useCachedAsyncData(
|
|
50
|
+
() => {
|
|
51
|
+
if (!user?.id) return Promise.resolve(null);
|
|
52
|
+
return fetchUserContact<{ ContactId?: string }>(user.id);
|
|
53
|
+
},
|
|
54
|
+
[user?.id],
|
|
55
|
+
{ key: `contact:${user?.id ?? ""}`, ttl: 300_000 },
|
|
56
|
+
);
|
|
57
|
+
const contactId = contactData?.ContactId ?? null;
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
data: property,
|
|
61
|
+
loading,
|
|
62
|
+
error: loadError,
|
|
63
|
+
} = useCachedAsyncData(
|
|
64
|
+
() => {
|
|
65
|
+
if (!propertyId?.trim()) return Promise.resolve(null);
|
|
66
|
+
return fetchPropertyDetailById(propertyId);
|
|
67
|
+
},
|
|
68
|
+
[propertyId],
|
|
69
|
+
{ key: `app-property:${propertyId}` },
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const listing = property?.Property_Listings__r?.edges?.[0]?.node ?? null;
|
|
73
|
+
const images = (property?.Property_Images__r?.edges ?? []).flatMap((e) =>
|
|
74
|
+
e?.node ? [e.node] : [],
|
|
75
|
+
);
|
|
76
|
+
const primaryImage =
|
|
77
|
+
images.find((i) => i.Image_Type__c?.value === "Primary") ?? images[0] ?? null;
|
|
78
|
+
const propertyImageUrl = primaryImage?.Image_URL__c?.value ?? null;
|
|
79
|
+
const propertyName = listing?.Name?.value ?? property?.Name?.value ?? null;
|
|
80
|
+
const propertyAddress = property?.Address__c?.value ?? null;
|
|
59
81
|
|
|
60
82
|
const [moveInDate, setMoveInDate] = useState("");
|
|
61
83
|
const [employment, setEmployment] = useState("");
|
|
@@ -65,61 +87,6 @@ export default function Application() {
|
|
|
65
87
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
66
88
|
const [submittedId, setSubmittedId] = useState<string | null>(null);
|
|
67
89
|
|
|
68
|
-
useEffect(() => {
|
|
69
|
-
if (!user?.id) return;
|
|
70
|
-
let mounted = true;
|
|
71
|
-
fetchUserContact<{ ContactId?: string }>(user.id)
|
|
72
|
-
.then((contact) => {
|
|
73
|
-
if (mounted) setContactId(contact.ContactId ?? null);
|
|
74
|
-
})
|
|
75
|
-
.catch((err) => {
|
|
76
|
-
if (mounted) console.error("Failed to fetch contact ID", err);
|
|
77
|
-
});
|
|
78
|
-
return () => {
|
|
79
|
-
mounted = false;
|
|
80
|
-
};
|
|
81
|
-
}, [user]);
|
|
82
|
-
|
|
83
|
-
useEffect(() => {
|
|
84
|
-
if (!listingId?.trim()) {
|
|
85
|
-
setLoading(false);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
let cancelled = false;
|
|
89
|
-
setLoadError(null);
|
|
90
|
-
(async () => {
|
|
91
|
-
try {
|
|
92
|
-
const listing = await fetchListingById(listingId);
|
|
93
|
-
if (cancelled) return;
|
|
94
|
-
if (!listing) {
|
|
95
|
-
setLoadError("Listing not found.");
|
|
96
|
-
setLoading(false);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
setListingName(listing.name);
|
|
100
|
-
if (listing.propertyId) {
|
|
101
|
-
setPropertyId(listing.propertyId);
|
|
102
|
-
const [property, primaryImages] = await Promise.all([
|
|
103
|
-
fetchPropertyById(listing.propertyId),
|
|
104
|
-
fetchPrimaryImagesByPropertyIds([listing.propertyId]),
|
|
105
|
-
]);
|
|
106
|
-
if (cancelled) return;
|
|
107
|
-
setPropertyAddress(property?.address ?? null);
|
|
108
|
-
setPropertyImageUrl(primaryImages[listing.propertyId] ?? null);
|
|
109
|
-
}
|
|
110
|
-
} catch (e) {
|
|
111
|
-
if (!cancelled) {
|
|
112
|
-
setLoadError(e instanceof Error ? e.message : "Failed to load listing.");
|
|
113
|
-
}
|
|
114
|
-
} finally {
|
|
115
|
-
if (!cancelled) setLoading(false);
|
|
116
|
-
}
|
|
117
|
-
})();
|
|
118
|
-
return () => {
|
|
119
|
-
cancelled = true;
|
|
120
|
-
};
|
|
121
|
-
}, [listingId]);
|
|
122
|
-
|
|
123
90
|
const handleSubmit = useCallback(
|
|
124
91
|
async (e: SubmitEvent<HTMLFormElement>) => {
|
|
125
92
|
e.preventDefault();
|
|
@@ -177,10 +144,10 @@ export default function Application() {
|
|
|
177
144
|
<div className="mx-auto max-w-[900px]">
|
|
178
145
|
<div className="mb-4">
|
|
179
146
|
<Link
|
|
180
|
-
to={
|
|
147
|
+
to={propertyId ? `/property/${propertyId}` : "/properties"}
|
|
181
148
|
className="text-sm text-primary no-underline hover:underline"
|
|
182
149
|
>
|
|
183
|
-
{
|
|
150
|
+
{propertyId ? "← Back to listing" : "← Back to search"}
|
|
184
151
|
</Link>
|
|
185
152
|
</div>
|
|
186
153
|
<Card className="mb-6 flex gap-4 rounded-2xl border border-border p-6 shadow-sm">
|
|
@@ -193,11 +160,11 @@ export default function Application() {
|
|
|
193
160
|
</div>
|
|
194
161
|
<div className="min-w-0 flex-1">
|
|
195
162
|
<h2 className="mb-1.5 text-2xl font-semibold text-foreground">
|
|
196
|
-
{
|
|
163
|
+
{propertyName ?? "Apply for a property"}
|
|
197
164
|
</h2>
|
|
198
165
|
<p className="text-sm text-muted-foreground">
|
|
199
166
|
{propertyAddress ??
|
|
200
|
-
(
|
|
167
|
+
(propertyId ? (
|
|
201
168
|
<Skeleton className="mt-1 h-4 w-48" />
|
|
202
169
|
) : (
|
|
203
170
|
"Select a property from search or listing detail to apply."
|