@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,38 +1,16 @@
1
- import { useState, useCallback, useEffect, type SubmitEvent } from "react";
1
+ import { useState, useCallback, type SubmitEvent } from "react";
2
2
  import { Link } from "react-router";
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, CardHeader, CardTitle } from "@/components/ui/card";
7
7
  import { CenteredPageLayout } from "@/features/authentication/layout/centered-page-layout";
8
- import { Skeleton } from "@/components/ui/skeleton";
9
- import { SkeletonField } from "@/components/SkeletonPrimitives";
10
- import { createContactUsLead } from "@/api/leadApi";
8
+ import { Loader2 } from "lucide-react";
9
+ import { createContactUsLead } from "@/api/leads/leadApi";
11
10
  import { useAuth } from "@/features/authentication/context/AuthContext";
12
11
  import { fetchUserProfile } from "@/features/authentication/api/userProfileApi";
13
- import type { UserInfo } from "@/api/leadApi";
14
-
15
- function LoadingCard() {
16
- return (
17
- <Card>
18
- <CardHeader>
19
- <Skeleton className="h-5 w-40" />
20
- </CardHeader>
21
- <CardContent className="space-y-4" role="status">
22
- <div className="grid gap-4 sm:grid-cols-2">
23
- <SkeletonField labelWidth="w-24" />
24
- <SkeletonField labelWidth="w-20" />
25
- </div>
26
- <SkeletonField labelWidth="w-12" />
27
- <SkeletonField labelWidth="w-14" />
28
- <SkeletonField labelWidth="w-16" />
29
- <SkeletonField labelWidth="w-20" height="h-[120px]" />
30
- <Skeleton className="h-9 w-32" />
31
- <span className="sr-only">Loading contact form…</span>
32
- </CardContent>
33
- </Card>
34
- );
35
- }
12
+ import { useCachedAsyncData } from "@/features/object-search/hooks/useCachedAsyncData";
13
+ import type { UserInfo } from "@/api/leads/leadApi";
36
14
 
