@salesforce/webapp-template-app-react-sample-b2x-experimental 1.84.0 → 1.85.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 (57) hide show
  1. package/dist/.a4drules/skills/{webapp-react-add-component → webapp-react}/SKILL.md +5 -3
  2. package/dist/.a4drules/skills/{webapp-react-add-component → webapp-react}/implementation/header-footer.md +8 -0
  3. package/dist/.a4drules/skills/{webapp-react-add-component → webapp-react}/implementation/page.md +8 -7
  4. package/dist/.a4drules/skills/webapp-ui-ux/SKILL.md +11 -8
  5. package/dist/.a4drules/webapp-react.md +54 -0
  6. package/dist/CHANGELOG.md +16 -0
  7. package/dist/README.md +24 -0
  8. package/dist/force-app/main/default/data/Property_Image__c.json +1 -1
  9. package/dist/force-app/main/default/data/Property_Listing__c.json +1 -1
  10. package/dist/force-app/main/default/data/prepare-import-unique-fields.js +85 -0
  11. package/dist/force-app/main/default/permissionsets/Property_Management_Access.permissionset-meta.xml +0 -7
  12. package/dist/force-app/main/default/webapplications/appreactsampleb2x/index.html +6 -0
  13. package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +3 -3
  14. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/applicationApi.ts +9 -9
  15. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphql-operations-types.ts +296 -0
  16. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +12 -7
  17. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/maintenanceRequestApi.ts +50 -38
  18. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyDetailGraphQL.ts +50 -102
  19. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyListingGraphQL.ts +211 -43
  20. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/userApi.ts +43 -0
  21. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +9 -208
  22. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/appliances.svg +13 -0
  23. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/electrical.svg +39 -0
  24. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/hvac.svg +78 -0
  25. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/pest.svg +5 -0
  26. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/plumbing.svg +7 -0
  27. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/zen-logo.svg +5 -0
  28. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/MaintenanceRequestIcon.tsx +46 -0
  29. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/NavMenu.tsx +53 -0
  30. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyListingCard.tsx +55 -58
  31. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyMap.tsx +93 -11
  32. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertySearchFilters.tsx +315 -0
  33. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/StatusBadge.tsx +36 -0
  34. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/TopBar.tsx +107 -0
  35. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +2 -2
  36. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingAmenities.ts +55 -0
  37. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingPriceRange.ts +64 -0
  38. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +14 -5
  39. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +54 -11
  40. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +1 -1
  41. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +42 -39
  42. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Contact.tsx +10 -10
  43. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +64 -91
  44. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/HelpCenter.tsx +1 -1
  45. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +19 -9
  46. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +79 -100
  47. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/NotFound.tsx +1 -1
  48. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +62 -47
  49. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +3 -3
  50. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +230 -34
  51. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +10 -1
  52. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/styles/global.css +64 -0
  53. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/geocode.ts +30 -5
  54. package/dist/package.json +1 -1
  55. package/dist/setup-cli.mjs +271 -0
  56. package/package.json +1 -1
  57. /package/dist/.a4drules/skills/{webapp-react-add-component → webapp-react}/implementation/component.md +0 -0
