@salesforce/webapp-template-app-react-sample-b2x-experimental 1.68.0 → 1.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/data/Lease__c.json +13 -0
  3. package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +13 -8
  4. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/applicationApi.ts +78 -0
  5. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +17 -0
  6. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/index.ts +19 -0
  7. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/leadApi.ts +69 -0
  8. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/maintenanceRequestApi.ts +177 -0
  9. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectDetailService.ts +125 -0
  10. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoGraphQLService.ts +194 -0
  11. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoService.ts +199 -0
  12. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyDetailGraphQL.ts +497 -0
  13. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyListingGraphQL.ts +190 -0
  14. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/recordListGraphQLService.ts +365 -0
  15. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +20 -30
  16. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/FiltersPanel.tsx +375 -0
  17. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/LoadingFallback.tsx +61 -0
  18. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyListingCard.tsx +164 -0
  19. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyMap.tsx +113 -0
  20. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/SearchResultCard.tsx +131 -0
  21. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/alerts/status-alert.tsx +45 -0
  22. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailFields.tsx +55 -0
  23. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailForm.tsx +146 -0
  24. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailHeader.tsx +34 -0
  25. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailLayoutSections.tsx +80 -0
  26. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/Section.tsx +108 -0
  27. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/SectionRow.tsx +20 -0
  28. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/UiApiDetailForm.tsx +140 -0
  29. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FieldValueDisplay.tsx +73 -0
  30. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedAddress.tsx +29 -0
  31. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedEmail.tsx +17 -0
  32. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedPhone.tsx +24 -0
  33. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedText.tsx +11 -0
  34. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedUrl.tsx +29 -0
  35. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/index.ts +6 -0
  36. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterField.tsx +54 -0
  37. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterInput.tsx +55 -0
  38. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterSelect.tsx +72 -0
  39. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/filters-form.tsx +114 -0
  40. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/submit-button.tsx +47 -0
  41. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/layout/card-layout.tsx +19 -0
  42. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/ResultCardFields.tsx +71 -0
  43. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchHeader.tsx +31 -0
  44. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchPagination.tsx +144 -0
  45. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchResultsPanel.tsx +197 -0
  46. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/shared/GlobalSearchInput.tsx +114 -0
  47. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/constants.ts +39 -0
  48. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/index.ts +33 -0
  49. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/form.tsx +204 -0
  50. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/index.ts +22 -0
  51. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useGeocode.ts +35 -0
  52. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useMaintenanceRequests.ts +39 -0
  53. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectInfoBatch.ts +65 -0
  54. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectSearchData.ts +395 -0
  55. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +36 -0
  56. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyDetail.ts +99 -0
  57. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +75 -0
  58. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +100 -0
  59. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +51 -0
  60. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordDetailLayout.ts +156 -0
  61. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordListGraphQL.ts +135 -0
  62. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useWeather.ts +173 -0
  63. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +263 -76
  64. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Contact.tsx +158 -0
  65. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +137 -65
  66. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/DetailPage.tsx +109 -0
  67. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/GlobalSearch.tsx +229 -0
  68. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +469 -21
  69. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +244 -95
  70. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +211 -39
  71. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +26 -10
  72. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +165 -0
  73. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearchPlaceholder.tsx +49 -0
  74. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-01.jpg +0 -0
  75. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-02.jpg +0 -0
  76. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-03.jpg +0 -0
  77. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-04.jpg +0 -0
  78. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-05.jpg +0 -0
  79. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-06.jpg +0 -0
  80. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-07.jpg +0 -0
  81. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-08.jpg +0 -0
  82. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-09.jpg +0 -0
  83. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-10.jpg +0 -0
  84. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-11.jpg +0 -0
  85. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-12.jpg +0 -0
  86. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-13.jpg +0 -0
  87. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-14.jpg +0 -0
  88. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-15.jpg +0 -0
  89. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-16.jpg +0 -0
  90. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-17.jpg +0 -0
  91. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-18.jpg +0 -0
  92. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-19.jpg +0 -0
  93. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-20.jpg +0 -0
  94. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-21.jpg +0 -0
  95. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-22.jpg +0 -0
  96. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-23.jpg +0 -0
  97. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-24.jpg +0 -0
  98. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-25.jpg +0 -0
  99. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +32 -6
  100. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/styles/global.css +23 -63
  101. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/filters.ts +120 -0
  102. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/picklist.ts +32 -0
  103. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/index.ts +4 -0
  104. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/leaflet.d.ts +17 -0
  105. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/objectInfo/objectInfo.ts +166 -0
  106. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/recordDetail/recordDetail.ts +61 -0
  107. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/search/searchResults.ts +229 -0
  108. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/apiUtils.ts +125 -0
  109. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/cacheUtils.ts +76 -0
  110. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/debounce.ts +89 -0
  111. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldUtils.ts +354 -0
  112. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldValueExtractor.ts +67 -0
  113. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/filterUtils.ts +32 -0
  114. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formDataTransformUtils.ts +260 -0
  115. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formUtils.ts +142 -0
  116. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/geocode.ts +65 -0
  117. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLNodeFieldUtils.ts +186 -0
  118. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLObjectInfoAdapter.ts +319 -0
  119. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLRecordAdapter.ts +90 -0
  120. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/index.ts +59 -0
  121. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/layoutTransformUtils.ts +236 -0
  122. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/linkUtils.ts +14 -0
  123. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/paginationUtils.ts +49 -0
  124. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/recordUtils.ts +159 -0
  125. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/sanitizationUtils.ts +49 -0
  126. package/dist/package.json +1 -1
  127. package/package.json +2 -2
  128. package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls +0 -111
  129. package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls-meta.xml +0 -6
  130. package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls +0 -93
  131. package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls-meta.xml +0 -6
