@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.
- package/dist/CHANGELOG.md +16 -0
- package/dist/force-app/main/default/data/Lease__c.json +13 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +13 -8
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/applicationApi.ts +78 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/index.ts +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/leadApi.ts +69 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/maintenanceRequestApi.ts +177 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectDetailService.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoGraphQLService.ts +194 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoService.ts +199 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyDetailGraphQL.ts +497 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyListingGraphQL.ts +190 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/recordListGraphQLService.ts +365 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +20 -30
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/FiltersPanel.tsx +375 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/LoadingFallback.tsx +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyListingCard.tsx +164 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyMap.tsx +113 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/SearchResultCard.tsx +131 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/alerts/status-alert.tsx +45 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailFields.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailForm.tsx +146 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailHeader.tsx +34 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailLayoutSections.tsx +80 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/Section.tsx +108 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/SectionRow.tsx +20 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/UiApiDetailForm.tsx +140 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FieldValueDisplay.tsx +73 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedAddress.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedEmail.tsx +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedPhone.tsx +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedText.tsx +11 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedUrl.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/index.ts +6 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterField.tsx +54 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/filters-form.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/submit-button.tsx +47 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/layout/card-layout.tsx +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/ResultCardFields.tsx +71 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchHeader.tsx +31 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchPagination.tsx +144 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchResultsPanel.tsx +197 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/shared/GlobalSearchInput.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/constants.ts +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/index.ts +33 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/form.tsx +204 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/index.ts +22 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useGeocode.ts +35 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useMaintenanceRequests.ts +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectInfoBatch.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectSearchData.ts +395 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +36 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyDetail.ts +99 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +75 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +100 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +51 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordDetailLayout.ts +156 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordListGraphQL.ts +135 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useWeather.ts +173 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +263 -76
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Contact.tsx +158 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +137 -65
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/DetailPage.tsx +109 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/GlobalSearch.tsx +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +469 -21
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +244 -95
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +211 -39
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +26 -10
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +165 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearchPlaceholder.tsx +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-01.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-02.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-03.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-04.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-05.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-06.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-07.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-08.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-09.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-10.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-11.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-12.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-13.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-14.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-15.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-16.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-17.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-18.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-19.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-20.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-21.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-22.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-23.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-24.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-25.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +32 -6
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/styles/global.css +23 -63
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/filters.ts +120 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/picklist.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/index.ts +4 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/leaflet.d.ts +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/objectInfo/objectInfo.ts +166 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/recordDetail/recordDetail.ts +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/search/searchResults.ts +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/apiUtils.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/cacheUtils.ts +76 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/debounce.ts +89 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldUtils.ts +354 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldValueExtractor.ts +67 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/filterUtils.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formDataTransformUtils.ts +260 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formUtils.ts +142 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/geocode.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLNodeFieldUtils.ts +186 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLObjectInfoAdapter.ts +319 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLRecordAdapter.ts +90 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/index.ts +59 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/layoutTransformUtils.ts +236 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/linkUtils.ts +14 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/paginationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/recordUtils.ts +159 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/sanitizationUtils.ts +49 -0
- package/dist/package.json +1 -1
- package/package.json +2 -2
- package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls +0 -111
- package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls-meta.xml +0 -6
- package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls +0 -93
- package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls-meta.xml +0 -6
package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx
CHANGED
|
@@ -1,99 +1,286 @@
|
|
|
1
|
-
import { Link } from "react-router";
|
|
1
|
+
import { useSearchParams, Link } from "react-router";
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
3
|
import { Button } from "../components/ui/button";
|
|
3
4
|
import { Input } from "../components/ui/input";
|
|
4
5
|
import { Label } from "../components/ui/label";
|
|
5
|
-
import { Card,
|
|
6
|
-
import {
|
|
6
|
+
import { Card, CardContent } from "../components/ui/card";
|
|
7
|
+
import {
|
|
8
|
+
fetchListingById,
|
|
9
|
+
fetchPropertyById,
|
|
10
|
+
fetchPrimaryImagesByPropertyIds,
|
|
11
|
+
} from "@/api/propertyDetailGraphQL";
|
|
12
|
+
import { createApplicationRecord } from "@/api/applicationApi";
|
|
13
|
+
|
|
14
|
+
const PREFERRED_TERM_OPTIONS = ["", "1 month", "6 months", "12 months"];
|
|
7
15
|
|
|
8
16
|
export default function Application() {
|
|
17
|
+
const [searchParams] = useSearchParams();
|
|
18
|
+
const listingId = searchParams.get("listingId") ?? "";
|
|
19
|
+
|
|
20
|
+
const [listingName, setListingName] = useState<string | null>(null);
|
|
21
|
+
const [propertyAddress, setPropertyAddress] = useState<string | null>(null);
|
|
22
|
+
const [propertyId, setPropertyId] = useState<string | null>(null);
|
|
23
|
+
const [propertyImageUrl, setPropertyImageUrl] = useState<string | null>(null);
|
|
24
|
+
const [loading, setLoading] = useState(!!listingId);
|
|
25
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
26
|
+
|
|
27
|
+
// Form state – all map to Application__c fields
|
|
28
|
+
const [firstName, setFirstName] = useState("");
|
|
29
|
+
const [lastName, setLastName] = useState("");
|
|
30
|
+
const [email, setEmail] = useState("");
|
|
31
|
+
const [phone, setPhone] = useState("");
|
|
32
|
+
const [moveInDate, setMoveInDate] = useState("");
|
|
33
|
+
const [preferredTerm, setPreferredTerm] = useState("");
|
|
34
|
+
const [employmentInfo, setEmploymentInfo] = useState("");
|
|
35
|
+
const [references, setReferences] = useState("");
|
|
36
|
+
|
|
37
|
+
const [submitting, setSubmitting] = useState(false);
|
|
38
|
+
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
39
|
+
const [submittedId, setSubmittedId] = useState<string | null>(null);
|
|
40
|
+
|
|
41
|
+
// Load listing and property when listingId is present
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!listingId?.trim()) {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let cancelled = false;
|
|
48
|
+
setLoadError(null);
|
|
49
|
+
(async () => {
|
|
50
|
+
try {
|
|
51
|
+
const listing = await fetchListingById(listingId);
|
|
52
|
+
if (cancelled) return;
|
|
53
|
+
if (!listing) {
|
|
54
|
+
setLoadError("Listing not found.");
|
|
55
|
+
setLoading(false);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
setListingName(listing.name);
|
|
59
|
+
if (listing.propertyId) {
|
|
60
|
+
setPropertyId(listing.propertyId);
|
|
61
|
+
const [property, primaryImages] = await Promise.all([
|
|
62
|
+
fetchPropertyById(listing.propertyId),
|
|
63
|
+
fetchPrimaryImagesByPropertyIds([listing.propertyId]),
|
|
64
|
+
]);
|
|
65
|
+
if (cancelled) return;
|
|
66
|
+
setPropertyAddress(property?.address ?? null);
|
|
67
|
+
setPropertyImageUrl(primaryImages[listing.propertyId] ?? null);
|
|
68
|
+
}
|
|
69
|
+
} catch (e) {
|
|
70
|
+
if (!cancelled) {
|
|
71
|
+
setLoadError(e instanceof Error ? e.message : "Failed to load listing.");
|
|
72
|
+
}
|
|
73
|
+
} finally {
|
|
74
|
+
if (!cancelled) setLoading(false);
|
|
75
|
+
}
|
|
76
|
+
})();
|
|
77
|
+
return () => {
|
|
78
|
+
cancelled = true;
|
|
79
|
+
};
|
|
80
|
+
}, [listingId]);
|
|
81
|
+
|
|
82
|
+
const handleSubmit = useCallback(
|
|
83
|
+
async (e: React.FormEvent) => {
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
setSubmitError(null);
|
|
86
|
+
setSubmitting(true);
|
|
87
|
+
try {
|
|
88
|
+
const id = await createApplicationRecord({
|
|
89
|
+
Property__c: propertyId || null,
|
|
90
|
+
Status__c: "Submitted",
|
|
91
|
+
First_Name__c: firstName.trim() || null,
|
|
92
|
+
Last_Name__c: lastName.trim() || null,
|
|
93
|
+
Email__c: email.trim() || null,
|
|
94
|
+
Phone__c: phone.trim() || null,
|
|
95
|
+
Start_Date__c: moveInDate.trim() || null,
|
|
96
|
+
Preferred_Term__c: preferredTerm.trim() || null,
|
|
97
|
+
Employment_Info__c: employmentInfo.trim() || null,
|
|
98
|
+
References__c: references.trim() || null,
|
|
99
|
+
});
|
|
100
|
+
setSubmittedId(id.id);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
setSubmitError(err instanceof Error ? err.message : "Failed to submit application.");
|
|
103
|
+
} finally {
|
|
104
|
+
setSubmitting(false);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
[
|
|
108
|
+
propertyId,
|
|
109
|
+
firstName,
|
|
110
|
+
lastName,
|
|
111
|
+
email,
|
|
112
|
+
phone,
|
|
113
|
+
moveInDate,
|
|
114
|
+
preferredTerm,
|
|
115
|
+
employmentInfo,
|
|
116
|
+
references,
|
|
117
|
+
],
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (loading) {
|
|
121
|
+
return (
|
|
122
|
+
<div className="mx-auto max-w-[800px]">
|
|
123
|
+
<div className="mb-6 h-48 animate-pulse rounded-xl bg-muted" />
|
|
124
|
+
<div className="h-64 animate-pulse rounded-lg bg-muted" />
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (submittedId) {
|
|
130
|
+
return (
|
|
131
|
+
<div className="mx-auto max-w-[800px]">
|
|
132
|
+
<Card className="mb-6 p-6">
|
|
133
|
+
<h2 className="mb-2 text-lg font-semibold text-primary">Application submitted</h2>
|
|
134
|
+
<p className="text-sm text-muted-foreground">
|
|
135
|
+
Your application has been saved. Reference: {submittedId}
|
|
136
|
+
</p>
|
|
137
|
+
<div className="mt-4 flex gap-2">
|
|
138
|
+
<Button asChild variant="outline" size="sm">
|
|
139
|
+
<Link to="/properties">Back to search</Link>
|
|
140
|
+
</Button>
|
|
141
|
+
<Button asChild size="sm">
|
|
142
|
+
<Link to="/application">Submit another</Link>
|
|
143
|
+
</Button>
|
|
144
|
+
</div>
|
|
145
|
+
</Card>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
9
150
|
return (
|
|
10
151
|
<div className="mx-auto max-w-[800px]">
|
|
11
152
|
<Card className="mb-6 flex gap-4 p-6">
|
|
12
|
-
<div className="size-[200px] shrink-0 rounded-xl bg-muted"
|
|
153
|
+
<div className="relative size-[200px] shrink-0 overflow-hidden rounded-xl bg-muted">
|
|
154
|
+
{propertyImageUrl ? (
|
|
155
|
+
<img src={propertyImageUrl} alt="" className="h-full w-full object-cover" />
|
|
156
|
+
) : (
|
|
157
|
+
<div className="h-full w-full bg-muted" aria-hidden />
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
13
160
|
<div className="min-w-0 flex-1">
|
|
14
|
-
<h2 className="mb-1 text-lg font-semibold text-primary">
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
161
|
+
<h2 className="mb-1 text-lg font-semibold text-primary">
|
|
162
|
+
{listingName ?? "Apply for a property"}
|
|
163
|
+
</h2>
|
|
164
|
+
<p className="text-sm text-muted-foreground">
|
|
165
|
+
{propertyAddress ??
|
|
166
|
+
(listingId
|
|
167
|
+
? "Loading…"
|
|
168
|
+
: "Select a property from search or listing detail to apply.")}
|
|
18
169
|
</p>
|
|
170
|
+
{loadError && <p className="mt-2 text-sm text-destructive">{loadError}</p>}
|
|
19
171
|
</div>
|
|
20
172
|
<div className="flex gap-2">
|
|
21
|
-
<Button variant="outline" size="sm">
|
|
22
|
-
|
|
173
|
+
<Button asChild variant="outline" size="sm">
|
|
174
|
+
<Link to={listingId ? `/property/${listingId}` : "/properties"}>Back to listing</Link>
|
|
23
175
|
</Button>
|
|
24
|
-
<Button size="sm">Save for Later</Button>
|
|
25
176
|
</div>
|
|
26
177
|
</Card>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
<TabsTrigger
|
|
30
|
-
value="applicants"
|
|
31
|
-
className="rounded-none border-b-2 border-primary bg-transparent px-0 pb-1 data-[state=active]:shadow-none"
|
|
32
|
-
>
|
|
33
|
-
APPLICANTS & OCCUPANTS
|
|
34
|
-
</TabsTrigger>
|
|
35
|
-
<TabsTrigger
|
|
36
|
-
value="details"
|
|
37
|
-
className="rounded-none border-b-0 bg-transparent px-0 pb-1 text-muted-foreground data-[state=active]:shadow-none"
|
|
38
|
-
>
|
|
39
|
-
YOUR DETAILS
|
|
40
|
-
</TabsTrigger>
|
|
41
|
-
<TabsTrigger
|
|
42
|
-
value="screening"
|
|
43
|
-
className="rounded-none border-b-0 bg-transparent px-0 pb-1 text-muted-foreground data-[state=active]:shadow-none"
|
|
44
|
-
>
|
|
45
|
-
SCREENING
|
|
46
|
-
</TabsTrigger>
|
|
47
|
-
<TabsTrigger
|
|
48
|
-
value="submit"
|
|
49
|
-
className="rounded-none border-b-0 bg-transparent px-0 pb-1 text-muted-foreground data-[state=active]:shadow-none"
|
|
50
|
-
>
|
|
51
|
-
FINISH & SUBMIT
|
|
52
|
-
</TabsTrigger>
|
|
53
|
-
</TabsList>
|
|
54
|
-
<TabsContent value="applicants" className="mt-0" />
|
|
55
|
-
</Tabs>
|
|
56
|
-
<Card>
|
|
178
|
+
|
|
179
|
+
<Card className="mb-6">
|
|
57
180
|
<CardContent className="pt-6">
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<div className="
|
|
63
|
-
<
|
|
64
|
-
|
|
181
|
+
<form onSubmit={handleSubmit}>
|
|
182
|
+
<h3 className="mb-4 text-xs font-semibold uppercase tracking-wider text-foreground">
|
|
183
|
+
YOUR INFO
|
|
184
|
+
</h3>
|
|
185
|
+
<div className="mb-4 grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
186
|
+
<div className="space-y-2">
|
|
187
|
+
<Label htmlFor="app-first-name">First Name *</Label>
|
|
188
|
+
<Input
|
|
189
|
+
id="app-first-name"
|
|
190
|
+
type="text"
|
|
191
|
+
value={firstName}
|
|
192
|
+
onChange={(e) => setFirstName(e.target.value)}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
<div className="space-y-2">
|
|
196
|
+
<Label htmlFor="app-last-name">Last Name</Label>
|
|
197
|
+
<Input
|
|
198
|
+
id="app-last-name"
|
|
199
|
+
type="text"
|
|
200
|
+
value={lastName}
|
|
201
|
+
onChange={(e) => setLastName(e.target.value)}
|
|
202
|
+
/>
|
|
203
|
+
</div>
|
|
65
204
|
</div>
|
|
66
|
-
<div className="space-y-2">
|
|
67
|
-
<Label>
|
|
68
|
-
<Input
|
|
205
|
+
<div className="mb-4 space-y-2">
|
|
206
|
+
<Label htmlFor="app-email">Email Address</Label>
|
|
207
|
+
<Input
|
|
208
|
+
id="app-email"
|
|
209
|
+
type="email"
|
|
210
|
+
value={email}
|
|
211
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
212
|
+
/>
|
|
69
213
|
</div>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
</div>
|
|
79
|
-
<h3 className="mb-4 mt-6 text-xs font-semibold uppercase tracking-wider text-foreground">
|
|
80
|
-
MOVE IN
|
|
81
|
-
</h3>
|
|
82
|
-
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
83
|
-
<div className="space-y-2">
|
|
84
|
-
<Label>MOVE IN DATE</Label>
|
|
85
|
-
<Input type="text" placeholder="Select Date" />
|
|
214
|
+
<div className="mb-4 space-y-2">
|
|
215
|
+
<Label htmlFor="app-phone">Phone Number</Label>
|
|
216
|
+
<Input
|
|
217
|
+
id="app-phone"
|
|
218
|
+
type="tel"
|
|
219
|
+
value={phone}
|
|
220
|
+
onChange={(e) => setPhone(e.target.value)}
|
|
221
|
+
/>
|
|
86
222
|
</div>
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<
|
|
93
|
-
<
|
|
94
|
-
|
|
223
|
+
<h3 className="mb-4 mt-6 text-xs font-semibold uppercase tracking-wider text-foreground">
|
|
224
|
+
MOVE IN
|
|
225
|
+
</h3>
|
|
226
|
+
<div className="mb-4 grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
227
|
+
<div className="space-y-2">
|
|
228
|
+
<Label htmlFor="app-move-in">Move in date</Label>
|
|
229
|
+
<Input
|
|
230
|
+
id="app-move-in"
|
|
231
|
+
type="date"
|
|
232
|
+
value={moveInDate}
|
|
233
|
+
onChange={(e) => setMoveInDate(e.target.value)}
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
<div className="space-y-2">
|
|
237
|
+
<Label htmlFor="app-term">Preferred term</Label>
|
|
238
|
+
<select
|
|
239
|
+
id="app-term"
|
|
240
|
+
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
241
|
+
value={preferredTerm}
|
|
242
|
+
onChange={(e) => setPreferredTerm(e.target.value)}
|
|
243
|
+
>
|
|
244
|
+
{PREFERRED_TERM_OPTIONS.map((opt) => (
|
|
245
|
+
<option key={opt || "empty"} value={opt}>
|
|
246
|
+
{opt || "Select one"}
|
|
247
|
+
</option>
|
|
248
|
+
))}
|
|
249
|
+
</select>
|
|
250
|
+
</div>
|
|
95
251
|
</div>
|
|
96
|
-
|
|
252
|
+
<div className="mb-4 space-y-2">
|
|
253
|
+
<Label htmlFor="app-employment">Employment info</Label>
|
|
254
|
+
<textarea
|
|
255
|
+
id="app-employment"
|
|
256
|
+
rows={3}
|
|
257
|
+
className="min-h-[80px] 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"
|
|
258
|
+
value={employmentInfo}
|
|
259
|
+
onChange={(e) => setEmploymentInfo(e.target.value)}
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
<div className="mb-4 space-y-2">
|
|
263
|
+
<Label htmlFor="app-references">References</Label>
|
|
264
|
+
<textarea
|
|
265
|
+
id="app-references"
|
|
266
|
+
rows={3}
|
|
267
|
+
className="min-h-[80px] 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"
|
|
268
|
+
value={references}
|
|
269
|
+
onChange={(e) => setReferences(e.target.value)}
|
|
270
|
+
/>
|
|
271
|
+
</div>
|
|
272
|
+
{submitError && <p className="mb-4 text-sm text-destructive">{submitError}</p>}
|
|
273
|
+
<div className="flex gap-2">
|
|
274
|
+
<Button
|
|
275
|
+
type="submit"
|
|
276
|
+
size="sm"
|
|
277
|
+
className="bg-teal-600 hover:bg-teal-700"
|
|
278
|
+
disabled={submitting}
|
|
279
|
+
>
|
|
280
|
+
{submitting ? "Submitting…" : "Submit application"}
|
|
281
|
+
</Button>
|
|
282
|
+
</div>
|
|
283
|
+
</form>
|
|
97
284
|
</CardContent>
|
|
98
285
|
</Card>
|
|
99
286
|
</div>
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
import { Link } from "react-router";
|
|
3
|
+
import { Button } from "../components/ui/button";
|
|
4
|
+
import { Input } from "../components/ui/input";
|
|
5
|
+
import { Label } from "../components/ui/label";
|
|
6
|
+
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
|
|
7
|
+
import { createContactUsLead } from "@/api/leadApi";
|
|
8
|
+
|
|
9
|
+
export default function Contact() {
|
|
10
|
+
const [firstName, setFirstName] = useState("");
|
|
11
|
+
const [lastName, setLastName] = useState("");
|
|
12
|
+
const [email, setEmail] = useState("");
|
|
13
|
+
const [phone, setPhone] = useState("");
|
|
14
|
+
const [subject, setSubject] = useState("");
|
|
15
|
+
const [message, setMessage] = useState("");
|
|
16
|
+
const [submitting, setSubmitting] = useState(false);
|
|
17
|
+
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
18
|
+
const [submitted, setSubmitted] = useState(false);
|
|
19
|
+
|
|
20
|
+
const handleSubmit = useCallback(
|
|
21
|
+
async (e: React.FormEvent) => {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
setSubmitError(null);
|
|
24
|
+
setSubmitting(true);
|
|
25
|
+
try {
|
|
26
|
+
await createContactUsLead({
|
|
27
|
+
FirstName: firstName.trim(),
|
|
28
|
+
LastName: lastName.trim(),
|
|
29
|
+
Email: email.trim(),
|
|
30
|
+
Phone: phone.trim() || undefined,
|
|
31
|
+
Subject: subject.trim() || undefined,
|
|
32
|
+
Message: message.trim(),
|
|
33
|
+
});
|
|
34
|
+
setSubmitted(true);
|
|
35
|
+
setFirstName("");
|
|
36
|
+
setLastName("");
|
|
37
|
+
setEmail("");
|
|
38
|
+
setPhone("");
|
|
39
|
+
setSubject("");
|
|
40
|
+
setMessage("");
|
|
41
|
+
} catch (err) {
|
|
42
|
+
setSubmitError(
|
|
43
|
+
err instanceof Error ? err.message : "Something went wrong. Please try again.",
|
|
44
|
+
);
|
|
45
|
+
} finally {
|
|
46
|
+
setSubmitting(false);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
[firstName, lastName, email, phone, subject, message],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (submitted) {
|
|
53
|
+
return (
|
|
54
|
+
<div className="mx-auto max-w-[600px]">
|
|
55
|
+
<Card>
|
|
56
|
+
<CardHeader>
|
|
57
|
+
<CardTitle className="text-xl">Message sent</CardTitle>
|
|
58
|
+
</CardHeader>
|
|
59
|
+
<CardContent className="space-y-4">
|
|
60
|
+
<p className="text-muted-foreground">
|
|
61
|
+
Thank you for reaching out. We’ve received your message and will get back to you soon.
|
|
62
|
+
</p>
|
|
63
|
+
<Button asChild variant="outline">
|
|
64
|
+
<Link to="/">Back to Home</Link>
|
|
65
|
+
</Button>
|
|
66
|
+
<Button asChild>
|
|
67
|
+
<Link to="/contact">Send another message</Link>
|
|
68
|
+
</Button>
|
|
69
|
+
</CardContent>
|
|
70
|
+
</Card>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="mx-auto max-w-[600px]">
|
|
77
|
+
<h1 className="mb-2 text-2xl font-semibold text-foreground">Contact Us</h1>
|
|
78
|
+
<p className="mb-6 text-muted-foreground">
|
|
79
|
+
Have a question or feedback? Send us a message and we’ll respond as soon as we can.
|
|
80
|
+
</p>
|
|
81
|
+
<Card>
|
|
82
|
+
<CardHeader>
|
|
83
|
+
<CardTitle className="text-lg">Send a message</CardTitle>
|
|
84
|
+
</CardHeader>
|
|
85
|
+
<CardContent>
|
|
86
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
87
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
88
|
+
<div className="space-y-2">
|
|
89
|
+
<Label htmlFor="contact-first">First name *</Label>
|
|
90
|
+
<Input
|
|
91
|
+
id="contact-first"
|
|
92
|
+
type="text"
|
|
93
|
+
value={firstName}
|
|
94
|
+
onChange={(e) => setFirstName(e.target.value)}
|
|
95
|
+
required
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
<div className="space-y-2">
|
|
99
|
+
<Label htmlFor="contact-last">Last name *</Label>
|
|
100
|
+
<Input
|
|
101
|
+
id="contact-last"
|
|
102
|
+
type="text"
|
|
103
|
+
value={lastName}
|
|
104
|
+
onChange={(e) => setLastName(e.target.value)}
|
|
105
|
+
required
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
<div className="space-y-2">
|
|
110
|
+
<Label htmlFor="contact-email">Email *</Label>
|
|
111
|
+
<Input
|
|
112
|
+
id="contact-email"
|
|
113
|
+
type="email"
|
|
114
|
+
value={email}
|
|
115
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
116
|
+
required
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
<div className="space-y-2">
|
|
120
|
+
<Label htmlFor="contact-phone">Phone</Label>
|
|
121
|
+
<Input
|
|
122
|
+
id="contact-phone"
|
|
123
|
+
type="tel"
|
|
124
|
+
value={phone}
|
|
125
|
+
onChange={(e) => setPhone(e.target.value)}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
<div className="space-y-2">
|
|
129
|
+
<Label htmlFor="contact-subject">Subject</Label>
|
|
130
|
+
<Input
|
|
131
|
+
id="contact-subject"
|
|
132
|
+
type="text"
|
|
133
|
+
value={subject}
|
|
134
|
+
onChange={(e) => setSubject(e.target.value)}
|
|
135
|
+
placeholder="e.g. General inquiry, Property question"
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
<div className="space-y-2">
|
|
139
|
+
<Label htmlFor="contact-message">Message *</Label>
|
|
140
|
+
<textarea
|
|
141
|
+
id="contact-message"
|
|
142
|
+
rows={5}
|
|
143
|
+
value={message}
|
|
144
|
+
onChange={(e) => setMessage(e.target.value)}
|
|
145
|
+
required
|
|
146
|
+
className="min-h-[120px] 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"
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
{submitError && <p className="text-sm text-destructive">{submitError}</p>}
|
|
150
|
+
<Button type="submit" disabled={submitting} className="bg-teal-600 hover:bg-teal-700">
|
|
151
|
+
{submitting ? "Sending…" : "Send message"}
|
|
152
|
+
</Button>
|
|
153
|
+
</form>
|
|
154
|
+
</CardContent>
|
|
155
|
+
</Card>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|