@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.
Files changed (44) hide show
  1. package/dist/CHANGELOG.md +8 -0
  2. package/dist/force-app/main/default/webapplications/propertyrentalapp/package.json +3 -3
  3. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/graphql-operations-types.ts +24594 -7234
  4. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/maintenanceRequests/maintenanceRequestApi.ts +21 -157
  5. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/maintenanceRequests/query/maintenanceRequests.graphql +60 -0
  6. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/propertyDetailGraphQL.ts +45 -444
  7. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/propertyNodeUtils.ts +29 -0
  8. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/propertySearchService.ts +56 -0
  9. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/distinctPropertyStatus.graphql +19 -0
  10. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/distinctPropertyType.graphql +19 -0
  11. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/listingById.graphql +29 -0
  12. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/propertyAddressesByIds.graphql +17 -0
  13. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/propertyDetailById.graphql +124 -0
  14. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/query/searchProperties.graphql +85 -0
  15. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/appLayout.tsx +1 -1
  16. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/SkeletonPrimitives.tsx +9 -6
  17. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/dashboard/WeatherWidget.tsx +35 -19
  18. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/maintenanceRequests/MaintenanceRequestList.tsx +7 -5
  19. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/maintenanceRequests/MaintenanceRequestListItem.tsx +11 -10
  20. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/maintenanceRequests/MaintenanceSummaryDetailsModal.tsx +20 -15
  21. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/properties/PropertyListingCard.tsx +11 -24
  22. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useGeocode.ts +23 -39
  23. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useMaintenanceRequests.ts +29 -25
  24. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyDetail.ts +42 -78
  25. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyMapMarkers.ts +20 -36
  26. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/useWeather.ts +10 -23
  27. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Application.tsx +40 -73
  28. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Contact.tsx +44 -55
  29. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Dashboard.tsx +1 -0
  30. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Home.tsx +63 -32
  31. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/Maintenance.tsx +95 -6
  32. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/PropertyDetails.tsx +67 -45
  33. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/pages/PropertySearch.tsx +299 -191
  34. package/dist/package-lock.json +2 -2
  35. package/dist/package.json +1 -1
  36. package/package.json +1 -1
  37. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/api/properties/propertyListingGraphQL.ts +0 -380
  38. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/components/properties/PropertyListingSearchPagination.tsx +0 -136
  39. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/constants/propertyListing.ts +0 -4
  40. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyAddresses.ts +0 -45
  41. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyListingAmenities.ts +0 -58
  42. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyListingSearch.ts +0 -84
  43. package/dist/force-app/main/default/webapplications/propertyrentalapp/src/hooks/usePropertyPrimaryImages.ts +0 -54
  44. /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, useEffect, useCallback } from "react";
