@burdenoff/microfe-housingvista 2026.703.2 → 2026.703.4

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 (80) hide show
  1. package/dist/housingvista/HousingVistaRoot.js +23 -23
  2. package/dist/housingvista/HousingVistaRoot.js.map +1 -1
  3. package/dist/housingvista/HousingVistaRoutes.js +37 -17
  4. package/dist/housingvista/HousingVistaRoutes.js.map +1 -1
  5. package/dist/housingvista/components/Breadcrumbs.js +32 -27
  6. package/dist/housingvista/components/Breadcrumbs.js.map +1 -1
  7. package/dist/housingvista/components/ContactOwnerDialog.js +104 -0
  8. package/dist/housingvista/components/ContactOwnerDialog.js.map +1 -0
  9. package/dist/housingvista/components/FileUploader.js +72 -0
  10. package/dist/housingvista/components/FileUploader.js.map +1 -0
  11. package/dist/housingvista/components/ReviewsSection.js +131 -0
  12. package/dist/housingvista/components/ReviewsSection.js.map +1 -0
  13. package/dist/housingvista/components/index.js +3 -0
  14. package/dist/housingvista/hooks/index.js +768 -223
  15. package/dist/housingvista/hooks/index.js.map +1 -1
  16. package/dist/housingvista/hooks/platform.js +228 -0
  17. package/dist/housingvista/hooks/platform.js.map +1 -0
  18. package/dist/housingvista/pages/HomePage.js +126 -134
  19. package/dist/housingvista/pages/HomePage.js.map +1 -1
  20. package/dist/housingvista/pages/amc/AMCManagementPage.js +209 -170
  21. package/dist/housingvista/pages/amc/AMCManagementPage.js.map +1 -1
  22. package/dist/housingvista/pages/analytics/AnalyticsPage.js +123 -145
  23. package/dist/housingvista/pages/analytics/AnalyticsPage.js.map +1 -1
  24. package/dist/housingvista/pages/assets/AssetDetailPage.js +287 -0
  25. package/dist/housingvista/pages/assets/AssetDetailPage.js.map +1 -0
  26. package/dist/housingvista/pages/assets/AssetManagementPage.js +407 -109
  27. package/dist/housingvista/pages/assets/AssetManagementPage.js.map +1 -1
  28. package/dist/housingvista/pages/finance/HomeLoanDetailPage.js +350 -0
  29. package/dist/housingvista/pages/finance/HomeLoanDetailPage.js.map +1 -0
  30. package/dist/housingvista/pages/finance/HomeLoansPage.js +160 -132
  31. package/dist/housingvista/pages/finance/HomeLoansPage.js.map +1 -1
  32. package/dist/housingvista/pages/investments/InvestmentDetailPage.js +615 -400
  33. package/dist/housingvista/pages/investments/InvestmentDetailPage.js.map +1 -1
  34. package/dist/housingvista/pages/investments/InvestmentsPage.js +210 -191
  35. package/dist/housingvista/pages/investments/InvestmentsPage.js.map +1 -1
  36. package/dist/housingvista/pages/legal/LegalPage.js +94 -74
  37. package/dist/housingvista/pages/legal/LegalPage.js.map +1 -1
  38. package/dist/housingvista/pages/legal/LegalServiceDetailPage.js +301 -0
  39. package/dist/housingvista/pages/legal/LegalServiceDetailPage.js.map +1 -0
  40. package/dist/housingvista/pages/market/AreaInsightsPage.js +95 -86
  41. package/dist/housingvista/pages/market/AreaInsightsPage.js.map +1 -1
  42. package/dist/housingvista/pages/market/MarketAnalyticsPage.js +73 -64
  43. package/dist/housingvista/pages/market/MarketAnalyticsPage.js.map +1 -1
  44. package/dist/housingvista/pages/marketplace/MarketplacePage.js +102 -81
  45. package/dist/housingvista/pages/marketplace/MarketplacePage.js.map +1 -1
  46. package/dist/housingvista/pages/marketplace/ServiceDetailPage.js +317 -0
  47. package/dist/housingvista/pages/marketplace/ServiceDetailPage.js.map +1 -0
  48. package/dist/housingvista/pages/offers/OffersPage.js +306 -0
  49. package/dist/housingvista/pages/offers/OffersPage.js.map +1 -0
  50. package/dist/housingvista/pages/properties/AddPropertyPage.js +240 -225
  51. package/dist/housingvista/pages/properties/AddPropertyPage.js.map +1 -1
  52. package/dist/housingvista/pages/properties/PropertiesPage.js +178 -204
  53. package/dist/housingvista/pages/properties/PropertiesPage.js.map +1 -1
  54. package/dist/housingvista/pages/properties/PropertyDetailPage.js +648 -385
  55. package/dist/housingvista/pages/properties/PropertyDetailPage.js.map +1 -1
  56. package/dist/housingvista/pages/rental/RentalManagementPage.js +552 -309
  57. package/dist/housingvista/pages/rental/RentalManagementPage.js.map +1 -1
  58. package/dist/housingvista/pages/settings/ProfilePage.js +81 -144
  59. package/dist/housingvista/pages/settings/ProfilePage.js.map +1 -1
  60. package/dist/housingvista/pages/settings/SettingsPage.js +80 -143
  61. package/dist/housingvista/pages/settings/SettingsPage.js.map +1 -1
  62. package/dist/housingvista/pages/shortlist/ShortlistPage.js +32 -19
  63. package/dist/housingvista/pages/shortlist/ShortlistPage.js.map +1 -1
  64. package/dist/housingvista/pages/transactions/TransactionDetailPage.js +103 -124
  65. package/dist/housingvista/pages/transactions/TransactionDetailPage.js.map +1 -1
  66. package/dist/housingvista/pages/transactions/TransactionsPage.js +205 -201
  67. package/dist/housingvista/pages/transactions/TransactionsPage.js.map +1 -1
  68. package/dist/housingvista/pages/verification/VerificationDetailPage.js +352 -0
  69. package/dist/housingvista/pages/verification/VerificationDetailPage.js.map +1 -0
  70. package/dist/housingvista/pages/verification/VerificationPage.js +224 -97
  71. package/dist/housingvista/pages/verification/VerificationPage.js.map +1 -1
  72. package/dist/housingvista/pages/viewings/ViewingsPage.js +259 -0
  73. package/dist/housingvista/pages/viewings/ViewingsPage.js.map +1 -0
  74. package/dist/housingvista/utils/helpers.js +11 -1
  75. package/dist/housingvista/utils/helpers.js.map +1 -1
  76. package/package.json +1 -1
  77. package/dist/housingvista/pages/chat/ChatPage.js +0 -28
  78. package/dist/housingvista/pages/chat/ChatPage.js.map +0 -1
  79. package/dist/housingvista/pages/notifications/NotificationsPage.js +0 -28
  80. package/dist/housingvista/pages/notifications/NotificationsPage.js.map +0 -1