@@ -0,0 +1,36 @@
1
+ import { Check } from "lucide-react";
2
+
3
+ interface StatusBadgeProps {
4
+ status: string;
5
+ }
6
+
7
+ export function StatusBadge({ status }: StatusBadgeProps) {
8
+ const statusLower = status.toLowerCase();
9
+
10
+ const getStyle = () => {
11
+ if (statusLower === "new") return "bg-pink-100 text-pink-700";
12
+ if (statusLower === "in progress") return "bg-yellow-100 text-yellow-700";
13
+ if (statusLower === "resolved") return "bg-green-100 text-green-700";
14
+ return "bg-gray-100 text-gray-700";
15
+ };
16
+
17
+ const getLabel = () => {
18
+ if (statusLower === "new") return "Needs Action";
19
+ if (statusLower === "in progress") return "In Progress";
20
+ if (statusLower === "resolved") return "Resolved";
21
+ return status;
22
+ };
23
+
24
+ const showCheckmark = statusLower === "resolved";
25
+ const showDot = statusLower === "new" || statusLower === "in progress";
26
+
27
+ return (
28
+ <span
29
+ className={`inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-medium ${getStyle()}`}
30
+ >
31
+ {showCheckmark && <Check className="size-4" aria-hidden />}
32
+ {showDot && <span className="size-2 rounded-full bg-current" aria-hidden />}
33
+ {getLabel()}
34
+ </span>
35
+ );
36
+ }
@@ -0,0 +1,107 @@
1
+ import { useEffect, useState } from "react";
2
+ import { Search, Bell, ChevronDown, Menu } from "lucide-react";
3
+ import { getUserInfo } from "@/api/userApi";
4
+ import zenLogo from "@/assets/icons/zen-logo.svg";
5
+
6
+ export interface TopBarProps {
7
+ onMenuClick?: () => void;
8
+ }
9
+
10
+ export function TopBar({ onMenuClick }: TopBarProps) {
11
+ const [userName, setUserName] = useState<string>("User");
12
+ const [showNotifications, setShowNotifications] = useState(false);
13
+
14
+ useEffect(() => {
15
+ const loadUserInfo = async () => {
16
+ const userInfo = await getUserInfo();
17
+ if (userInfo) {
18
+ setUserName(userInfo.name);
19
+ }
20
+ };
21
+ loadUserInfo();
22
+ }, []);
23
+
24
+ const handleNotificationClick = () => {
25
+ setShowNotifications(!showNotifications);
26
+ };
27
+
28
+ const handleCloseNotifications = () => {
29
+ setShowNotifications(false);
30
+ };
31
+
32
+ return (
33
+ <header
34
+ className="flex h-16 items-center justify-between bg-teal-700 px-6 text-white"
35
+ role="banner"
36
+ >
37
+ <div className="flex items-center gap-4">
38
+ <button
39
+ type="button"
40
+ onClick={onMenuClick}
41
+ className="rounded-md p-2 transition-colors hover:bg-teal-600 md:hidden"
42
+ aria-label="Toggle menu"
43
+ >
44
+ <Menu className="size-6" aria-hidden />
45
+ </button>
46
+ <div className="flex items-center gap-2">
47
+ <img src={zenLogo} alt="Zenlease Logo" className="size-8" />
48
+ <span className="text-xl tracking-wide">
49
+ <span className="font-light">ZEN</span>
50
+ <span className="font-semibold">LEASE</span>
51
+ </span>
52
+ </div>
53
+ </div>
54
+
55
+ <div className="flex items-center gap-4">
56
+ <button
57
+ type="button"
58
+ className="rounded-md p-2 transition-colors hover:bg-teal-600 md:hidden"
59
+ aria-label="Search"
60
+ >
61
+ <Search className="size-5" aria-hidden />
62
+ </button>
63
+
64
+ <div className="relative">
65
+ <button
66
+ type="button"
67
+ onClick={handleNotificationClick}
68
+ className="relative rounded-md p-2 transition-colors hover:bg-teal-600"
69
+ aria-label="Notifications"
70
+ >
71
+ <Bell className="size-5" aria-hidden />
72
+ </button>
73
+
74
+ {showNotifications && (
75
+ <>
76
+ <div className="fixed inset-0 z-40" onClick={handleCloseNotifications} aria-hidden />
77
+ <div className="absolute right-0 top-full z-50 mt-2 w-80 overflow-hidden rounded-lg bg-white shadow-xl">
78
+ <div className="border-b border-gray-200 p-4">
79
+ <h3 className="text-sm font-semibold text-gray-900">Notifications</h3>
80
+ </div>
81
+ <div className="p-8 text-center">
82
+ <Bell className="mx-auto mb-3 size-12 text-gray-300" aria-hidden />
83
+ <p className="text-sm text-gray-500">No new notifications</p>
84
+ </div>
85
+ </div>
86
+ </>
87
+ )}
88
+ </div>
89
+
90
+ <button
91
+ type="button"
92
+ className="flex items-center gap-2 rounded-md px-3 py-2 transition-colors hover:bg-teal-600"
93
+ aria-label="User menu"
94
+ >
95
+ <div
96
+ className="flex size-8 items-center justify-center rounded-full bg-teal-300 font-semibold text-teal-900"
97
+ aria-hidden
98
+ >
99
+ {userName.charAt(0).toUpperCase()}
100
+ </div>
101
+ <span className="hidden font-medium md:inline">{userName.toUpperCase()}</span>
102
+ <ChevronDown className="hidden size-4 md:inline" aria-hidden />
103
+ </button>
104
+ </div>
105
+ </header>
106
+ );
107
+ }
@@ -4,8 +4,8 @@
4
4
  */