4
+ import { useState, useCallback } from "react";
5
5
  import {
6
- queryMaintenanceRequests,
7
- type MaintenanceRequestSummary,
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: MaintenanceRequestSummary[];
26
+ requests: MaintenanceRequestNode[];
12
27
  loading: boolean;
13
28
  error: string | null;
14
- refetch: () => Promise<void>;
29
+ refetch: () => void;
15
30
  } {
16
- const [requests, setRequests] = useState<MaintenanceRequestSummary[]>([]);
17
- const [loading, setLoading] = useState(true);
18
- const [error, setError] = useState<string | null>(null);
31
+ const [generation, setGeneration] = useState(0);
19
32
 
20
- const fetchList = useCallback(async () => {
21
- setLoading(true);
22
- setError(null);
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
- useEffect(() => {
35
- fetchList();
36
- }, [fetchList]);
37
+ const refetch = useCallback(() => {
38
+ clearCacheEntry(`${CACHE_KEY}:${generation}`);
39
+ setGeneration((g) => g + 1);
40
+ }, [generation]);
37
41
 
38
- return { requests, loading, error, refetch: fetchList };
42
+ return { requests: data ?? [], loading, error, refetch };
39
43
  }
@@ -1,99 +1,63 @@
1
1
  /**
2
- * Fetches Property_Listing__c by id, then related Property__c, images, costs, and features.
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, useEffect, useCallback } from "react";
5
+ import { useState, useCallback } from "react";
5
6
  import {
7
+ fetchPropertyDetailById,
6
8
  fetchListingById,
7
- fetchPropertyById,
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
- listing: ListingDetail | null;
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
- listingId: string | undefined,
37
+ id: string | undefined,
30
38
  ): PropertyDetailState & { refetch: () => void } {
31
- const [listing, setListing] = useState<ListingDetail | null>(null);
32
- const [property, setProperty] = useState<PropertyDetail | null>(null);
33
- const [images, setImages] = useState<PropertyImageRecord[]>([]);
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 load = useCallback(async () => {
40
- if (!listingId?.trim()) {
41
- setListing(null);
42
- setProperty(null);
43
- setImages([]);
44
- setCosts([]);
45
- setFeatures([]);
46
- setLoading(false);
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
- useEffect(() => {
86
- load();
87
- }, [load]);
52
+ const refetch = useCallback(() => {
53
+ clearCacheEntry(cacheKey);
54
+ setGeneration((g) => g + 1);
55
+ }, [cacheKey]);
88
56
 
89
57
  return {
90
- listing,
91
- property,
92
- images,
93
- costs,
94
- features,
58
+ property: data ?? null,
95
59
  loading,
96
60
  error,
97
- refetch: load,
61
+ refetch,
98
62
  };
99
63
  }
@@ -1,21 +1,17 @@
1
1
  /**
2
- * Fetches property addresses for the current page of results only, geocodes them in parallel,
3
- * and returns map markers (one pin per property in the current window).
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 { getPropertyIdFromRecord } from "@/hooks/usePropertyPrimaryImages";
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(record: {
13
- fields?: Record<string, { value?: unknown; displayValue?: string | null }>;
14
- }): string {
15
- const f = record.fields?.Name;
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 getCoordinatesFromRecord(record: {
32
- fields?: Record<string, { value?: unknown }>;
33
- }): { lat: number; lng: number } | null {
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: SearchResultRecord[]): {
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
- // Only the current page / current window of results
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 r of results) {
91
- if (!r?.record) continue;
92
- const id = getPropertyIdFromRecord(r.record);
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 r of currentResults) {
120
- if (!r?.record) continue;
121
- const id = getPropertyIdFromRecord(r.record);
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: currentLabels.get(id) ?? "Property",
129
- propertyId: id,
112
+ label: propertyIdToLabel.get(node.Id) ?? "Property",
113
+ propertyId: node.Id,
130
114
  });
131
115
  }
132
116
  }
@@ -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 [data, setData] = useState<WeatherData | null>(null);
300
- const [loading, setLoading] = useState(true);
301
- const [error, setError] = useState<string | null>(null);
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
- useEffect(() => {
304
- if (!canFetch) return;
305
- let cancelled = false;
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
  }
@@ -1,19 +1,16 @@
1
1
  import { useSearchParams, Link } from "react-router";
2
- import { useCallback, useEffect, useState, type ChangeEvent, type SubmitEvent } from "react";
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 listingId = searchParams.get("listingId") ?? "";
47
+ const propertyId = searchParams.get("propertyId") ?? "";
51
48
 
52
- const [listingName, setListingName] = useState<string | null>(null);
53
- const [propertyAddress, setPropertyAddress] = useState<string | null>(null);
54
- const [propertyId, setPropertyId] = useState<string | null>(null);
55
- const [propertyImageUrl, setPropertyImageUrl] = useState<string | null>(null);
56
- const [contactId, setContactId] = useState<string | null>(null);
57
- const [loading, setLoading] = useState(!!listingId);
58
- const [loadError, setLoadError] = useState<string | null>(null);
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={listingId ? `/property/${listingId}` : "/properties"}
147
+ to={propertyId ? `/property/${propertyId}` : "/properties"}
181
148
  className="text-sm text-primary no-underline hover:underline"
182
149
  >
183
- {listingId ? "← Back to listing" : "← Back to search"}
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
- {listingName ?? "Apply for a property"}
163
+ {propertyName ?? "Apply for a property"}
197
164
  </h2>
198
165
  <p className="text-sm text-muted-foreground">
199
166
  {propertyAddress ??
200
- (listingId ? (
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."