37
15
  function SuccessCard() {
38
16
  return (
@@ -61,6 +39,7 @@ interface ContactFormProps {
61
39
  onSubmit: (e: SubmitEvent<HTMLFormElement>) => void;
62
40
  submitting: boolean;
63
41
  submitError: string | null;
42
+ loading?: boolean;
64
43
  }
65
44
 
66
45
  function ContactForm({
@@ -69,6 +48,7 @@ function ContactForm({
69
48
  onSubmit,
70
49
  submitting,
71
50
  submitError,
51
+ loading,
72
52
  }: ContactFormProps) {
73
53
  const [firstName, setFirstName] = useState("");
74
54
  const [lastName, setLastName] = useState("");
@@ -78,12 +58,23 @@ function ContactForm({
78
58
  const [message, setMessage] = useState("");
79
59
 
80
60
  return (
81
- <Card>
61
+ <Card className="relative">
62
+ {loading && (
63
+ <div className="absolute inset-0 z-10 flex items-center justify-center rounded-[inherit] bg-muted/60">
64
+ <Loader2 className="size-8 animate-spin text-muted-foreground" />
65
+ <span className="sr-only">Loading contact form…</span>
66
+ </div>
67
+ )}
82
68
  <CardHeader>
83
69
  <CardTitle className="text-lg">Send a message</CardTitle>
84
70
  </CardHeader>
85
71
  <CardContent>
86
- <form onSubmit={onSubmit} className="space-y-4">
72
+ <form
73
+ onSubmit={onSubmit}
74
+ className="space-y-4"
75
+ aria-disabled={loading}
76
+ inert={loading ? true : undefined}
77
+ >
87
78
  <div className="grid gap-4 sm:grid-cols-2">
88
79
  <div className="space-y-2">
89
80
  <Label htmlFor="contact-first">First name {!isAuthenticated && "*"}</Label>
@@ -168,25 +159,18 @@ function ContactForm({
168
159
 
169
160
  export default function Contact() {
170
161
  const { isAuthenticated, loading, user } = useAuth();
171
- const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
172
162
  const [submitting, setSubmitting] = useState(false);
173
163
  const [submitError, setSubmitError] = useState<string | null>(null);
174
164
  const [submitted, setSubmitted] = useState(false);
175
165
 
176
- useEffect(() => {
177
- if (!isAuthenticated) return;
178
- let mounted = true;
179
- fetchUserProfile<UserInfo>(user?.id ?? "")
180
- .then((data) => {
181
- if (mounted) setUserInfo(data);
182
- })
183
- .catch((err) => {
184
- if (mounted) console.error("Failed to load user contact info", err);
185
- });
186
- return () => {
187
- mounted = false;
188
- };
189
- }, [isAuthenticated, user]);
166
+ const { data: userInfo } = useCachedAsyncData(
167
+ () => {
168
+ if (!isAuthenticated || !user?.id) return Promise.resolve(null);
169
+ return fetchUserProfile<UserInfo>(user.id);
170
+ },
171
+ [isAuthenticated, user?.id],
172
+ { key: `contact-user-profile:${user?.id ?? ""}`, ttl: 300_000 },
173
+ );
190
174
 
191
175
  const handleSubmit = useCallback(
192
176
  async (e: SubmitEvent<HTMLFormElement>) => {
@@ -228,17 +212,15 @@ export default function Contact() {
228
212
 
229
213
  const isLoading = loading || (isAuthenticated && !userInfo);
230
214
 
231
- function renderCard() {
232
- if (isLoading) return <LoadingCard />;
233
- if (submitted) return <SuccessCard />;
215
+ if (submitted) {
234
216
  return (
235
- <ContactForm
236
- isAuthenticated={isAuthenticated}
237
- userInfo={userInfo}
238
- onSubmit={handleSubmit}
239
- submitting={submitting}
240
- submitError={submitError}
241
- />
217
+ <CenteredPageLayout contentMaxWidth="md">
218
+ <h1 className="mb-2 text-2xl font-semibold text-foreground">Contact Us</h1>
219
+ <p className="mb-6 text-muted-foreground">
220
+ Have a question or feedback? Send us a message and we'll respond as soon as we can.
221
+ </p>
222
+ <SuccessCard />
223
+ </CenteredPageLayout>
242
224
  );
243
225
  }
244
226
 
@@ -248,7 +230,14 @@ export default function Contact() {
248
230
  <p className="mb-6 text-muted-foreground">
249
231
  Have a question or feedback? Send us a message and we'll respond as soon as we can.
250
232
  </p>
251
- {renderCard()}
233
+ <ContactForm
234
+ isAuthenticated={isAuthenticated}
235
+ userInfo={userInfo}
236
+ onSubmit={handleSubmit}
237
+ submitting={submitting}
238
+ submitError={submitError}
239
+ loading={isLoading}
240
+ />
252
241
  </CenteredPageLayout>
253
242
  );
254
243
  }
@@ -32,6 +32,7 @@ export default function Dashboard() {
32
32
  requests={recentMaintenance}
33
33
  loading={maintenanceLoading}
34
34
  error={maintenanceError}
35
+ skeletonCount={5}
35
36
  />
36
37
  </CardContent>
37
38
  </Card>
@@ -1,20 +1,23 @@
1
1
  import { Link, useNavigate } from "react-router";
2
- import { useRef, useState, type ChangeEvent } from "react";
2
+ import { useRef, useState, useMemo, type ChangeEvent } from "react";
3
3
  import { Button } from "@/components/ui/button";
4
4
  import { Input } from "@/components/ui/input";
5
5
  import { Card, CardContent } from "@/components/ui/card";
6
- import { usePropertyListingSearch } from "@/hooks/usePropertyListingSearch";
7
6
  import {
8
- usePropertyPrimaryImages,
9
- getPropertyIdFromRecord,
10
- } from "@/hooks/usePropertyPrimaryImages";
11
- import { usePropertyAddresses } from "@/hooks/usePropertyAddresses";
12
- import { usePropertyListingAmenities } from "@/hooks/usePropertyListingAmenities";
7
+ searchProperties,
8
+ type PropertySearchNode,
9
+ type PropertySearchResult,
10
+ } from "@/api/properties/propertySearchService";
11
+ import {
12
+ extractPrimaryImageUrl,
13
+ extractAmenities,
14
+ extractAddress,
15
+ } from "@/api/properties/propertyNodeUtils";
16
+ import { useCachedAsyncData } from "@/features/object-search/hooks/useCachedAsyncData";
13
17
  import PropertyListingCard, {
14
18
  PropertyListingCardSkeleton,
15
19
  } from "@/components/properties/PropertyListingCard";
16
- import type { SearchResultRecord } from "@/types/searchResults.js";
17
- import { createNewsletterLead } from "@/api/leadApi";
20
+ import { createNewsletterLead } from "@/api/leads/leadApi";
18
21
  import {
19
22
  Phone,
20
23
  Send,
@@ -62,29 +65,24 @@ function FeaturedPropertiesGrid({
62
65
  propertyAddressMap,
63
66
  amenitiesMap,
64
67
  }: {
65
- results: SearchResultRecord[];
66
- primaryImagesMap: Record<string, string> & { loading: boolean };
67
- propertyAddressMap: Record<string, string> & { loading: boolean };
68
- amenitiesMap: Record<string, string> & { loading: boolean };
68
+ results: PropertySearchNode[];
69
+ primaryImagesMap: Record<string, string>;
70
+ propertyAddressMap: Record<string, string>;
71
+ amenitiesMap: Record<string, string>;
69
72
  }) {
70
73
  return (
71
74
  <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
72
- {results.map((item, index) => {
73
- const record = item.record;
74
- const propertyId = getPropertyIdFromRecord(record);
75
- const imageUrl = propertyId ? (primaryImagesMap[propertyId] ?? null) : null;
76
- const address = propertyId ? (propertyAddressMap[propertyId] ?? null) : null;
77
- const amenities = propertyId ? (amenitiesMap[propertyId] ?? null) : null;
75
+ {results.map((node, index) => {
76
+ const imageUrl = primaryImagesMap[node.Id] ?? null;
77
+ const address = propertyAddressMap[node.Id] ?? null;
78
+ const amenities = amenitiesMap[node.Id] ?? null;
78
79
  return (
79
- <div key={record.id ?? index} className="min-h-0">
80
+ <div key={node.Id ?? index} className="min-h-0">
80
81
  <PropertyListingCard
81
- record={record}
82
+ node={node}
82
83
  imageUrl={imageUrl}
83
84
  address={address}
84
85
  amenities={amenities ?? undefined}
85
- loading={
86
- primaryImagesMap.loading || propertyAddressMap.loading || amenitiesMap.loading
87
- }
88
86
  />
89
87
  </div>
90
88
  );
@@ -104,14 +102,47 @@ export default function Home() {
104
102
  text: string;
105
103
  } | null>(null);
106
104
 
107
- const { results: featuredResults, resultsLoading: featuredLoading } = usePropertyListingSearch(
108
- "",
109
- FEATURED_PAGE_SIZE,
110
- "0",
105
+ const { data: searchResult, loading: featuredLoading } = useCachedAsyncData<PropertySearchResult>(
106
+ () => searchProperties({ first: FEATURED_PAGE_SIZE }),
107
+ [],
108
+ { key: "featuredProperties" },
109
+ );
110
+
111
+ const featuredResults = useMemo(
112
+ () =>
113
+ (searchResult?.edges ?? []).reduce<PropertySearchNode[]>((acc, edge) => {
114
+ if (edge?.node) acc.push(edge.node);
115
+ return acc;
116
+ }, []),
117
+ [searchResult?.edges],
111
118
  );
112
- const primaryImagesMap = usePropertyPrimaryImages(featuredResults);
113
- const propertyAddressMap = usePropertyAddresses(featuredResults);
114
- const amenitiesMap = usePropertyListingAmenities(featuredResults);
119
+
120
+ const primaryImagesMap = useMemo(() => {
121
+ const map: Record<string, string> = {};
122
+ for (const node of featuredResults) {
123
+ const url = extractPrimaryImageUrl(node);
124
+ if (url) map[node.Id] = url;
125
+ }
126
+ return map;
127
+ }, [featuredResults]);
128
+
129
+ const propertyAddressMap = useMemo(() => {
130
+ const map: Record<string, string> = {};
131
+ for (const node of featuredResults) {
132
+ const addr = extractAddress(node);
133
+ if (addr) map[node.Id] = String(addr);
134
+ }
135
+ return map;
136
+ }, [featuredResults]);
137
+
138
+ const amenitiesMap = useMemo(() => {
139
+ const map: Record<string, string> = {};
140
+ for (const node of featuredResults) {
141
+ const amenities = extractAmenities(node);
142
+ if (amenities) map[node.Id] = amenities;
143
+ }
144
+ return map;
145
+ }, [featuredResults]);
115
146
 
116
147
  const handleFindHome = (e: React.FormEvent) => {
117
148
  e.preventDefault();
@@ -136,7 +167,7 @@ export default function Home() {
136
167
  }
137
168
  };
138
169
 
139
- const validFeatured = featuredResults.filter((r) => r?.record?.id);
170
+ const validFeatured = featuredResults.filter((r) => r?.Id);
140
171
 
141
172
  return (
142
173
  <div className="space-y-0">
@@ -1,4 +1,4 @@
1
- import { useState, useCallback, type ChangeEvent } from "react";
1
+ import { useState, useCallback, useMemo, type ChangeEvent } from "react";
2
2
  import { Button } from "@/components/ui/button";
3
3
  import { Input } from "@/components/ui/input";
4
4
  import { Label } from "@/components/ui/label";
@@ -7,9 +7,28 @@ import { Skeleton } from "@/components/ui/skeleton";
7
7
  import { Calendar, ArrowRight } from "lucide-react";
8
8
  import MaintenanceRequestList from "@/components/maintenanceRequests/MaintenanceRequestList";
9
9
  import { SkeletonListRows, SkeletonField } from "@/components/SkeletonPrimitives";
10
- import { useMaintenanceRequests } from "@/hooks/useMaintenanceRequests";
11
- import { createMaintenanceRequest } from "@/api/maintenanceRequests/maintenanceRequestApi";
10
+ import {
11
+ searchMaintenanceRequests,
12
+ createMaintenanceRequest,
13
+ type MaintenanceRequestNode,
14
+ } from "@/api/maintenanceRequests/maintenanceRequestApi";
12
15
  import { useAuth } from "@/features/authentication/context/AuthContext";
16
+ import {
17
+ useObjectSearchParams,
18
+ type PaginationConfig,
19
+ } from "@/features/object-search/hooks/useObjectSearchParams";
20
+ import {
21
+ useCachedAsyncData,
22
+ clearCacheEntry,
23
+ } from "@/features/object-search/hooks/useCachedAsyncData";
24
+ import PaginationControls from "@/features/object-search/components/PaginationControls";
25
+ import type {
26
+ Maintenance_Request__C_Filter,
27
+ Maintenance_Request__C_OrderBy,
28
+ } from "@/api/graphql-operations-types";
29
+ import { ResultOrder } from "@/api/graphql-operations-types";
30
+ import type { FilterFieldConfig } from "@/features/object-search/utils/filterUtils";
31
+ import type { SortFieldConfig } from "@/features/object-search/utils/sortUtils";
13
32
 
14
33
  const TYPE_OPTIONS = [
15
34
  "Plumbing",
@@ -29,6 +48,15 @@ const PRIORITY_OPTIONS = [
29
48
  { value: "Emergency", label: "Emergency (2hr)" },
30
49
  ] as const;
31
50
 
51
+ const FILTER_CONFIGS: FilterFieldConfig[] = [];
52
+
53
+ const SORT_CONFIGS: SortFieldConfig[] = [{ field: "Scheduled__c", label: "Scheduled" }];
54
+
55
+ const PAGINATION_CONFIG: PaginationConfig = {
56
+ defaultPageSize: 10,
57
+ validPageSizes: [10, 20, 50],
58
+ };
59
+
32
60
  function MaintenanceSkeleton() {
33
61
  return (
34
62
  <div className="mx-auto max-w-[900px]" role="status">
@@ -62,7 +90,50 @@ function MaintenanceSkeleton() {
62
90
 
63
91
  export default function Maintenance() {
64
92
  const { loading: authLoading } = useAuth();
65
- const { requests, loading, error, refetch } = useMaintenanceRequests();
93
+
94
+ const { query, pagination } = useObjectSearchParams<
95
+ Maintenance_Request__C_Filter,
96
+ Maintenance_Request__C_OrderBy
97
+ >(FILTER_CONFIGS, SORT_CONFIGS, PAGINATION_CONFIG);
98
+
99
+ const [refreshCounter, setRefreshCounter] = useState(0);
100
+
101
+ const searchKey = `maintenance-requests:${JSON.stringify({
102
+ orderBy: query.orderBy,
103
+ first: pagination.pageSize,
104
+ after: pagination.afterCursor,
105
+ refresh: refreshCounter,
106
+ })}`;
107
+
108
+ const {
109
+ data: searchResult,
110
+ loading,
111
+ error,
112
+ } = useCachedAsyncData(
113
+ () =>
114
+ searchMaintenanceRequests({
115
+ where: query.where,
116
+ orderBy: { Scheduled__c: { order: ResultOrder.Desc } },
117
+ first: pagination.pageSize,
118
+ after: pagination.afterCursor,
119
+ }),
120
+ [query.where, query.orderBy, pagination.pageSize, pagination.afterCursor, refreshCounter],
121
+ { key: searchKey },
122
+ );
123
+
124
+ const requests = useMemo(
125
+ () =>
126
+ (searchResult?.edges ?? []).reduce<MaintenanceRequestNode[]>((acc, edge) => {
127
+ if (edge?.node) acc.push(edge.node);
128
+ return acc;
129
+ }, []),
130
+ [searchResult?.edges],
131
+ );
132
+
133
+ const pageInfo = searchResult?.pageInfo;
134
+ const hasNextPage = pageInfo?.hasNextPage ?? false;
135
+ const hasPreviousPage = pagination.pageIndex > 0;
136
+
66
137
  const [title, setTitle] = useState("");
67
138
  const [description, setDescription] = useState("");
68
139
  const [type, setType] = useState<string>("");
@@ -101,14 +172,15 @@ export default function Maintenance() {
101
172
  setType("");
102
173
  setPriority("Standard");
103
174
  setDateRequested(new Date().toISOString().slice(0, 10));
104
- await refetch();
175
+ clearCacheEntry(searchKey);
176
+ setRefreshCounter((c) => c + 1);
105
177
  } catch (err) {
106
178
  setSubmitError(err instanceof Error ? err.message : "Failed to submit request");
107
179
  } finally {
108
180
  setSubmitting(false);
109
181
  }
110
182
  },
111
- [title, description, type, priority, dateRequested, refetch],
183
+ [title, description, type, priority, dateRequested, searchKey],
112
184
  );
113
185
 
114
186
  if (authLoading) return <MaintenanceSkeleton />;
@@ -235,6 +307,23 @@ export default function Maintenance() {
235
307
  emptyMessage="No maintenance requests yet. Submit one above."
236
308
  />
237
309
  </CardContent>
310
+ {!loading && !error && requests.length > 0 && (
311
+ <div className="px-6 pb-6">
312
+ <PaginationControls
313
+ pageIndex={pagination.pageIndex}
314
+ hasNextPage={hasNextPage}
315
+ hasPreviousPage={hasPreviousPage}
316
+ pageSize={pagination.pageSize}
317
+ pageSizeOptions={PAGINATION_CONFIG.validPageSizes}
318
+ onNextPage={() => {
319
+ if (pageInfo?.endCursor) pagination.goToNextPage(pageInfo.endCursor);
320
+ }}
321
+ onPreviousPage={pagination.goToPreviousPage}
322
+ onPageSizeChange={pagination.setPageSize}
323
+ disabled={loading || !!error}
324
+ />
325
+ </div>
326
+ )}
238
327
  </Card>
239
328
  </div>
240
329
  );
@@ -107,20 +107,38 @@ function PropertyDetailsSkeleton() {
107
107
 
108
108
  export default function PropertyDetails() {
109
109
  const { id } = useParams<{ id: string }>();
110
- const { listing, property, images, costs, features, loading, error } = usePropertyDetail(id);
111
- const addressForGeocode = property?.address?.replace(/\n/g, ", ") ?? null;
110
+ const { property, loading, error } = usePropertyDetail(id);
111
+
112
+ // Extract nested relationships from the single property node.
113
+ const images = (property?.Property_Images__r?.edges ?? []).flatMap((e) =>
114
+ e?.node ? [e.node] : [],
115
+ );
116
+ const features = (property?.Property_Features__r?.edges ?? []).flatMap((e) =>
117
+ e?.node ? [e.node] : [],
118
+ );
119
+ const costs = (property?.Property_Costs__r?.edges ?? []).flatMap((e) =>
120
+ e?.node ? [e.node] : [],
121
+ );
122
+ const listing = property?.Property_Listings__r?.edges?.[0]?.node ?? null;
123
+
124
+ const addressForGeocode = property?.Address__c?.value?.replace(/\n/g, ", ") ?? null;
125
+ const hasCoordinates =
126
+ property?.Coordinates__Latitude__s?.value != null &&
127
+ property?.Coordinates__Longitude__s?.value != null;
112
128
  // Always call hook in the same order; disable geocoding when coordinates already exist.
113
- const { coords: geocodedCoords } = useGeocode(property?.coordinates ? null : addressForGeocode);
114
- const addressCoords: GeocodeResult | null =
115
- property?.coordinates?.lat != null && property?.coordinates?.lng != null
116
- ? { lat: property.coordinates.lat, lng: property.coordinates.lng }
117
- : geocodedCoords;
129
+ const { coords: geocodedCoords } = useGeocode(hasCoordinates ? null : addressForGeocode);
130
+ const addressCoords: GeocodeResult | null = hasCoordinates
131
+ ? {
132
+ lat: Number(property!.Coordinates__Latitude__s!.value),
133
+ lng: Number(property!.Coordinates__Longitude__s!.value),
134
+ }
135
+ : geocodedCoords;
118
136
 
119
137
  if (loading) {
120
138
  return <PropertyDetailsSkeleton />;
121
139
  }
122
140
 
123
- if (error || (!listing && id)) {
141
+ if (error || (!property && id)) {
124
142
  return (
125
143
  <div className="mx-auto max-w-[900px]">
126
144
  <div className="mb-4">
@@ -137,8 +155,8 @@ export default function PropertyDetails() {
137
155
  );
138
156
  }
139
157
 
140
- const primaryImage = images.find((i) => i.imageType === "Primary") ?? images[0];
141
- const otherImages = images.filter((i) => i.id !== primaryImage?.id);
158
+ const primaryImage = images.find((i) => i.Image_Type__c?.value === "Primary") ?? images[0];
159
+ const otherImages = images.filter((i) => i.Id !== primaryImage?.Id);
142
160
 
143
161
  return (
144
162
  <div className="mx-auto max-w-[900px]">
@@ -151,10 +169,10 @@ export default function PropertyDetails() {
151
169
  {/* Hero image + thumbnails */}
152
170
  <div className="mb-4 grid grid-cols-1 gap-4 md:grid-cols-2">
153
171
  <div className="relative aspect-[4/3] overflow-hidden rounded-xl bg-muted">
154
- {primaryImage?.imageUrl ? (
172
+ {primaryImage?.Image_URL__c?.value ? (
155
173
  <img
156
- src={primaryImage.imageUrl}
157
- alt={primaryImage.altText ?? primaryImage.name ?? "Property"}
174
+ src={primaryImage.Image_URL__c.value}
175
+ alt={primaryImage.Alt_Text__c?.value ?? primaryImage.Name?.value ?? "Property"}
158
176
  className="h-full w-full object-cover"
159
177
  />
160
178
  ) : (
@@ -163,11 +181,11 @@ export default function PropertyDetails() {
163
181
  </div>
164
182
  <div className="flex flex-col gap-2">
165
183
  {otherImages.slice(0, 5).map((img) => (
166
- <div key={img.id} className="relative h-20 overflow-hidden rounded-lg bg-muted">
167
- {img.imageUrl ? (
184
+ <div key={img.Id} className="relative h-20 overflow-hidden rounded-lg bg-muted">
185
+ {img.Image_URL__c?.value ? (
168
186
  <img
169
- src={img.imageUrl}
170
- alt={img.altText ?? img.name ?? "Property"}
187
+ src={img.Image_URL__c.value}
188
+ alt={img.Alt_Text__c?.value ?? img.Name?.value ?? "Property"}
171
189
  className="h-full w-full object-cover"
172
190
  />
173
191
  ) : null}
@@ -186,7 +204,7 @@ export default function PropertyDetails() {
186
204
  {
187
205
  lat: addressCoords.lat,
188
206
  lng: addressCoords.lng,
189
- label: listing?.name ?? property?.name ?? "Property",
207
+ label: listing?.Name?.value ?? property?.Name?.value ?? "Property",
190
208
  },
191
209
  ]}
192
210
  className="h-[280px] w-full rounded-xl"
@@ -194,56 +212,56 @@ export default function PropertyDetails() {
194
212
  </div>
195
213
  )}
196
214
 
197
- {/* Name, address, price (same order and price format as PropertyListingCard) */}
215
+ {/* Name, address, price */}
198
216
  <Card className="mb-4 rounded-2xl border border-border shadow-sm">
199
217
  <CardContent className="pt-3">
200
218
  <h1 className="mb-1.5 text-2xl font-semibold text-foreground">
201
- {listing?.name ?? property?.name ?? "Untitled"}
219
+ {listing?.Name?.value ?? property?.Name?.value ?? "Untitled"}
202
220
  </h1>
203
- {property?.address && (
221
+ {property?.Address__c?.value && (
204
222
  <p className="mb-1.5 text-sm text-muted-foreground">
205
- {property.address.replace(/\n/g, ", ")}
223
+ {property.Address__c.value.replace(/\n/g, ", ")}
206
224
  </p>
207
225
  )}
208
226
  <p className="mb-4 text-2xl font-semibold text-primary">
209
- {listing?.listingPrice != null
210
- ? formatListingPrice(listing.listingPrice)
211
- : property?.monthlyRent != null
212
- ? formatListingPrice(property.monthlyRent) + " / Month"
227
+ {listing?.Listing_Price__c?.value != null
228
+ ? formatListingPrice(listing.Listing_Price__c.value)
229
+ : property?.Monthly_Rent__c?.value != null
230
+ ? formatListingPrice(property.Monthly_Rent__c.value) + " / Month"
213
231
  : "—"}
214
232
  </p>
215
- {/* Stat cards: value on top, label below, rounded panels (same order as reference) */}
233
+ {/* Stat cards */}
216
234
  <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
217
235
  <div className="flex flex-col items-center justify-center rounded-xl bg-primary px-4 py-3 text-center">
218
236
  <span className="text-xl font-semibold text-primary-foreground">
219
- {property?.bedrooms ?? "—"}
237
+ {property?.Bedrooms__c?.value ?? "—"}
220
238
  </span>
221
239
  <span className="text-xs text-primary-foreground/90">Bedrooms</span>
222
240
  </div>
223
241
  <div className="flex flex-col items-center justify-center rounded-xl bg-primary px-4 py-3 text-center">
224
242
  <span className="text-xl font-semibold text-primary-foreground">
225
- {property?.bathrooms ?? "—"}
243
+ {property?.Bathrooms__c?.value ?? "—"}
226
244
  </span>
227
245
  <span className="text-xs text-primary-foreground/90">Baths</span>
228
246
  </div>
229
247
  <div className="flex flex-col items-center justify-center rounded-xl bg-primary px-4 py-3 text-center">
230
248
  <span className="text-xl font-semibold text-primary-foreground">
231
- {property?.squareFootage ?? "—"}
249
+ {property?.Sq_Ft__c?.value ?? "—"}
232
250
  </span>
233
251
  <span className="text-xs text-primary-foreground/90">Square Feet</span>
234
252
  </div>
235
253
  <div className="flex flex-col items-center justify-center rounded-xl bg-primary px-4 py-3 text-center">
236
254
  <span className="text-xl font-semibold text-primary-foreground">
237
- {listing?.listingStatus ?? "Now"}
255
+ {listing?.Listing_Status__c?.value ?? "Now"}
238
256
  </span>
239
257
  <span className="text-xs text-primary-foreground/90">Available</span>
240
258
  </div>
241
259
  </div>
242
- {property?.propertyType && (
243
- <p className="mt-3 text-sm text-muted-foreground">{property.propertyType}</p>
260
+ {property?.Type__c?.value && (
261
+ <p className="mt-3 text-sm text-muted-foreground">{property.Type__c.value}</p>
244
262
  )}
245
- {property?.description && (
246
- <p className="mt-4 text-sm text-foreground">{property.description}</p>
263
+ {property?.Description__c?.value && (
264
+ <p className="mt-4 text-sm text-foreground">{property.Description__c.value}</p>
247
265
  )}
248
266
  </CardContent>
249
267
  </Card>
@@ -258,17 +276,21 @@ export default function PropertyDetails() {
258
276
  <ul className="space-y-2">
259
277
  {costs.slice(0, 10).map((c) => (
260
278
  <li
261
- key={c.id}
279
+ key={c.Id}
262
280
  className="flex flex-wrap items-baseline justify-between gap-2 border-b border-border/50 pb-2 last:border-0"
263
281
  >
264
- <span className="text-sm font-medium">{c.category ?? "Cost"}</span>
265
- <span className="text-sm text-muted-foreground">{formatCurrency(c.amount)}</span>
266
- {c.date && (
282
+ <span className="text-sm font-medium">{c.Cost_Category__c?.value ?? "Cost"}</span>
283
+ <span className="text-sm text-muted-foreground">
284
+ {formatCurrency(c.Cost_Amount__c?.value ?? null)}
285
+ </span>
286
+ {c.Cost_Date__c?.value && (
267
287
  <span className="w-full text-xs text-muted-foreground">
268
- {formatDate(c.date)}
288
+ {formatDate(c.Cost_Date__c.value)}
269
289
  </span>
270
290
  )}
271
- {c.description && <span className="w-full text-xs">{c.description}</span>}
291
+ {c.Description__c?.value && (
292
+ <span className="w-full text-xs">{c.Description__c.value}</span>
293
+ )}
272
294
  </li>
273
295
  ))}
274
296
  </ul>
@@ -289,11 +311,11 @@ export default function PropertyDetails() {
289
311
  <div className="flex flex-wrap gap-1.5">
290
312
  {features.map((f) => (
291
313
  <span
292
- key={f.id}
314
+ key={f.Id}
293
315
  className="rounded-full border border-border bg-muted/60 px-2.5 py-0.5 text-xs font-medium text-muted-foreground"
294
316
  >
295
- {f.category ? `${f.category}: ` : ""}
296
- {f.description ?? f.name ?? "—"}
317
+ {f.Feature_Category__c?.value ? `${f.Feature_Category__c.value}: ` : ""}
318
+ {f.Description__c?.value ?? f.Name?.value ?? "—"}
297
319
  </span>
298
320
  ))}
299
321
  </div>
@@ -307,7 +329,7 @@ export default function PropertyDetails() {
307
329
  size="sm"
308
330
  className="w-full cursor-pointer rounded-xl bg-primary px-5 py-5 text-lg font-medium transition-colors duration-200 hover:bg-primary/90"
309
331
  >
310
- <Link to={`/application?listingId=${encodeURIComponent(id ?? "")}`}>
332
+ <Link to={`/application?propertyId=${encodeURIComponent(id ?? "")}`}>
311
333
  Fill out an application
312
334
  </Link>
313
335
  </Button>