5
5
  import { useState, useEffect } from "react";
6
6
  import { fetchPropertyAddresses } from "@/api/propertyDetailGraphQL";
7
- import { getPropertyIdFromRecord } from "./usePropertyPrimaryImages";
8
- import type { SearchResultRecord } from "../features/global-search/types/search/searchResults";
7
+ import { getPropertyIdFromRecord } from "@/hooks/usePropertyPrimaryImages";
8
+ import type { SearchResultRecord } from "@/features/global-search/types/search/searchResults.js";
9
9
 
10
10
  export function usePropertyAddresses(results: SearchResultRecord[]): Record<string, string> {
11
11
  const [map, setMap] = useState<Record<string, string>>({});
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Fetches amenities (feature descriptions) for each property in search results.
3
+ * Returns a map of propertyId -> "Amenity 1 | Amenity 2 | ..." for use in listing cards.
4
+ */
5
+ import { useState, useEffect } from "react";
6
+ import { fetchFeaturesByPropertyId } from "@/api/propertyDetailGraphQL";
7
+ import { getPropertyIdFromRecord } from "@/hooks/usePropertyPrimaryImages";
8
+ import type { SearchResultRecord } from "@/features/global-search/types/search/searchResults.js";
9
+
10
+ const AMENITIES_SEPARATOR = " | ";
11
+
12
+ export function usePropertyListingAmenities(
13
+ results: SearchResultRecord[],
14
+ ): Record<string, string> & { loading: boolean } {
15
+ const [map, setMap] = useState<Record<string, string>>({});
16
+ const [loading, setLoading] = useState(false);
17
+
18
+ const propertyIds = results
19
+ .map((r) => r?.record && getPropertyIdFromRecord(r.record))
20
+ .filter((id): id is string => Boolean(id));
21
+ const uniqueIds = [...new Set(propertyIds)];
22
+
23
+ useEffect(() => {
24
+ if (uniqueIds.length === 0) {
25
+ setMap({});
26
+ return;
27
+ }
28
+ let cancelled = false;
29
+ setLoading(true);
30
+ Promise.all(uniqueIds.map((id) => fetchFeaturesByPropertyId(id)))
31
+ .then((featuresPerProperty) => {
32
+ if (cancelled) return;
33
+ const next: Record<string, string> = {};
34
+ uniqueIds.forEach((id, i) => {
35
+ const features = featuresPerProperty[i] ?? [];
36
+ const descriptions = features
37
+ .map((f) => f.description)
38
+ .filter((d): d is string => d != null && d.trim() !== "");
39
+ next[id] = descriptions.join(AMENITIES_SEPARATOR);
40
+ });
41
+ setMap(next);
42
+ })
43
+ .catch(() => {
44
+ if (!cancelled) setMap({});
45
+ })
46
+ .finally(() => {
47
+ if (!cancelled) setLoading(false);
48
+ });
49
+ return () => {
50
+ cancelled = true;
51
+ };
52
+ }, [uniqueIds.join(",")]);
53
+
54
+ return Object.assign(map, { loading });
55
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Fetches the available min/max listing price for the current search (no price/bedroom filters).
3
+ * Use to render the filter bar only after knowing the available price range.
4
+ */
5
+ import { useState, useEffect, useRef } from "react";
6
+ import { queryPropertyListingPriceRange } from "@/api/propertyListingGraphQL";
7
+
8
+ /** Cap for slider max when dataset has outliers; UI never sees a higher max. */
9
+ const SLIDER_PRICE_CAP = 50_000;
10
+
11
+ export interface PropertyListingPriceRange {
12
+ priceMin: number;
13
+ priceMax: number;
14
+ /** True when raw max was > cap and we capped for the slider (show "50,000+"). */
15
+ maxCapped?: boolean;
16
+ }
17
+
18
+ /** Fallback when the price-range API call fails. */
19
+ const DEFAULT_PRICE_RANGE: PropertyListingPriceRange = { priceMin: 0, priceMax: 100_000 };
20
+
21
+ function capPriceRange(range: { priceMin: number; priceMax: number }): PropertyListingPriceRange {
22
+ if (range.priceMax <= SLIDER_PRICE_CAP)
23
+ return { priceMin: range.priceMin, priceMax: range.priceMax };
24
+ return {
25
+ priceMin: range.priceMin,
26
+ priceMax: SLIDER_PRICE_CAP,
27
+ maxCapped: true,
28
+ };
29
+ }
30
+
31
+ export function usePropertyListingPriceRange(searchQuery: string): {
32
+ priceRange: PropertyListingPriceRange | null;
33
+ loading: boolean;
34
+ error: string | null;
35
+ } {
36
+ const [priceRange, setPriceRange] = useState<PropertyListingPriceRange | null>(null);
37
+ const [loading, setLoading] = useState(true);
38
+ const [error, setError] = useState<string | null>(null);
39
+ const cancelledRef = useRef(false);
40
+
41
+ useEffect(() => {
42
+ cancelledRef.current = false;
43
+ setLoading(true);
44
+ setError(null);
45
+ queryPropertyListingPriceRange(searchQuery)
46
+ .then((range) => {
47
+ if (!cancelledRef.current) setPriceRange(range ? capPriceRange(range) : null);
48
+ })
49
+ .catch((err) => {
50
+ if (!cancelledRef.current) {
51
+ setError(err instanceof Error ? err.message : "Failed to load price range");
52
+ setPriceRange(capPriceRange(DEFAULT_PRICE_RANGE));
53
+ }
54
+ })
55
+ .finally(() => {
56
+ if (!cancelledRef.current) setLoading(false);
57
+ });
58
+ return () => {
59
+ cancelledRef.current = true;
60
+ };
61
+ }, [searchQuery]);
62
+
63
+ return { priceRange, loading, error };
64
+ }
@@ -1,11 +1,19 @@
1
1
  /**
2
- * Property Listing search via GraphQL. Shows all when no search term.
2
+ * Property Listing search via GraphQL. Optional text, price range, and bedrooms filters.
3
3
  */
4
4
  import { useState, useEffect, useRef, useCallback } from "react";
5
- import { queryPropertyListingsGraphQL } from "@/api/propertyListingGraphQL";
6
- import type { SearchResultRecord } from "../features/global-search/types/search/searchResults";
5
+ import {
6
+ queryPropertyListingsGraphQL,
7
+ type PropertyListingFilters,
8
+ } from "@/api/propertyListingGraphQL";
9
+ import type { SearchResultRecord } from "@/features/global-search/types/search/searchResults.js";
7
10
 
8
- export function usePropertyListingSearch(searchQuery: string, pageSize: number, pageToken: string) {
11
+ export function usePropertyListingSearch(
12
+ searchQuery: string,
13
+ pageSize: number,
14
+ pageToken: string,
15
+ filters?: PropertyListingFilters,
16
+ ) {
9
17
  const [results, setResults] = useState<SearchResultRecord[]>([]);
10
18
  const [nextPageToken, setNextPageToken] = useState<string | null>(null);
11
19
  const [previousPageToken, setPreviousPageToken] = useState<string | null>(null);
@@ -27,6 +35,7 @@ export function usePropertyListingSearch(searchQuery: string, pageSize: number,
27
35
  pageSize,
28
36
  afterCursor,
29
37
  ac.signal,
38
+ filters,
30
39
  );
31
40
  if (ac.signal.aborted) return;
32
41
  setResults(result.records);
@@ -41,7 +50,7 @@ export function usePropertyListingSearch(searchQuery: string, pageSize: number,
41
50
  } finally {
42
51
  if (!ac.signal.aborted) setLoading(false);
43
52
  }
44
- }, [searchQuery, pageSize, pageToken]);
53
+ }, [searchQuery, pageSize, pageToken, filters]);
45
54
 
