@salesforce/webapp-template-app-react-sample-b2x-experimental 1.84.1 → 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 (51) hide show
  1. package/dist/CHANGELOG.md +8 -0
  2. package/dist/README.md +24 -0
  3. package/dist/force-app/main/default/data/Property_Image__c.json +1 -1
  4. package/dist/force-app/main/default/data/Property_Listing__c.json +1 -1
  5. package/dist/force-app/main/default/data/prepare-import-unique-fields.js +85 -0
  6. package/dist/force-app/main/default/permissionsets/Property_Management_Access.permissionset-meta.xml +0 -7
  7. package/dist/force-app/main/default/webapplications/appreactsampleb2x/index.html +6 -0
  8. package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +3 -3
  9. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/applicationApi.ts +9 -9
  10. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphql-operations-types.ts +296 -0
  11. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +12 -7
  12. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/maintenanceRequestApi.ts +50 -38
  13. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyDetailGraphQL.ts +50 -102
  14. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyListingGraphQL.ts +211 -43
  15. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/userApi.ts +43 -0
  16. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +9 -208
  17. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/appliances.svg +13 -0
  18. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/electrical.svg +39 -0
  19. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/hvac.svg +78 -0
  20. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/pest.svg +5 -0
  21. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/plumbing.svg +7 -0
  22. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/assets/icons/zen-logo.svg +5 -0
  23. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/MaintenanceRequestIcon.tsx +46 -0
  24. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/NavMenu.tsx +53 -0
  25. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyListingCard.tsx +55 -58
  26. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyMap.tsx +93 -11
  27. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertySearchFilters.tsx +315 -0
  28. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/StatusBadge.tsx +36 -0
  29. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/TopBar.tsx +107 -0
  30. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +2 -2
  31. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingAmenities.ts +55 -0
  32. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingPriceRange.ts +64 -0
  33. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +14 -5
  34. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +54 -11
  35. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +1 -1
  36. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +42 -39
  37. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Contact.tsx +10 -10
  38. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +64 -91
  39. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/HelpCenter.tsx +1 -1
  40. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +19 -9
  41. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +79 -100
  42. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/NotFound.tsx +1 -1
  43. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +62 -47
  44. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +3 -3
  45. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +230 -34
  46. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +10 -1
  47. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/styles/global.css +64 -0
  48. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/geocode.ts +30 -5
  49. package/dist/package.json +1 -1
  50. package/dist/setup-cli.mjs +271 -0
  51. package/package.json +1 -1
