@burdenoff/microfe-ggnomad 2026.830.1 → 2026.1029.1
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/ggnomad/GGNomadRoot.js +4 -2
- package/dist/ggnomad/GGNomadRoot.js.map +1 -1
- package/dist/ggnomad/GGNomadRoutes.js.map +1 -1
- package/dist/ggnomad/components/AuditTrailDialog.js.map +1 -1
- package/dist/ggnomad/components/AvailabilityPanel.js.map +1 -1
- package/dist/ggnomad/components/BookingPanel.js +6 -6
- package/dist/ggnomad/components/BookingPanel.js.map +1 -1
- package/dist/ggnomad/components/CreateProviderDialog.d.ts +22 -0
- package/dist/ggnomad/components/CreateProviderDialog.d.ts.map +1 -0
- package/dist/ggnomad/components/CreateProviderDialog.js +209 -0
- package/dist/ggnomad/components/CreateProviderDialog.js.map +1 -0
- package/dist/ggnomad/components/CreateTravelPolicyDialog.js.map +1 -1
- package/dist/ggnomad/components/GroupTripPanel.js.map +1 -1
- package/dist/ggnomad/components/ListingEditDialog.js.map +1 -1
- package/dist/ggnomad/components/ListingFilters.js.map +1 -1
- package/dist/ggnomad/components/PhotoGallery.js.map +1 -1
- package/dist/ggnomad/components/ProviderCard.js.map +1 -1
- package/dist/ggnomad/components/ProviderDashboardCard.d.ts +28 -0
- package/dist/ggnomad/components/ProviderDashboardCard.d.ts.map +1 -0
- package/dist/ggnomad/components/ProviderDashboardCard.js +222 -0
- package/dist/ggnomad/components/ProviderDashboardCard.js.map +1 -0
- package/dist/ggnomad/components/RatingSummary.js.map +1 -1
- package/dist/ggnomad/components/RedeemTicketPanel.js.map +1 -1
- package/dist/ggnomad/components/ReviewsSection.js.map +1 -1
- package/dist/ggnomad/components/StatusBadge.js.map +1 -1
- package/dist/ggnomad/components/TicketDialog.js +1 -1
- package/dist/ggnomad/components/TicketDialog.js.map +1 -1
- package/dist/ggnomad/components/index.d.ts +2 -0
- package/dist/ggnomad/components/index.d.ts.map +1 -1
- package/dist/ggnomad/components/index.js +26 -23
- package/dist/ggnomad/graphql.d.ts +1 -0
- package/dist/ggnomad/graphql.d.ts.map +1 -1
- package/dist/ggnomad/graphql.js +65 -47
- package/dist/ggnomad/graphql.js.map +1 -1
- package/dist/ggnomad/index.js +3 -2
- package/dist/ggnomad/pages/ActivityDetailsPage.js.map +1 -1
- package/dist/ggnomad/pages/AnalyticsPage.js.map +1 -1
- package/dist/ggnomad/pages/BookingsPage.js.map +1 -1
- package/dist/ggnomad/pages/DashboardPage.js.map +1 -1
- package/dist/ggnomad/pages/EntertainmentPage.js.map +1 -1
- package/dist/ggnomad/pages/EventDetailsPage.js.map +1 -1
- package/dist/ggnomad/pages/FavoritesPage.js.map +1 -1
- package/dist/ggnomad/pages/HospitalityPage.js.map +1 -1
- package/dist/ggnomad/pages/OperationsQueuePage.js.map +1 -1
- package/dist/ggnomad/pages/PageShell.js.map +1 -1
- package/dist/ggnomad/pages/PropertyDetailsPage.js.map +1 -1
- package/dist/ggnomad/pages/ProviderAnalyticsPage.js.map +1 -1
- package/dist/ggnomad/pages/ProviderPage.d.ts.map +1 -1
- package/dist/ggnomad/pages/ProviderPage.js +249 -134
- package/dist/ggnomad/pages/ProviderPage.js.map +1 -1
- package/dist/ggnomad/pages/SearchPage.js +1 -1
- package/dist/ggnomad/pages/SearchPage.js.map +1 -1
- package/dist/ggnomad/pages/StorefrontPage.js.map +1 -1
- package/dist/ggnomad/pages/StorefrontPublicPage.js.map +1 -1
- package/dist/ggnomad/pages/TourDetailsPage.js.map +1 -1
- package/dist/ggnomad/pages/TourismPage.js.map +1 -1
- package/dist/ggnomad/pages/TravelPage.js +1 -1
- package/dist/ggnomad/pages/TravelPage.js.map +1 -1
- package/dist/ggnomad/pages/TravelPolicyPage.js.map +1 -1
- package/dist/ggnomad/pages/TripPlanDetailPage.js.map +1 -1
- package/dist/ggnomad/pages/TripsPage.js.map +1 -1
- package/dist/ggnomad/providers/GGNomadProvider.js.map +1 -1
- package/dist/ggnomad/utils/format.d.ts +18 -0
- package/dist/ggnomad/utils/format.d.ts.map +1 -1
- package/dist/ggnomad/utils/format.js +23 -7
- package/dist/ggnomad/utils/format.js.map +1 -1
- package/dist/ggnomad/utils/notify.js +1 -1
- package/dist/ggnomad/utils/providerListControls.d.ts +53 -0
- package/dist/ggnomad/utils/providerListControls.d.ts.map +1 -0
- package/dist/ggnomad/utils/providerListControls.js +50 -0
- package/dist/ggnomad/utils/providerListControls.js.map +1 -0
- package/dist/ggnomad/utils/providerTypeMeta.d.ts +79 -0
- package/dist/ggnomad/utils/providerTypeMeta.d.ts.map +1 -0
- package/dist/ggnomad/utils/providerTypeMeta.js +81 -0
- package/dist/ggnomad/utils/providerTypeMeta.js.map +1 -0
- package/dist/utils/pageContext.js +1 -1
- package/dist/utils/pageContext.js.map +1 -1
- package/package.json +18 -17
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { formatDate as e, formatRevenue as t } from "../utils/format.js";
|
|
2
|
+
import { bannerTintFor as n, initialsFor as r, labelForProviderType as i, sentenceCase as a } from "../utils/providerTypeMeta.js";
|
|
3
|
+
import { CalendarPlus as o, CircleDot as s, Globe as c, Mail as l, Phone as u, Star as d, Store as f } from "lucide-react";
|
|
4
|
+
import { useI18n as p } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
5
|
+
import { Link as m } from "react-router";
|
|
6
|
+
import { GlassCard as h } from "@burdenoff/fe-libs/ui";
|
|
7
|
+
import { Fragment as g, jsx as _, jsxs as v } from "react/jsx-runtime";
|
|
8
|
+
//#region src/ggnomad/components/ProviderDashboardCard.tsx
|
|
9
|
+
function y({ id: a, name: s, type: g, status: y, description: T, listingCount: E, totalRevenue: D, rating: O, reviewCount: k, contactEmail: A, contactPhone: j, website: M, createdAt: N }) {
|
|
10
|
+
let { t: P } = p(), F = n(g), I = s, L = e(N), R = r(s), z = x(M), B = E >= 1e4 ? new Intl.NumberFormat("en-US", {
|
|
11
|
+
notation: "compact",
|
|
12
|
+
maximumFractionDigits: 1
|
|
13
|
+
}).format(E) : E.toLocaleString(), V = t(D), H = O && Number(O) > 0 ? Number(O).toFixed(1) : null;
|
|
14
|
+
return /* @__PURE__ */ v(h, {
|
|
15
|
+
treatment: "plain",
|
|
16
|
+
className: "flex h-full flex-col overflow-hidden rounded-2xl border border-border-seam",
|
|
17
|
+
children: [/* @__PURE__ */ v("div", {
|
|
18
|
+
className: "relative flex h-[84px] items-start justify-end p-3",
|
|
19
|
+
children: [/* @__PURE__ */ _("div", {
|
|
20
|
+
className: "absolute inset-0",
|
|
21
|
+
style: { backgroundColor: F.backgroundColor },
|
|
22
|
+
"aria-hidden": "true"
|
|
23
|
+
}), /* @__PURE__ */ _("div", {
|
|
24
|
+
className: "relative z-10",
|
|
25
|
+
children: /* @__PURE__ */ _(w, {
|
|
26
|
+
status: y,
|
|
27
|
+
t: P
|
|
28
|
+
})
|
|
29
|
+
})]
|
|
30
|
+
}), /* @__PURE__ */ v("div", {
|
|
31
|
+
className: "relative flex flex-1 flex-col px-[22px] pb-[22px] pt-0",
|
|
32
|
+
children: [
|
|
33
|
+
/* @__PURE__ */ _("div", {
|
|
34
|
+
className: "-mt-[38px] mb-3 flex h-[76px] w-[76px] items-center justify-center overflow-hidden rounded-full border-[4px] border-bg-canvas bg-bg-canvas",
|
|
35
|
+
children: /* @__PURE__ */ _("span", {
|
|
36
|
+
className: "flex size-full items-center justify-center text-2xl font-medium",
|
|
37
|
+
style: F,
|
|
38
|
+
children: R
|
|
39
|
+
})
|
|
40
|
+
}),
|
|
41
|
+
/* @__PURE__ */ _("p", {
|
|
42
|
+
className: "truncate text-xl font-medium text-text-primary",
|
|
43
|
+
title: I,
|
|
44
|
+
children: I
|
|
45
|
+
}),
|
|
46
|
+
/* @__PURE__ */ _("p", {
|
|
47
|
+
className: "mt-0.5 text-sm text-text-secondary",
|
|
48
|
+
children: i(g, P)
|
|
49
|
+
}),
|
|
50
|
+
/* @__PURE__ */ _("p", {
|
|
51
|
+
className: "mt-1.5 min-h-[20px] truncate text-sm " + (T ? "text-text-secondary" : "italic text-text-secondary/60"),
|
|
52
|
+
title: T ?? "",
|
|
53
|
+
children: T || P("ggnomad.provider.noDescription", "No description")
|
|
54
|
+
}),
|
|
55
|
+
/* @__PURE__ */ v("dl", {
|
|
56
|
+
className: "mt-5 grid border-y border-border-seam",
|
|
57
|
+
style: { gridTemplateColumns: "1fr 1.4fr 1fr" },
|
|
58
|
+
children: [
|
|
59
|
+
/* @__PURE__ */ _(S, {
|
|
60
|
+
label: P("ggnomad.provider.listings", "Listings"),
|
|
61
|
+
value: /* @__PURE__ */ _("span", {
|
|
62
|
+
title: String(E),
|
|
63
|
+
className: "min-w-0 truncate",
|
|
64
|
+
children: B
|
|
65
|
+
})
|
|
66
|
+
}),
|
|
67
|
+
/* @__PURE__ */ _(S, {
|
|
68
|
+
label: P("ggnomad.provider.revenue", "Revenue"),
|
|
69
|
+
border: !0,
|
|
70
|
+
value: /* @__PURE__ */ _("span", {
|
|
71
|
+
title: V.exact,
|
|
72
|
+
className: "min-w-0 truncate",
|
|
73
|
+
children: V.display
|
|
74
|
+
})
|
|
75
|
+
}),
|
|
76
|
+
/* @__PURE__ */ _(S, {
|
|
77
|
+
label: P("ggnomad.provider.rating", "Rating"),
|
|
78
|
+
border: !0,
|
|
79
|
+
value: H === null ? /* @__PURE__ */ _("span", {
|
|
80
|
+
className: "min-w-0 truncate text-text-secondary/70",
|
|
81
|
+
title: P("ggnomad.provider.noRatings", "No ratings yet"),
|
|
82
|
+
children: P("ggnomad.provider.ratingNew", "New")
|
|
83
|
+
}) : /* @__PURE__ */ v("span", {
|
|
84
|
+
className: "inline-flex items-center gap-1",
|
|
85
|
+
title: H + (k > 0 ? P("ggnomad.provider.reviewCount", " ({{n}} reviews)", { n: k }) : ""),
|
|
86
|
+
children: [/* @__PURE__ */ _(d, {
|
|
87
|
+
className: "size-4 fill-status-warning-text text-status-warning-text",
|
|
88
|
+
"aria-hidden": "true"
|
|
89
|
+
}), /* @__PURE__ */ _("span", {
|
|
90
|
+
className: "min-w-0 truncate",
|
|
91
|
+
children: H
|
|
92
|
+
})]
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
]
|
|
96
|
+
}),
|
|
97
|
+
/* @__PURE__ */ v("div", {
|
|
98
|
+
className: "mt-4 space-y-2",
|
|
99
|
+
children: [
|
|
100
|
+
/* @__PURE__ */ _(C, {
|
|
101
|
+
icon: l,
|
|
102
|
+
value: A ?? null,
|
|
103
|
+
href: A ? `mailto:${A}` : null,
|
|
104
|
+
label: P("ggnomad.provider.contactEmail", "Email"),
|
|
105
|
+
t: P
|
|
106
|
+
}),
|
|
107
|
+
/* @__PURE__ */ _(C, {
|
|
108
|
+
icon: u,
|
|
109
|
+
value: j ?? null,
|
|
110
|
+
href: j ? `tel:${j}` : null,
|
|
111
|
+
label: P("ggnomad.provider.contactPhone", "Phone"),
|
|
112
|
+
t: P
|
|
113
|
+
}),
|
|
114
|
+
/* @__PURE__ */ _(C, {
|
|
115
|
+
icon: c,
|
|
116
|
+
value: b(M),
|
|
117
|
+
href: z,
|
|
118
|
+
label: P("ggnomad.provider.website", "Website"),
|
|
119
|
+
external: !0,
|
|
120
|
+
t: P
|
|
121
|
+
})
|
|
122
|
+
]
|
|
123
|
+
}),
|
|
124
|
+
/* @__PURE__ */ v("footer", {
|
|
125
|
+
className: "mt-auto pt-5",
|
|
126
|
+
children: [/* @__PURE__ */ v("p", {
|
|
127
|
+
className: "flex items-center gap-1.5 text-[13px] text-text-secondary",
|
|
128
|
+
children: [/* @__PURE__ */ _(o, {
|
|
129
|
+
className: "size-3.5 shrink-0",
|
|
130
|
+
"aria-hidden": "true"
|
|
131
|
+
}), /* @__PURE__ */ _("span", { children: L ? P("ggnomad.provider.joined", "Joined {{date}}").replace("{{date}}", L) : P("ggnomad.provider.joinedEmpty", "Joined —") })]
|
|
132
|
+
}), /* @__PURE__ */ v(m, {
|
|
133
|
+
to: `${a}/storefront`,
|
|
134
|
+
className: "mt-2 flex h-[42px] w-full items-center justify-center gap-1.5 rounded-md border border-border-seam bg-bg-canvas text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2 focus-visible:ring-offset-bg-canvas",
|
|
135
|
+
children: [/* @__PURE__ */ _(f, {
|
|
136
|
+
className: "size-4",
|
|
137
|
+
"aria-hidden": "true"
|
|
138
|
+
}), P("ggnomad.provider.manageStorefront", "Manage storefront")]
|
|
139
|
+
})]
|
|
140
|
+
})
|
|
141
|
+
]
|
|
142
|
+
})]
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function b(e) {
|
|
146
|
+
return e ? e.replace(/^https?:\/\//i, "").replace(/\/+$/, "") : null;
|
|
147
|
+
}
|
|
148
|
+
function x(e) {
|
|
149
|
+
return e && /^https?:\/\//i.test(e) ? e : null;
|
|
150
|
+
}
|
|
151
|
+
function S({ label: e, value: t, border: n }) {
|
|
152
|
+
return /* @__PURE__ */ v("div", {
|
|
153
|
+
className: "flex flex-col items-start py-2.5 pr-3 " + (n ? "border-l border-border-seam pl-3.5" : "pl-0"),
|
|
154
|
+
children: [/* @__PURE__ */ _("dt", {
|
|
155
|
+
className: "text-xs text-text-secondary",
|
|
156
|
+
children: e
|
|
157
|
+
}), /* @__PURE__ */ _("dd", {
|
|
158
|
+
className: "mt-0.5 text-[22px] font-medium tabular-nums text-text-primary",
|
|
159
|
+
children: t
|
|
160
|
+
})]
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function C({ icon: e, value: t, href: n, label: r, external: i, t: a }) {
|
|
164
|
+
let o = !!(n && t), s = /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(e, {
|
|
165
|
+
className: "size-4 shrink-0 text-text-secondary",
|
|
166
|
+
"aria-hidden": "true"
|
|
167
|
+
}), /* @__PURE__ */ _("span", {
|
|
168
|
+
className: "min-w-0 truncate " + (o ? "text-action-primary-bg hover:underline" : "text-text-secondary/60"),
|
|
169
|
+
title: t ?? a("ggnomad.provider.notAddedFor", "{{label}} not added", { label: r }),
|
|
170
|
+
children: t ?? a("ggnomad.provider.notAdded", "Not added")
|
|
171
|
+
})] }), c = "flex h-[22px] items-center gap-2 text-sm";
|
|
172
|
+
return o && n ? /* @__PURE__ */ _("a", {
|
|
173
|
+
href: n,
|
|
174
|
+
target: i ? "_blank" : void 0,
|
|
175
|
+
rel: i ? "noreferrer noopener" : void 0,
|
|
176
|
+
className: c,
|
|
177
|
+
children: s
|
|
178
|
+
}) : /* @__PURE__ */ _("div", {
|
|
179
|
+
className: c,
|
|
180
|
+
style: { cursor: "default" },
|
|
181
|
+
children: s
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function w({ status: e, t }) {
|
|
185
|
+
let { bg: n, fg: r, label: i } = T(e, t);
|
|
186
|
+
return /* @__PURE__ */ v("span", {
|
|
187
|
+
className: "inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium " + n + " " + r,
|
|
188
|
+
children: [/* @__PURE__ */ _(s, {
|
|
189
|
+
className: "size-3",
|
|
190
|
+
"aria-hidden": "true"
|
|
191
|
+
}), i]
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function T(e, t) {
|
|
195
|
+
switch (e) {
|
|
196
|
+
case "ACTIVE": return {
|
|
197
|
+
bg: "bg-status-success-bg-subtle border-status-success-text/40",
|
|
198
|
+
fg: "text-status-success-text",
|
|
199
|
+
label: t("ggnomad.provider.statusActive", "Active")
|
|
200
|
+
};
|
|
201
|
+
case "PENDING": return {
|
|
202
|
+
bg: "bg-status-warning-bg-subtle border-status-warning-text/40",
|
|
203
|
+
fg: "text-status-warning-text",
|
|
204
|
+
label: t("ggnomad.provider.statusPending", "Pending")
|
|
205
|
+
};
|
|
206
|
+
case "SUSPENDED":
|
|
207
|
+
case "ARCHIVED": return {
|
|
208
|
+
bg: "bg-status-error-bg-subtle border-status-error-text/40",
|
|
209
|
+
fg: "text-status-error-text",
|
|
210
|
+
label: t("ggnomad.provider.status" + a(e), a(e))
|
|
211
|
+
};
|
|
212
|
+
default: return {
|
|
213
|
+
bg: "bg-bg-sunken border-border-seam",
|
|
214
|
+
fg: "text-text-secondary",
|
|
215
|
+
label: a(e) || e
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
//#endregion
|
|
220
|
+
export { y as ProviderDashboardCard };
|
|
221
|
+
|
|
222
|
+
//# sourceMappingURL=ProviderDashboardCard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProviderDashboardCard.js","names":[],"sources":["../../../src/ggnomad/components/ProviderDashboardCard.tsx"],"sourcesContent":["import { Link } from 'react-router';\nimport { CalendarPlus, CircleDot, Globe, Mail, Phone, Star, Store } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\nimport { formatDate, formatRevenue } from '../utils/format';\nimport {\n bannerTintFor,\n initialsFor,\n labelForProviderType,\n sentenceCase,\n} from '../utils/providerTypeMeta';\n\ninterface ProviderDashboardCardProps {\n id: string;\n name: string;\n type: string;\n status: string;\n description?: string | null;\n listingCount: number;\n totalRevenue: number;\n rating?: number | null;\n reviewCount: number;\n contactEmail?: string | null;\n contactPhone?: string | null;\n website?: string | null;\n createdAt: string;\n}\n\n/**\n * Provider Dashboard card. Distinct from the shared EntityCard on\n * purpose: EntityCard is built for listing inventory with a 16:10\n * cover image; providers use a flat banner + avatar treatment instead\n * so each operator's card stands apart visually.\n *\n * The banner color is keyed by provider type so the tint reads as a\n * type indicator. Green/red are intentionally not in the banner\n * palette because green means \"Active\" and red means \"error\".\n */\nexport function ProviderDashboardCard({\n id,\n name,\n type,\n status,\n description,\n listingCount,\n totalRevenue,\n rating,\n reviewCount,\n contactEmail,\n contactPhone,\n website,\n createdAt,\n}: ProviderDashboardCardProps) {\n const { t } = useI18n();\n const tint = bannerTintFor(type);\n const displayName = name;\n const joined = formatDate(createdAt);\n const initials = initialsFor(name);\n const websiteHref = safeWebsiteHref(website);\n\n const listingsDisplay =\n listingCount >= 10_000\n ? new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(listingCount)\n : listingCount.toLocaleString();\n const revenue = formatRevenue(totalRevenue);\n const ratingDisplay = rating && Number(rating) > 0 ? Number(rating).toFixed(1) : null;\n\n return (\n <GlassCard\n treatment=\"plain\"\n className=\"flex h-full flex-col overflow-hidden rounded-2xl border border-border-seam\"\n >\n {/* 1. BANNER — solid tinted band, status pill pinned top-right.\n * The aria-hidden wrapper carries only the decorative background\n * so the status badge (its sibling) stays in the accessibility\n * tree — without this scoping, the badge's status text would be\n * invisible to screen readers because aria-hidden cascades to\n * all descendants. */}\n <div className=\"relative flex h-[84px] items-start justify-end p-3\">\n <div\n className=\"absolute inset-0\"\n style={{ backgroundColor: tint.backgroundColor }}\n aria-hidden=\"true\"\n />\n <div className=\"relative z-10\">\n <ProviderDashboardStatusBadge status={status} t={t} />\n </div>\n </div>\n\n {/* Body. The negative top margin pulls the avatar up so it overlaps\n * the banner by ~half its diameter. min-h-[96px] on the avatar row\n * reserves the vertical space so the name/type rows below don't\n * shift on cards without descriptions or contact rows. */}\n <div className=\"relative flex flex-1 flex-col px-[22px] pb-[22px] pt-0\">\n {/* 3. AVATAR — 76px circle, 4px ring in the card surface color. */}\n <div className=\"-mt-[38px] mb-3 flex h-[76px] w-[76px] items-center justify-center overflow-hidden rounded-full border-[4px] border-bg-canvas bg-bg-canvas\">\n <span\n className=\"flex size-full items-center justify-center text-2xl font-medium\"\n style={tint}\n >\n {initials}\n </span>\n </div>\n\n {/* 4. IDENTITY BLOCK — name, type, description (single line, fixed\n * min-height so all cards align). */}\n <p className=\"truncate text-xl font-medium text-text-primary\" title={displayName}>\n {displayName}\n </p>\n <p className=\"mt-0.5 text-sm text-text-secondary\">{labelForProviderType(type, t)}</p>\n <p\n className={\n 'mt-1.5 min-h-[20px] truncate text-sm ' +\n (description ? 'text-text-secondary' : 'italic text-text-secondary/60')\n }\n title={description ?? ''}\n >\n {description || t('ggnomad.provider.noDescription', 'No description')}\n </p>\n\n {/* 5. STATS ROW — three columns, hairline borders above + below and\n * between, no individual tiles. Revenue column gets extra room.\n * Every value gets min-w-0 truncate as a safety net for extreme\n * strings. */}\n <dl\n className=\"mt-5 grid border-y border-border-seam\"\n style={{ gridTemplateColumns: '1fr 1.4fr 1fr' }}\n >\n <ProviderDashboardStat\n label={t('ggnomad.provider.listings', 'Listings')}\n value={\n <span title={String(listingCount)} className=\"min-w-0 truncate\">\n {listingsDisplay}\n </span>\n }\n />\n <ProviderDashboardStat\n label={t('ggnomad.provider.revenue', 'Revenue')}\n border\n value={\n <span title={revenue.exact} className=\"min-w-0 truncate\">\n {revenue.display}\n </span>\n }\n />\n <ProviderDashboardStat\n label={t('ggnomad.provider.rating', 'Rating')}\n border\n value={\n ratingDisplay !== null ? (\n <span\n className=\"inline-flex items-center gap-1\"\n title={\n ratingDisplay +\n (reviewCount > 0\n ? t('ggnomad.provider.reviewCount', ' ({{n}} reviews)', { n: reviewCount })\n : '')\n }\n >\n <Star\n className=\"size-4 fill-status-warning-text text-status-warning-text\"\n aria-hidden=\"true\"\n />\n <span className=\"min-w-0 truncate\">{ratingDisplay}</span>\n </span>\n ) : (\n <span\n className=\"min-w-0 truncate text-text-secondary/70\"\n title={t('ggnomad.provider.noRatings', 'No ratings yet')}\n >\n {t('ggnomad.provider.ratingNew', 'New')}\n </span>\n )\n }\n />\n </dl>\n\n {/* 6. CONTACT ROWS — ALWAYS exactly three, always rendered with the\n * same icon and 22px height so all cards in a row are the same\n * height. Missing values render as \"Not added\" in muted color, no\n * link, no hover, no pointer cursor. */}\n <div className=\"mt-4 space-y-2\">\n <ProviderDashboardContactRow\n icon={Mail}\n value={contactEmail ?? null}\n href={contactEmail ? `mailto:${contactEmail}` : null}\n label={t('ggnomad.provider.contactEmail', 'Email')}\n t={t}\n />\n <ProviderDashboardContactRow\n icon={Phone}\n value={contactPhone ?? null}\n href={contactPhone ? `tel:${contactPhone}` : null}\n label={t('ggnomad.provider.contactPhone', 'Phone')}\n t={t}\n />\n <ProviderDashboardContactRow\n icon={Globe}\n value={formatWebsiteDisplay(website)}\n href={websiteHref}\n label={t('ggnomad.provider.website', 'Website')}\n external\n t={t}\n />\n </div>\n\n {/* 7. FOOTER — pinned to the bottom of the card. Date on its own\n * line, then a full-width outlined secondary button. */}\n <footer className=\"mt-auto pt-5\">\n <p className=\"flex items-center gap-1.5 text-[13px] text-text-secondary\">\n <CalendarPlus className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n <span>\n {joined\n ? t('ggnomad.provider.joined', 'Joined {{date}}').replace('{{date}}', joined)\n : t('ggnomad.provider.joinedEmpty', 'Joined —')}\n </span>\n </p>\n <Link\n to={`${id}/storefront`}\n className=\"mt-2 flex h-[42px] w-full items-center justify-center gap-1.5 rounded-md border border-border-seam bg-bg-canvas text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2 focus-visible:ring-offset-bg-canvas\"\n >\n <Store className=\"size-4\" aria-hidden=\"true\" />\n {t('ggnomad.provider.manageStorefront', 'Manage storefront')}\n </Link>\n </footer>\n </div>\n </GlassCard>\n );\n}\n\n/**\n * Strip the protocol and trailing slash from a URL for display, e.g.\n * \"https://chrisleverseo.com/tools/random-url-generator/\" becomes\n * \"chrisleverseo.com/tools/random-url-generator\". Never used for the\n * actual href, only the visible label, so this can be aggressive.\n */\nfunction formatWebsiteDisplay(url: string | null | undefined): string | null {\n if (!url) return null;\n return url.replace(/^https?:\\/\\//i, '').replace(/\\/+$/, '');\n}\n\n/**\n * Only return the URL as a clickable href if it starts with http(s)://.\n * Providers created via CLI/MCP don't go through the create-dialog input\n * validator, so a malformed value like \"acme.com\" would otherwise be passed\n * to the anchor's `href` and break navigation. Returns null otherwise so\n * the row renders as plain text instead.\n */\nfunction safeWebsiteHref(url: string | null | undefined): string | null {\n if (!url) return null;\n return /^https?:\\/\\//i.test(url) ? url : null;\n}\n\ninterface StatProps {\n label: string;\n value: React.ReactNode;\n /** Render the left vertical divider for non-first columns. */\n border?: boolean;\n}\n\nfunction ProviderDashboardStat({ label, value, border }: StatProps) {\n // Spec: each column left-aligns label and value, gets 14px left padding\n // except the first, and a hairline vertical divider on cols 2 & 3. The\n // parent <dl> already supplies the top + bottom border; this cell only\n // owns its left divider + padding. Right-edge padding is added in the\n // Rating column (the last one) so the row is balanced.\n return (\n <div\n className={\n 'flex flex-col items-start py-2.5 pr-3 ' +\n (border ? 'border-l border-border-seam pl-3.5' : 'pl-0')\n }\n >\n <dt className=\"text-xs text-text-secondary\">{label}</dt>\n <dd className=\"mt-0.5 text-[22px] font-medium tabular-nums text-text-primary\">{value}</dd>\n </div>\n );\n}\n\ninterface ContactRowProps {\n icon: typeof Mail;\n value: string | null;\n href: string | null;\n label: string;\n external?: boolean;\n t: (key: string, fallback: string, vars?: Record<string, string | number>) => string;\n}\n\nfunction ProviderDashboardContactRow({\n icon: Icon,\n value,\n href,\n label,\n external,\n t,\n}: ContactRowProps) {\n const isLink = Boolean(href && value);\n const content = (\n <>\n <Icon className=\"size-4 shrink-0 text-text-secondary\" aria-hidden=\"true\" />\n <span\n className={\n 'min-w-0 truncate ' +\n (isLink ? 'text-action-primary-bg hover:underline' : 'text-text-secondary/60')\n }\n title={value ?? t('ggnomad.provider.notAddedFor', '{{label}} not added', { label })}\n >\n {value ?? t('ggnomad.provider.notAdded', 'Not added')}\n </span>\n </>\n );\n\n const rowStyle = 'flex h-[22px] items-center gap-2 text-sm';\n\n if (isLink && href) {\n return (\n <a\n href={href}\n target={external ? '_blank' : undefined}\n rel={external ? 'noreferrer noopener' : undefined}\n className={rowStyle}\n >\n {content}\n </a>\n );\n }\n // Missing value — render as plain text, no link, no hover, no pointer.\n return (\n <div className={rowStyle} style={{ cursor: 'default' }}>\n {content}\n </div>\n );\n}\n\ninterface StatusBadgeProps {\n status: string;\n t: (key: string, fallback: string) => string;\n}\n\n/**\n * Small status pill that lives inside the banner top-right. Inline here\n * (not the shared StatusBadge) because the shared component takes the\n * raw uppercase enum and renders the dot inline with the label; we\n * want sentence-case for the card.\n */\nfunction ProviderDashboardStatusBadge({ status, t }: StatusBadgeProps) {\n const { bg, fg, label } = statusTone(status, t);\n return (\n <span\n className={\n 'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium ' +\n bg +\n ' ' +\n fg\n }\n >\n <CircleDot className=\"size-3\" aria-hidden=\"true\" />\n {label}\n </span>\n );\n}\n\ninterface StatusTone {\n bg: string;\n fg: string;\n label: string;\n}\n\nfunction statusTone(status: string, t: (key: string, fallback: string) => string): StatusTone {\n switch (status) {\n case 'ACTIVE':\n return {\n bg: 'bg-status-success-bg-subtle border-status-success-text/40',\n fg: 'text-status-success-text',\n label: t('ggnomad.provider.statusActive', 'Active'),\n };\n case 'PENDING':\n return {\n bg: 'bg-status-warning-bg-subtle border-status-warning-text/40',\n fg: 'text-status-warning-text',\n label: t('ggnomad.provider.statusPending', 'Pending'),\n };\n case 'SUSPENDED':\n case 'ARCHIVED':\n return {\n bg: 'bg-status-error-bg-subtle border-status-error-text/40',\n fg: 'text-status-error-text',\n label: t('ggnomad.provider.status' + sentenceCase(status), sentenceCase(status)),\n };\n default:\n return {\n bg: 'bg-bg-sunken border-border-seam',\n fg: 'text-text-secondary',\n label: sentenceCase(status) || status,\n };\n }\n}\n"],"mappings":";;;;;;;;AAuCA,SAAgB,EAAsB,EACpC,OACA,SACA,SACA,WACA,gBACA,iBACA,iBACA,WACA,gBACA,iBACA,iBACA,YACA,gBAC6B;CAC7B,IAAM,EAAE,SAAM,EAAQ,GAChB,IAAO,EAAc,CAAI,GACzB,IAAc,GACd,IAAS,EAAW,CAAS,GAC7B,IAAW,EAAY,CAAI,GAC3B,IAAc,EAAgB,CAAO,GAErC,IACJ,KAAgB,MACZ,IAAI,KAAK,aAAa,SAAS;EAC7B,UAAU;EACV,uBAAuB;CACzB,CAAC,CAAC,CAAC,OAAO,CAAY,IACtB,EAAa,eAAe,GAC5B,IAAU,EAAc,CAAY,GACpC,IAAgB,KAAU,OAAO,CAAM,IAAI,IAAI,OAAO,CAAM,CAAC,CAAC,QAAQ,CAAC,IAAI;CAEjF,OACE,kBAAC,GAAD;EACE,WAAU;EACV,WAAU;YAFZ,CAUE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IACE,WAAU;IACV,OAAO,EAAE,iBAAiB,EAAK,gBAAgB;IAC/C,eAAY;GACb,CAAA,GACD,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAAsC;KAAW;IAAI,CAAA;GAClD,CAAA,CACF;MAML,kBAAC,OAAD;GAAK,WAAU;aAAf;IAEE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,QAAD;MACE,WAAU;MACV,OAAO;gBAEN;KACG,CAAA;IACH,CAAA;IAIL,kBAAC,KAAD;KAAG,WAAU;KAAiD,OAAO;eAClE;IACA,CAAA;IACH,kBAAC,KAAD;KAAG,WAAU;eAAsC,EAAqB,GAAM,CAAC;IAAK,CAAA;IACpF,kBAAC,KAAD;KACE,WACE,2CACC,IAAc,wBAAwB;KAEzC,OAAO,KAAe;eAErB,KAAe,EAAE,kCAAkC,gBAAgB;IACnE,CAAA;IAMH,kBAAC,MAAD;KACE,WAAU;KACV,OAAO,EAAE,qBAAqB,gBAAgB;eAFhD;MAIE,kBAAC,GAAD;OACE,OAAO,EAAE,6BAA6B,UAAU;OAChD,OACE,kBAAC,QAAD;QAAM,OAAO,OAAO,CAAY;QAAG,WAAU;kBAC1C;OACG,CAAA;MAET,CAAA;MACD,kBAAC,GAAD;OACE,OAAO,EAAE,4BAA4B,SAAS;OAC9C,QAAA;OACA,OACE,kBAAC,QAAD;QAAM,OAAO,EAAQ;QAAO,WAAU;kBACnC,EAAQ;OACL,CAAA;MAET,CAAA;MACD,kBAAC,GAAD;OACE,OAAO,EAAE,2BAA2B,QAAQ;OAC5C,QAAA;OACA,OACE,MAAkB,OAiBhB,kBAAC,QAAD;QACE,WAAU;QACV,OAAO,EAAE,8BAA8B,gBAAgB;kBAEtD,EAAE,8BAA8B,KAAK;OAClC,CAAA,IArBN,kBAAC,QAAD;QACE,WAAU;QACV,OACE,KACC,IAAc,IACX,EAAE,gCAAgC,oBAAoB,EAAE,GAAG,EAAY,CAAC,IACxE;kBANR,CASE,kBAAC,GAAD;SACE,WAAU;SACV,eAAY;QACb,CAAA,GACD,kBAAC,QAAD;SAAM,WAAU;mBAAoB;QAAoB,CAAA,CACpD;;MAUX,CAAA;KACC;;IAMJ,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD;OACE,MAAM;OACN,OAAO,KAAgB;OACvB,MAAM,IAAe,UAAU,MAAiB;OAChD,OAAO,EAAE,iCAAiC,OAAO;OAC9C;MACJ,CAAA;MACD,kBAAC,GAAD;OACE,MAAM;OACN,OAAO,KAAgB;OACvB,MAAM,IAAe,OAAO,MAAiB;OAC7C,OAAO,EAAE,iCAAiC,OAAO;OAC9C;MACJ,CAAA;MACD,kBAAC,GAAD;OACE,MAAM;OACN,OAAO,EAAqB,CAAO;OACnC,MAAM;OACN,OAAO,EAAE,4BAA4B,SAAS;OAC9C,UAAA;OACG;MACJ,CAAA;KACE;;IAIL,kBAAC,UAAD;KAAQ,WAAU;eAAlB,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAb,CACE,kBAAC,GAAD;OAAc,WAAU;OAAoB,eAAY;MAAQ,CAAA,GAChE,kBAAC,QAAD,EAAA,UACG,IACG,EAAE,2BAA2B,iBAAiB,CAAC,CAAC,QAAQ,YAAY,CAAM,IAC1E,EAAE,gCAAgC,UAAU,EAC5C,CAAA,CACL;SACH,kBAAC,GAAD;MACE,IAAI,GAAG,EAAG;MACV,WAAU;gBAFZ,CAIE,kBAAC,GAAD;OAAO,WAAU;OAAS,eAAY;MAAQ,CAAA,GAC7C,EAAE,qCAAqC,mBAAmB,CACvD;OACA;;GACL;IACI;;AAEf;AAQA,SAAS,EAAqB,GAA+C;CAE3E,OADK,IACE,EAAI,QAAQ,iBAAiB,EAAE,CAAC,CAAC,QAAQ,QAAQ,EAAE,IADzC;AAEnB;AASA,SAAS,EAAgB,GAA+C;CAEtE,OADK,KACE,gBAAgB,KAAK,CAAG,IAAI,IAAM;AAC3C;AASA,SAAS,EAAsB,EAAE,UAAO,UAAO,aAAqB;CAMlE,OACE,kBAAC,OAAD;EACE,WACE,4CACC,IAAS,uCAAuC;YAHrD,CAME,kBAAC,MAAD;GAAI,WAAU;aAA+B;EAAU,CAAA,GACvD,kBAAC,MAAD;GAAI,WAAU;aAAiE;EAAU,CAAA,CACtF;;AAET;AAWA,SAAS,EAA4B,EACnC,MAAM,GACN,UACA,SACA,UACA,aACA,QACkB;CAClB,IAAM,IAAS,GAAQ,KAAQ,IACzB,IACJ,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;EAAM,WAAU;EAAsC,eAAY;CAAQ,CAAA,GAC1E,kBAAC,QAAD;EACE,WACE,uBACC,IAAS,2CAA2C;EAEvD,OAAO,KAAS,EAAE,gCAAgC,uBAAuB,EAAE,SAAM,CAAC;YAEjF,KAAS,EAAE,6BAA6B,WAAW;CAChD,CAAA,CACN,EAAA,CAAA,GAGE,IAAW;CAejB,OAbI,KAAU,IAEV,kBAAC,KAAD;EACQ;EACN,QAAQ,IAAW,WAAW,KAAA;EAC9B,KAAK,IAAW,wBAAwB,KAAA;EACxC,WAAW;YAEV;CACA,CAAA,IAKL,kBAAC,OAAD;EAAK,WAAW;EAAU,OAAO,EAAE,QAAQ,UAAU;YAClD;CACE,CAAA;AAET;AAaA,SAAS,EAA6B,EAAE,WAAQ,KAAuB;CACrE,IAAM,EAAE,OAAI,OAAI,aAAU,EAAW,GAAQ,CAAC;CAC9C,OACE,kBAAC,QAAD;EACE,WACE,wFACA,IACA,MACA;YALJ,CAQE,kBAAC,GAAD;GAAW,WAAU;GAAS,eAAY;EAAQ,CAAA,GACjD,CACG;;AAEV;AAQA,SAAS,EAAW,GAAgB,GAA0D;CAC5F,QAAQ,GAAR;EACE,KAAK,UACH,OAAO;GACL,IAAI;GACJ,IAAI;GACJ,OAAO,EAAE,iCAAiC,QAAQ;EACpD;EACF,KAAK,WACH,OAAO;GACL,IAAI;GACJ,IAAI;GACJ,OAAO,EAAE,kCAAkC,SAAS;EACtD;EACF,KAAK;EACL,KAAK,YACH,OAAO;GACL,IAAI;GACJ,IAAI;GACJ,OAAO,EAAE,4BAA4B,EAAa,CAAM,GAAG,EAAa,CAAM,CAAC;EACjF;EACF,SACE,OAAO;GACL,IAAI;GACJ,IAAI;GACJ,OAAO,EAAa,CAAM,KAAK;EACjC;CACJ;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RatingSummary.js","names":[],"sources":["../../../src/ggnomad/components/RatingSummary.tsx"],"sourcesContent":["import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Star } from 'lucide-react';\n\nimport { cn } from '../utils/cn';\n\ninterface RatingSummaryProps {\n /** `rating` is nullable on every GGNomad listing type — null means \"not rated yet\". */\n rating?: number | null;\n reviewCount?: number | null;\n className?: string;\n}\n\n/**\n * Rating + review count, the pair every listing type carries (`rating: Float`,\n * `reviewCount: Int!`). Shows the unrated state explicitly instead of printing a\n * misleading zero.\n */\nexport function RatingSummary({ rating, reviewCount, className }: RatingSummaryProps) {\n const { t } = useI18n();\n const count = reviewCount ?? 0;\n\n if (rating === null || rating === undefined) {\n return (\n <span className={cn('text-sm text-text-secondary', className)}>\n {t('ggnomad.rating.none', 'Not rated yet')}\n </span>\n );\n }\n\n return (\n <span className={cn('inline-flex items-center gap-1.5 text-sm', className)}>\n <Star\n className=\"size-3.5 fill-status-warning-text text-status-warning-text\"\n aria-hidden=\"true\"\n />\n <span className=\"font-medium tabular-nums text-text-primary\">{rating.toFixed(1)}</span>\n <span className=\"tabular-nums text-text-secondary\">\n {count === 1\n ? t('ggnomad.rating.oneReview', '(1 review)')\n : t('ggnomad.rating.reviews', '({{n}} reviews)').replace('{{n}}', count.toLocaleString())}\n </span>\n </span>\n );\n}\n"],"mappings":";;;;;AAiBA,SAAgB,EAAc,EAAE,WAAQ,gBAAa,gBAAiC;CACpF,IAAM,EAAE,SAAM,EAAQ,GAChB,IAAQ,KAAe;CAU7B,OARI,KAAW,OAEX,kBAAC,QAAD;EAAM,WAAW,EAAG,+BAA+B,CAAS;YACzD,EAAE,uBAAuB,eAAe;CACrC,CAAA,IAKR,kBAAC,QAAD;EAAM,WAAW,EAAG,4CAA4C,CAAS;YAAzE;GACE,kBAAC,GAAD;IACE,WAAU;IACV,eAAY;GACb,CAAA;GACD,kBAAC,QAAD;IAAM,WAAU;cAA8C,EAAO,QAAQ,CAAC;GAAQ,CAAA;GACtF,kBAAC,QAAD;IAAM,WAAU;cACb,MAAU,IACP,EAAE,4BAA4B,YAAY,IAC1C,EAAE,0BAA0B,iBAAiB,
|
|
1
|
+
{"version":3,"file":"RatingSummary.js","names":[],"sources":["../../../src/ggnomad/components/RatingSummary.tsx"],"sourcesContent":["import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Star } from 'lucide-react';\n\nimport { cn } from '../utils/cn';\n\ninterface RatingSummaryProps {\n /** `rating` is nullable on every GGNomad listing type — null means \"not rated yet\". */\n rating?: number | null;\n reviewCount?: number | null;\n className?: string;\n}\n\n/**\n * Rating + review count, the pair every listing type carries (`rating: Float`,\n * `reviewCount: Int!`). Shows the unrated state explicitly instead of printing a\n * misleading zero.\n */\nexport function RatingSummary({ rating, reviewCount, className }: RatingSummaryProps) {\n const { t } = useI18n();\n const count = reviewCount ?? 0;\n\n if (rating === null || rating === undefined) {\n return (\n <span className={cn('text-sm text-text-secondary', className)}>\n {t('ggnomad.rating.none', 'Not rated yet')}\n </span>\n );\n }\n\n return (\n <span className={cn('inline-flex items-center gap-1.5 text-sm', className)}>\n <Star\n className=\"size-3.5 fill-status-warning-text text-status-warning-text\"\n aria-hidden=\"true\"\n />\n <span className=\"font-medium tabular-nums text-text-primary\">{rating.toFixed(1)}</span>\n <span className=\"tabular-nums text-text-secondary\">\n {count === 1\n ? t('ggnomad.rating.oneReview', '(1 review)')\n : t('ggnomad.rating.reviews', '({{n}} reviews)').replace('{{n}}', count.toLocaleString())}\n </span>\n </span>\n );\n}\n"],"mappings":";;;;;AAiBA,SAAgB,EAAc,EAAE,WAAQ,gBAAa,gBAAiC;CACpF,IAAM,EAAE,SAAM,EAAQ,GAChB,IAAQ,KAAe;CAU7B,OARI,KAAW,OAEX,kBAAC,QAAD;EAAM,WAAW,EAAG,+BAA+B,CAAS;YACzD,EAAE,uBAAuB,eAAe;CACrC,CAAA,IAKR,kBAAC,QAAD;EAAM,WAAW,EAAG,4CAA4C,CAAS;YAAzE;GACE,kBAAC,GAAD;IACE,WAAU;IACV,eAAY;GACb,CAAA;GACD,kBAAC,QAAD;IAAM,WAAU;cAA8C,EAAO,QAAQ,CAAC;GAAQ,CAAA;GACtF,kBAAC,QAAD;IAAM,WAAU;cACb,MAAU,IACP,EAAE,4BAA4B,YAAY,IAC1C,EAAE,0BAA0B,iBAAiB,CAAC,CAAC,QAAQ,SAAS,EAAM,eAAe,CAAC;GACtF,CAAA;EACF;;AAEV"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RedeemTicketPanel.js","names":[],"sources":["../../../src/ggnomad/components/RedeemTicketPanel.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ScanLine } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { REDEEM_GGNOMAD_TICKET_MUTATION } from '../graphql';\nimport { notify } from '../utils/notify';\n\ninterface RedeemedTicket {\n id: string;\n bookingId: string;\n ticketCode: string;\n status: string;\n redeemedAt: string | null;\n redeemedBy: string | null;\n}\n\ninterface RedeemTicketPanelProps {\n onRedeemed?: () => void;\n}\n\n/**\n * Provider-facing check-in panel (BOFF-4362). A venue operator pastes/scans a\n * guest's QR token here to validate + redeem it — the same\n * `redeemGgnomadTicket` mutation a dedicated scanner device would call.\n * Redemption is server-side validated (signature + live ticket status), so\n * this panel never has to trust anything about the token itself.\n */\nexport function RedeemTicketPanel({ onRedeemed }: RedeemTicketPanelProps) {\n const { t } = useI18n();\n const [token, setToken] = useState('');\n\n const [redeemTicket, { loading }] = useMutation(\n REDEEM_GGNOMAD_TICKET_MUTATION as TypedDocumentNode<\n { redeemGgnomadTicket: RedeemedTicket },\n { token: string }\n >\n );\n\n const handleRedeem = async () => {\n const trimmed = token.trim();\n if (!trimmed) return;\n\n try {\n const result = await redeemTicket({ variables: { token: trimmed } });\n if (result.error) {\n notify.error(\n t('ggnomad.redeem.errorTitle', 'Could not redeem ticket'),\n result.error.message\n );\n return;\n }\n const redeemed = result.data?.redeemGgnomadTicket;\n notify.success(\n t('ggnomad.redeem.successTitle', 'Guest checked in'),\n redeemed\n ? t('ggnomad.redeem.successBody', 'Ticket {{code}} redeemed.').replace(\n '{{code}}',\n redeemed.ticketCode\n )\n : undefined\n );\n setToken('');\n onRedeemed?.();\n } catch (err) {\n notify.error(\n t('ggnomad.redeem.errorTitle', 'Could not redeem ticket'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n return (\n <GlassCard treatment=\"glass\" className=\"border-border-seam p-5\">\n <div className=\"mb-3 flex items-center gap-2\">\n <ScanLine className=\"size-4 text-text-secondary\" aria-hidden=\"true\" />\n <h2 className=\"text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.redeem.title', 'Check in a guest')}\n </h2>\n </div>\n <p className=\"mb-4 text-sm text-text-secondary\">\n {t(\n 'ggnomad.redeem.purpose',\n 'Paste the token from a scanned QR ticket to validate and check the guest in. Revoked or already-redeemed tickets are rejected automatically.'\n )}\n </p>\n <form\n className=\"flex flex-col gap-2 sm:flex-row\"\n onSubmit={(event) => {\n event.preventDefault();\n void handleRedeem();\n }}\n >\n <input\n type=\"text\"\n value={token}\n onChange={(e) => setToken(e.target.value)}\n disabled={loading}\n placeholder={t('ggnomad.redeem.placeholder', 'Scanned ticket token')}\n aria-label={t('ggnomad.redeem.inputLabel', 'Ticket token')}\n className=\"min-w-0 flex-1 rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm font-mono text-text-primary placeholder-text-secondary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg disabled:opacity-50\"\n />\n <button\n type=\"submit\"\n disabled={loading || !token.trim()}\n className=\"shrink-0 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bgHover disabled:opacity-50\"\n >\n {loading\n ? t('ggnomad.redeem.checking', 'Checking in…')\n : t('ggnomad.redeem.action', 'Check in')}\n </button>\n </form>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;;;;AA8BA,SAAgB,EAAkB,EAAE,iBAAsC;CACxE,IAAM,EAAE,SAAM,EAAQ,GAChB,CAAC,GAAO,KAAY,EAAS,EAAE,GAE/B,CAAC,GAAc,EAAE,gBAAa,EAClC,CAIF,GAEM,IAAe,YAAY;EAC/B,IAAM,IAAU,EAAM,KAAK;EACtB,OAEL,IAAI;GACF,IAAM,IAAS,MAAM,EAAa,EAAE,WAAW,EAAE,OAAO,EAAQ,EAAE,CAAC;GACnE,IAAI,EAAO,OAAO;IAChB,EAAO,MACL,EAAE,6BAA6B,yBAAyB,GACxD,EAAO,MAAM,OACf;IACA;GACF;GACA,IAAM,IAAW,EAAO,MAAM;GAW9B,AAVA,EAAO,QACL,EAAE,+BAA+B,kBAAkB,GACnD,IACI,EAAE,8BAA8B,2BAA2B,
|
|
1
|
+
{"version":3,"file":"RedeemTicketPanel.js","names":[],"sources":["../../../src/ggnomad/components/RedeemTicketPanel.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ScanLine } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { REDEEM_GGNOMAD_TICKET_MUTATION } from '../graphql';\nimport { notify } from '../utils/notify';\n\ninterface RedeemedTicket {\n id: string;\n bookingId: string;\n ticketCode: string;\n status: string;\n redeemedAt: string | null;\n redeemedBy: string | null;\n}\n\ninterface RedeemTicketPanelProps {\n onRedeemed?: () => void;\n}\n\n/**\n * Provider-facing check-in panel (BOFF-4362). A venue operator pastes/scans a\n * guest's QR token here to validate + redeem it — the same\n * `redeemGgnomadTicket` mutation a dedicated scanner device would call.\n * Redemption is server-side validated (signature + live ticket status), so\n * this panel never has to trust anything about the token itself.\n */\nexport function RedeemTicketPanel({ onRedeemed }: RedeemTicketPanelProps) {\n const { t } = useI18n();\n const [token, setToken] = useState('');\n\n const [redeemTicket, { loading }] = useMutation(\n REDEEM_GGNOMAD_TICKET_MUTATION as TypedDocumentNode<\n { redeemGgnomadTicket: RedeemedTicket },\n { token: string }\n >\n );\n\n const handleRedeem = async () => {\n const trimmed = token.trim();\n if (!trimmed) return;\n\n try {\n const result = await redeemTicket({ variables: { token: trimmed } });\n if (result.error) {\n notify.error(\n t('ggnomad.redeem.errorTitle', 'Could not redeem ticket'),\n result.error.message\n );\n return;\n }\n const redeemed = result.data?.redeemGgnomadTicket;\n notify.success(\n t('ggnomad.redeem.successTitle', 'Guest checked in'),\n redeemed\n ? t('ggnomad.redeem.successBody', 'Ticket {{code}} redeemed.').replace(\n '{{code}}',\n redeemed.ticketCode\n )\n : undefined\n );\n setToken('');\n onRedeemed?.();\n } catch (err) {\n notify.error(\n t('ggnomad.redeem.errorTitle', 'Could not redeem ticket'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n return (\n <GlassCard treatment=\"glass\" className=\"border-border-seam p-5\">\n <div className=\"mb-3 flex items-center gap-2\">\n <ScanLine className=\"size-4 text-text-secondary\" aria-hidden=\"true\" />\n <h2 className=\"text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.redeem.title', 'Check in a guest')}\n </h2>\n </div>\n <p className=\"mb-4 text-sm text-text-secondary\">\n {t(\n 'ggnomad.redeem.purpose',\n 'Paste the token from a scanned QR ticket to validate and check the guest in. Revoked or already-redeemed tickets are rejected automatically.'\n )}\n </p>\n <form\n className=\"flex flex-col gap-2 sm:flex-row\"\n onSubmit={(event) => {\n event.preventDefault();\n void handleRedeem();\n }}\n >\n <input\n type=\"text\"\n value={token}\n onChange={(e) => setToken(e.target.value)}\n disabled={loading}\n placeholder={t('ggnomad.redeem.placeholder', 'Scanned ticket token')}\n aria-label={t('ggnomad.redeem.inputLabel', 'Ticket token')}\n className=\"min-w-0 flex-1 rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm font-mono text-text-primary placeholder-text-secondary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg disabled:opacity-50\"\n />\n <button\n type=\"submit\"\n disabled={loading || !token.trim()}\n className=\"shrink-0 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bgHover disabled:opacity-50\"\n >\n {loading\n ? t('ggnomad.redeem.checking', 'Checking in…')\n : t('ggnomad.redeem.action', 'Check in')}\n </button>\n </form>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;;;;AA8BA,SAAgB,EAAkB,EAAE,iBAAsC;CACxE,IAAM,EAAE,SAAM,EAAQ,GAChB,CAAC,GAAO,KAAY,EAAS,EAAE,GAE/B,CAAC,GAAc,EAAE,gBAAa,EAClC,CAIF,GAEM,IAAe,YAAY;EAC/B,IAAM,IAAU,EAAM,KAAK;EACtB,OAEL,IAAI;GACF,IAAM,IAAS,MAAM,EAAa,EAAE,WAAW,EAAE,OAAO,EAAQ,EAAE,CAAC;GACnE,IAAI,EAAO,OAAO;IAChB,EAAO,MACL,EAAE,6BAA6B,yBAAyB,GACxD,EAAO,MAAM,OACf;IACA;GACF;GACA,IAAM,IAAW,EAAO,MAAM;GAW9B,AAVA,EAAO,QACL,EAAE,+BAA+B,kBAAkB,GACnD,IACI,EAAE,8BAA8B,2BAA2B,CAAC,CAAC,QAC3D,YACA,EAAS,UACX,IACA,KAAA,CACN,GACA,EAAS,EAAE,GACX,IAAa;EACf,SAAS,GAAK;GACZ,EAAO,MACL,EAAE,6BAA6B,yBAAyB,GACxD,aAAe,QAAQ,EAAI,UAAU,OAAO,CAAG,CACjD;EACF;CACF;CAEA,OACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YAAvC;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAU,WAAU;KAA6B,eAAY;IAAQ,CAAA,GACrE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAE,wBAAwB,kBAAkB;IAC3C,CAAA,CACD;;GACL,kBAAC,KAAD;IAAG,WAAU;cACV,EACC,0BACA,8IACF;GACC,CAAA;GACH,kBAAC,QAAD;IACE,WAAU;IACV,WAAW,MAAU;KAEnB,AADA,EAAM,eAAe,GACrB,EAAkB;IACpB;cALF,CAOE,kBAAC,SAAD;KACE,MAAK;KACL,OAAO;KACP,WAAW,MAAM,EAAS,EAAE,OAAO,KAAK;KACxC,UAAU;KACV,aAAa,EAAE,8BAA8B,sBAAsB;KACnE,cAAY,EAAE,6BAA6B,cAAc;KACzD,WAAU;IACX,CAAA,GACD,kBAAC,UAAD;KACE,MAAK;KACL,UAAU,KAAW,CAAC,EAAM,KAAK;KACjC,WAAU;eAET,IACG,EAAE,2BAA2B,cAAc,IAC3C,EAAE,yBAAyB,UAAU;IACnC,CAAA,CACJ;;EACG;;AAEf"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReviewsSection.js","names":[],"sources":["../../../src/ggnomad/components/ReviewsSection.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Star, MessageSquare, AlertCircle } from 'lucide-react';\nimport { Button, GlassCard, Input, Textarea } from '@burdenoff/fe-libs/ui';\n\nimport {\n GGNOMAD_REVIEW_SUMMARY_QUERY,\n GGNOMAD_REVIEWS_QUERY,\n CREATE_GGNOMAD_REVIEW_MUTATION,\n} from '../graphql';\nimport { notify } from '../utils/notify';\nimport { cn } from '../utils/cn';\n\ntype GgnomadBookingType = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ntype ReviewsSectionProps = {\n targetType: GgnomadBookingType;\n targetId: string;\n};\n\ntype ReviewSummaryData = {\n ggnomadReviewSummary: {\n average: number;\n count: number;\n distribution: number[];\n };\n};\n\ntype ReviewsData = {\n ggnomadReviews: Array<{\n id: string;\n rating: number;\n title?: string;\n content?: string;\n isVerified: boolean;\n helpfulCount: number;\n response?: string;\n respondedAt?: string;\n createdAt: string;\n }>;\n};\n\ntype CreateReviewInput = {\n targetType: GgnomadBookingType;\n targetId: string;\n rating: number;\n title?: string;\n content?: string;\n images?: string[];\n};\n\n/**\n * Star distribution bar chart component.\n * Displays a 5-row breakdown of rating distribution.\n */\nfunction StarDistributionChart({ distribution }: { distribution: number[] }) {\n const total = distribution.reduce((a, b) => a + b, 0);\n\n return (\n <div className=\"space-y-2\">\n {[5, 4, 3, 2, 1].map((stars, idx) => {\n const count = distribution[4 - idx] ?? 0;\n const percentage = total > 0 ? (count / total) * 100 : 0;\n\n return (\n <div key={stars} className=\"flex items-center gap-2\">\n <div className=\"flex items-center gap-1 whitespace-nowrap text-xs text-text-secondary\">\n <span>{stars}</span>\n <Star className=\"size-3 text-status-warning-text\" aria-hidden=\"true\" />\n </div>\n <div className=\"flex-1 h-2 rounded-full bg-bg-sunken overflow-hidden\">\n <div\n className=\"h-full bg-status-warning-bg transition-all\"\n style={{ width: `${percentage}%` }}\n role=\"progressbar\"\n aria-label={`${count} reviews with ${stars} stars`}\n aria-valuenow={percentage}\n aria-valuemin={0}\n aria-valuemax={100}\n />\n </div>\n <span className=\"text-xs text-text-secondary tabular-nums w-8 text-right\">{count}</span>\n </div>\n );\n })}\n </div>\n );\n}\n\n/**\n * Individual review card component.\n */\nfunction ReviewCard({ review }: { review: ReviewsData['ggnomadReviews'][0] }) {\n const { t } = useI18n();\n const createdDate = new Date(review.createdAt);\n const formattedDate = createdDate.toLocaleDateString(undefined, {\n month: 'short',\n day: 'numeric',\n year: createdDate.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined,\n });\n\n return (\n <div className=\"rounded-lg border border-border-seam bg-bg-surface/40 p-4 space-y-3\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex items-center gap-2\">\n <div className=\"flex gap-0.5\">\n {[...Array(5)].map((_, i) => (\n <Star\n key={i}\n className={cn(\n 'size-3.5',\n i < review.rating\n ? 'fill-status-warning-text text-status-warning-text'\n : 'text-border-seam'\n )}\n aria-hidden=\"true\"\n />\n ))}\n </div>\n <span className=\"text-xs text-text-secondary font-medium\">{review.rating}/5</span>\n </div>\n {review.isVerified && (\n <span className=\"inline-flex items-center gap-1 text-xs font-medium text-status-success-text bg-status-success-bg-subtle px-2 py-1 rounded-full\">\n <span className=\"size-1.5 bg-status-success-text rounded-full\" aria-hidden=\"true\" />\n {t('ggnomad.reviews.verified', 'Verified')}\n </span>\n )}\n </div>\n\n {review.title && <h4 className=\"font-medium text-text-primary text-sm\">{review.title}</h4>}\n\n {review.content && (\n <p className=\"text-sm text-text-secondary leading-relaxed\">{review.content}</p>\n )}\n\n <div className=\"flex items-center justify-between pt-2 border-t border-border-seam\">\n <span className=\"text-xs text-text-secondary\">{formattedDate}</span>\n {review.helpfulCount > 0 && (\n <span className=\"text-xs text-text-secondary flex items-center gap-1\">\n <MessageSquare className=\"size-3\" aria-hidden=\"true\" />\n {review.helpfulCount}\n </span>\n )}\n </div>\n\n {review.response && (\n <div className=\"mt-3 pt-3 border-t border-border-seam bg-bg-sunken/50 rounded p-3 text-sm\">\n <p className=\"font-medium text-text-primary mb-1\">\n {t('ggnomad.reviews.hostResponse', 'Host response')}\n </p>\n <p className=\"text-text-secondary\">{review.response}</p>\n {review.respondedAt && (\n <p className=\"text-xs text-text-secondary mt-2\">\n {new Date(review.respondedAt).toLocaleDateString()}\n </p>\n )}\n </div>\n )}\n </div>\n );\n}\n\n/**\n * Review composer component.\n */\nfunction ReviewComposer({\n targetType,\n targetId,\n onReviewCreated,\n}: {\n targetType: GgnomadBookingType;\n targetId: string;\n onReviewCreated: () => void;\n}) {\n const { t } = useI18n();\n const [rating, setRating] = useState(5);\n const [title, setTitle] = useState('');\n const [content, setContent] = useState('');\n\n const [createReview, { loading: creating }] = useMutation(\n CREATE_GGNOMAD_REVIEW_MUTATION as TypedDocumentNode<\n {\n createGgnomadReview: {\n id: string;\n rating: number;\n title?: string;\n content?: string;\n };\n },\n { input: CreateReviewInput }\n >\n );\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n if (rating === 0) {\n notify.warning(\n t('ggnomad.reviews.composer.ratingRequired', 'Rating required'),\n t('ggnomad.reviews.composer.selectRating', 'Please select a rating before submitting.')\n );\n return;\n }\n\n try {\n const result = await createReview({\n variables: {\n input: {\n targetType,\n targetId,\n rating,\n title: title.trim() || undefined,\n content: content.trim() || undefined,\n },\n },\n });\n\n if (result.error) {\n notify.error(\n t('ggnomad.reviews.composer.error', 'Could not post review'),\n result.error.message\n );\n return;\n }\n\n notify.success(\n t('ggnomad.reviews.composer.success', 'Review posted'),\n t('ggnomad.reviews.composer.successDesc', 'Thank you for sharing your experience!')\n );\n\n // Reset form\n setRating(5);\n setTitle('');\n setContent('');\n\n // Trigger refresh\n onReviewCreated();\n } catch (err) {\n notify.error(\n t('ggnomad.reviews.composer.error', 'Could not post review'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n return (\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <h3 className=\"mb-4 text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.reviews.composer.title', 'Share your experience')}\n </h3>\n\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {/* Rating selector */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-2\">\n {t('ggnomad.reviews.composer.rating', 'Your rating')}\n </label>\n <div className=\"flex gap-2\">\n {[1, 2, 3, 4, 5].map((star) => (\n <button\n key={star}\n type=\"button\"\n onClick={() => setRating(star)}\n className={cn(\n 'p-1.5 rounded-lg transition-all',\n rating >= star\n ? 'bg-status-warning-bg-subtle'\n : 'bg-bg-sunken hover:bg-bg-surface'\n )}\n aria-label={t(\n 'ggnomad.reviews.composer.ratingLabel',\n `Rate {{stars}} out of 5 stars`\n ).replace('{{stars}}', String(star))}\n aria-pressed={rating >= star}\n >\n <Star\n className={cn(\n 'size-5 transition-colors',\n rating >= star\n ? 'fill-status-warning-text text-status-warning-text'\n : 'text-border-seam'\n )}\n aria-hidden=\"true\"\n />\n </button>\n ))}\n </div>\n </div>\n\n {/* Title input */}\n <div>\n <label\n htmlFor=\"review-title\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {t('ggnomad.reviews.composer.titleLabel', 'Title (optional)')}\n </label>\n <Input\n id=\"review-title\"\n placeholder={t(\n 'ggnomad.reviews.composer.titlePlaceholder',\n 'e.g., \"Great location, friendly host\"'\n )}\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n maxLength={100}\n disabled={creating}\n className=\"bg-bg-surface border-border-seam\"\n />\n <p className=\"text-xs text-text-secondary mt-1\">{title.length}/100</p>\n </div>\n\n {/* Content textarea */}\n <div>\n <label\n htmlFor=\"review-content\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {t('ggnomad.reviews.composer.contentLabel', 'Your review (optional)')}\n </label>\n <Textarea\n id=\"review-content\"\n placeholder={t(\n 'ggnomad.reviews.composer.contentPlaceholder',\n 'Share the details of your experience...'\n )}\n value={content}\n onChange={(e) => setContent(e.target.value)}\n maxLength={1000}\n rows={4}\n disabled={creating}\n className=\"bg-bg-surface border-border-seam resize-none\"\n />\n <p className=\"text-xs text-text-secondary mt-1\">{content.length}/1000</p>\n </div>\n\n {/* Submit button */}\n <Button type=\"submit\" disabled={creating || rating === 0} className=\"w-full\">\n {creating\n ? t('ggnomad.reviews.composer.submitting', 'Posting…')\n : t('ggnomad.reviews.composer.submit', 'Post review')}\n </Button>\n </form>\n </GlassCard>\n );\n}\n\n/**\n * ReviewsSection component.\n * Displays review summary, list, and composition UI for a bookable item.\n */\nexport function ReviewsSection({ targetType, targetId }: ReviewsSectionProps) {\n const { t } = useI18n();\n const [showComposer, setShowComposer] = useState(false);\n\n // Fetch review summary\n const { data: summaryData, refetch: refetchSummary } = useQuery(\n GGNOMAD_REVIEW_SUMMARY_QUERY as TypedDocumentNode<\n ReviewSummaryData,\n { targetType: GgnomadBookingType; targetId: string }\n >,\n { variables: { targetType, targetId } }\n );\n\n // Fetch reviews list\n const { data: reviewsData, refetch: refetchReviews } = useQuery(\n GGNOMAD_REVIEWS_QUERY as TypedDocumentNode<\n ReviewsData,\n { targetType: GgnomadBookingType; targetId: string; limit?: number; offset?: number }\n >,\n {\n variables: { targetType, targetId, limit: 10, offset: 0 },\n }\n );\n\n const summary = summaryData?.ggnomadReviewSummary;\n const reviews = reviewsData?.ggnomadReviews ?? [];\n\n const handleReviewCreated = () => {\n setShowComposer(false);\n void refetchSummary();\n void refetchReviews();\n };\n\n return (\n <div className=\"space-y-6\">\n {/* Summary Section */}\n {summary && summary.count > 0 && (\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <h2 className=\"mb-4 text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.reviews.summary', 'Ratings & reviews')}\n </h2>\n\n <div className=\"grid gap-6 sm:grid-cols-2\">\n {/* Average Rating */}\n <div className=\"flex flex-col items-center justify-center py-4\">\n <div className=\"text-5xl font-bold text-text-primary tabular-nums\">\n {summary.average.toFixed(1)}\n </div>\n <div className=\"flex gap-0.5 mt-2\">\n {[...Array(5)].map((_, i) => (\n <Star\n key={i}\n className={cn(\n 'size-4',\n i < Math.round(summary.average)\n ? 'fill-status-warning-text text-status-warning-text'\n : 'text-border-seam'\n )}\n aria-hidden=\"true\"\n />\n ))}\n </div>\n <p className=\"text-sm text-text-secondary mt-2\">\n {t('ggnomad.reviews.totalCount', '{{count}} {{plural}}')\n .replace('{{count}}', String(summary.count))\n .replace(\n '{{plural}}',\n summary.count === 1\n ? t('ggnomad.reviews.review', 'review')\n : t('ggnomad.reviews.reviews', 'reviews')\n )}\n </p>\n </div>\n\n {/* Distribution Chart */}\n <div>\n <StarDistributionChart distribution={summary.distribution} />\n </div>\n </div>\n </GlassCard>\n )}\n\n {/* Write Review Composer */}\n {!showComposer && (\n <Button onClick={() => setShowComposer(true)} variant=\"secondary\" className=\"w-full\">\n {t('ggnomad.reviews.writeReview', 'Write a review')}\n </Button>\n )}\n\n {showComposer && (\n <ReviewComposer\n targetType={targetType}\n targetId={targetId}\n onReviewCreated={handleReviewCreated}\n />\n )}\n\n {/* Reviews List */}\n {reviews.length > 0 ? (\n <div>\n <h3 className=\"mb-4 text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.reviews.allReviews', 'All reviews')}\n </h3>\n <div className=\"grid gap-3\">\n {reviews.map((review) => (\n <ReviewCard key={review.id} review={review} />\n ))}\n </div>\n </div>\n ) : summary && summary.count === 0 ? (\n <div className=\"text-center py-8 rounded-lg border border-border-seam bg-bg-surface/40\">\n <AlertCircle className=\"size-6 text-text-secondary mx-auto mb-2\" aria-hidden=\"true\" />\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.reviews.noReviewsYet', 'Be the first to review')}\n </p>\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ReviewsSection.js","names":[],"sources":["../../../src/ggnomad/components/ReviewsSection.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Star, MessageSquare, AlertCircle } from 'lucide-react';\nimport { Button, GlassCard, Input, Textarea } from '@burdenoff/fe-libs/ui';\n\nimport {\n GGNOMAD_REVIEW_SUMMARY_QUERY,\n GGNOMAD_REVIEWS_QUERY,\n CREATE_GGNOMAD_REVIEW_MUTATION,\n} from '../graphql';\nimport { notify } from '../utils/notify';\nimport { cn } from '../utils/cn';\n\ntype GgnomadBookingType = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ntype ReviewsSectionProps = {\n targetType: GgnomadBookingType;\n targetId: string;\n};\n\ntype ReviewSummaryData = {\n ggnomadReviewSummary: {\n average: number;\n count: number;\n distribution: number[];\n };\n};\n\ntype ReviewsData = {\n ggnomadReviews: Array<{\n id: string;\n rating: number;\n title?: string;\n content?: string;\n isVerified: boolean;\n helpfulCount: number;\n response?: string;\n respondedAt?: string;\n createdAt: string;\n }>;\n};\n\ntype CreateReviewInput = {\n targetType: GgnomadBookingType;\n targetId: string;\n rating: number;\n title?: string;\n content?: string;\n images?: string[];\n};\n\n/**\n * Star distribution bar chart component.\n * Displays a 5-row breakdown of rating distribution.\n */\nfunction StarDistributionChart({ distribution }: { distribution: number[] }) {\n const total = distribution.reduce((a, b) => a + b, 0);\n\n return (\n <div className=\"space-y-2\">\n {[5, 4, 3, 2, 1].map((stars, idx) => {\n const count = distribution[4 - idx] ?? 0;\n const percentage = total > 0 ? (count / total) * 100 : 0;\n\n return (\n <div key={stars} className=\"flex items-center gap-2\">\n <div className=\"flex items-center gap-1 whitespace-nowrap text-xs text-text-secondary\">\n <span>{stars}</span>\n <Star className=\"size-3 text-status-warning-text\" aria-hidden=\"true\" />\n </div>\n <div className=\"flex-1 h-2 rounded-full bg-bg-sunken overflow-hidden\">\n <div\n className=\"h-full bg-status-warning-bg transition-all\"\n style={{ width: `${percentage}%` }}\n role=\"progressbar\"\n aria-label={`${count} reviews with ${stars} stars`}\n aria-valuenow={percentage}\n aria-valuemin={0}\n aria-valuemax={100}\n />\n </div>\n <span className=\"text-xs text-text-secondary tabular-nums w-8 text-right\">{count}</span>\n </div>\n );\n })}\n </div>\n );\n}\n\n/**\n * Individual review card component.\n */\nfunction ReviewCard({ review }: { review: ReviewsData['ggnomadReviews'][0] }) {\n const { t } = useI18n();\n const createdDate = new Date(review.createdAt);\n const formattedDate = createdDate.toLocaleDateString(undefined, {\n month: 'short',\n day: 'numeric',\n year: createdDate.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined,\n });\n\n return (\n <div className=\"rounded-lg border border-border-seam bg-bg-surface/40 p-4 space-y-3\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex items-center gap-2\">\n <div className=\"flex gap-0.5\">\n {[...Array(5)].map((_, i) => (\n <Star\n key={i}\n className={cn(\n 'size-3.5',\n i < review.rating\n ? 'fill-status-warning-text text-status-warning-text'\n : 'text-border-seam'\n )}\n aria-hidden=\"true\"\n />\n ))}\n </div>\n <span className=\"text-xs text-text-secondary font-medium\">{review.rating}/5</span>\n </div>\n {review.isVerified && (\n <span className=\"inline-flex items-center gap-1 text-xs font-medium text-status-success-text bg-status-success-bg-subtle px-2 py-1 rounded-full\">\n <span className=\"size-1.5 bg-status-success-text rounded-full\" aria-hidden=\"true\" />\n {t('ggnomad.reviews.verified', 'Verified')}\n </span>\n )}\n </div>\n\n {review.title && <h4 className=\"font-medium text-text-primary text-sm\">{review.title}</h4>}\n\n {review.content && (\n <p className=\"text-sm text-text-secondary leading-relaxed\">{review.content}</p>\n )}\n\n <div className=\"flex items-center justify-between pt-2 border-t border-border-seam\">\n <span className=\"text-xs text-text-secondary\">{formattedDate}</span>\n {review.helpfulCount > 0 && (\n <span className=\"text-xs text-text-secondary flex items-center gap-1\">\n <MessageSquare className=\"size-3\" aria-hidden=\"true\" />\n {review.helpfulCount}\n </span>\n )}\n </div>\n\n {review.response && (\n <div className=\"mt-3 pt-3 border-t border-border-seam bg-bg-sunken/50 rounded p-3 text-sm\">\n <p className=\"font-medium text-text-primary mb-1\">\n {t('ggnomad.reviews.hostResponse', 'Host response')}\n </p>\n <p className=\"text-text-secondary\">{review.response}</p>\n {review.respondedAt && (\n <p className=\"text-xs text-text-secondary mt-2\">\n {new Date(review.respondedAt).toLocaleDateString()}\n </p>\n )}\n </div>\n )}\n </div>\n );\n}\n\n/**\n * Review composer component.\n */\nfunction ReviewComposer({\n targetType,\n targetId,\n onReviewCreated,\n}: {\n targetType: GgnomadBookingType;\n targetId: string;\n onReviewCreated: () => void;\n}) {\n const { t } = useI18n();\n const [rating, setRating] = useState(5);\n const [title, setTitle] = useState('');\n const [content, setContent] = useState('');\n\n const [createReview, { loading: creating }] = useMutation(\n CREATE_GGNOMAD_REVIEW_MUTATION as TypedDocumentNode<\n {\n createGgnomadReview: {\n id: string;\n rating: number;\n title?: string;\n content?: string;\n };\n },\n { input: CreateReviewInput }\n >\n );\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n if (rating === 0) {\n notify.warning(\n t('ggnomad.reviews.composer.ratingRequired', 'Rating required'),\n t('ggnomad.reviews.composer.selectRating', 'Please select a rating before submitting.')\n );\n return;\n }\n\n try {\n const result = await createReview({\n variables: {\n input: {\n targetType,\n targetId,\n rating,\n title: title.trim() || undefined,\n content: content.trim() || undefined,\n },\n },\n });\n\n if (result.error) {\n notify.error(\n t('ggnomad.reviews.composer.error', 'Could not post review'),\n result.error.message\n );\n return;\n }\n\n notify.success(\n t('ggnomad.reviews.composer.success', 'Review posted'),\n t('ggnomad.reviews.composer.successDesc', 'Thank you for sharing your experience!')\n );\n\n // Reset form\n setRating(5);\n setTitle('');\n setContent('');\n\n // Trigger refresh\n onReviewCreated();\n } catch (err) {\n notify.error(\n t('ggnomad.reviews.composer.error', 'Could not post review'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n return (\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <h3 className=\"mb-4 text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.reviews.composer.title', 'Share your experience')}\n </h3>\n\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {/* Rating selector */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-2\">\n {t('ggnomad.reviews.composer.rating', 'Your rating')}\n </label>\n <div className=\"flex gap-2\">\n {[1, 2, 3, 4, 5].map((star) => (\n <button\n key={star}\n type=\"button\"\n onClick={() => setRating(star)}\n className={cn(\n 'p-1.5 rounded-lg transition-all',\n rating >= star\n ? 'bg-status-warning-bg-subtle'\n : 'bg-bg-sunken hover:bg-bg-surface'\n )}\n aria-label={t(\n 'ggnomad.reviews.composer.ratingLabel',\n `Rate {{stars}} out of 5 stars`\n ).replace('{{stars}}', String(star))}\n aria-pressed={rating >= star}\n >\n <Star\n className={cn(\n 'size-5 transition-colors',\n rating >= star\n ? 'fill-status-warning-text text-status-warning-text'\n : 'text-border-seam'\n )}\n aria-hidden=\"true\"\n />\n </button>\n ))}\n </div>\n </div>\n\n {/* Title input */}\n <div>\n <label\n htmlFor=\"review-title\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {t('ggnomad.reviews.composer.titleLabel', 'Title (optional)')}\n </label>\n <Input\n id=\"review-title\"\n placeholder={t(\n 'ggnomad.reviews.composer.titlePlaceholder',\n 'e.g., \"Great location, friendly host\"'\n )}\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n maxLength={100}\n disabled={creating}\n className=\"bg-bg-surface border-border-seam\"\n />\n <p className=\"text-xs text-text-secondary mt-1\">{title.length}/100</p>\n </div>\n\n {/* Content textarea */}\n <div>\n <label\n htmlFor=\"review-content\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {t('ggnomad.reviews.composer.contentLabel', 'Your review (optional)')}\n </label>\n <Textarea\n id=\"review-content\"\n placeholder={t(\n 'ggnomad.reviews.composer.contentPlaceholder',\n 'Share the details of your experience...'\n )}\n value={content}\n onChange={(e) => setContent(e.target.value)}\n maxLength={1000}\n rows={4}\n disabled={creating}\n className=\"bg-bg-surface border-border-seam resize-none\"\n />\n <p className=\"text-xs text-text-secondary mt-1\">{content.length}/1000</p>\n </div>\n\n {/* Submit button */}\n <Button type=\"submit\" disabled={creating || rating === 0} className=\"w-full\">\n {creating\n ? t('ggnomad.reviews.composer.submitting', 'Posting…')\n : t('ggnomad.reviews.composer.submit', 'Post review')}\n </Button>\n </form>\n </GlassCard>\n );\n}\n\n/**\n * ReviewsSection component.\n * Displays review summary, list, and composition UI for a bookable item.\n */\nexport function ReviewsSection({ targetType, targetId }: ReviewsSectionProps) {\n const { t } = useI18n();\n const [showComposer, setShowComposer] = useState(false);\n\n // Fetch review summary\n const { data: summaryData, refetch: refetchSummary } = useQuery(\n GGNOMAD_REVIEW_SUMMARY_QUERY as TypedDocumentNode<\n ReviewSummaryData,\n { targetType: GgnomadBookingType; targetId: string }\n >,\n { variables: { targetType, targetId } }\n );\n\n // Fetch reviews list\n const { data: reviewsData, refetch: refetchReviews } = useQuery(\n GGNOMAD_REVIEWS_QUERY as TypedDocumentNode<\n ReviewsData,\n { targetType: GgnomadBookingType; targetId: string; limit?: number; offset?: number }\n >,\n {\n variables: { targetType, targetId, limit: 10, offset: 0 },\n }\n );\n\n const summary = summaryData?.ggnomadReviewSummary;\n const reviews = reviewsData?.ggnomadReviews ?? [];\n\n const handleReviewCreated = () => {\n setShowComposer(false);\n void refetchSummary();\n void refetchReviews();\n };\n\n return (\n <div className=\"space-y-6\">\n {/* Summary Section */}\n {summary && summary.count > 0 && (\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <h2 className=\"mb-4 text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.reviews.summary', 'Ratings & reviews')}\n </h2>\n\n <div className=\"grid gap-6 sm:grid-cols-2\">\n {/* Average Rating */}\n <div className=\"flex flex-col items-center justify-center py-4\">\n <div className=\"text-5xl font-bold text-text-primary tabular-nums\">\n {summary.average.toFixed(1)}\n </div>\n <div className=\"flex gap-0.5 mt-2\">\n {[...Array(5)].map((_, i) => (\n <Star\n key={i}\n className={cn(\n 'size-4',\n i < Math.round(summary.average)\n ? 'fill-status-warning-text text-status-warning-text'\n : 'text-border-seam'\n )}\n aria-hidden=\"true\"\n />\n ))}\n </div>\n <p className=\"text-sm text-text-secondary mt-2\">\n {t('ggnomad.reviews.totalCount', '{{count}} {{plural}}')\n .replace('{{count}}', String(summary.count))\n .replace(\n '{{plural}}',\n summary.count === 1\n ? t('ggnomad.reviews.review', 'review')\n : t('ggnomad.reviews.reviews', 'reviews')\n )}\n </p>\n </div>\n\n {/* Distribution Chart */}\n <div>\n <StarDistributionChart distribution={summary.distribution} />\n </div>\n </div>\n </GlassCard>\n )}\n\n {/* Write Review Composer */}\n {!showComposer && (\n <Button onClick={() => setShowComposer(true)} variant=\"secondary\" className=\"w-full\">\n {t('ggnomad.reviews.writeReview', 'Write a review')}\n </Button>\n )}\n\n {showComposer && (\n <ReviewComposer\n targetType={targetType}\n targetId={targetId}\n onReviewCreated={handleReviewCreated}\n />\n )}\n\n {/* Reviews List */}\n {reviews.length > 0 ? (\n <div>\n <h3 className=\"mb-4 text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.reviews.allReviews', 'All reviews')}\n </h3>\n <div className=\"grid gap-3\">\n {reviews.map((review) => (\n <ReviewCard key={review.id} review={review} />\n ))}\n </div>\n </div>\n ) : summary && summary.count === 0 ? (\n <div className=\"text-center py-8 rounded-lg border border-border-seam bg-bg-surface/40\">\n <AlertCircle className=\"size-6 text-text-secondary mx-auto mb-2\" aria-hidden=\"true\" />\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.reviews.noReviewsYet', 'Be the first to review')}\n </p>\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/ggnomad/components/StatusBadge.tsx"],"sourcesContent":["import { cn } from '../utils/cn';\n\ntype Tone = 'success' | 'warning' | 'danger' | 'info' | 'neutral';\n\nconst TONE: Record<Tone, { dot: string; text: string; bg: string }> = {\n success: {\n dot: 'bg-status-success-text',\n text: 'text-status-success-text',\n bg: 'bg-status-success-bg-subtle',\n },\n warning: {\n dot: 'bg-status-warning-text',\n text: 'text-status-warning-text',\n bg: 'bg-status-warning-bg-subtle',\n },\n danger: {\n dot: 'bg-status-error-text',\n text: 'text-status-error-text',\n bg: 'bg-status-error-bg-subtle',\n },\n info: {\n dot: 'bg-status-info-text',\n text: 'text-status-info-text',\n bg: 'bg-status-info-bg-subtle',\n },\n neutral: {\n dot: 'bg-text-secondary',\n text: 'text-text-secondary',\n bg: 'bg-bg-sunken',\n },\n};\n\n/** Maps a free-form status string to a semantic tone. */\nexport function toneForStatus(status: string): Tone {\n const s = status.toUpperCase();\n if (['CONFIRMED', 'ACTIVE', 'COMPLETED', 'PAID', 'APPROVED'].includes(s)) {\n return 'success';\n }\n if (['PENDING', 'PROCESSING', 'AWAITING', 'IN_PROGRESS', 'ONBOARDING'].includes(s)) {\n return 'warning';\n }\n if (['CANCELLED', 'FAILED', 'REJECTED', 'SUSPENDED'].includes(s)) {\n return 'danger';\n }\n return 'info';\n}\n\ntype StatusBadgeProps = {\n status: string;\n /** Pulses the dot for live/active states (opacity only, reduced-motion-safe). */\n live?: boolean;\n className?: string;\n};\n\nexport function StatusBadge({ status, live = false, className }: StatusBadgeProps) {\n const tone = TONE[toneForStatus(status)];\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium',\n tone.bg,\n tone.text,\n className\n )}\n >\n <span\n className={cn(\n 'size-1.5 rounded-full',\n tone.dot,\n live ? 'motion-safe:animate-vc-glow-pulse' : ''\n )}\n aria-hidden=\"true\"\n />\n {status}\n </span>\n );\n}\n"],"mappings":";;;AAIA,IAAM,IAAgE;CACpE,SAAS;EACP,KAAK;EACL,MAAM;EACN,IAAI;CACN;CACA,SAAS;EACP,KAAK;EACL,MAAM;EACN,IAAI;CACN;CACA,QAAQ;EACN,KAAK;EACL,MAAM;EACN,IAAI;CACN;CACA,MAAM;EACJ,KAAK;EACL,MAAM;EACN,IAAI;CACN;CACA,SAAS;EACP,KAAK;EACL,MAAM;EACN,IAAI;CACN;AACF;AAGA,SAAgB,EAAc,GAAsB;CAClD,IAAM,IAAI,EAAO,YAAY;CAU7B,OATI;EAAC;EAAa;EAAU;EAAa;EAAQ;CAAU,
|
|
1
|
+
{"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/ggnomad/components/StatusBadge.tsx"],"sourcesContent":["import { cn } from '../utils/cn';\n\ntype Tone = 'success' | 'warning' | 'danger' | 'info' | 'neutral';\n\nconst TONE: Record<Tone, { dot: string; text: string; bg: string }> = {\n success: {\n dot: 'bg-status-success-text',\n text: 'text-status-success-text',\n bg: 'bg-status-success-bg-subtle',\n },\n warning: {\n dot: 'bg-status-warning-text',\n text: 'text-status-warning-text',\n bg: 'bg-status-warning-bg-subtle',\n },\n danger: {\n dot: 'bg-status-error-text',\n text: 'text-status-error-text',\n bg: 'bg-status-error-bg-subtle',\n },\n info: {\n dot: 'bg-status-info-text',\n text: 'text-status-info-text',\n bg: 'bg-status-info-bg-subtle',\n },\n neutral: {\n dot: 'bg-text-secondary',\n text: 'text-text-secondary',\n bg: 'bg-bg-sunken',\n },\n};\n\n/** Maps a free-form status string to a semantic tone. */\nexport function toneForStatus(status: string): Tone {\n const s = status.toUpperCase();\n if (['CONFIRMED', 'ACTIVE', 'COMPLETED', 'PAID', 'APPROVED'].includes(s)) {\n return 'success';\n }\n if (['PENDING', 'PROCESSING', 'AWAITING', 'IN_PROGRESS', 'ONBOARDING'].includes(s)) {\n return 'warning';\n }\n if (['CANCELLED', 'FAILED', 'REJECTED', 'SUSPENDED'].includes(s)) {\n return 'danger';\n }\n return 'info';\n}\n\ntype StatusBadgeProps = {\n status: string;\n /** Pulses the dot for live/active states (opacity only, reduced-motion-safe). */\n live?: boolean;\n className?: string;\n};\n\nexport function StatusBadge({ status, live = false, className }: StatusBadgeProps) {\n const tone = TONE[toneForStatus(status)];\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium',\n tone.bg,\n tone.text,\n className\n )}\n >\n <span\n className={cn(\n 'size-1.5 rounded-full',\n tone.dot,\n live ? 'motion-safe:animate-vc-glow-pulse' : ''\n )}\n aria-hidden=\"true\"\n />\n {status}\n </span>\n );\n}\n"],"mappings":";;;AAIA,IAAM,IAAgE;CACpE,SAAS;EACP,KAAK;EACL,MAAM;EACN,IAAI;CACN;CACA,SAAS;EACP,KAAK;EACL,MAAM;EACN,IAAI;CACN;CACA,QAAQ;EACN,KAAK;EACL,MAAM;EACN,IAAI;CACN;CACA,MAAM;EACJ,KAAK;EACL,MAAM;EACN,IAAI;CACN;CACA,SAAS;EACP,KAAK;EACL,MAAM;EACN,IAAI;CACN;AACF;AAGA,SAAgB,EAAc,GAAsB;CAClD,IAAM,IAAI,EAAO,YAAY;CAU7B,OATI;EAAC;EAAa;EAAU;EAAa;EAAQ;CAAU,CAAC,CAAC,SAAS,CAAC,IAC9D,YAEL;EAAC;EAAW;EAAc;EAAY;EAAe;CAAY,CAAC,CAAC,SAAS,CAAC,IACxE,YAEL;EAAC;EAAa;EAAU;EAAY;CAAW,CAAC,CAAC,SAAS,CAAC,IACtD,WAEF;AACT;AASA,SAAgB,EAAY,EAAE,WAAQ,UAAO,IAAO,gBAA+B;CACjF,IAAM,IAAO,EAAK,EAAc,CAAM;CACtC,OACE,kBAAC,QAAD;EACE,WAAW,EACT,mFACA,EAAK,IACL,EAAK,MACL,CACF;YANF,CAQE,kBAAC,QAAD;GACE,WAAW,EACT,yBACA,EAAK,KACL,IAAO,sCAAsC,EAC/C;GACA,eAAY;EACb,CAAA,GACA,CACG;;AAEV"}
|
|
@@ -7,7 +7,7 @@ import { useMutation as s, useQuery as c } from "@apollo/client/react";
|
|
|
7
7
|
import { AlertDialog as l, AlertDialogCancel as u, AlertDialogContent as d, AlertDialogDescription as f, AlertDialogFooter as p, AlertDialogHeader as m, AlertDialogTitle as h } from "@burdenoff/fe-libs/ui";
|
|
8
8
|
import { jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
9
9
|
//#region src/ggnomad/components/TicketDialog.tsx
|
|
10
|
-
var v = new Set([
|
|
10
|
+
var v = /* @__PURE__ */ new Set([
|
|
11
11
|
"CONFIRMED",
|
|
12
12
|
"CHECKED_IN",
|
|
13
13
|
"COMPLETED"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TicketDialog.js","names":[],"sources":["../../../src/ggnomad/components/TicketDialog.tsx"],"sourcesContent":["import { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { QrCode, Ticket as TicketIcon } from 'lucide-react';\nimport {\n AlertDialog,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_TICKETS_QUERY, ISSUE_GGNOMAD_TICKET_MUTATION } from '../graphql';\nimport { StatusBadge } from './StatusBadge';\nimport { notify } from '../utils/notify';\n\nexport interface GgnomadTicket {\n id: string;\n bookingId: string;\n ticketCode: string;\n qrCodeDataUrl: string;\n status: 'ISSUED' | 'REDEEMED' | 'REVOKED';\n issuedAt: string;\n redeemedAt: string | null;\n redeemedBy: string | null;\n revokedAt: string | null;\n revokedReason: string | null;\n}\n\n/** Bookings reach CONFIRMED (payment verified) before a ticket can ever exist. */\nconst TICKET_ELIGIBLE_STATUSES = new Set(['CONFIRMED', 'CHECKED_IN', 'COMPLETED']);\n\ninterface TicketDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n bookingId: string;\n referenceCode: string;\n bookingStatus: string;\n}\n\nfunction formatDateTime(value: string | null): string {\n if (!value) return '';\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return value;\n return date.toLocaleString();\n}\n\n/**\n * Traveler-facing mobile ticket view (BOFF-4362). Shows every QR ticket\n * issued for a booking — each ticket's QR code is rendered server-side from\n * its signed, revocable token (see wspace-ggnomad-svc `qrCodeDataUrl`), so\n * what's displayed here always reflects the ticket's live redemption state.\n */\nexport function TicketDialog({\n open,\n onOpenChange,\n bookingId,\n referenceCode,\n bookingStatus,\n}: TicketDialogProps) {\n const { t } = useI18n();\n\n const { data, loading, refetch } = useQuery(\n GGNOMAD_TICKETS_QUERY as TypedDocumentNode<\n { ggnomadTickets: GgnomadTicket[] },\n { bookingId: string }\n >,\n { variables: { bookingId }, skip: !open || !bookingId, fetchPolicy: 'network-only' }\n );\n\n const [issueTicket, { loading: issuing }] = useMutation(\n ISSUE_GGNOMAD_TICKET_MUTATION as TypedDocumentNode<\n { issueGgnomadTicket: GgnomadTicket },\n { bookingId: string }\n >\n );\n\n const tickets = data?.ggnomadTickets ?? [];\n const canIssue = TICKET_ELIGIBLE_STATUSES.has(bookingStatus.toUpperCase());\n\n const handleIssue = async () => {\n try {\n const result = await issueTicket({ variables: { bookingId } });\n if (result.error) {\n notify.error(\n t('ggnomad.ticket.issueErrorTitle', 'Could not issue ticket'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.ticket.issueSuccessTitle', 'Ticket issued'),\n t('ggnomad.ticket.issueSuccessBody', 'A new QR ticket is ready to show at check-in.')\n );\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.ticket.issueErrorTitle', 'Could not issue ticket'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n return (\n <AlertDialog open={open} onOpenChange={onOpenChange}>\n <AlertDialogContent className=\"max-w-md\">\n <AlertDialogHeader>\n <AlertDialogTitle className=\"flex items-center gap-2\">\n <TicketIcon className=\"size-4\" />\n {t('ggnomad.ticket.title', 'Mobile ticket')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t('ggnomad.ticket.description', 'Show this QR code at check-in for {{ref}}.').replace(\n '{{ref}}',\n referenceCode\n )}\n </AlertDialogDescription>\n </AlertDialogHeader>\n\n <div className=\"max-h-[65vh] space-y-4 overflow-y-auto\">\n {loading ? (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n {t('ggnomad.ticket.loading', 'Loading your ticket…')}\n </p>\n ) : tickets.length === 0 ? (\n <div className=\"flex flex-col items-center gap-3 rounded-lg border border-dashed border-border-seam py-8 text-center\">\n <QrCode className=\"size-8 text-text-secondary\" aria-hidden=\"true\" />\n <p className=\"text-sm text-text-secondary\">\n {canIssue\n ? t(\n 'ggnomad.ticket.noneYet',\n 'No QR ticket has been issued for this booking yet.'\n )\n : t(\n 'ggnomad.ticket.notConfirmed',\n 'A QR ticket will be issued automatically once this booking is confirmed.'\n )}\n </p>\n {canIssue ? (\n <button\n type=\"button\"\n onClick={() => void handleIssue()}\n disabled={issuing}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bgHover disabled:opacity-50\"\n >\n {issuing\n ? t('ggnomad.ticket.issuing', 'Issuing…')\n : t('ggnomad.ticket.issueAction', 'Issue QR ticket')}\n </button>\n ) : null}\n </div>\n ) : (\n tickets.map((ticket) => (\n <div\n key={ticket.id}\n className=\"flex flex-col items-center gap-3 rounded-lg border border-border-seam bg-bg-surface/40 p-4\"\n >\n <div className=\"flex w-full items-center justify-between\">\n <span className=\"font-mono text-xs text-text-secondary\">{ticket.ticketCode}</span>\n <StatusBadge status={ticket.status} live={ticket.status === 'ISSUED'} />\n </div>\n\n {ticket.status === 'ISSUED' ? (\n <img\n src={ticket.qrCodeDataUrl}\n alt={t('ggnomad.ticket.qrAlt', 'QR code for ticket {{code}}').replace(\n '{{code}}',\n ticket.ticketCode\n )}\n className=\"size-48 rounded-md border border-border-seam bg-white p-2\"\n width={192}\n height={192}\n />\n ) : (\n <div className=\"flex size-48 items-center justify-center rounded-md border border-dashed border-border-seam bg-bg-sunken text-center text-xs text-text-secondary\">\n {ticket.status === 'REDEEMED'\n ? t('ggnomad.ticket.alreadyRedeemed', 'Already redeemed — QR retired')\n : t('ggnomad.ticket.revokedNotice', 'Revoked — no longer valid')}\n </div>\n )}\n\n <div className=\"w-full space-y-1 text-xs text-text-secondary\">\n <p>\n {t('ggnomad.ticket.issuedAt', 'Issued')} {formatDateTime(ticket.issuedAt)}\n </p>\n {ticket.redeemedAt ? (\n <p>\n {t('ggnomad.ticket.redeemedAt', 'Redeemed')}{' '}\n {formatDateTime(ticket.redeemedAt)}\n </p>\n ) : null}\n {ticket.revokedAt ? (\n <p>\n {t('ggnomad.ticket.revokedAt', 'Revoked')} {formatDateTime(ticket.revokedAt)}\n {ticket.revokedReason ? ` — ${ticket.revokedReason}` : ''}\n </p>\n ) : null}\n </div>\n </div>\n ))\n )}\n </div>\n\n <AlertDialogFooter>\n <AlertDialogCancel>{t('ggnomad.ticket.close', 'Close')}</AlertDialogCancel>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TicketDialog.js","names":[],"sources":["../../../src/ggnomad/components/TicketDialog.tsx"],"sourcesContent":["import { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { QrCode, Ticket as TicketIcon } from 'lucide-react';\nimport {\n AlertDialog,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_TICKETS_QUERY, ISSUE_GGNOMAD_TICKET_MUTATION } from '../graphql';\nimport { StatusBadge } from './StatusBadge';\nimport { notify } from '../utils/notify';\n\nexport interface GgnomadTicket {\n id: string;\n bookingId: string;\n ticketCode: string;\n qrCodeDataUrl: string;\n status: 'ISSUED' | 'REDEEMED' | 'REVOKED';\n issuedAt: string;\n redeemedAt: string | null;\n redeemedBy: string | null;\n revokedAt: string | null;\n revokedReason: string | null;\n}\n\n/** Bookings reach CONFIRMED (payment verified) before a ticket can ever exist. */\nconst TICKET_ELIGIBLE_STATUSES = new Set(['CONFIRMED', 'CHECKED_IN', 'COMPLETED']);\n\ninterface TicketDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n bookingId: string;\n referenceCode: string;\n bookingStatus: string;\n}\n\nfunction formatDateTime(value: string | null): string {\n if (!value) return '';\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return value;\n return date.toLocaleString();\n}\n\n/**\n * Traveler-facing mobile ticket view (BOFF-4362). Shows every QR ticket\n * issued for a booking — each ticket's QR code is rendered server-side from\n * its signed, revocable token (see wspace-ggnomad-svc `qrCodeDataUrl`), so\n * what's displayed here always reflects the ticket's live redemption state.\n */\nexport function TicketDialog({\n open,\n onOpenChange,\n bookingId,\n referenceCode,\n bookingStatus,\n}: TicketDialogProps) {\n const { t } = useI18n();\n\n const { data, loading, refetch } = useQuery(\n GGNOMAD_TICKETS_QUERY as TypedDocumentNode<\n { ggnomadTickets: GgnomadTicket[] },\n { bookingId: string }\n >,\n { variables: { bookingId }, skip: !open || !bookingId, fetchPolicy: 'network-only' }\n );\n\n const [issueTicket, { loading: issuing }] = useMutation(\n ISSUE_GGNOMAD_TICKET_MUTATION as TypedDocumentNode<\n { issueGgnomadTicket: GgnomadTicket },\n { bookingId: string }\n >\n );\n\n const tickets = data?.ggnomadTickets ?? [];\n const canIssue = TICKET_ELIGIBLE_STATUSES.has(bookingStatus.toUpperCase());\n\n const handleIssue = async () => {\n try {\n const result = await issueTicket({ variables: { bookingId } });\n if (result.error) {\n notify.error(\n t('ggnomad.ticket.issueErrorTitle', 'Could not issue ticket'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.ticket.issueSuccessTitle', 'Ticket issued'),\n t('ggnomad.ticket.issueSuccessBody', 'A new QR ticket is ready to show at check-in.')\n );\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.ticket.issueErrorTitle', 'Could not issue ticket'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n return (\n <AlertDialog open={open} onOpenChange={onOpenChange}>\n <AlertDialogContent className=\"max-w-md\">\n <AlertDialogHeader>\n <AlertDialogTitle className=\"flex items-center gap-2\">\n <TicketIcon className=\"size-4\" />\n {t('ggnomad.ticket.title', 'Mobile ticket')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t('ggnomad.ticket.description', 'Show this QR code at check-in for {{ref}}.').replace(\n '{{ref}}',\n referenceCode\n )}\n </AlertDialogDescription>\n </AlertDialogHeader>\n\n <div className=\"max-h-[65vh] space-y-4 overflow-y-auto\">\n {loading ? (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n {t('ggnomad.ticket.loading', 'Loading your ticket…')}\n </p>\n ) : tickets.length === 0 ? (\n <div className=\"flex flex-col items-center gap-3 rounded-lg border border-dashed border-border-seam py-8 text-center\">\n <QrCode className=\"size-8 text-text-secondary\" aria-hidden=\"true\" />\n <p className=\"text-sm text-text-secondary\">\n {canIssue\n ? t(\n 'ggnomad.ticket.noneYet',\n 'No QR ticket has been issued for this booking yet.'\n )\n : t(\n 'ggnomad.ticket.notConfirmed',\n 'A QR ticket will be issued automatically once this booking is confirmed.'\n )}\n </p>\n {canIssue ? (\n <button\n type=\"button\"\n onClick={() => void handleIssue()}\n disabled={issuing}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bgHover disabled:opacity-50\"\n >\n {issuing\n ? t('ggnomad.ticket.issuing', 'Issuing…')\n : t('ggnomad.ticket.issueAction', 'Issue QR ticket')}\n </button>\n ) : null}\n </div>\n ) : (\n tickets.map((ticket) => (\n <div\n key={ticket.id}\n className=\"flex flex-col items-center gap-3 rounded-lg border border-border-seam bg-bg-surface/40 p-4\"\n >\n <div className=\"flex w-full items-center justify-between\">\n <span className=\"font-mono text-xs text-text-secondary\">{ticket.ticketCode}</span>\n <StatusBadge status={ticket.status} live={ticket.status === 'ISSUED'} />\n </div>\n\n {ticket.status === 'ISSUED' ? (\n <img\n src={ticket.qrCodeDataUrl}\n alt={t('ggnomad.ticket.qrAlt', 'QR code for ticket {{code}}').replace(\n '{{code}}',\n ticket.ticketCode\n )}\n className=\"size-48 rounded-md border border-border-seam bg-white p-2\"\n width={192}\n height={192}\n />\n ) : (\n <div className=\"flex size-48 items-center justify-center rounded-md border border-dashed border-border-seam bg-bg-sunken text-center text-xs text-text-secondary\">\n {ticket.status === 'REDEEMED'\n ? t('ggnomad.ticket.alreadyRedeemed', 'Already redeemed — QR retired')\n : t('ggnomad.ticket.revokedNotice', 'Revoked — no longer valid')}\n </div>\n )}\n\n <div className=\"w-full space-y-1 text-xs text-text-secondary\">\n <p>\n {t('ggnomad.ticket.issuedAt', 'Issued')} {formatDateTime(ticket.issuedAt)}\n </p>\n {ticket.redeemedAt ? (\n <p>\n {t('ggnomad.ticket.redeemedAt', 'Redeemed')}{' '}\n {formatDateTime(ticket.redeemedAt)}\n </p>\n ) : null}\n {ticket.revokedAt ? (\n <p>\n {t('ggnomad.ticket.revokedAt', 'Revoked')} {formatDateTime(ticket.revokedAt)}\n {ticket.revokedReason ? ` — ${ticket.revokedReason}` : ''}\n </p>\n ) : null}\n </div>\n </div>\n ))\n )}\n </div>\n\n <AlertDialogFooter>\n <AlertDialogCancel>{t('ggnomad.ticket.close', 'Close')}</AlertDialogCancel>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n"],"mappings":"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"}
|
|
@@ -9,6 +9,7 @@ export { BookingPanel } from './BookingPanel';
|
|
|
9
9
|
export { ReviewsSection } from './ReviewsSection';
|
|
10
10
|
export { ListingEditDialog } from './ListingEditDialog';
|
|
11
11
|
export { CreateTravelPolicyDialog } from './CreateTravelPolicyDialog';
|
|
12
|
+
export { CreateProviderDialog } from './CreateProviderDialog';
|
|
12
13
|
export { TicketDialog } from './TicketDialog';
|
|
13
14
|
export type { GgnomadTicket } from './TicketDialog';
|
|
14
15
|
export { RedeemTicketPanel } from './RedeemTicketPanel';
|
|
@@ -21,6 +22,7 @@ export { RatingSummary } from './RatingSummary';
|
|
|
21
22
|
export { EcoAccessPanel } from './EcoAccessPanel';
|
|
22
23
|
export { ProviderCard } from './ProviderCard';
|
|
23
24
|
export type { ListingProvider } from './ProviderCard';
|
|
25
|
+
export { ProviderDashboardCard } from './ProviderDashboardCard';
|
|
24
26
|
export { AvailabilityPanel } from './AvailabilityPanel';
|
|
25
27
|
export { ListingFilters, ACCESSIBILITY_FEATURES, EMPTY_LISTING_FILTERS, hasActiveListingFilters, } from './ListingFilters';
|
|
26
28
|
export type { ListingFilterState, ListingStatusFilter } from './ListingFilters';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ggnomad/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,YAAY,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ggnomad/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,YAAY,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -1,23 +1,26 @@
|
|
|
1
|
-
import "./StatCard.js";
|
|
2
|
-
import "./StatusBadge.js";
|
|
3
|
-
import "./SectionCard.js";
|
|
4
|
-
import "./EntityCard.js";
|
|
5
|
-
import "./DetailRow.js";
|
|
6
|
-
import "./LoadMoreButton.js";
|
|
7
|
-
import "./DetailHero.js";
|
|
8
|
-
import "./BookingPanel.js";
|
|
9
|
-
import "./ReviewsSection.js";
|
|
10
|
-
import "./ListingEditDialog.js";
|
|
11
|
-
import "./CreateTravelPolicyDialog.js";
|
|
12
|
-
import "./
|
|
13
|
-
import "./
|
|
14
|
-
import "./
|
|
15
|
-
import "./
|
|
16
|
-
import "./
|
|
17
|
-
import "./
|
|
18
|
-
import "./
|
|
19
|
-
import "./
|
|
20
|
-
import "./
|
|
21
|
-
import "./
|
|
22
|
-
import "./
|
|
23
|
-
import "./
|
|
1
|
+
import { StatCard as e } from "./StatCard.js";
|
|
2
|
+
import { StatusBadge as t } from "./StatusBadge.js";
|
|
3
|
+
import { SectionCard as n } from "./SectionCard.js";
|
|
4
|
+
import { EntityCard as r } from "./EntityCard.js";
|
|
5
|
+
import { DetailRow as i } from "./DetailRow.js";
|
|
6
|
+
import { LoadMoreButton as a } from "./LoadMoreButton.js";
|
|
7
|
+
import { DetailHero as o } from "./DetailHero.js";
|
|
8
|
+
import { BookingPanel as s } from "./BookingPanel.js";
|
|
9
|
+
import { ReviewsSection as c } from "./ReviewsSection.js";
|
|
10
|
+
import { ListingEditDialog as l } from "./ListingEditDialog.js";
|
|
11
|
+
import { CreateTravelPolicyDialog as u } from "./CreateTravelPolicyDialog.js";
|
|
12
|
+
import { CreateProviderDialog as d } from "./CreateProviderDialog.js";
|
|
13
|
+
import { TicketDialog as f } from "./TicketDialog.js";
|
|
14
|
+
import { RedeemTicketPanel as p } from "./RedeemTicketPanel.js";
|
|
15
|
+
import { AuditTrailDialog as m } from "./AuditTrailDialog.js";
|
|
16
|
+
import { PhotoGallery as h } from "./PhotoGallery.js";
|
|
17
|
+
import { TagChips as g } from "./TagChips.js";
|
|
18
|
+
import { FactGrid as _ } from "./FactGrid.js";
|
|
19
|
+
import { RatingSummary as v } from "./RatingSummary.js";
|
|
20
|
+
import { EcoAccessPanel as y } from "./EcoAccessPanel.js";
|
|
21
|
+
import { ProviderCard as b } from "./ProviderCard.js";
|
|
22
|
+
import { ProviderDashboardCard as x } from "./ProviderDashboardCard.js";
|
|
23
|
+
import { AvailabilityPanel as S } from "./AvailabilityPanel.js";
|
|
24
|
+
import { EMPTY_LISTING_FILTERS as C, ListingFilters as w, hasActiveListingFilters as T } from "./ListingFilters.js";
|
|
25
|
+
import { GroupTripPanel as E } from "./GroupTripPanel.js";
|
|
26
|
+
export { m as AuditTrailDialog, S as AvailabilityPanel, s as BookingPanel, d as CreateProviderDialog, u as CreateTravelPolicyDialog, o as DetailHero, i as DetailRow, C as EMPTY_LISTING_FILTERS, y as EcoAccessPanel, r as EntityCard, _ as FactGrid, E as GroupTripPanel, l as ListingEditDialog, w as ListingFilters, a as LoadMoreButton, h as PhotoGallery, b as ProviderCard, x as ProviderDashboardCard, v as RatingSummary, p as RedeemTicketPanel, c as ReviewsSection, n as SectionCard, e as StatCard, t as StatusBadge, g as TagChips, f as TicketDialog, T as hasActiveListingFilters };
|
|
@@ -40,6 +40,7 @@ export declare const UPDATE_GGNOMAD_TRIP_PLAN_MUTATION: import('graphql').Docume
|
|
|
40
40
|
export declare const DELETE_GGNOMAD_TRIP_PLAN_MUTATION: import('graphql').DocumentNode;
|
|
41
41
|
export declare const ADD_GGNOMAD_TRIP_PLAN_ITEM_MUTATION: import('graphql').DocumentNode;
|
|
42
42
|
export declare const REMOVE_GGNOMAD_TRIP_PLAN_ITEM_MUTATION: import('graphql').DocumentNode;
|
|
43
|
+
export declare const CREATE_GGNOMAD_PROVIDER_MUTATION: import('graphql').DocumentNode;
|
|
43
44
|
export declare const GGNOMAD_REVIEW_SUMMARY_QUERY: import('graphql').DocumentNode;
|
|
44
45
|
export declare const GGNOMAD_REVIEWS_QUERY: import('graphql').DocumentNode;
|
|
45
46
|
export declare const CREATE_GGNOMAD_REVIEW_MUTATION: import('graphql').DocumentNode;
|