@@ -1,3 +1,4 @@
1
+ import { useState, useCallback } from "react";
1
2
  import { Button } from "../components/ui/button";
2
3
  import { Input } from "../components/ui/input";
3
4
  import { Label } from "../components/ui/label";
@@ -10,121 +11,269 @@ import {
10
11
  TableHeader,
11
12
  TableRow,
12
13
  } from "../components/ui/table";
13
- import { Calendar, Upload, ArrowRight } from "lucide-react";
14
+ import { Calendar, ArrowRight } from "lucide-react";
15
+ import { useMaintenanceRequests } from "@/hooks/useMaintenanceRequests";
16
+ import { createMaintenanceRequest } from "@/api/maintenanceRequestApi";
17
+
18
+ const TYPE_OPTIONS = [
19
+ "Plumbing",
20
+ "Electrical",
21
+ "HVAC",
22
+ "Appliance",
23
+ "Structural",
24
+ "Cleaning",
25
+ "Security",
26
+ "Pest",
27
+ "Other",
28
+ ] as const;
29
+
30
+ const PRIORITY_OPTIONS = [
31
+ { value: "Standard", label: "Standard" },
32
+ { value: "High", label: "High (Same Day)" },
33
+ { value: "Emergency", label: "Emergency (2hr)" },
34
+ ] as const;
35
+
36
+ function formatDate(iso: string | null): string {
37
+ if (!iso) return "—";
38
+ try {
39
+ const d = new Date(iso);
40
+ return d.toLocaleDateString("en-US", {
41
+ month: "short",
42
+ day: "numeric",
43
+ year: "numeric",
44
+ });
45
+ } catch {
46
+ return iso;
47
+ }
48
+ }
49
+
50
+ function statusBadgeClass(status: string | null): string {
51
+ if (!status) return "bg-muted text-muted-foreground";
52
+ switch (status) {
53
+ case "Resolved":
54
+ return "bg-green-100 text-green-700";
55
+ case "In Progress":
56
+ case "Assigned":
57
+ return "bg-blue-100 text-blue-700";
58
+ case "On Hold":
59
+ return "bg-amber-100 text-amber-700";
60
+ case "New":
61
+ default:
62
+ return "bg-muted text-muted-foreground";
63
+ }
64
+ }
14
65
 