@@ -0,0 +1,72 @@
1
+ import { cn as e } from "../utils/helpers.js";
2
+ import { useFileUpload as t } from "../hooks/platform.js";
3
+ import "../hooks/index.js";
4
+ import { notify as n } from "../lib/toast.js";
5
+ import { useRef as r, useState as i } from "react";
6
+ import { FileText as a, Loader2 as o, Upload as s, X as c } from "lucide-react";
7
+ import { jsx as l, jsxs as u } from "react/jsx-runtime";
8
+ //#region src/housingvista/components/FileUploader.tsx
9
+ var d = ({ onUploaded: d, value: f = [], onRemove: p, accept: m, multiple: h = !0, label: g = "Choose files", className: _ }) => {
10
+ let { uploadFile: v } = t(), y = r(null), [b, x] = i(!1), S = async (e) => {
11
+ if (!e || e.length === 0) return;
12
+ x(!0);
13
+ let t = [];
14
+ try {
15
+ for (let n of Array.from(e)) {
16
+ let e = await v(n);
17
+ t.push(e);
18
+ }
19
+ t.length > 0 && (d(t), n.success(`${t.length} file${t.length > 1 ? "s" : ""} uploaded`));
20
+ } catch (e) {
21
+ n.error("Upload failed", e.message);
22
+ } finally {
23
+ x(!1), y.current && (y.current.value = "");
24
+ }
25
+ };
26
+ return /* @__PURE__ */ u("div", {
27
+ className: e("space-y-3", _),
28
+ children: [
29
+ /* @__PURE__ */ u("button", {
30
+ type: "button",
31
+ disabled: b,
32
+ onClick: () => y.current?.click(),
33
+ className: "flex w-full items-center justify-center gap-2 rounded-md border border-dashed border-border-subtle bg-bg-sunken px-4 py-6 text-sm font-medium text-text-secondary transition-colors hover:bg-bg-surface hover:text-text-primary disabled:opacity-60",
34
+ children: [b ? /* @__PURE__ */ l(o, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ l(s, { className: "h-4 w-4" }), b ? "Uploading…" : g]
35
+ }),
36
+ /* @__PURE__ */ l("input", {
37
+ ref: y,
38
+ type: "file",
39
+ accept: m,
40
+ multiple: h,
41
+ className: "hidden",
42
+ onChange: (e) => S(e.target.files)
43
+ }),
44
+ f.length > 0 && /* @__PURE__ */ l("ul", {
45
+ className: "space-y-1.5",
46
+ children: f.map((e) => /* @__PURE__ */ u("li", {
47
+ className: "flex items-center justify-between gap-2 rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm",
48
+ children: [/* @__PURE__ */ u("a", {
49
+ href: e.url,
50
+ target: "_blank",
51
+ rel: "noopener noreferrer",
52
+ className: "flex min-w-0 items-center gap-2 text-text-primary hover:underline",
53
+ children: [/* @__PURE__ */ l(a, { className: "h-4 w-4 shrink-0 text-text-secondary" }), /* @__PURE__ */ l("span", {
54
+ className: "truncate",
55
+ children: e.name
56
+ })]
57
+ }), p && /* @__PURE__ */ l("button", {
58
+ type: "button",
59
+ onClick: () => p(e.id),
60
+ "aria-label": `Remove ${e.name}`,
61
+ className: "shrink-0 text-text-secondary hover:text-status-error-text",
62
+ children: /* @__PURE__ */ l(c, { className: "h-4 w-4" })
63
+ })]
64
+ }, e.id))
65
+ })
66
+ ]
67
+ });
68
+ };
69
+ //#endregion
70
+ export { d as FileUploader };
71
+
72
+ //# sourceMappingURL=FileUploader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileUploader.js","names":[],"sources":["../../../src/housingvista/components/FileUploader.tsx"],"sourcesContent":["/**\n * Real file upload widget backed by wspace-files-svc (presigned initiate → PUT\n * → complete). Emits durable File ids + download URLs via `onUploaded`. Replaces\n * every dead \"Choose Files\" / \"Upload Document\" button in the MFE.\n * @module housingvista\n */\n\nimport { useRef, useState, type FC } from 'react';\nimport { Upload, X, FileText, Loader2 } from 'lucide-react';\nimport { useFileUpload, type HvUploadedFile } from '../hooks';\nimport { notify } from '../lib/toast';\nimport { cn } from '../utils/helpers';\n\ninterface FileUploaderProps {\n onUploaded: (files: HvUploadedFile[]) => void;\n /** Files already attached (rendered as removable chips). */\n value?: HvUploadedFile[];\n onRemove?: (fileId: string) => void;\n accept?: string;\n multiple?: boolean;\n label?: string;\n className?: string;\n}\n\nexport const FileUploader: FC<FileUploaderProps> = ({\n onUploaded,\n value = [],\n onRemove,\n accept,\n multiple = true,\n label = 'Choose files',\n className,\n}) => {\n const { uploadFile } = useFileUpload();\n const inputRef = useRef<HTMLInputElement>(null);\n const [busy, setBusy] = useState(false);\n\n const handleFiles = async (fileList: FileList | null) => {\n if (!fileList || fileList.length === 0) return;\n setBusy(true);\n const uploaded: HvUploadedFile[] = [];\n try {\n for (const file of Array.from(fileList)) {\n const result = await uploadFile(file);\n uploaded.push(result);\n }\n if (uploaded.length > 0) {\n onUploaded(uploaded);\n notify.success(`${uploaded.length} file${uploaded.length > 1 ? 's' : ''} uploaded`);\n }\n } catch (err) {\n notify.error('Upload failed', (err as Error).message);\n } finally {\n setBusy(false);\n if (inputRef.current) inputRef.current.value = '';\n }\n };\n\n return (\n <div className={cn('space-y-3', className)}>\n <button\n type=\"button\"\n disabled={busy}\n onClick={() => inputRef.current?.click()}\n className=\"flex w-full items-center justify-center gap-2 rounded-md border border-dashed border-border-subtle bg-bg-sunken px-4 py-6 text-sm font-medium text-text-secondary transition-colors hover:bg-bg-surface hover:text-text-primary disabled:opacity-60\"\n >\n {busy ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <Upload className=\"h-4 w-4\" />}\n {busy ? 'Uploading…' : label}\n </button>\n <input\n ref={inputRef}\n type=\"file\"\n accept={accept}\n multiple={multiple}\n className=\"hidden\"\n onChange={(event) => handleFiles(event.target.files)}\n />\n\n {value.length > 0 && (\n <ul className=\"space-y-1.5\">\n {value.map((file) => (\n <li\n key={file.id}\n className=\"flex items-center justify-between gap-2 rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm\"\n >\n <a\n href={file.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex min-w-0 items-center gap-2 text-text-primary hover:underline\"\n >\n <FileText className=\"h-4 w-4 shrink-0 text-text-secondary\" />\n <span className=\"truncate\">{file.name}</span>\n </a>\n {onRemove && (\n <button\n type=\"button\"\n onClick={() => onRemove(file.id)}\n aria-label={`Remove ${file.name}`}\n className=\"shrink-0 text-text-secondary hover:text-status-error-text\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n )}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;;;AAwBA,IAAa,KAAuC,EAClD,eACA,WAAQ,EAAE,EACV,aACA,WACA,cAAW,IACX,WAAQ,gBACR,mBACI;CACJ,IAAM,EAAE,kBAAe,GAAe,EAChC,IAAW,EAAyB,KAAK,EACzC,CAAC,GAAM,KAAW,EAAS,GAAM,EAEjC,IAAc,OAAO,MAA8B;AACvD,MAAI,CAAC,KAAY,EAAS,WAAW,EAAG;AACxC,IAAQ,GAAK;EACb,IAAM,IAA6B,EAAE;AACrC,MAAI;AACF,QAAK,IAAM,KAAQ,MAAM,KAAK,EAAS,EAAE;IACvC,IAAM,IAAS,MAAM,EAAW,EAAK;AACrC,MAAS,KAAK,EAAO;;AAEvB,GAAI,EAAS,SAAS,MACpB,EAAW,EAAS,EACpB,EAAO,QAAQ,GAAG,EAAS,OAAO,OAAO,EAAS,SAAS,IAAI,MAAM,GAAG,WAAW;WAE9E,GAAK;AACZ,KAAO,MAAM,iBAAkB,EAAc,QAAQ;YAC7C;AAER,GADA,EAAQ,GAAM,EACV,EAAS,YAAS,EAAS,QAAQ,QAAQ;;;AAInD,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,aAAa,EAAU;YAA1C;GACE,kBAAC,UAAD;IACE,MAAK;IACL,UAAU;IACV,eAAe,EAAS,SAAS,OAAO;IACxC,WAAU;cAJZ,CAMG,IAAO,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,GAAG,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA,EACpF,IAAO,eAAe,EAChB;;GACT,kBAAC,SAAD;IACE,KAAK;IACL,MAAK;IACG;IACE;IACV,WAAU;IACV,WAAW,MAAU,EAAY,EAAM,OAAO,MAAM;IACpD,CAAA;GAED,EAAM,SAAS,KACd,kBAAC,MAAD;IAAI,WAAU;cACX,EAAM,KAAK,MACV,kBAAC,MAAD;KAEE,WAAU;eAFZ,CAIE,kBAAC,KAAD;MACE,MAAM,EAAK;MACX,QAAO;MACP,KAAI;MACJ,WAAU;gBAJZ,CAME,kBAAC,GAAD,EAAU,WAAU,wCAAyC,CAAA,EAC7D,kBAAC,QAAD;OAAM,WAAU;iBAAY,EAAK;OAAY,CAAA,CAC3C;SACH,KACC,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAS,EAAK,GAAG;MAChC,cAAY,UAAU,EAAK;MAC3B,WAAU;gBAEV,kBAAC,GAAD,EAAG,WAAU,WAAY,CAAA;MAClB,CAAA,CAER;OAtBE,EAAK,GAsBP,CACL;IACC,CAAA;GAEH"}
@@ -0,0 +1,131 @@
1
+ import { cn as e, formatDate as t } from "../utils/helpers.js";
2
+ import { useCreateEntityComment as n, useEntityComments as r } from "../hooks/platform.js";
3
+ import "../hooks/index.js";
4
+ import { notify as i } from "../lib/toast.js";
5
+ import { useState as a } from "react";
6
+ import { Star as o } from "lucide-react";
7
+ import { jsx as s, jsxs as c } from "react/jsx-runtime";
8
+ import { Button as l } from "@burdenoff/fe-libs/ui";
9
+ //#region src/housingvista/components/ReviewsSection.tsx
10
+ var u = (e) => {
11
+ let t = typeof e == "number" ? e : Number(e);
12
+ return Number.isFinite(t) ? Math.min(5, Math.max(0, Math.round(t))) : 0;
13
+ }, d = ({ value: t, onSelect: n }) => /* @__PURE__ */ s("div", {
14
+ className: "flex items-center gap-0.5",
15
+ children: [
16
+ 1,
17
+ 2,
18
+ 3,
19
+ 4,
20
+ 5
21
+ ].map((r) => {
22
+ let i = /* @__PURE__ */ s(o, { className: e("h-4 w-4", r <= t ? "fill-status-warning text-status-warning" : "text-text-secondary") });
23
+ return n ? /* @__PURE__ */ s("button", {
24
+ type: "button",
25
+ onClick: () => n(r),
26
+ "aria-label": `${r} star`,
27
+ children: i
28
+ }, r) : /* @__PURE__ */ s("span", { children: i }, r);
29
+ })
30
+ }), f = ({ entityId: e, entityType: o, aggregateRating: f, reviewCount: p }) => {
31
+ let { data: m, total: h, loading: g } = r(o, e), { createComment: _, loading: v } = n(), [y, b] = a(0), [x, S] = a("");
32
+ return /* @__PURE__ */ c("section", {
33
+ className: "space-y-4",
34
+ children: [
35
+ /* @__PURE__ */ c("div", {
36
+ className: "flex items-center justify-between",
37
+ children: [/* @__PURE__ */ s("h2", {
38
+ className: "text-lg font-semibold text-text-primary",
39
+ children: "Reviews"
40
+ }), (f ?? 0) > 0 && /* @__PURE__ */ c("div", {
41
+ className: "flex items-center gap-2 text-sm text-text-secondary",
42
+ children: [/* @__PURE__ */ s(d, { value: u(f) }), /* @__PURE__ */ c("span", { children: [Number(f).toFixed(1), p ? ` · ${p} reviews` : ""] })]
43
+ })]
44
+ }),
45
+ /* @__PURE__ */ c("div", {
46
+ className: "rounded-lg border border-border-subtle bg-bg-surface p-4",
47
+ children: [
48
+ /* @__PURE__ */ s("p", {
49
+ className: "mb-2 text-sm font-medium text-text-primary",
50
+ children: "Write a review"
51
+ }),
52
+ /* @__PURE__ */ s(d, {
53
+ value: y,
54
+ onSelect: b
55
+ }),
56
+ /* @__PURE__ */ s("textarea", {
57
+ value: x,
58
+ onChange: (e) => S(e.target.value),
59
+ rows: 3,
60
+ placeholder: "Share your experience with this provider…",
61
+ className: "mt-3 w-full resize-none rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]"
62
+ }),
63
+ /* @__PURE__ */ s("div", {
64
+ className: "mt-2 flex justify-end",
65
+ children: /* @__PURE__ */ s(l, {
66
+ type: "button",
67
+ onClick: async () => {
68
+ if (y === 0) {
69
+ i.warning("Pick a star rating");
70
+ return;
71
+ }
72
+ if (!x.trim()) {
73
+ i.warning("Write a short review");
74
+ return;
75
+ }
76
+ try {
77
+ await _({
78
+ entityType: o,
79
+ entityId: e,
80
+ text: x.trim(),
81
+ metadata: { stars: y }
82
+ }), i.success("Review posted", "Thanks for sharing your feedback."), b(0), S("");
83
+ } catch (e) {
84
+ i.error("Could not post review", e.message);
85
+ }
86
+ },
87
+ disabled: v,
88
+ children: v ? "Posting…" : "Post review"
89
+ })
90
+ })
91
+ ]
92
+ }),
93
+ g ? /* @__PURE__ */ s("p", {
94
+ className: "text-sm text-text-secondary",
95
+ children: "Loading reviews…"
96
+ }) : m.length === 0 ? /* @__PURE__ */ s("p", {
97
+ className: "text-sm text-text-secondary",
98
+ children: "No reviews yet. Be the first to review."
99
+ }) : /* @__PURE__ */ s("ul", {
100
+ className: "space-y-3",
101
+ children: m.map((e) => /* @__PURE__ */ c("li", {
102
+ className: "rounded-lg border border-border-subtle bg-bg-surface p-4",
103
+ children: [/* @__PURE__ */ c("div", {
104
+ className: "mb-1 flex items-center justify-between",
105
+ children: [/* @__PURE__ */ s(d, { value: u(e.metadata?.stars) }), /* @__PURE__ */ s("span", {
106
+ className: "text-xs text-text-secondary",
107
+ children: t(e.createdAt)
108
+ })]
109
+ }), /* @__PURE__ */ s("p", {
110
+ className: "text-sm text-text-primary",
111
+ children: e.text
112
+ })]
113
+ }, e.id))
114
+ }),
115
+ h > m.length && /* @__PURE__ */ c("p", {
116
+ className: "text-xs text-text-secondary",
117
+ children: [
118
+ "Showing ",
119
+ m.length,
120
+ " of ",
121
+ h,
122
+ " reviews."
123
+ ]
124
+ })
125
+ ]
126
+ });
127
+ };
128
+ //#endregion
129
+ export { f as ReviewsSection };
130
+
131
+ //# sourceMappingURL=ReviewsSection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ReviewsSection.js","names":[],"sources":["../../../src/housingvista/components/ReviewsSection.tsx"],"sourcesContent":["/**\n * Reviews / Q&A surface backed by wspace-conversations-svc EntityComments.\n * Displays persisted reviews (a comment whose `metadata.stars` is set) and lets\n * the user submit a real review — no fabricated ratings. The denormalized\n * aggregate (rating/reviewCount) stays for at-a-glance display.\n * @module housingvista\n */\n\nimport { useState, type FC } from 'react';\nimport { Star } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { useEntityComments, useCreateEntityComment } from '../hooks';\nimport { notify } from '../lib/toast';\nimport { cn, formatDate } from '../utils/helpers';\n\ninterface ReviewsSectionProps {\n entityId: string;\n entityType: string;\n /** Denormalized aggregate for the header, if the entity carries one. */\n aggregateRating?: number | null;\n reviewCount?: number | null;\n}\n\nconst toStars = (value: unknown): number => {\n const n = typeof value === 'number' ? value : Number(value);\n return Number.isFinite(n) ? Math.min(5, Math.max(0, Math.round(n))) : 0;\n};\n\nconst StarRow: FC<{ value: number; onSelect?: (v: number) => void }> = ({ value, onSelect }) => (\n <div className=\"flex items-center gap-0.5\">\n {[1, 2, 3, 4, 5].map((star) => {\n const filled = star <= value;\n const icon = (\n <Star className={cn('h-4 w-4', filled ? 'fill-status-warning text-status-warning' : 'text-text-secondary')} />\n );\n return onSelect ? (\n <button key={star} type=\"button\" onClick={() => onSelect(star)} aria-label={`${star} star`}>\n {icon}\n </button>\n ) : (\n <span key={star}>{icon}</span>\n );\n })}\n </div>\n);\n\nexport const ReviewsSection: FC<ReviewsSectionProps> = ({ entityId, entityType, aggregateRating, reviewCount }) => {\n const { data: comments, total, loading } = useEntityComments(entityType, entityId);\n const { createComment, loading: submitting } = useCreateEntityComment();\n const [stars, setStars] = useState(0);\n const [text, setText] = useState('');\n\n const handleSubmit = async () => {\n if (stars === 0) {\n notify.warning('Pick a star rating');\n return;\n }\n if (!text.trim()) {\n notify.warning('Write a short review');\n return;\n }\n try {\n await createComment({ entityType, entityId, text: text.trim(), metadata: { stars } });\n notify.success('Review posted', 'Thanks for sharing your feedback.');\n setStars(0);\n setText('');\n } catch (err) {\n notify.error('Could not post review', (err as Error).message);\n }\n };\n\n return (\n <section className=\"space-y-4\">\n <div className=\"flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-text-primary\">Reviews</h2>\n {(aggregateRating ?? 0) > 0 && (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <StarRow value={toStars(aggregateRating)} />\n <span>\n {Number(aggregateRating).toFixed(1)}\n {reviewCount ? ` · ${reviewCount} reviews` : ''}\n </span>\n </div>\n )}\n </div>\n\n {/* Write a review */}\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <p className=\"mb-2 text-sm font-medium text-text-primary\">Write a review</p>\n <StarRow value={stars} onSelect={setStars} />\n <textarea\n value={text}\n onChange={(event) => setText(event.target.value)}\n rows={3}\n placeholder=\"Share your experience with this provider…\"\n className=\"mt-3 w-full resize-none rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n <div className=\"mt-2 flex justify-end\">\n <Button type=\"button\" onClick={handleSubmit} disabled={submitting}>\n {submitting ? 'Posting…' : 'Post review'}\n </Button>\n </div>\n </div>\n\n {/* Review list */}\n {loading ? (\n <p className=\"text-sm text-text-secondary\">Loading reviews…</p>\n ) : comments.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">No reviews yet. Be the first to review.</p>\n ) : (\n <ul className=\"space-y-3\">\n {comments.map((comment) => (\n <li key={comment.id} className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"mb-1 flex items-center justify-between\">\n <StarRow value={toStars(comment.metadata?.stars)} />\n <span className=\"text-xs text-text-secondary\">{formatDate(comment.createdAt)}</span>\n </div>\n <p className=\"text-sm text-text-primary\">{comment.text}</p>\n </li>\n ))}\n </ul>\n )}\n {total > comments.length && (\n <p className=\"text-xs text-text-secondary\">Showing {comments.length} of {total} reviews.</p>\n )}\n </section>\n );\n};\n"],"mappings":"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"}
@@ -9,3 +9,6 @@ import "./ErrorBoundary.js";
9
9
  import "./Breadcrumbs.js";
10
10
  import "./LoadingSkeleton.js";
11
11
  import "./PropertyCard.js";
12
+ import "./ContactOwnerDialog.js";
13
+ import "./ReviewsSection.js";
14
+ import "./FileUploader.js";