@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
|
@@ -1,30 +1,478 @@
|
|
|
1
|
+
import { Link, useNavigate } from "react-router";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
1
3
|
import { Button } from "../components/ui/button";
|
|
2
4
|
import { Input } from "../components/ui/input";
|
|
5
|
+
import { Card, CardContent } from "../components/ui/card";
|
|
6
|
+
import { usePropertyListingSearch } from "@/hooks/usePropertyListingSearch";
|
|
7
|
+
import {
|
|
8
|
+
usePropertyPrimaryImages,
|
|
9
|
+
getPropertyIdFromRecord,
|
|
10
|
+
} from "@/hooks/usePropertyPrimaryImages";
|
|
11
|
+
import { usePropertyAddresses } from "@/hooks/usePropertyAddresses";
|
|
12
|
+
import { createNewsletterLead } from "@/api/leadApi";
|
|
13
|
+
import { Phone, Send, ChevronDown, ChevronUp, MessageCircle, HelpCircle } from "lucide-react";
|
|
14
|
+
|
|
15
|
+
const HERO_IMAGE = "https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=1200&q=85";
|
|
16
|
+
const CITY_IMAGE = "https://images.unsplash.com/photo-1514565131-fce0801e5785?w=800&q=85";
|
|
17
|
+
const RELAX_IMAGE = "https://images.unsplash.com/photo-1586023492125-27b2c045efd7?w=600&q=85";
|
|
18
|
+
|
|
19
|
+
const FEATURED_PAGE_SIZE = 3;
|
|
20
|
+
|
|
21
|
+
function fieldVal(
|
|
22
|
+
fields: Record<string, { value?: unknown; displayValue?: string | null }> | undefined,
|
|
23
|
+
apiName: string,
|
|
24
|
+
): string | null {
|
|
25
|
+
const f = fields?.[apiName];
|
|
26
|
+
if (!f || typeof f !== "object") return null;
|
|
27
|
+
if (f.displayValue != null && f.displayValue !== "") return String(f.displayValue);
|
|
28
|
+
if (f.value != null) return typeof f.value === "object" ? null : String(f.value);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatPrice(val: string | number | null): string {
|
|
33
|
+
if (val == null) return "—";
|
|
34
|
+
const n = typeof val === "number" ? val : Number(val);
|
|
35
|
+
if (Number.isNaN(n)) return String(val);
|
|
36
|
+
return new Intl.NumberFormat("en-US", {
|
|
37
|
+
style: "currency",
|
|
38
|
+
currency: "USD",
|
|
39
|
+
maximumFractionDigits: 0,
|
|
40
|
+
}).format(n);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const FAQ_ITEMS = [
|
|
44
|
+
{
|
|
45
|
+
q: "How do I apply for an apartment?",
|
|
46
|
+
a: "Applications can be completed online directly through our website at your convenience. Go to Property Search, choose a listing, and click Apply to start. If you need assistance, please reach out via Contact Us.",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
q: "How long does the application approval process take?",
|
|
50
|
+
a: "Most applications are reviewed within 2–3 business days. You'll receive an email once your application has been processed.",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
q: "Do you offer flexible lease terms?",
|
|
54
|
+
a: "Yes. Many of our properties offer 1-, 6-, or 12-month lease terms. Check the listing details or contact us for options.",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
q: "What is the penalty for breaking a lease early?",
|
|
58
|
+
a: "Early termination terms vary by property. Please review your lease agreement or contact the property manager for details.",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
q: "What are the income and credit qualifications?",
|
|
62
|
+
a: "Requirements vary by property. Typical guidelines include income of at least 3x monthly rent and a credit check. See each listing for specifics.",
|
|
63
|
+
},
|
|
64
|
+
];
|
|
3
65
|
|
|
4
66
|
export default function Home() {
|
|
67
|
+
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
68
|
+
const navigate = useNavigate();
|
|
69
|
+
const [faqOpen, setFaqOpen] = useState<number | null>(0);
|
|
70
|
+
const [footerEmail, setFooterEmail] = useState("");
|
|
71
|
+
const [newsletterSubmitting, setNewsletterSubmitting] = useState(false);
|
|
72
|
+
const [newsletterMessage, setNewsletterMessage] = useState<{
|
|
73
|
+
type: "success" | "error";
|
|
74
|
+
text: string;
|
|
75
|
+
} | null>(null);
|
|
76
|
+
|
|
77
|
+
const { results: featuredResults } = usePropertyListingSearch("", FEATURED_PAGE_SIZE, "0");
|
|
78
|
+
const primaryImagesMap = usePropertyPrimaryImages(featuredResults);
|
|
79
|
+
const propertyAddressMap = usePropertyAddresses(featuredResults);
|
|
80
|
+
|
|
81
|
+
const handleFindHome = (e: React.FormEvent) => {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
const q = searchInputRef.current?.value?.trim() ?? "";
|
|
84
|
+
navigate(q ? `/properties?search=${encodeURIComponent(q)}` : "/properties");
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const handleFooterSubmit = async (e: React.FormEvent) => {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
const email = footerEmail.trim();
|
|
90
|
+
if (!email) return;
|
|
91
|
+
setNewsletterMessage(null);
|
|
92
|
+
setNewsletterSubmitting(true);
|
|
93
|
+
try {
|
|
94
|
+
await createNewsletterLead(email);
|
|
95
|
+
setFooterEmail("");
|
|
96
|
+
setNewsletterMessage({ type: "success", text: "Thanks! You’re subscribed." });
|
|
97
|
+
} catch {
|
|
98
|
+
setNewsletterMessage({ type: "error", text: "Something went wrong. Try again later." });
|
|
99
|
+
} finally {
|
|
100
|
+
setNewsletterSubmitting(false);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const validFeatured = featuredResults.filter((r) => r?.record?.id);
|
|
105
|
+
|
|
5
106
|
return (
|
|
6
|
-
<div className="
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
107
|
+
<div className="space-y-0">
|
|
108
|
+
{/* ——— Hero ——— */}
|
|
109
|
+
<div className="relative w-full overflow-hidden rounded-2xl">
|
|
110
|
+
<div className="relative aspect-[21/9] min-h-[280px] w-full md:aspect-[3/1] md:min-h-[320px]">
|
|
111
|
+
<img
|
|
112
|
+
src={HERO_IMAGE}
|
|
113
|
+
alt=""
|
|
114
|
+
className="h-full w-full object-cover"
|
|
115
|
+
loading="eager"
|
|
116
|
+
fetchPriority="high"
|
|
117
|
+
/>
|
|
118
|
+
<div className="absolute inset-0 bg-black/40" />
|
|
119
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center gap-6 px-4">
|
|
120
|
+
<div className="w-full max-w-2xl text-center">
|
|
121
|
+
<p className="mb-2 text-xs font-medium uppercase tracking-[0.2em] text-white/80">
|
|
122
|
+
The Zen Way to Lease
|
|
123
|
+
</p>
|
|
124
|
+
<h1 className="mb-2 text-3xl font-semibold tracking-tight text-white drop-shadow-md md:text-4xl lg:text-5xl">
|
|
125
|
+
Your Dream Place Starts Here
|
|
126
|
+
</h1>
|
|
127
|
+
<p className="mb-6 text-base text-white/90 md:text-lg">
|
|
128
|
+
Search properties, manage applications, and move in with ease.
|
|
129
|
+
</p>
|
|
130
|
+
</div>
|
|
131
|
+
<form
|
|
132
|
+
onSubmit={handleFindHome}
|
|
133
|
+
className="flex w-full max-w-xl flex-col gap-2 sm:flex-row sm:rounded-lg"
|
|
134
|
+
>
|
|
135
|
+
<Input
|
|
136
|
+
ref={searchInputRef}
|
|
137
|
+
type="text"
|
|
138
|
+
name="search"
|
|
139
|
+
placeholder="Search by address, city, or zip code"
|
|
140
|
+
aria-label="Search properties"
|
|
141
|
+
className="min-h-12 flex-1 rounded-lg border-0 bg-white/95 px-4 text-foreground shadow-lg placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-white"
|
|
142
|
+
/>
|
|
143
|
+
<Button
|
|
144
|
+
type="submit"
|
|
145
|
+
size="lg"
|
|
146
|
+
className="min-h-12 shrink-0 cursor-pointer rounded-xl bg-primary px-8 font-medium text-primary-foreground shadow-md transition-colors duration-200 hover:bg-primary/90"
|
|
147
|
+
>
|
|
148
|
+
Find Home
|
|
149
|
+
</Button>
|
|
150
|
+
</form>
|
|
151
|
+
<p className="text-sm text-white/80">
|
|
152
|
+
or{" "}
|
|
153
|
+
<Link
|
|
154
|
+
to="/properties"
|
|
155
|
+
className="cursor-pointer font-medium text-white underline underline-offset-2 transition-colors duration-200 hover:no-underline"
|
|
156
|
+
>
|
|
157
|
+
browse all properties
|
|
158
|
+
</Link>
|
|
159
|
+
</p>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
27
162
|
</div>
|
|
163
|
+
|
|
164
|
+
{/* ——— Featured Properties ——— */}
|
|
165
|
+
<section className="relative overflow-hidden rounded-2xl bg-gradient-to-br from-violet-100/80 to-pink-100/80 px-4 py-12 md:px-6">
|
|
166
|
+
<div className="relative mx-auto max-w-6xl">
|
|
167
|
+
<h2 className="mb-8 text-2xl font-bold tracking-tight text-violet-900 md:text-3xl">
|
|
168
|
+
Featured Properties
|
|
169
|
+
</h2>
|
|
170
|
+
{validFeatured.length > 0 ? (
|
|
171
|
+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
172
|
+
{validFeatured.map((item, index) => {
|
|
173
|
+
const record = item.record;
|
|
174
|
+
const propertyId = getPropertyIdFromRecord(record);
|
|
175
|
+
const imageUrl = propertyId ? (primaryImagesMap[propertyId] ?? null) : null;
|
|
176
|
+
const address = propertyId ? (propertyAddressMap[propertyId] ?? null) : null;
|
|
177
|
+
const name = fieldVal(record.fields, "Name") ?? "Untitled";
|
|
178
|
+
const priceRaw = fieldVal(record.fields, "Listing_Price__c");
|
|
179
|
+
const price = formatPrice(priceRaw);
|
|
180
|
+
const displayAddress = (address ?? "").trim().replace(/\n/g, ", ") || null;
|
|
181
|
+
return (
|
|
182
|
+
<Card
|
|
183
|
+
key={record.id ?? index}
|
|
184
|
+
className="overflow-hidden rounded-2xl border-0 shadow-md"
|
|
185
|
+
>
|
|
186
|
+
<Link to={`/property/${record.id}`} className="block cursor-pointer">
|
|
187
|
+
<div className="aspect-[4/3] overflow-hidden bg-muted">
|
|
188
|
+
{imageUrl ? (
|
|
189
|
+
<img
|
|
190
|
+
src={imageUrl}
|
|
191
|
+
alt=""
|
|
192
|
+
className="h-full w-full object-cover transition-transform hover:scale-105"
|
|
193
|
+
/>
|
|
194
|
+
) : (
|
|
195
|
+
<div className="flex h-full items-center justify-center text-muted-foreground">
|
|
196
|
+
No image
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
<CardContent className="p-4">
|
|
201
|
+
<h3 className="mb-1 font-semibold text-foreground">{name}</h3>
|
|
202
|
+
{displayAddress && (
|
|
203
|
+
<p className="mb-2 line-clamp-1 text-sm text-muted-foreground">
|
|
204
|
+
{displayAddress}
|
|
205
|
+
</p>
|
|
206
|
+
)}
|
|
207
|
+
<p className="mb-3 text-sm font-medium text-primary">{price}</p>
|
|
208
|
+
<Button
|
|
209
|
+
size="sm"
|
|
210
|
+
className="w-full cursor-pointer rounded-xl bg-primary transition-colors duration-200 hover:bg-primary/90"
|
|
211
|
+
>
|
|
212
|
+
View Property Details
|
|
213
|
+
</Button>
|
|
214
|
+
</CardContent>
|
|
215
|
+
</Link>
|
|
216
|
+
</Card>
|
|
217
|
+
);
|
|
218
|
+
})}
|
|
219
|
+
</div>
|
|
220
|
+
) : (
|
|
221
|
+
<div className="rounded-xl bg-white/60 px-6 py-10 text-center text-muted-foreground">
|
|
222
|
+
<p>No featured properties at the moment.</p>
|
|
223
|
+
<Button asChild className="mt-3">
|
|
224
|
+
<Link to="/properties">Browse all properties</Link>
|
|
225
|
+
</Button>
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
</section>
|
|
230
|
+
|
|
231
|
+
{/* ——— Stats / Trust ——— */}
|
|
232
|
+
<section className="grid min-h-[320px] grid-cols-1 overflow-hidden rounded-2xl bg-card shadow-md md:grid-cols-2">
|
|
233
|
+
<div className="relative min-h-[240px] md:min-h-0">
|
|
234
|
+
<img src={CITY_IMAGE} alt="" className="h-full w-full object-cover" />
|
|
235
|
+
<div className="absolute inset-0 bg-gradient-to-r from-black/20 to-transparent md:from-transparent" />
|
|
236
|
+
</div>
|
|
237
|
+
<div className="flex flex-col justify-center gap-6 bg-gradient-to-br from-amber-50 to-stone-100 px-6 py-10 md:px-10">
|
|
238
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
239
|
+
{[
|
|
240
|
+
{ value: "20K+", label: "Satisfied tenants and still counting" },
|
|
241
|
+
{ value: "10+", label: "Years of experience you can count on" },
|
|
242
|
+
{ value: "15+", label: "Award-winning service, trusted results" },
|
|
243
|
+
{ value: "5K+", label: "Happy tenants and clients" },
|
|
244
|
+
].map((stat, i) => (
|
|
245
|
+
<div key={i} className="rounded-xl bg-white/80 px-4 py-4 shadow-sm">
|
|
246
|
+
<p className="text-2xl font-bold text-teal-800">{stat.value}</p>
|
|
247
|
+
<p className="text-sm text-muted-foreground">{stat.label}</p>
|
|
248
|
+
</div>
|
|
249
|
+
))}
|
|
250
|
+
</div>
|
|
251
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
252
|
+
<Button
|
|
253
|
+
asChild
|
|
254
|
+
size="lg"
|
|
255
|
+
className="cursor-pointer rounded-xl bg-primary transition-colors duration-200 hover:bg-primary/90"
|
|
256
|
+
>
|
|
257
|
+
<Link to="/contact">More About Us</Link>
|
|
258
|
+
</Button>
|
|
259
|
+
<a
|
|
260
|
+
href="tel:18005550120"
|
|
261
|
+
className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-foreground transition-colors duration-200 hover:underline"
|
|
262
|
+
>
|
|
263
|
+
<Phone className="size-4" />
|
|
264
|
+
Talk to an Agent 1.800.555.0120
|
|
265
|
+
</a>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</section>
|
|
269
|
+
|
|
270
|
+
{/* ——— Testimonials ——— */}
|
|
271
|
+
<section className="overflow-hidden rounded-2xl bg-gradient-to-br from-violet-100/80 to-pink-100/80 px-4 py-12 md:px-6">
|
|
272
|
+
<div className="relative mx-auto grid max-w-6xl gap-8 lg:grid-cols-3">
|
|
273
|
+
<div className="lg:col-span-2">
|
|
274
|
+
<h2 className="mb-8 text-2xl font-bold tracking-tight text-violet-900 md:text-3xl">
|
|
275
|
+
What Our Clients Are Saying
|
|
276
|
+
</h2>
|
|
277
|
+
<div className="grid gap-6 sm:grid-cols-2">
|
|
278
|
+
<Card className="border-0 bg-white/80 shadow-md">
|
|
279
|
+
<CardContent className="p-6">
|
|
280
|
+
<h3 className="mb-2 font-semibold text-foreground">Game-changing convenience</h3>
|
|
281
|
+
<p className="mb-4 text-sm text-muted-foreground">
|
|
282
|
+
Applying online was so easy. I had a response within a day and moved in the
|
|
283
|
+
following week. Highly recommend.
|
|
284
|
+
</p>
|
|
285
|
+
<p className="text-sm font-medium text-foreground">— Des M.</p>
|
|
286
|
+
</CardContent>
|
|
287
|
+
</Card>
|
|
288
|
+
<Card className="border-0 bg-white/80 shadow-md">
|
|
289
|
+
<CardContent className="p-6">
|
|
290
|
+
<h3 className="mb-2 font-semibold text-foreground">
|
|
291
|
+
Highly personalized experience
|
|
292
|
+
</h3>
|
|
293
|
+
<p className="mb-4 text-sm text-muted-foreground">
|
|
294
|
+
The team was responsive and helped me find exactly what I was looking for. The
|
|
295
|
+
whole process felt smooth and professional.
|
|
296
|
+
</p>
|
|
297
|
+
<p className="text-sm font-medium text-foreground">— Zoe T.</p>
|
|
298
|
+
</CardContent>
|
|
299
|
+
</Card>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
<div className="relative hidden overflow-hidden rounded-2xl lg:block">
|
|
303
|
+
<img src={RELAX_IMAGE} alt="" className="h-full w-full object-cover" />
|
|
304
|
+
<div className="absolute inset-0 flex items-end bg-gradient-to-t from-black/60 to-transparent p-6">
|
|
305
|
+
<p className="text-lg font-semibold text-white">
|
|
306
|
+
Relax and Get Zen. Your Home Awaits.
|
|
307
|
+
</p>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
</section>
|
|
312
|
+
|
|
313
|
+
{/* ——— AI / Peace of Mind ——— */}
|
|
314
|
+
<section className="rounded-2xl bg-teal-50 px-4 py-12 md:px-6">
|
|
315
|
+
<div className="mx-auto max-w-3xl text-center">
|
|
316
|
+
<h2 className="mb-3 text-2xl font-bold tracking-tight text-teal-900 md:text-3xl">
|
|
317
|
+
AI-Powered, Peace of Mind
|
|
318
|
+
</h2>
|
|
319
|
+
<p className="mb-8 text-muted-foreground">
|
|
320
|
+
From inquiry to move-in: our AI assists with the hard part so you can focus on living.
|
|
321
|
+
</p>
|
|
322
|
+
<div className="flex flex-wrap justify-center gap-3">
|
|
323
|
+
<Button asChild variant="secondary" size="lg" className="rounded-full">
|
|
324
|
+
<Link to="/properties">Find My Home</Link>
|
|
325
|
+
</Button>
|
|
326
|
+
<Button asChild variant="outline" size="lg" className="rounded-full gap-2">
|
|
327
|
+
<Link to="/contact">
|
|
328
|
+
<MessageCircle className="size-4" />
|
|
329
|
+
Contact
|
|
330
|
+
</Link>
|
|
331
|
+
</Button>
|
|
332
|
+
<Button asChild variant="ghost" size="lg" className="rounded-full gap-2">
|
|
333
|
+
<Link to="/contact">
|
|
334
|
+
<HelpCircle className="size-4" />
|
|
335
|
+
Help
|
|
336
|
+
</Link>
|
|
337
|
+
</Button>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</section>
|
|
341
|
+
|
|
342
|
+
{/* ——— FAQ ——— */}
|
|
343
|
+
<section className="rounded-2xl bg-teal-50/80 px-4 py-12 md:px-6">
|
|
344
|
+
<div className="mx-auto max-w-3xl">
|
|
345
|
+
<h2 className="mb-8 text-2xl font-bold tracking-tight text-teal-900 md:text-3xl">
|
|
346
|
+
Frequently Asked Questions
|
|
347
|
+
</h2>
|
|
348
|
+
<div className="space-y-2">
|
|
349
|
+
{FAQ_ITEMS.map((item, index) => (
|
|
350
|
+
<div
|
|
351
|
+
key={index}
|
|
352
|
+
className="overflow-hidden rounded-xl border border-teal-100 bg-white shadow-sm"
|
|
353
|
+
>
|
|
354
|
+
<button
|
|
355
|
+
type="button"
|
|
356
|
+
onClick={() => setFaqOpen(faqOpen === index ? null : index)}
|
|
357
|
+
className="flex w-full cursor-pointer items-center justify-between gap-4 px-5 py-4 text-left font-medium text-foreground transition-colors duration-200 hover:bg-muted/50"
|
|
358
|
+
>
|
|
359
|
+
<span>{item.q}</span>
|
|
360
|
+
{faqOpen === index ? (
|
|
361
|
+
<ChevronUp className="size-5 shrink-0 text-muted-foreground" />
|
|
362
|
+
) : (
|
|
363
|
+
<ChevronDown className="size-5 shrink-0 text-muted-foreground" />
|
|
364
|
+
)}
|
|
365
|
+
</button>
|
|
366
|
+
{faqOpen === index && (
|
|
367
|
+
<div className="border-t border-teal-100 px-5 py-4 text-sm text-muted-foreground">
|
|
368
|
+
{item.a}
|
|
369
|
+
</div>
|
|
370
|
+
)}
|
|
371
|
+
</div>
|
|
372
|
+
))}
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</section>
|
|
376
|
+
|
|
377
|
+
{/* ——— Footer ——— */}
|
|
378
|
+
<footer className="overflow-hidden rounded-2xl bg-teal-800 text-teal-100">
|
|
379
|
+
<div className="grid gap-8 px-6 py-10 md:grid-cols-2 lg:grid-cols-4">
|
|
380
|
+
<div className="lg:col-span-2">
|
|
381
|
+
<p className="mb-4 text-xl font-semibold tracking-wide text-white">ZENLEASE</p>
|
|
382
|
+
<p className="mb-4 text-sm text-teal-200">Stay updated with new listings and tips.</p>
|
|
383
|
+
<form onSubmit={handleFooterSubmit} className="flex flex-col gap-2">
|
|
384
|
+
<div className="flex gap-2">
|
|
385
|
+
<Input
|
|
386
|
+
type="email"
|
|
387
|
+
placeholder="Enter your email"
|
|
388
|
+
value={footerEmail}
|
|
389
|
+
onChange={(e) => setFooterEmail(e.target.value)}
|
|
390
|
+
disabled={newsletterSubmitting}
|
|
391
|
+
className="max-w-xs border-teal-600 bg-teal-900/50 text-white placeholder:text-teal-300"
|
|
392
|
+
aria-label="Email for updates"
|
|
393
|
+
/>
|
|
394
|
+
<Button
|
|
395
|
+
type="submit"
|
|
396
|
+
size="icon"
|
|
397
|
+
className="shrink-0 bg-teal-600 hover:bg-teal-700"
|
|
398
|
+
disabled={newsletterSubmitting}
|
|
399
|
+
>
|
|
400
|
+
<Send className="size-4" aria-hidden />
|
|
401
|
+
</Button>
|
|
402
|
+
</div>
|
|
403
|
+
{newsletterMessage && (
|
|
404
|
+
<p
|
|
405
|
+
className={
|
|
406
|
+
newsletterMessage.type === "success" ? "text-teal-200" : "text-red-300"
|
|
407
|
+
}
|
|
408
|
+
>
|
|
409
|
+
{newsletterMessage.text}
|
|
410
|
+
</p>
|
|
411
|
+
)}
|
|
412
|
+
</form>
|
|
413
|
+
</div>
|
|
414
|
+
<div>
|
|
415
|
+
<h4 className="mb-3 text-sm font-semibold uppercase tracking-wider text-white">
|
|
416
|
+
Explore
|
|
417
|
+
</h4>
|
|
418
|
+
<ul className="space-y-2 text-sm">
|
|
419
|
+
<li>
|
|
420
|
+
<Link to="/" className="text-teal-200 hover:text-white">
|
|
421
|
+
Home
|
|
422
|
+
</Link>
|
|
423
|
+
</li>
|
|
424
|
+
<li>
|
|
425
|
+
<Link to="/properties" className="text-teal-200 hover:text-white">
|
|
426
|
+
Property Search
|
|
427
|
+
</Link>
|
|
428
|
+
</li>
|
|
429
|
+
<li>
|
|
430
|
+
<Link to="/application" className="text-teal-200 hover:text-white">
|
|
431
|
+
Apply
|
|
432
|
+
</Link>
|
|
433
|
+
</li>
|
|
434
|
+
</ul>
|
|
435
|
+
</div>
|
|
436
|
+
<div>
|
|
437
|
+
<h4 className="mb-3 text-sm font-semibold uppercase tracking-wider text-white">
|
|
438
|
+
Support
|
|
439
|
+
</h4>
|
|
440
|
+
<ul className="space-y-2 text-sm">
|
|
441
|
+
<li>
|
|
442
|
+
<Link to="/contact" className="text-teal-200 hover:text-white">
|
|
443
|
+
Contact Us
|
|
444
|
+
</Link>
|
|
445
|
+
</li>
|
|
446
|
+
<li>
|
|
447
|
+
<Link to="/maintenance" className="text-teal-200 hover:text-white">
|
|
448
|
+
Maintenance
|
|
449
|
+
</Link>
|
|
450
|
+
</li>
|
|
451
|
+
<li>
|
|
452
|
+
<a href="tel:18005550120" className="text-teal-200 hover:text-white">
|
|
453
|
+
Talk to an agent
|
|
454
|
+
</a>
|
|
455
|
+
</li>
|
|
456
|
+
</ul>
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
<div className="flex flex-col items-center justify-between gap-4 border-t border-teal-700 px-6 py-4 sm:flex-row">
|
|
460
|
+
<p className="text-xs text-teal-300">
|
|
461
|
+
© {new Date().getFullYear()} ZENLEASE. All rights reserved. Terms & Conditions
|
|
462
|
+
</p>
|
|
463
|
+
<div className="flex gap-4">
|
|
464
|
+
<a href="#" className="text-teal-300 hover:text-white" aria-label="Facebook">
|
|
465
|
+
f
|
|
466
|
+
</a>
|
|
467
|
+
<a href="#" className="text-teal-300 hover:text-white" aria-label="Instagram">
|
|
468
|
+
ig
|
|
469
|
+
</a>
|
|
470
|
+
<a href="#" className="text-teal-300 hover:text-white" aria-label="Twitter">
|
|
471
|
+
X
|
|
472
|
+
</a>
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
</footer>
|
|
28
476
|
</div>
|
|
29
477
|
);
|
|
30
478
|
}
|