@@ -0,0 +1,78 @@
1
+ <?xml version="1.0" encoding="iso-8859-1"?>
2
+ <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
3
+ <svg fill="currentColor" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" xml:space="preserve">
4
+ <g>
5
+ <g>
6
+ <path d="M482.815,93.937H29.185C13.093,93.937,0,107.029,0,123.123v265.755c0,16.093,13.093,29.186,29.185,29.186h453.63
7
+ c16.092,0,29.185-13.093,29.185-29.186V123.123C512,107.029,498.907,93.937,482.815,93.937z M482.815,397.665H29.185
8
+ c-4.845,0-8.787-3.942-8.787-8.788V123.123c0-4.845,3.942-8.788,8.787-8.788h453.63c4.845,0,8.787,3.942,8.787,8.788v265.755h0
9
+ C491.602,393.722,487.66,397.665,482.815,397.665z"/>
10
+ </g>
11
+ </g>
12
+ <g>
13
+ <g>
14
+ <path d="M165.227,138.709c-64.674,0-117.291,52.617-117.291,117.291s52.617,117.291,117.291,117.291S282.518,320.674,282.518,256
15
+ S229.901,138.709,165.227,138.709z M165.227,352.892c-53.427,0-96.892-43.466-96.892-96.892c0-53.426,43.466-96.892,96.892-96.892
16
+ c53.426,0,96.892,43.466,96.892,96.892C262.12,309.427,218.654,352.892,165.227,352.892z"/>
17
+ </g>
18
+ </g>
19
+ <g>
20
+ <g>
21
+ <path d="M252.137,262.786c-4.959-7.802-13.735-11.878-22.893-10.65c-11.264,1.517-22.867,0.816-33.891-2.002
22
+ c-1.243-5.677-4.005-10.79-7.84-14.892c1.022-2.523,2.468-4.881,4.257-6.924c7.147-8.16,11.081-18.629,11.081-29.475
23
+ c0-16.945-13.258-30.862-30.184-31.684l-24.291-1.179c-9.244-0.453-17.627,4.364-21.904,12.56
24
+ c-4.276,8.196-3.424,17.833,2.224,25.151c6.784,8.789,11.772,18.574,14.875,29.144c-4.704,4.049-8.212,9.444-9.922,15.585
25
+ c-2.285,0.198-4.6,0.078-6.844-0.367c-10.64-2.107-21.672-0.283-31.066,5.141c-14.675,8.472-20.099,26.913-12.347,41.981
26
+ l11.124,21.627c4.07,7.911,11.968,12.71,20.788,12.71c0.346,0,0.694-0.007,1.041-0.021c9.236-0.395,17.155-5.951,20.67-14.502
27
+ c4.25-10.345,10.288-19.619,17.974-27.634c2.942,0.902,6.063,1.389,9.298,1.389c2.924,0,5.754-0.405,8.446-1.147
28
+ c1.567,2.072,2.812,4.394,3.646,6.848c3.495,10.269,10.593,18.911,19.985,24.333c4.998,2.885,10.461,4.264,15.855,4.264
29
+ c10.443,0,20.627-5.167,26.675-14.561l13.167-20.447C257.068,280.262,257.096,270.587,252.137,262.786z M118.148,307.238
30
+ c-0.608,1.481-1.788,1.836-2.67,1.874c-0.882,0.035-2.089-0.215-2.821-1.639l-11.124-21.626
31
+ c-2.767-5.379-0.83-11.962,4.407-14.986c5.111-2.951,11.115-3.943,16.901-2.798c3.914,0.775,7.946,1.007,11.937,0.719
32
+ c0.934,2.318,2.133,4.501,3.56,6.513C129.792,284.674,123.018,295.385,118.148,307.238z M164.289,268.347
33
+ c-6.29,0-11.408-5.118-11.408-11.408s5.118-11.407,11.408-11.407s11.407,5.117,11.407,11.407S170.579,268.347,164.289,268.347z
34
+ M176.422,214.879c-2.8,3.198-5.136,6.819-6.93,10.688c-1.694-0.28-3.431-0.432-5.204-0.432c-0.578,0-1.151,0.017-1.723,0.048
35
+ c-3.845-12.279-9.783-23.67-17.721-33.954c-0.978-1.267-0.697-2.466-0.288-3.25c0.409-0.784,1.206-1.696,2.829-1.623l24.291,1.179
36
+ c6.042,0.294,10.774,5.261,10.774,11.309C182.452,204.744,180.31,210.439,176.422,214.879z M234.912,276.991l-13.168,20.448
37
+ c-3.273,5.085-9.942,6.699-15.181,3.676c-5.111-2.951-8.972-7.652-10.873-13.239c-1.421-4.174-3.49-8.141-6.072-11.73
38
+ c1.318-1.734,2.462-3.605,3.407-5.59c12.734,2.905,26.019,3.537,38.94,1.798c1.581-0.216,2.482,0.627,2.957,1.374
39
+ C235.396,274.474,235.779,275.645,234.912,276.991z"/>
40
+ </g>
41
+ </g>
42
+ <g>
43
+ <g>
44
+ <path d="M456.924,141.769H321.275c-5.632,0-10.199,4.566-10.199,10.199s4.567,10.199,10.199,10.199h135.649
45
+ c5.632,0,10.199-4.566,10.199-10.199S462.556,141.769,456.924,141.769z"/>
46
+ </g>
47
+ </g>
48
+ <g>
49
+ <g>
50
+ <path d="M456.924,193.785H321.275c-5.632,0-10.199,4.566-10.199,10.199s4.567,10.199,10.199,10.199h135.649
51
+ c5.632,0,10.199-4.566,10.199-10.199S462.556,193.785,456.924,193.785z"/>
52
+ </g>
53
+ </g>
54
+ <g>
55
+ <g>
56
+ <path d="M456.924,245.801H321.275c-5.632,0-10.199,4.566-10.199,10.199c0,5.633,4.567,10.199,10.199,10.199h135.649
57
+ c5.632,0,10.199-4.566,10.199-10.199C467.124,250.367,462.556,245.801,456.924,245.801z"/>
58
+ </g>
59
+ </g>
60
+ <g>
61
+ <g>
62
+ <path d="M456.924,297.817H321.275c-5.632,0-10.199,4.566-10.199,10.199c0,5.633,4.567,10.199,10.199,10.199h135.649
63
+ c5.632,0,10.199-4.566,10.199-10.199C467.124,302.383,462.556,297.817,456.924,297.817z"/>
64
+ </g>
65
+ </g>
66
+ <g>
67
+ <g>
68
+ <path d="M416.128,349.833h-94.853c-5.632,0-10.199,4.566-10.199,10.199c0,5.633,4.567,10.199,10.199,10.199h94.853
69
+ c5.632,0,10.199-4.566,10.199-10.199C426.327,354.399,421.76,349.833,416.128,349.833z"/>
70
+ </g>
71
+ </g>
72
+ <g>
73
+ <g>
74
+ <path d="M456.924,349.833h-5.1c-5.632,0-10.199,4.566-10.199,10.199c0,5.633,4.567,10.199,10.199,10.199h5.1
75
+ c5.632,0,10.199-4.566,10.199-10.199C467.124,354.399,462.556,349.833,456.924,349.833z"/>
76
+ </g>
77
+ </g>
78
+ </svg>
@@ -0,0 +1,5 @@
1
+ <?xml version='1.0' encoding='iso-8859-1'?>
2
+ <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
3
+ <svg fill="currentColor" height="800px" width="800px" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 463 463" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 463 463">
4
+ <path d="m355.256,145.405c-34.083-19.724-60.085-50.573-73.704-87.254 4.229-2.018 8.958-3.151 13.947-3.151h15.971c0.008,0 0.015,0.002 0.023,0.002 0.051,0 0.102-0.008 0.153-0.009 0.234-0.005 0.465-0.017 0.693-0.042 0.079-0.009 0.157-0.025 0.236-0.036 0.216-0.031 0.43-0.07 0.639-0.119 0.034-0.008 0.069-0.011 0.103-0.019l25.941-6.485c6.912-1.729 11.74-7.912 11.74-15.037v-11.509c0-7.125-4.828-13.309-11.741-15.037l-25.94-6.485c-0.04-0.01-0.081-0.014-0.121-0.024-0.146-0.034-0.293-0.062-0.442-0.087-0.11-0.019-0.221-0.037-0.331-0.051-0.124-0.015-0.25-0.026-0.376-0.035-0.136-0.01-0.271-0.018-0.406-0.021-0.047,2.45463e-15-0.093-0.006-0.142-0.006h-98.745c-10.551,0-20.471,4.109-27.931,11.569l-56.97,56.971c-4.449,4.449-5.768,11.079-3.36,16.892s8.028,9.568 14.32,9.568h29.187v9h-0.5c-8.547,0-15.5,6.953-15.5,15.5v22.273c0,10.239-2.784,20.291-8.052,29.069l-20.639,34.398c-12.632,21.054-19.31,45.161-19.31,69.714v132.546c0,30.603 24.897,55.5 55.5,55.5h96c30.603,0 55.5-24.897 55.5-55.5v-132.546c0-24.553-6.677-48.66-19.31-69.714l-20.639-34.398c-5.268-8.778-8.052-18.83-8.052-29.069v-22.273c0-8.547-6.953-15.5-15.5-15.5h-0.5v-9h8.5c4.142,0 7.5-3.358 7.5-7.5 0-7.219 2.369-13.893 6.365-19.292 15.273,37.82 42.778,69.577 78.378,90.18 1.183,0.685 2.475,1.01 3.75,1.01 2.589,0 5.108-1.343 6.498-3.745 2.076-3.586 0.852-8.174-2.733-10.248zm-19.257-123.659v11.508c0,0.23-0.156,0.43-0.378,0.485l-16.622,4.155v-20.788l16.621,4.155c0.224,0.055 0.379,0.255 0.379,0.485zm-199.828,191.212l20.639-34.398c5.073-8.455 8.32-17.844 9.585-27.56h33.604v144.037c-1.835-0.565-3.396-1.343-5.197-2.244-4.268-2.135-9.579-4.792-19.346-4.792-9.765,0-15.076,2.658-19.343,4.793-3.721,1.862-6.409,3.207-12.631,3.207-6.225,0-8.914-1.345-12.636-3.208-3.02-1.511-6.572-3.279-11.847-4.186v-13.652c-4.26326e-14-21.836 5.938-43.274 17.172-61.997zm71.328,186.042c4.687,0 8.5,3.813 8.5,8.5v8.5h-17v-8.5c0-4.687 3.813-8.5 8.5-8.5zm48,49h-96c-22.332,0-40.5-18.168-40.5-40.5v-103.517c1.808,0.564 3.356,1.334 5.136,2.225 4.268,2.135 9.58,4.792 19.347,4.792 9.766,0 15.076-2.658 19.344-4.793 3.721-1.862 6.409-3.207 12.63-3.207 6.224,0 8.912,1.345 12.634,3.208 3.032,1.517 6.6,3.293 11.909,4.196v73.096c0,0.575 0.071,1.132 0.193,1.669-9.39,3.081-16.193,11.924-16.193,22.331v16c0,4.142 3.358,7.5 7.5,7.5h32c4.142,0 7.5-3.358 7.5-7.5v-16c0-10.407-6.803-19.25-16.193-22.331 0.122-0.537 0.193-1.094 0.193-1.669v-73.117c5.241-0.91 8.776-2.671 11.785-4.176 3.723-1.862 6.413-3.208 12.639-3.208 6.241,0 8.937,1.346 12.668,3.21 4.273,2.134 9.592,4.79 19.37,4.79s15.097-2.656 19.371-4.79c1.791-0.895 3.347-1.668 5.168-2.233v103.524c-0.001,22.332-18.169,40.5-40.501,40.5zm23.329-235.042c11.234,18.724 17.172,40.162 17.172,61.997v13.65c-5.286,0.906-8.844,2.674-11.87,4.185-3.731,1.864-6.427,3.21-12.668,3.21-6.241,0-8.937-1.346-12.668-3.21-4.273-2.134-9.592-4.79-19.37-4.79-9.768,0-15.081,2.657-19.349,4.792-1.76,0.881-3.293,1.643-5.074,2.205v-143.997h33.604c1.265,9.715 4.512,19.104 9.585,27.56l20.638,34.398zm-30.829-93.458v16.5h-81v-16.5c0-0.276 0.224-0.5 0.5-0.5h80c0.276,0 0.5,0.224 0.5,0.5zm-16-15.5h-49v-9h49v9zm16.592-24h-109.778c-0.179,0-0.334,0-0.462-0.309-0.127-0.309-0.018-0.418 0.108-0.545l56.971-56.971c4.628-4.627 10.78-7.175 17.324-7.175h91.245v25h-8.5c-23.64,0-43.302,17.359-46.908,40z"/>
5
+ </svg>
@@ -0,0 +1,7 @@
1
+ <?xml version='1.0' encoding='iso-8859-1'?>
2
+ <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
3
+ <svg fill="currentColor" height="800px" width="800px" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 463 463" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 463 463">
4
+ <g>
5
+ <path d="m447,341.234v-17.734c0-79.126-64.374-143.5-143.5-143.5h-113.734c-3.138-9.29-11.93-16-22.266-16-4.687,0-8.5-3.813-8.5-8.5v-8.5h0.5c4.142,0 7.5-3.358 7.5-7.5s-3.358-7.5-7.5-7.5h-16.5v-27.077l47.496,9.543c1.786,0.357 3.582,0.533 5.365,0.533 6.219,0 12.28-2.137 17.192-6.165 6.321-5.182 9.947-12.842 9.947-21.016s-3.625-15.833-9.946-21.016c-6.322-5.183-14.544-7.236-22.558-5.632l-50.889,10.177c-4.123-6.796-11.593-11.347-20.107-11.347s-15.984,4.551-20.107,11.348l-50.889-10.178c-8.013-1.602-16.237,0.45-22.558,5.632-6.321,5.182-9.946,12.842-9.946,21.016s3.625,15.834 9.947,21.016c4.913,4.028 10.975,6.166 17.192,6.166 1.781,0 3.575-0.176 5.357-0.532l47.504-9.443v26.975h-16.5c-4.142,0-7.5,3.358-7.5,7.5s3.358,7.5 7.5,7.5h0.5v8.5c0,4.687-3.813,8.5-8.5,8.5-10.336,0-19.128,6.71-22.266,16h-25.734c-12.958,0-23.5,10.542-23.5,23.5v64c0,12.958 10.542,23.5 23.5,23.5h25.734c3.138,9.29 11.93,16 22.266,16h96c10.336,0 19.128-6.71 22.266-16h113.734c17.92,0 32.5,14.58 32.5,32.5v17.734c-9.29,3.138-16,11.93-16,22.266v16c0,12.958 10.542,23.5 23.5,23.5h96c12.958,0 23.5-10.542 23.5-23.5v-16c0-10.336-6.71-19.128-16-22.266zm-253.563-265.355c3.589-0.719 7.274,0.201 10.107,2.523 2.832,2.322 4.456,5.753 4.456,9.416s-1.625,7.094-4.457,9.416c-2.832,2.322-6.516,3.242-10.1,2.524l-50.443-10.135v-3.657l50.437-10.087zm-73.937-.879c4.687,0 8.5,3.813 8.5,8.5v16.5h-17v-16.5c0-4.687 3.813-8.5 8.5-8.5zm-73.937,24.757c-3.592,0.719-7.275-0.202-10.106-2.523-2.832-2.322-4.457-5.754-4.457-9.416s1.624-7.094 4.456-9.416c2.832-2.322 6.515-3.243 10.107-2.523l50.437,10.088v3.765l-50.437,10.025zm65.437,15.243h17v17h-17v-17zm-96,152.5v-64c0-4.687 3.813-8.5 8.5-8.5h24.5v81h-24.5c-4.687,0-8.5-3.813-8.5-8.5zm161,16c0,4.687-3.813,8.5-8.5,8.5h-96c-4.68,0-8.488-3.803-8.499-8.481 0-0.007 0.001-0.013 0.001-0.019 0-0.013-0.002-0.026-0.002-0.039v-95.923c0-0.013 0.002-0.026 0.002-0.039 0-0.007-0.001-0.013-0.001-0.019 0.011-4.677 3.819-8.48 8.499-8.48 12.958,0 23.5-10.542 23.5-23.5v-8.5h49v8.5c0,12.958 10.542,23.5 23.5,23.5 4.687,0 8.5,3.813 8.5,8.5v96zm127.5-7.5h-112.5v-81h112.5c70.855,0 128.5,57.645 128.5,128.5v16.5h-81v-16.5c0-26.191-21.309-47.5-47.5-47.5zm144.5,103.5c0,4.687-3.813,8.5-8.5,8.5h-96c-4.687,0-8.5-3.813-8.5-8.5v-16c0-4.687 3.813-8.5 8.5-8.5h96c4.687,0 8.5,3.813 8.5,8.5v16z"/>
6
+ </g>
7
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="40" height="40" rx="8" fill="black" fill-opacity="0.2"/>
3
+ <path d="M10 31V16.3333C13.4868 16.6744 19.9209 18.0046 20.3194 20.5969C20.4604 21.5144 20.45 31 20.45 31M13.4972 31L13.7641 13.9457C13.7641 10.1 25.95 9 25.95 9V31" stroke="white" stroke-width="1.5"/>
4
+ <path d="M29.2494 31.0001V23.4681C25.2137 22.8637 21.6875 23.9923 19.3778 25.2149C18.3982 25.7334 17.6374 26.2688 17.1494 26.6961V31.0001" stroke="white" stroke-width="1.5"/>
5
+ </svg>
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Renders a type-specific icon for maintenance requests (same approach as b2e MaintenanceTable).
3
+ * Uses imported SVG assets; fallback to 🔧 when type has no icon.
4
+ */
5
+ import PlumbingIcon from "@/assets/icons/plumbing.svg";
6
+ import HVACIcon from "@/assets/icons/hvac.svg";
7
+ import ElectricalIcon from "@/assets/icons/electrical.svg";
8
+ import AppliancesIcon from "@/assets/icons/appliances.svg";
9
+ import PestIcon from "@/assets/icons/pest.svg";
10
+
11
+ const issueIcons: Record<string, string> = {
12
+ Plumbing: PlumbingIcon,
13
+ HVAC: HVACIcon,
14
+ Electrical: ElectricalIcon,
15
+ Appliance: AppliancesIcon,
16
+ Pest: PestIcon,
17
+ };
18
+
19
+ const issueIconColors: Record<string, string> = {
20
+ Plumbing: "bg-teal-100",
21
+ HVAC: "bg-teal-100",
22
+ Electrical: "bg-teal-100",
23
+ Appliance: "bg-teal-100",
24
+ Pest: "bg-teal-100",
25
+ };
26
+
27
+ export function MaintenanceRequestIcon({ type }: { type: string | null }) {
28
+ const issueType = type?.trim() ?? "";
29
+ const iconSrc = issueIcons[issueType];
30
+ const bgClass = issueIconColors[issueType] ?? "bg-teal-100";
31
+
32
+ return (
33
+ <div
34
+ className={`flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-lg ${bgClass}`}
35
+ aria-hidden
36
+ >
37
+ {iconSrc ? (
38
+ <img src={iconSrc} alt={issueType || "Request"} className="h-6 w-6" />
39
+ ) : (
40
+ <span className="text-2xl" aria-hidden>
41
+ 🔧
42
+ </span>
43
+ )}
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,53 @@
1
+ import { Link, useLocation } from "react-router";
2
+ import { Home, Search, BarChart3, Wrench, Phone, type LucideIcon } from "lucide-react";
3
+
4
+ interface NavItem {
5
+ path: string;
6
+ icon: LucideIcon;
7
+ label: string;
8
+ }
9
+
10
+ const navItems: NavItem[] = [
11
+ { path: "/", icon: Home, label: "Home" },
12
+ { path: "/dashboard", icon: BarChart3, label: "Dashboard" },
13
+ { path: "/properties", icon: Search, label: "Property Search" },
14
+ { path: "/maintenance/requests", icon: Wrench, label: "Maintenance Requests" },
15
+ { path: "/contact", icon: Phone, label: "Contact Us" },
16
+ ];
17
+
18
+ export function NavMenu() {
19
+ const location = useLocation();
20
+
21
+ const isActive = (path: string) => {
22
+ if (path === "/") return location.pathname === "/";
23
+ return location.pathname.startsWith(path);
24
+ };
25
+
26
+ return (
27
+ <nav
28
+ className="flex w-24 flex-col border-r border-gray-200 bg-white py-8"
29
+ aria-label="Main navigation"
30
+ >
31
+ {navItems.map((item) => {
32
+ const Icon = item.icon;
33
+ const active = isActive(item.path);
34
+ return (
35
+ <Link
36
+ key={item.path}
37
+ to={item.path}
38
+ className={`flex flex-col items-center justify-center gap-2 px-2 py-4 transition-colors ${
39
+ active
40
+ ? "border-l-4 border-teal-700 bg-teal-100 text-teal-700"
41
+ : "text-gray-600 hover:bg-gray-100 hover:text-gray-900"
42
+ }`}
43
+ title={item.label}
44
+ aria-label={item.label}
45
+ >
46
+ <Icon className="size-6 shrink-0" aria-hidden />
47
+ <span className="text-center text-xs font-medium leading-tight">{item.label}</span>
48
+ </Link>
49
+ );
50
+ })}
51
+ </nav>
52
+ );
53
+ }
@@ -1,11 +1,11 @@
1
1
  /**
2
- * ZENLEASE-style listing card: image carousel area, name + address, price by beds, amenities, Apply button.
2
+ * Stacked property listing card: image on top, details below. Single price + bedrooms, amenity list, Apply button.
3
+ * No phone or secondary price. Virtual Tours / Videos pills on image.
3
4
  */
4
5
  import { useNavigate } from "react-router";
5
- import { useCallback, useState } from "react";
6
- import { Button } from "./ui/button";
7
- import type { SearchResultRecordData } from "../features/global-search/types/search/searchResults";
8
- import { Heart } from "lucide-react";
6
+ import { useCallback, type MouseEvent } from "react";
7
+ import { Button } from "@/components/ui/button";
8
+ import type { SearchResultRecordData } from "@/features/global-search/types/search/searchResults.js";
9
9
 
10
10
  function fieldDisplay(
11
11
  fields: Record<string, { value?: unknown; displayValue?: string | null }> | undefined,
@@ -36,18 +36,26 @@ interface PropertyListingCardProps {
36
36
  imageUrl: string | null;
37
37
  /** Property address (Address__c from Property__c) when available */
38
38
  address?: string | null;
39
+ /** Amenities string (e.g. "In-unit washer | Pool | Gym"), separated by | */
40
+ amenities?: string | null;
39
41
  }
40
42
 
41
43
  export default function PropertyListingCard({
42
44
  record,
43
45
  imageUrl,
44
46
  address,
47
+ amenities,
45
48
  }: PropertyListingCardProps) {
46
49
  const navigate = useNavigate();
47
- const [favorited, setFavorited] = useState(false);
48
50
  const name = fieldDisplay(record.fields, "Name") ?? "Untitled";
49
51
  const price = fieldDisplay(record.fields, "Listing_Price__c");
50
52
  const propertyRef = fieldDisplay(record.fields, "Property__c");
53
+ const bedroomsRaw = fieldDisplay(record.fields, "Property__r.Bedrooms__c");
54
+ const bedroomsNum = bedroomsRaw != null && bedroomsRaw !== "" ? Number(bedroomsRaw) : NaN;
55
+ const bedroomsLabel =
56
+ !Number.isNaN(bedroomsNum) && bedroomsNum >= 0
57
+ ? `${bedroomsNum} Bedroom${bedroomsNum !== 1 ? "s" : ""}`
58
+ : null;
51
59
  const detailPath = `/property/${record.id}`;
52
60
  const displayAddress = (address ?? propertyRef ?? "").trim().replace(/\n/g, ", ") || null;
53
61
 
@@ -65,21 +73,16 @@ export default function PropertyListingCard({
65
73
  [handleClick],
66
74
  );
67
75
 
68
- const toggleFavorite = useCallback((e: React.MouseEvent) => {
69
- e.stopPropagation();
70
- setFavorited((v) => !v);
71
- }, []);
72
-
73
76
  return (
74
77
  <article
75
- className="cursor-pointer overflow-hidden rounded-2xl border border-border bg-card shadow-sm transition-all duration-200 hover:shadow-md focus-within:ring-2 focus-within:ring-primary focus-within:ring-offset-2"
78
+ className="flex cursor-pointer flex-col overflow-hidden rounded-2xl border border-border bg-card shadow-sm transition-all duration-200 hover:shadow-md focus-within:ring-2 focus-within:ring-primary focus-within:ring-offset-2"
76
79
  onClick={handleClick}
77
80
  onKeyDown={handleKeyDown}
78
81
  role="button"
79
82
  tabIndex={0}
80
83
  aria-label={`View details for ${name}`}
81
84
  >
82
- {/* Image area with carousel affordances + Virtual Tours / Videos overlays */}
85
+ {/* Image on top, full width */}
83
86
  <div className="relative aspect-[16/10] w-full overflow-hidden rounded-t-2xl bg-muted">
84
87
  {imageUrl ? (
85
88
  <img src={imageUrl} alt="" className="h-full w-full object-cover" />
@@ -88,25 +91,7 @@ export default function PropertyListingCard({
88
91
  No image
89
92
  </div>
90
93
  )}
91
- {/* Left/right carousel arrows (visual only for now) */}
92
- <button
93
- type="button"
94
- className="absolute left-2 top-1/2 flex h-8 w-8 -translate-y-1/2 cursor-pointer items-center justify-center rounded-full bg-black/40 text-white transition-colors duration-200 hover:bg-black/60"
95
- aria-label="Previous image"
96
- onClick={(e) => e.stopPropagation()}
97
- >
98
-
99
- </button>
100
- <button
101
- type="button"
102
- className="absolute right-2 top-1/2 flex h-8 w-8 -translate-y-1/2 cursor-pointer items-center justify-center rounded-full bg-black/40 text-white transition-colors duration-200 hover:bg-black/60"
103
- aria-label="Next image"
104
- onClick={(e) => e.stopPropagation()}
105
- >
106
-
107
- </button>
108
- {/* Virtual Tours / Videos pills – purple per ZENLEASE screenshots */}
109
- <div className="absolute left-2 top-2 flex flex-col gap-1">
94
+ <div className="absolute left-1.5 top-1.5 flex flex-col gap-0.5">
110
95
  <span className="rounded-full bg-violet-600 px-2 py-0.5 text-xs font-medium text-white">
111
96
  Virtual Tours
112
97
  </span>
@@ -116,42 +101,54 @@ export default function PropertyListingCard({
116
101
  </div>
117
102
  </div>
118
103
 
119
- <div className="p-3">
120
- {/* Name + address row with favorite */}
121
- <div className="mb-2 flex items-start justify-between gap-2">
122
- <div className="min-w-0">
123
- <h3 className="font-semibold text-foreground">{name}</h3>
104
+ {/* Content below: name, address, price+beds, amenities, Apply */}
105
+ <div className="flex flex-col justify-between p-3">
106
+ <div>
107
+ <div className="mb-1.5">
108
+ <h3 className="text-2xl font-semibold text-foreground">{name}</h3>
124
109
  {displayAddress && (
125
110
  <p className="truncate text-sm text-muted-foreground">{displayAddress}</p>
126
111
  )}
127
112
  </div>
128
- <button
129
- type="button"
130
- className="shrink-0 cursor-pointer rounded-xl p-1 text-muted-foreground transition-colors duration-200 hover:bg-muted hover:text-foreground"
131
- aria-label={favorited ? "Remove from favorites" : "Add to favorites"}
132
- onClick={toggleFavorite}
133
- >
134
- <Heart className={`h-5 w-5 ${favorited ? "fill-primary text-primary" : ""}`} />
135
- </button>
136
- </div>
137
113
 
138
- {/* Price by bedssingle price shown as main */}
139
- <div className="mb-2 flex flex-wrap gap-3 text-sm">
140
- {price != null && (
141
- <span className="font-medium text-foreground">
142
- {formatPrice(price)} <span className="font-normal text-muted-foreground">2 Beds</span>
143
- </span>
114
+ {/* Single price + bedrooms – price bold, teal, 2x size */}
115
+ <div className="mb-1.5 flex flex-wrap items-baseline gap-x-3 gap-y-0.5 text-base">
116
+ {price != null && (
117
+ <span className="text-2xl font-semibold text-primary">
118
+ {formatPrice(price)}
119
+ {bedroomsLabel != null ? (
120
+ <span className="ml-1 text-base font-normal text-muted-foreground">
121
+ {bedroomsLabel}
122
+ </span>
123
+ ) : null}
124
+ </span>
125
+ )}
126
+ </div>
127
+
128
+ {/* Amenity pills (fetched per property, string separated by |) */}
129
+ {amenities != null && amenities.trim() !== "" && (
130
+ <div className="mb-2 flex flex-wrap gap-1.5">
131
+ {amenities
132
+ .split(/\s*\|\s*/)
133
+ .map((label) => label.trim())
134
+ .filter(Boolean)
135
+ .map((label) => (
136
+ <span
137
+ key={label}
138
+ className="rounded-full border border-border bg-muted/60 px-2.5 py-0.5 text-xs font-medium text-muted-foreground"
139
+ >
140
+ {label}
141
+ </span>
142
+ ))}
143
+ </div>
144
144
  )}
145
145
  </div>
146
146
 
147
- {/* Amenities line */}
148
- <p className="mb-3 text-xs text-muted-foreground">View details for amenities</p>
149
-
150
- {/* Apply – teal primary button */}
147
+ {/* Apply button – no phone, no email */}
151
148
  <Button
152
149
  size="sm"
153
- className="w-full cursor-pointer rounded-xl bg-primary transition-colors duration-200 hover:bg-primary/90"
154
- onClick={(e) => {
150
+ className="mt-4 w-full cursor-pointer rounded-xl bg-primary px-5 py-5 text-lg font-medium transition-colors duration-200 hover:bg-primary/90"
151
+ onClick={(e: MouseEvent<HTMLButtonElement>) => {
155
152
  e.stopPropagation();
156
153
  navigate(`/application?listingId=${encodeURIComponent(record.id)}`);
157
154
  }}
@@ -2,7 +2,7 @@
2
2
  * Leaflet map for property search and detail. Uses OpenStreetMap tiles (no API key).
3
3
  * Renders one pin per property (each marker in the markers array).
4
4
  */
5
- import { useMemo, useState, useEffect } from "react";
5
+ import { useMemo, useState, useEffect, type ReactNode } from "react";
6
6
  import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet";
7
7
  import L from "leaflet";
8
8
  import "leaflet/dist/leaflet.css";
@@ -18,19 +18,32 @@ const Leaflet = L as {
18
18
  }) => unknown;
19
19
  };
20
20
 
21
- // Custom pin icon so each property shows a visible pin (no dependency on external marker images)
21
+ // Lucide-style map pin (teardrop + circle), filled, 1.5x size
22
+ const PIN_SVG =
23
+ '<svg xmlns="http://www.w3.org/2000/svg" width="42" height="60" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" fill="currentColor"/><circle cx="12" cy="10" r="3" fill="white" stroke="none"/></svg>';
24
+
22
25
  const pinIcon = Leaflet.divIcon({
23
26
  className: "property-map-pin",
24
- html: `<span class="property-pin-shape" aria-hidden="true"></span>`,
25
- iconSize: [28, 40],
26
- iconAnchor: [14, 40],
27
- popupAnchor: [0, -40],
27
+ html: PIN_SVG,
28
+ iconSize: [42, 60],
29
+ iconAnchor: [21, 60],
30
+ popupAnchor: [0, -60],
28
31
  });
29
32
 
30
33
  export interface MapMarker {
31
34
  lat: number;
32
35
  lng: number;
33
36
  label?: string;
37
+ /** Property__c id; used to look up listing record for popup card */
38
+ propertyId?: string;
39
+ }
40
+
41
+ /** Bounding box in lat/lng (Leaflet getBounds()). */
42
+ export interface MapBounds {
43
+ north: number;
44
+ south: number;
45
+ east: number;
46
+ west: number;
34
47
  }
35
48
 
36
49
  interface PropertyMapProps {
@@ -40,6 +53,10 @@ interface PropertyMapProps {
40
53
  zoom?: number;
41
54
  /** Optional markers */
42
55
  markers?: MapMarker[];
56
+ /** Optional: render custom content in each marker popup (e.g. PropertyListingCard) */
57
+ popupContent?: (marker: MapMarker) => ReactNode;
58
+ /** Called when the user pans or zooms; use to filter list by visible pins */
59
+ onBoundsChange?: (bounds: MapBounds | null) => void;
43
60
  /** CSS class for the container (e.g. height) */
44
61
  className?: string;
45
62
  }
@@ -52,10 +69,65 @@ function MapCenterUpdater({ center, zoom = 13 }: { center: [number, number]; zoo
52
69
  return null;
53
70
  }
54
71
 
72
+ function MapBoundsReporter({
73
+ onBoundsChange,
74
+ }: {
75
+ onBoundsChange?: (bounds: MapBounds | null) => void;
76
+ }) {
77
+ const map = useMap() as {
78
+ getBounds?: () => {
79
+ getNorth: () => number;
80
+ getSouth: () => number;
81
+ getEast: () => number;
82
+ getWest: () => number;
83
+ };
84
+ on?: (event: string, fn: () => void) => void;
85
+ off?: (event: string, fn: () => void) => void;
86
+ };
87
+ useEffect(() => {
88
+ if (!onBoundsChange || !map.getBounds) return;
89
+ const report = () => {
90
+ const b = map.getBounds!();
91
+ onBoundsChange({
92
+ north: b.getNorth(),
93
+ south: b.getSouth(),
94
+ east: b.getEast(),
95
+ west: b.getWest(),
96
+ });
97
+ };
98
+ // Only report on moveend (user pan/zoom). Reporting on mount caused setState -> re-render -> map setView -> moveend -> loop.
99
+ map.on?.("moveend", report);
100
+ return () => {
101
+ map.off?.("moveend", report);
102
+ };
103
+ }, [map, onBoundsChange]);
104
+ return null;
105
+ }
106
+
107
+ /** Wraps popup content with an in-card close button and shadow; removes need for Leaflet's default chrome */
108
+ function PopupContentWrapper({ children }: { children: ReactNode }) {
109
+ const map = useMap() as { closePopup?: () => void };
110
+ return (
111
+ <div className="relative min-w-0">
112
+ <button
113
+ type="button"
114
+ onClick={() => map.closePopup?.()}
115
+ className="absolute right-2 top-2 z-10 flex h-7 w-7 items-center justify-center rounded-full bg-black/60 text-white transition-colors hover:bg-black/80"
116
+ aria-label="Close"
117
+ >
118
+ ×
119
+ </button>
120
+ <div className="rounded-2xl shadow-lg overflow-hidden">{children}</div>
121
+ </div>
122
+ );
123
+ }
124
+
55
125
  export default function PropertyMap({
56
126
  center,
57
127
  zoom = 13,
58
128
  markers = [],
129
+ popupContent,
130
+ onBoundsChange,
59
131
  className = "h-[400px] w-full rounded-xl overflow-hidden",
60
132
  }: PropertyMapProps) {
61
133
  const [mounted, setMounted] = useState(false);
@@ -66,8 +138,7 @@ export default function PropertyMap({
66
138
  const hasMarkers = markers.length > 0;
67
139
  const effectiveCenter = useMemo((): [number, number] => {
68
140
  if (hasMarkers) {
69
- const sum = markers.reduce((acc, m) => [acc[0] + m.lat, acc[1] + m.lng], [0, 0]);
70
- return [sum[0] / markers.length, sum[1] / markers.length];
141
+ return [markers[0].lat, markers[0].lng];
71
142
  }
72
143
  return center;
73
144
  }, [center, hasMarkers, markers]);
@@ -95,14 +166,25 @@ export default function PropertyMap({
95
166
  style={{ minHeight: 200 }}
96
167
  >
97
168
  <MapCenterUpdater center={effectiveCenter} zoom={zoom} />
169
+ <MapBoundsReporter onBoundsChange={onBoundsChange} />
98
170
  <TileLayer
99
- attribution='Data by &copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
171
+ attribution='&copy; <a href="https://leafletjs.com/">Leaflet</a> | Data by &copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
100
172
  url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
101
173
  maxZoom={19}
102
174
  />
103
175
  {markers.map((m, i) => (
104
- <Marker key={`${m.lat}-${m.lng}-${i}`} position={[m.lat, m.lng]} icon={pinIcon}>
105
- <Popup>{m.label ?? "Property"}</Popup>
176
+ <Marker
177
+ key={`${m.lat}-${m.lng}-${m.propertyId ?? i}`}
178
+ position={[m.lat, m.lng]}
179
+ icon={pinIcon}
180
+ >
181
+ <Popup>
182
+ {popupContent ? (
183
+ <PopupContentWrapper>{popupContent(m)}</PopupContentWrapper>
184
+ ) : (
185
+ (m.label ?? "Property")
186
+ )}
187
+ </Popup>
106
188
  </Marker>
107
189
  ))}
108
190
  </MapContainer>