15
66
  export default function Maintenance() {
67
+ const { requests, loading, error, refetch } = useMaintenanceRequests();
68
+ const [title, setTitle] = useState("");
69
+ const [description, setDescription] = useState("");
70
+ const [type, setType] = useState<string>("");
71
+ const [priority, setPriority] = useState<string>("Standard");
72
+ const [dateRequested, setDateRequested] = useState(() => {
73
+ const d = new Date();
74
+ return d.toISOString().slice(0, 10);
75
+ });
76
+ const [submitting, setSubmitting] = useState(false);
77
+ const [submitError, setSubmitError] = useState<string | null>(null);
78
+ const [submitSuccess, setSubmitSuccess] = useState(false);
79
+
80
+ const handleSubmit = useCallback(
81
+ async (e: React.FormEvent) => {
82
+ e.preventDefault();
83
+ const t = title.trim();
84
+ if (!t) {
85
+ setSubmitError("Title is required");
86
+ return;
87
+ }
88
+ setSubmitting(true);
89
+ setSubmitError(null);
90
+ setSubmitSuccess(false);
91
+ try {
92
+ await createMaintenanceRequest({
93
+ Title__c: t,
94
+ Description__c: description.trim() || undefined,
95
+ Type__c: type.trim() || undefined,
96
+ Priority__c: priority,
97
+ Status__c: "New",
98
+ Date_Requested__c: dateRequested || undefined,
99
+ });
100
+ setSubmitSuccess(true);
101
+ setTitle("");
102
+ setDescription("");
103
+ setType("");
104
+ setPriority("Standard");
105
+ setDateRequested(new Date().toISOString().slice(0, 10));
106
+ await refetch();
107
+ } catch (err) {
108
+ setSubmitError(err instanceof Error ? err.message : "Failed to submit request");
109
+ } finally {
110
+ setSubmitting(false);
111
+ }
112
+ },
113
+ [title, description, type, priority, dateRequested, refetch],
114
+ );
115
+
16
116
  return (
17
117
  <div className="mx-auto max-w-[900px]">
18
- <Card className="mb-6">
118
+ <Card className="mb-6 rounded-2xl shadow-md">
19
119
  <CardHeader>
20
- <CardTitle className="text-2xl text-primary">Maintenance</CardTitle>
120
+ <CardTitle className="text-2xl text-primary">New maintenance request</CardTitle>
21
121
  </CardHeader>
22
122
  <CardContent className="space-y-4">
23
- <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
24
- <div className="space-y-2">
25
- <Label>Date Reported</Label>
26
- <div className="relative">
123
+ <form onSubmit={handleSubmit} className="space-y-4">
124
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
125
+ <div className="space-y-2">
126
+ <Label htmlFor="maintenance-title">Title *</Label>
27
127
  <Input
128
+ id="maintenance-title"
28
129
  type="text"
29
- readOnly
30
- value="Today"
31
- className="pr-10"
32
- aria-label="Date reported"
130
+ value={title}
131
+ onChange={(e) => setTitle(e.target.value)}
132
+ placeholder="e.g. Kitchen faucet leak"
133
+ aria-label="Title"
134
+ required
33
135
  />
34
- <span className="absolute right-3 top-1/2 -translate-y-1/2">
35
- <Calendar className="size-[18px] text-muted-foreground" />
36
- </span>
136
+ </div>
137
+ <div className="space-y-2">
138
+ <Label htmlFor="maintenance-priority">Priority</Label>
139
+ <select
140
+ id="maintenance-priority"
141
+ className="flex h-9 w-full rounded-xl border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-[color,box-shadow] duration-200 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-primary"
142
+ aria-label="Priority"
143
+ value={priority}
144
+ onChange={(e) => setPriority(e.target.value)}
145
+ >
146
+ {PRIORITY_OPTIONS.map((o) => (
147
+ <option key={o.value} value={o.value}>
148
+ {o.label}
149
+ </option>
150
+ ))}
151
+ </select>
152
+ </div>
153
+ </div>
154
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
155
+ <div className="space-y-2">
156
+ <Label htmlFor="maintenance-date">Date reported</Label>
157
+ <div className="relative">
158
+ <Input
159
+ id="maintenance-date"
160
+ type="date"
161
+ value={dateRequested}
162
+ onChange={(e) => setDateRequested(e.target.value)}
163
+ className="pr-10"
164
+ aria-label="Date reported"
165
+ />
166
+ <span className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
167
+ <Calendar className="size-[18px] text-muted-foreground" aria-hidden />
168
+ </span>
169
+ </div>
170
+ </div>
171
+ <div className="space-y-2">
172
+ <Label htmlFor="maintenance-type">Type</Label>
173
+ <select
174
+ id="maintenance-type"
175
+ className="flex h-9 w-full rounded-xl border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-[color,box-shadow] duration-200 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-primary"
176
+ aria-label="Type"
177
+ value={type}
178
+ onChange={(e) => setType(e.target.value)}
179
+ >
180
+ <option value="">—</option>
181
+ {TYPE_OPTIONS.map((o) => (
182
+ <option key={o} value={o}>
183
+ {o}
184
+ </option>
185
+ ))}
186
+ </select>
37
187
  </div>
38
188
  </div>
39
189
  <div className="space-y-2">
40
- <Label>Urgency</Label>
41
- <select
42
- className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-2 focus-visible:ring-ring"
43
- aria-label="Urgency"
44
- defaultValue="urgent"
45
- >
46
- <option value="urgent">Urgent</option>
47
- <option value="normal">Normal</option>
48
- <option value="low">Low</option>
49
- </select>
190
+ <Label htmlFor="maintenance-description">Description</Label>
191
+ <textarea
192
+ id="maintenance-description"
193
+ rows={4}
194
+ placeholder="Describe the issue"
195
+ className="min-h-[100px] w-full resize-y rounded-xl border border-input bg-transparent px-3 py-2 text-sm shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-primary transition-colors duration-200"
196
+ aria-label="Description"
197
+ value={description}
198
+ onChange={(e) => setDescription(e.target.value)}
199
+ />
50
200
  </div>
51
- </div>
52
- <div className="space-y-2">
53
- <Label>Room</Label>
54
- <select
55
- className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-2 focus-visible:ring-ring"
56
- aria-label="Room"
57
- defaultValue="living-room"
58
- >
59
- <option value="living-room">Living Room</option>
60
- <option value="bathroom">Bathroom</option>
61
- <option value="bedroom">Bedroom</option>
62
- <option value="kitchen">Kitchen</option>
63
- </select>
64
- </div>
65
- <div className="space-y-2">
66
- <Label>Description</Label>
67
- <textarea
68
- rows={4}
69
- placeholder="Input text"
70
- className="min-h-[100px] w-full resize-y rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
71
- aria-label="Description"
72
- />
73
- </div>
74
- <div className="grid grid-cols-2 gap-4">
75
- <div />
76
- <div className="flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-border bg-muted/30 p-6 text-center">
77
- <Upload className="mx-auto mb-2 size-10 text-muted-foreground" />
78
- <p className="m-0 text-xs text-muted-foreground">
79
- Choose a file or drag & drop for reference here.
201
+ {submitError && (
202
+ <p className="text-sm text-destructive" role="alert">
203
+ {submitError}
80
204
  </p>
81
- <p className="mt-1 text-[0.75rem] text-muted-foreground">
82
- JPEG, PNG, PDF, and MP4 formats, up to 50MB
205
+ )}
206
+ {submitSuccess && (
207
+ <p className="text-sm text-green-600" role="status">
208
+ Request submitted. It will appear in the list below.
83
209
  </p>
210
+ )}
211
+ <div className="flex justify-end">
212
+ <Button
213
+ type="submit"
214
+ className="cursor-pointer gap-2 rounded-xl transition-colors duration-200"
215
+ disabled={submitting}
216
+ >
217
+ {submitting ? "Submitting…" : "Submit Request"}
218
+ <ArrowRight className="size-[18px]" aria-hidden />
219
+ </Button>
84
220
  </div>
85
- </div>
86
- <div className="flex justify-end">
87
- <Button className="gap-2">
88
- Submit Request
89
- <ArrowRight className="size-[18px]" />
90
- </Button>
91
- </div>
221
+ </form>
92
222
  </CardContent>
93
223
  </Card>
94
- <Card>
224
+ <Card className="rounded-2xl shadow-md">
225
+ <CardHeader>
226
+ <CardTitle className="text-xl text-primary">Your requests</CardTitle>
227
+ </CardHeader>
95
228
  <CardContent className="p-0">
96
- <div className="overflow-x-auto">
97
- <Table>
98
- <TableHeader>
99
- <TableRow>
100
- {["Description", "Work Order", "Type", "Room", "Date", "Status"].map((h) => (
101
- <TableHead key={h} className="font-semibold text-primary">
102
- {h}
103
- </TableHead>
104
- ))}
105
- </TableRow>
106
- </TableHeader>
107
- <TableBody>
108
- <TableRow>
109
- <TableCell>
110
- <div className="flex items-center gap-2">
111
- <div className="size-10 shrink-0 rounded-lg bg-muted" />
112
- <span>Shower head leaking</span>
113
- </div>
114
- </TableCell>
115
- <TableCell>#12548796</TableCell>
116
- <TableCell>Plumbing</TableCell>
117
- <TableCell>Bathroom</TableCell>
118
- <TableCell>28 Jan, 12.30 AM</TableCell>
119
- <TableCell>
120
- <span className="inline-block rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-700">
121
- Completed
122
- </span>
123
- </TableCell>
124
- </TableRow>
125
- </TableBody>
126
- </Table>
127
- </div>
229
+ {error && (
230
+ <p className="px-6 py-4 text-sm text-destructive" role="alert">
231
+ {error}
232
+ </p>
233
+ )}
234
+ {loading && <p className="px-6 py-4 text-sm text-muted-foreground">Loading…</p>}
235
+ {!loading && !error && (
236
+ <div className="overflow-x-auto">
237
+ <Table>
238
+ <TableHeader>
239
+ <TableRow>
240
+ <TableHead className="font-semibold text-primary">Title</TableHead>
241
+ <TableHead className="font-semibold text-primary">Work order</TableHead>
242
+ <TableHead className="font-semibold text-primary">Type</TableHead>
243
+ <TableHead className="font-semibold text-primary">Date</TableHead>
244
+ <TableHead className="font-semibold text-primary">Status</TableHead>
245
+ </TableRow>
246
+ </TableHeader>
247
+ <TableBody>
248
+ {requests.length === 0 ? (
249
+ <TableRow>
250
+ <TableCell colSpan={5} className="text-center text-muted-foreground py-8">
251
+ No maintenance requests yet. Submit one above.
252
+ </TableCell>
253
+ </TableRow>
254
+ ) : (
255
+ requests.map((r) => (
256
+ <TableRow key={r.id}>
257
+ <TableCell className="font-medium">
258
+ {r.title ?? r.description ?? "—"}
259
+ </TableCell>
260
+ <TableCell>{r.name ?? "—"}</TableCell>
261
+ <TableCell>{r.type ?? "—"}</TableCell>
262
+ <TableCell>{formatDate(r.dateRequested)}</TableCell>
263
+ <TableCell>
264
+ <span
265
+ className={`inline-block rounded-full px-3 py-1 text-xs font-medium ${statusBadgeClass(r.status)}`}
266
+ >
267
+ {r.status ?? "—"}
268
+ </span>
269
+ </TableCell>
270
+ </TableRow>
271
+ ))
272
+ )}
273
+ </TableBody>
274
+ </Table>
275
+ </div>
276
+ )}
128
277
  </CardContent>
129
278
  </Card>
130
279
  </div>
@@ -1,9 +1,68 @@
1
1
  import { useParams, Link } from "react-router";
2
2
  import { Button } from "../components/ui/button";
3
- import { Card, CardHeader, CardContent } from "../components/ui/card";
3
+ import { Card, CardHeader, CardTitle, CardContent } from "../components/ui/card";
4
+ import PropertyMap from "@/components/PropertyMap";
5
+ import { usePropertyDetail } from "@/hooks/usePropertyDetail";
6
+ import { useGeocode } from "@/hooks/useGeocode";
7
+
8
+ function formatCurrency(val: number | string | null): string {
9
+ if (val == null) return "—";
10
+ const n = typeof val === "number" ? val : Number(val);
11
+ return Number.isNaN(n)
12
+ ? String(val)
13
+ : new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(n);
14
+ }
15
+
16
+ function formatDate(val: string | null): string {
17
+ if (!val) return "—";
18
+ try {
19
+ return new Date(val).toLocaleDateString();
20
+ } catch {
21
+ return val;
22
+ }
23
+ }
4
24
 
5
25
  export default function PropertyDetails() {
6
- const { id } = useParams();
26
+ const { id } = useParams<{ id: string }>();
27
+ const { listing, property, images, costs, features, loading, error } = usePropertyDetail(id);
28
+ const addressForGeocode = property?.address?.replace(/\n/g, ", ") ?? null;
29
+ const { coords: addressCoords } = useGeocode(addressForGeocode);
30
+
31
+ if (loading) {
32
+ return (
33
+ <div className="mx-auto max-w-[900px]">
34
+ <div className="mb-4 h-4 w-32 animate-pulse rounded bg-muted" />
35
+ <div className="mb-4 grid grid-cols-1 gap-4 md:grid-cols-2">
36
+ <div className="h-72 animate-pulse rounded-xl bg-muted" />
37
+ <div className="flex flex-col gap-2">
38
+ {[1, 2, 3, 4, 5].map((i) => (
39
+ <div key={i} className="h-12 animate-pulse rounded-lg bg-muted" />
40
+ ))}
41
+ </div>
42
+ </div>
43
+ </div>
44
+ );
45
+ }
46
+
47
+ if (error || (!listing && id)) {
48
+ return (
49
+ <div className="mx-auto max-w-[900px]">
50
+ <div className="mb-4">
51
+ <Link to="/properties" className="text-sm text-primary no-underline hover:underline">
52
+ ← Back to listings
53
+ </Link>
54
+ </div>
55
+ <Card className="rounded-2xl shadow-md">
56
+ <CardContent className="pt-6">
57
+ <p className="text-destructive">{error ?? "Listing not found."}</p>
58
+ </CardContent>
59
+ </Card>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ const primaryImage = images.find((i) => i.imageType === "Primary") ?? images[0];
65
+ const otherImages = images.filter((i) => i.id !== primaryImage?.id);
7
66
 
8
67
  return (
9
68
  <div className="mx-auto max-w-[900px]">
@@ -12,57 +71,170 @@ export default function PropertyDetails() {
12
71
  ← Back to listings
13
72
  </Link>
14
73
  </div>
74
+
75
+ {/* Hero image + thumbnails */}
15
76
  <div className="mb-4 grid grid-cols-1 gap-4 md:grid-cols-2">
16
- <div className="h-72 rounded-xl bg-muted" />
77
+ <div className="relative aspect-[4/3] overflow-hidden rounded-xl bg-muted">
78
+ {primaryImage?.imageUrl ? (
79
+ <img
80
+ src={primaryImage.imageUrl}
81
+ alt={primaryImage.altText ?? primaryImage.name ?? "Property"}
82
+ className="h-full w-full object-cover"
83
+ />
84
+ ) : (
85
+ <div className="flex h-full items-center justify-center text-muted-foreground">
86
+ No image
87
+ </div>
88
+ )}
89
+ </div>
17
90
  <div className="flex flex-col gap-2">
18
- {[1, 2, 3, 4, 5].map((i) => (
19
- <div key={i} className="h-12 rounded-lg bg-muted" />
91
+ {otherImages.slice(0, 5).map((img) => (
92
+ <div key={img.id} className="relative h-20 overflow-hidden rounded-lg bg-muted">
93
+ {img.imageUrl ? (
94
+ <img
95
+ src={img.imageUrl}
96
+ alt={img.altText ?? img.name ?? "Property"}
97
+ className="h-full w-full object-cover"
98
+ />
99
+ ) : null}
100
+ </div>
20
101
  ))}
21
102
  </div>
22
103
  </div>
23
- <div className="mb-4 flex flex-wrap gap-2">
24
- <Button size="sm">23 Photos</Button>
25
- <Button size="sm" variant="outline">
26
- 8 Virtual Tours
27
- </Button>
28
- <Button size="sm" variant="outline">
29
- Property Map
30
- </Button>
31
- </div>
32
- <Card className="mb-4">
104
+
105
+ {/* Map - geocoded from property address */}
106
+ {addressCoords && (
107
+ <div className="mb-4">
108
+ <PropertyMap
109
+ center={[addressCoords.lat, addressCoords.lng]}
110
+ zoom={15}
111
+ markers={[
112
+ {
113
+ lat: addressCoords.lat,
114
+ lng: addressCoords.lng,
115
+ label: listing?.name ?? property?.name ?? "Property",
116
+ },
117
+ ]}
118
+ className="h-[280px] w-full rounded-xl"
119
+ />
120
+ </div>
121
+ )}
122
+
123
+ {/* Listing + address */}
124
+ <Card className="mb-4 rounded-2xl shadow-md">
33
125
  <CardContent className="pt-6">
34
- <p className="mb-2 text-sm text-primary">
35
- California / San Francisco County / San Francisco / South Beach
126
+ {property?.address && (
127
+ <p className="mb-2 text-sm text-muted-foreground">
128
+ {property.address.replace(/\n/g, ", ")}
129
+ </p>
130
+ )}
131
+ <p className="mb-1 text-2xl font-bold text-foreground">
132
+ {listing?.listingPrice != null
133
+ ? formatCurrency(listing.listingPrice)
134
+ : property?.monthlyRent != null
135
+ ? formatCurrency(property.monthlyRent) + " / Month"
136
+ : "—"}
36
137
  </p>
37
- <p className="mb-1 text-2xl font-bold text-foreground">$4,600 / Month</p>
38
138
  <p className="mb-4 text-sm text-muted-foreground">
39
- 301 Bryant St. Unit 5B, San Francisco, CA 94107
139
+ {listing?.name ?? property?.name ?? "Untitled"}
40
140
  </p>
41
141
  <div className="flex flex-wrap gap-3">
42
- {["2 Bedroom", "2 Baths", "1040 sq ft", "Now Available"].map((s) => (
43
- <span key={s} className="rounded-lg bg-primary/10 px-3 py-1.5 text-sm text-primary">
44
- {s}
142
+ {property?.bedrooms != null && (
143
+ <span className="rounded-lg bg-primary/10 px-3 py-1.5 text-sm text-primary">
144
+ {property.bedrooms} Bedroom{Number(property.bedrooms) !== 1 ? "s" : ""}
45
145
  </span>
46
- ))}
146
+ )}
147
+ {property?.bathrooms != null && (
148
+ <span className="rounded-lg bg-primary/10 px-3 py-1.5 text-sm text-primary">
149
+ {property.bathrooms} Bath{Number(property.bathrooms) !== 1 ? "s" : ""}
150
+ </span>
151
+ )}
152
+ {property?.squareFootage != null && (
153
+ <span className="rounded-lg bg-primary/10 px-3 py-1.5 text-sm text-primary">
154
+ {property.squareFootage} sq ft
155
+ </span>
156
+ )}
157
+ {listing?.listingStatus && (
158
+ <span className="rounded-lg bg-primary/10 px-3 py-1.5 text-sm text-primary">
159
+ {listing.listingStatus}
160
+ </span>
161
+ )}
162
+ {property?.propertyType && (
163
+ <span className="rounded-lg bg-primary/10 px-3 py-1.5 text-sm text-primary">
164
+ {property.propertyType}
165
+ </span>
166
+ )}
47
167
  </div>
168
+ {property?.description && (
169
+ <p className="mt-4 text-sm text-foreground">{property.description}</p>
170
+ )}
48
171
  </CardContent>
49
172
  </Card>
50
- <Card>
51
- <CardHeader>
52
- <CardTitle className="text-base">Contact Property</CardTitle>
53
- </CardHeader>
54
- <CardContent className="flex flex-col gap-2">
55
- <Button variant="outline" className="justify-start">
56
- Request Tour
57
- </Button>
58
- <Button variant="outline" className="justify-start">
59
- Send Message
60
- </Button>
61
- <Button asChild variant="secondary" className="justify-center">
62
- <Link to="/application?property=1">Fill out an application</Link>
63
- </Button>
64
- </CardContent>
65
- </Card>
173
+
174
+ {/* Related: Costs */}
175
+ {costs.length > 0 && (
176
+ <Card className="mb-4 rounded-2xl shadow-md">
177
+ <CardHeader>
178
+ <CardTitle className="text-base">Related costs</CardTitle>
179
+ </CardHeader>
180
+ <CardContent>
181
+ <ul className="space-y-2">
182
+ {costs.slice(0, 10).map((c) => (
183
+ <li
184
+ key={c.id}
185
+ className="flex flex-wrap items-baseline justify-between gap-2 border-b border-border/50 pb-2 last:border-0"
186
+ >
187
+ <span className="text-sm font-medium">{c.category ?? "Cost"}</span>
188
+ <span className="text-sm text-muted-foreground">{formatCurrency(c.amount)}</span>
189
+ {c.date && (
190
+ <span className="w-full text-xs text-muted-foreground">
191
+ {formatDate(c.date)}
192
+ </span>
193
+ )}
194
+ {c.description && <span className="w-full text-xs">{c.description}</span>}
195
+ </li>
196
+ ))}
197
+ </ul>
198
+ {costs.length > 10 && (
199
+ <p className="mt-2 text-xs text-muted-foreground">+ {costs.length - 10} more</p>
200
+ )}
201
+ </CardContent>
202
+ </Card>
203
+ )}
204
+
205
+ {/* Related: Features */}
206
+ {features.length > 0 && (
207
+ <Card className="mb-4 rounded-2xl shadow-md">
208
+ <CardHeader>
209
+ <CardTitle className="text-base">Features & amenities</CardTitle>
210
+ </CardHeader>
211
+ <CardContent>
212
+ <ul className="flex flex-wrap gap-2">
213
+ {features.map((f) => (
214
+ <li
215
+ key={f.id}
216
+ className="rounded-md border border-border bg-muted/50 px-3 py-1.5 text-sm"
217
+ >
218
+ {f.category && <span className="text-muted-foreground">{f.category}: </span>}
219
+ {f.description ?? f.name ?? "—"}
220
+ </li>
221
+ ))}
222
+ </ul>
223
+ </CardContent>
224
+ </Card>
225
+ )}
226
+
227
+ <div className="mb-4">
228
+ <Button
229
+ asChild
230
+ size="lg"
231
+ className="w-full cursor-pointer rounded-xl bg-violet-600 text-base font-semibold transition-colors duration-200 hover:bg-violet-700"
232
+ >
233
+ <Link to={`/application?listingId=${encodeURIComponent(id ?? "")}`}>
234
+ Fill out an application
235
+ </Link>
236
+ </Button>
237
+ </div>
66
238
  </div>
67
239
  );
68
240
  }