46
55
  useEffect(() => {
47
56
  if (debounceRef.current) {
@@ -4,9 +4,9 @@
4
4
  */
5
5
  import { useState, useEffect } from "react";
6
6
  import { fetchPropertyAddresses } from "@/api/propertyDetailGraphQL";
7
- import { geocodeAddress } from "@/utils/geocode";
8
- import { getPropertyIdFromRecord } from "./usePropertyPrimaryImages";
9
- import type { SearchResultRecord } from "../features/global-search/types/search/searchResults";
7
+ import { geocodeAddress, getStateZipFromAddress } from "@/utils/geocode";
8
+ import { getPropertyIdFromRecord } from "@/hooks/usePropertyPrimaryImages";
9
+ import type { SearchResultRecord } from "@/features/global-search/types/search/searchResults.js";
10
10
  import type { MapMarker } from "@/components/PropertyMap";
11
11
 
12
12
  function getListingName(record: {
@@ -19,6 +19,42 @@ function getListingName(record: {
19
19
  return "Property";
20
20
  }
21
21
 
22
+ /** Round to 5 decimals (~1 m) so near-duplicate coords group together */
23
+ function key(lat: number, lng: number): string {
24
+ return `${lat.toFixed(5)},${lng.toFixed(5)}`;
25
+ }
26
+
27
+ /**
28
+ * When multiple markers share the same lat/lng, offset them in a small circle so they appear
29
+ * close together but are all visible (no stacking).
30
+ */
31
+ function spreadDuplicateMarkers(markers: MapMarker[]): MapMarker[] {
32
+ const groups = new Map<string, MapMarker[]>();
33
+ for (const m of markers) {
34
+ const k = key(m.lat, m.lng);
35
+ if (!groups.has(k)) groups.set(k, []);
36
+ groups.get(k)!.push(m);
37
+ }
38
+ const result: MapMarker[] = [];
39
+ const radiusDeg = 0.0004; // ~40–50 m so pins sit close but visible
40
+ for (const group of groups.values()) {
41
+ if (group.length === 1) {
42
+ result.push(group[0]);
43
+ continue;
44
+ }
45
+ for (let i = 0; i < group.length; i++) {
46
+ const m = group[i];
47
+ const angle = (i / group.length) * 2 * Math.PI;
48
+ result.push({
49
+ ...m,
50
+ lat: m.lat + radiusDeg * Math.cos(angle),
51
+ lng: m.lng + radiusDeg * Math.sin(angle),
52
+ });
53
+ }
54
+ }
55
+ return result;
56
+ }
57
+
22
58
  export function usePropertyMapMarkers(results: SearchResultRecord[]): {
23
59
  markers: MapMarker[];
24
60
  loading: boolean;
@@ -59,24 +95,31 @@ export function usePropertyMapMarkers(results: SearchResultRecord[]): {
59
95
  setLoading(false);
60
96
  return;
61
97
  }
62
- // Geocode all addresses in parallel (only current window of results)
98
+ // Geocode all addresses in parallel; fallback to City, State Zip if full address fails
63
99
  Promise.all(
64
- toGeocode.map(([id, address]) =>
65
- geocodeAddress(address.replace(/\n/g, ", ")).then((coords) =>
66
- coords ? { id, coords } : null,
67
- ),
68
- ),
100
+ toGeocode.map(async ([id, address]) => {
101
+ const normalized = address.replace(/\n/g, ", ").trim();
102
+ let coords = await geocodeAddress(normalized);
103
+ if (!coords) {
104
+ const stateZip = getStateZipFromAddress(normalized);
105
+ if (stateZip !== normalized) {
106
+ coords = await geocodeAddress(stateZip);
107
+ }
108
+ }
109
+ return coords ? { id, coords } : null;
110
+ }),
69
111
  )
70
112
  .then((resolved) => {
71
113
  if (cancelled) return;
72
- const nextMarkers: MapMarker[] = resolved
114
+ const raw: MapMarker[] = resolved
73
115
  .filter((r): r is { id: string; coords: { lat: number; lng: number } } => r != null)
74
116
  .map(({ id, coords }) => ({
75
117
  lat: coords.lat,
76
118
  lng: coords.lng,
77
119
  label: propertyIdToLabel.get(id) ?? "Property",
120
+ propertyId: id,
78
121
  }));
79
- setMarkers(nextMarkers);
122
+ setMarkers(spreadDuplicateMarkers(raw));
80
123
  })
81
124
  .catch(() => {
82
125
  if (!cancelled) setMarkers([]);
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { useState, useEffect } from "react";
6
6
  import { fetchPrimaryImagesByPropertyIds } from "@/api/propertyDetailGraphQL";
7
- import type { SearchResultRecord } from "../features/global-search/types/search/searchResults";
7
+ import type { SearchResultRecord } from "@/features/global-search/types/search/searchResults.js";
8
8
 
9
9
  export function getPropertyIdFromRecord(record: {
10
10
  fields?: Record<string, { value?: unknown }>;
@@ -1,9 +1,9 @@
1
1
  import { useSearchParams, Link } from "react-router";
2
- import { useCallback, useEffect, useState } from "react";
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 } from "../components/ui/card";
2
+ import { useCallback, useEffect, useState, type ChangeEvent } from "react";
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 } from "@/components/ui/card";
7
7
  import {
8
8
  fetchListingById,
9
9
  fetchPropertyById,
@@ -94,7 +94,7 @@ export default function Application() {
94
94
  Phone__c: phone.trim() || null,
95
95
  Start_Date__c: moveInDate.trim() || null,
96
96
  Preferred_Term__c: preferredTerm.trim() || null,
97
- Employment_Info__c: employmentInfo.trim() || null,
97
+ Employment__c: employmentInfo.trim() || null,
98
98
  References__c: references.trim() || null,
99
99
  });
100
100
  setSubmittedId(id.id);
@@ -119,26 +119,30 @@ export default function Application() {
119
119
 
120
120
  if (loading) {
121
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" />
122
+ <div className="mx-auto max-w-[900px]">
123
+ <div className="mb-6 h-48 animate-pulse rounded-2xl bg-muted" />
124
+ <div className="h-64 animate-pulse rounded-2xl bg-muted" />
125
125
  </div>
126
126
  );
127
127
  }
128
128
 
129
129
  if (submittedId) {
130
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>
131
+ <div className="mx-auto max-w-[900px]">
132
+ <Card className="mb-6 rounded-2xl border border-border p-6 shadow-sm">
133
+ <h2 className="mb-2 text-2xl font-semibold text-foreground">Application submitted</h2>
134
134
  <p className="text-sm text-muted-foreground">
135
135
  Your application has been saved. Reference: {submittedId}
136
136
  </p>
137
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">
138
+ <Link to="/properties" className="text-sm text-primary no-underline hover:underline">
139
+ Back to search
140
+ </Link>
141
+ <Button
142
+ asChild
143
+ size="sm"
144
+ className="rounded-xl bg-primary px-5 py-5 text-lg font-medium transition-colors duration-200 hover:bg-primary/90"
145
+ >
142
146
  <Link to="/application">Submit another</Link>
143
147
  </Button>
144
148
  </div>
@@ -148,8 +152,16 @@ export default function Application() {
148
152
  }
149
153
 
150
154
  return (
151
- <div className="mx-auto max-w-[800px]">
152
- <Card className="mb-6 flex gap-4 p-6">
155
+ <div className="mx-auto max-w-[900px]">
156
+ <div className="mb-4">
157
+ <Link
158
+ to={listingId ? `/property/${listingId}` : "/properties"}
159
+ className="text-sm text-primary no-underline hover:underline"
160
+ >
161
+ {listingId ? "← Back to listing" : "← Back to search"}
162
+ </Link>
163
+ </div>
164
+ <Card className="mb-6 flex gap-4 rounded-2xl border border-border p-6 shadow-sm">
153
165
  <div className="relative size-[200px] shrink-0 overflow-hidden rounded-xl bg-muted">
154
166
  {propertyImageUrl ? (
155
167
  <img src={propertyImageUrl} alt="" className="h-full w-full object-cover" />
@@ -158,7 +170,7 @@ export default function Application() {
158
170
  )}
159
171
  </div>
160
172
  <div className="min-w-0 flex-1">
161
- <h2 className="mb-1 text-lg font-semibold text-primary">
173
+ <h2 className="mb-1.5 text-2xl font-semibold text-foreground">
162
174
  {listingName ?? "Apply for a property"}
163
175
  </h2>
164
176
  <p className="text-sm text-muted-foreground">
@@ -169,19 +181,12 @@ export default function Application() {
169
181
  </p>
170
182
  {loadError && <p className="mt-2 text-sm text-destructive">{loadError}</p>}
171
183
  </div>
172
- <div className="flex gap-2">
173
- <Button asChild variant="outline" size="sm">
174
- <Link to={listingId ? `/property/${listingId}` : "/properties"}>Back to listing</Link>
175
- </Button>
176
- </div>
177
184
  </Card>
178
185
 
179
- <Card className="mb-6">
180
- <CardContent className="pt-6">
186
+ <Card className="mb-6 rounded-2xl border border-border shadow-sm">
187
+ <CardContent className="pt-3">
181
188
  <form onSubmit={handleSubmit}>
182
- <h3 className="mb-4 text-xs font-semibold uppercase tracking-wider text-foreground">
183
- YOUR INFO
184
- </h3>
189
+ <h3 className="mb-4 text-base font-semibold text-foreground">Your info</h3>
185
190
  <div className="mb-4 grid grid-cols-1 gap-4 md:grid-cols-2">
186
191
  <div className="space-y-2">
187
192
  <Label htmlFor="app-first-name">First Name *</Label>
@@ -189,7 +194,7 @@ export default function Application() {
189
194
  id="app-first-name"
190
195
  type="text"
191
196
  value={firstName}
192
- onChange={(e) => setFirstName(e.target.value)}
197
+ onChange={(e: ChangeEvent<HTMLInputElement>) => setFirstName(e.target.value)}
193
198
  />
194
199
  </div>
195
200
  <div className="space-y-2">
@@ -198,7 +203,7 @@ export default function Application() {
198
203
  id="app-last-name"
199
204
  type="text"
200
205
  value={lastName}
201
- onChange={(e) => setLastName(e.target.value)}
206
+ onChange={(e: ChangeEvent<HTMLInputElement>) => setLastName(e.target.value)}
202
207
  />
203
208
  </div>
204
209
  </div>
@@ -208,7 +213,7 @@ export default function Application() {
208
213
  id="app-email"
209
214
  type="email"
210
215
  value={email}
211
- onChange={(e) => setEmail(e.target.value)}
216
+ onChange={(e: ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)}
212
217
  />
213
218
  </div>
214
219
  <div className="mb-4 space-y-2">
@@ -217,12 +222,10 @@ export default function Application() {
217
222
  id="app-phone"
218
223
  type="tel"
219
224
  value={phone}
220
- onChange={(e) => setPhone(e.target.value)}
225
+ onChange={(e: ChangeEvent<HTMLInputElement>) => setPhone(e.target.value)}
221
226
  />
222
227
  </div>
223
- <h3 className="mb-4 mt-6 text-xs font-semibold uppercase tracking-wider text-foreground">
224
- MOVE IN
225
- </h3>
228
+ <h3 className="mb-4 mt-6 text-base font-semibold text-foreground">Move in</h3>
226
229
  <div className="mb-4 grid grid-cols-1 gap-4 md:grid-cols-2">
227
230
  <div className="space-y-2">
228
231
  <Label htmlFor="app-move-in">Move in date</Label>
@@ -230,7 +233,7 @@ export default function Application() {
230
233
  id="app-move-in"
231
234
  type="date"
232
235
  value={moveInDate}
233
- onChange={(e) => setMoveInDate(e.target.value)}
236
+ onChange={(e: ChangeEvent<HTMLInputElement>) => setMoveInDate(e.target.value)}
234
237
  />
235
238
  </div>
236
239
  <div className="space-y-2">
@@ -239,7 +242,7 @@ export default function Application() {
239
242
  id="app-term"
240
243
  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
244
  value={preferredTerm}
242
- onChange={(e) => setPreferredTerm(e.target.value)}
245
+ onChange={(e: ChangeEvent<HTMLSelectElement>) => setPreferredTerm(e.target.value)}
243
246
  >
244
247
  {PREFERRED_TERM_OPTIONS.map((opt) => (
245
248
  <option key={opt || "empty"} value={opt}>
@@ -274,7 +277,7 @@ export default function Application() {
274
277
  <Button
275
278
  type="submit"
276
279
  size="sm"
277
- className="bg-teal-600 hover:bg-teal-700"
280
+ className="w-full cursor-pointer rounded-xl bg-primary px-5 py-5 text-lg font-medium transition-colors duration-200 hover:bg-primary/90 disabled:opacity-50"
278
281
  disabled={submitting}
279
282
  >
280
283
  {submitting ? "Submitting…" : "Submit application"}
@@ -1,15 +1,15 @@
1
1
  import { useState, useCallback, useEffect, type SubmitEvent } from "react";
2
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 { CenteredPageLayout } from "../features/authentication/layout/centered-page-layout";
8
- import { Skeleton } from "../components/ui/skeleton";
9
- import { createContactUsLead } from "../api/leadApi";
10
- import { useAuth } from "../features/authentication/context/AuthContext";
11
- import { fetchUserProfile } from "../features/authentication/api/userProfileApi";
12
- import type { UserInfo } from "../api/leadApi";
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 { CenteredPageLayout } from "@/features/authentication/layout/centered-page-layout";
8
+ import { Skeleton } from "@/components/ui/skeleton";
9
+ import { createContactUsLead } from "@/api/leadApi";
10
+ import { useAuth } from "@/features/authentication/context/AuthContext";
11
+ import { fetchUserProfile } from "@/features/authentication/api/userProfileApi";
12
+ import type { UserInfo } from "@/api/leadApi";
13
13
 
14
14
  function LoadingCard() {
15
15
  return (