@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
|
@@ -1,145 +1,260 @@
|
|
|
1
1
|
import { GGNOMAD_PROVIDERS_QUERY as e } from "../graphql.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
2
|
+
import { PageShell as t } from "./PageShell.js";
|
|
3
|
+
import { LoadMoreButton as n } from "../components/LoadMoreButton.js";
|
|
4
|
+
import { notify as r } from "../utils/notify.js";
|
|
5
|
+
import { getProviderTypeOptions as i } from "../utils/providerTypeMeta.js";
|
|
6
|
+
import { CreateProviderDialog as a } from "../components/CreateProviderDialog.js";
|
|
7
|
+
import { ProviderDashboardCard as o } from "../components/ProviderDashboardCard.js";
|
|
8
8
|
import "../components/index.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { GlassCard as
|
|
15
|
-
import { jsx as
|
|
9
|
+
import { DEFAULT_PROVIDER_QUERY as s, applyProviderControls as c } from "../utils/providerListControls.js";
|
|
10
|
+
import { useCallback as l, useEffect as u, useMemo as d, useRef as f, useState as p } from "react";
|
|
11
|
+
import { Briefcase as m, Plus as h, Search as g, X as _ } from "lucide-react";
|
|
12
|
+
import { useI18n as ee } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
13
|
+
import { useApolloClient as te } from "@apollo/client/react";
|
|
14
|
+
import { CTAOverflowMenu as ne, GlassCard as v, IllustratedEmptyState as y, PagePurpose as b } from "@burdenoff/fe-libs/ui";
|
|
15
|
+
import { jsx as x, jsxs as S } from "react/jsx-runtime";
|
|
16
16
|
//#region src/ggnomad/pages/ProviderPage.tsx
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
17
|
+
var C = 100, w = 20, T = 250, E = "rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder-text-secondary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg", D = E, O = "rounded-full border border-border-seam px-3 py-1.5 text-sm transition-colors", k = " border-border-seam bg-bg-sunken font-medium text-text-primary", A = " text-text-secondary hover:bg-bg-sunken", j = [
|
|
18
|
+
{
|
|
19
|
+
value: "newest",
|
|
20
|
+
labelKey: "ggnomad.provider.toolbar.sortNewest",
|
|
21
|
+
labelFallback: "Newest first"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
value: "oldest",
|
|
25
|
+
labelKey: "ggnomad.provider.toolbar.sortOldest",
|
|
26
|
+
labelFallback: "Oldest first"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
value: "nameAsc",
|
|
30
|
+
labelKey: "ggnomad.provider.toolbar.sortNameAsc",
|
|
31
|
+
labelFallback: "Name A to Z"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
value: "nameDesc",
|
|
35
|
+
labelKey: "ggnomad.provider.toolbar.sortNameDesc",
|
|
36
|
+
labelFallback: "Name Z to A"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
value: "revenueDesc",
|
|
40
|
+
labelKey: "ggnomad.provider.toolbar.sortRevenue",
|
|
41
|
+
labelFallback: "Most revenue"
|
|
42
|
+
}
|
|
43
|
+
];
|
|
44
|
+
function M() {
|
|
45
|
+
let { t: M } = ee(), N = te(), [P, re] = p(null), [ie, F] = p(!1), [I, L] = p(!1), R = f(null);
|
|
46
|
+
u(() => {
|
|
47
|
+
R.current = P;
|
|
48
|
+
}, [P]);
|
|
49
|
+
let [z, B] = p(""), [V, H] = p(""), [U, W] = p(null), [G, K] = p(s.sort), [q, J] = p(12), Y = f(null), X = l(async () => {
|
|
50
|
+
try {
|
|
51
|
+
let t = [], n = 0;
|
|
52
|
+
for (let r = 0; r < w; r += 1) {
|
|
53
|
+
let { data: r } = await N.query({
|
|
54
|
+
query: e,
|
|
55
|
+
variables: {
|
|
56
|
+
limit: C,
|
|
57
|
+
offset: n
|
|
58
|
+
},
|
|
59
|
+
fetchPolicy: "network-only"
|
|
60
|
+
}), i = r?.ggnomadProviders ?? [];
|
|
61
|
+
if (t.push(...i), i.length < C) break;
|
|
62
|
+
n += C;
|
|
63
|
+
}
|
|
64
|
+
re(t), F(!1);
|
|
65
|
+
} catch {
|
|
66
|
+
R.current === null ? F(!0) : r.error(M("ggnomad.provider.reloadError", "Could not refresh provider list"), "");
|
|
67
|
+
}
|
|
68
|
+
}, [N, M]);
|
|
69
|
+
u(() => {
|
|
70
|
+
let e = setTimeout(() => void X(), 0);
|
|
71
|
+
return () => clearTimeout(e);
|
|
72
|
+
}, [X]), u(() => () => {
|
|
73
|
+
Y.current && clearTimeout(Y.current);
|
|
74
|
+
}, []);
|
|
75
|
+
let ae = (e) => {
|
|
76
|
+
B(e), Y.current && clearTimeout(Y.current), Y.current = setTimeout(() => {
|
|
77
|
+
H(e), J(12);
|
|
78
|
+
}, T);
|
|
79
|
+
}, oe = () => {
|
|
80
|
+
Y.current && clearTimeout(Y.current), B(""), H(""), J(12);
|
|
81
|
+
}, Z = (e) => {
|
|
82
|
+
W(e), J(12);
|
|
83
|
+
}, se = (e) => {
|
|
84
|
+
K(e), J(12);
|
|
85
|
+
}, Q = d(() => ({
|
|
86
|
+
search: V,
|
|
87
|
+
type: U,
|
|
88
|
+
sort: G
|
|
89
|
+
}), [
|
|
90
|
+
V,
|
|
91
|
+
U,
|
|
92
|
+
G
|
|
93
|
+
]), $ = d(() => P === null ? null : c(P, Q, q), [
|
|
94
|
+
P,
|
|
95
|
+
Q,
|
|
96
|
+
q
|
|
97
|
+
]), ce = () => L(!0), le = async () => {
|
|
98
|
+
await X();
|
|
99
|
+
}, ue = () => {
|
|
100
|
+
Y.current && clearTimeout(Y.current), B(""), H(""), W(null), K(s.sort), J(12);
|
|
101
|
+
}, de = i(M);
|
|
102
|
+
return /* @__PURE__ */ S(t, {
|
|
103
|
+
title: M("ggnomad.provider.title", "Provider Dashboard"),
|
|
104
|
+
titleAccent: M("ggnomad.provider.title.accent", "Provider"),
|
|
105
|
+
subtitle: M("ggnomad.provider.subtitle", "B2B provider operations and onboarding metrics"),
|
|
106
|
+
actions: /* @__PURE__ */ x(ne, {
|
|
107
|
+
primary: {
|
|
108
|
+
label: M("ggnomad.provider.actions.newProvider", "New provider"),
|
|
109
|
+
onSelect: ce,
|
|
110
|
+
icon: /* @__PURE__ */ x(h, {})
|
|
111
|
+
},
|
|
112
|
+
actions: []
|
|
113
|
+
}),
|
|
114
|
+
children: [
|
|
115
|
+
/* @__PURE__ */ x(b, {
|
|
116
|
+
icon: /* @__PURE__ */ x(m, { className: "size-4" }),
|
|
117
|
+
children: M("ggnomad.provider.purpose", "Track the supply side of the marketplace — the hotels, tour operators and venues that publish inventory. Use it to monitor each provider’s status, listing count, revenue and rating, and to spot who needs onboarding attention.")
|
|
118
|
+
}),
|
|
119
|
+
ie ? /* @__PURE__ */ x("p", {
|
|
120
|
+
className: "text-sm text-text-secondary",
|
|
121
|
+
children: M("ggnomad.provider.loadError", "Providers could not be loaded. Refresh the page to try again.")
|
|
122
|
+
}) : P === null ? /* @__PURE__ */ x("p", {
|
|
123
|
+
className: "text-sm text-text-secondary",
|
|
124
|
+
children: M("ggnomad.provider.loading", "Loading providers…")
|
|
125
|
+
}) : P.length === 0 ? /* @__PURE__ */ x(y, {
|
|
126
|
+
illustration: "empty-people",
|
|
127
|
+
title: M("ggnomad.provider.emptyTitle", "No providers onboarded"),
|
|
128
|
+
description: M("ggnomad.provider.emptyDescription", "Onboarded B2B providers and their performance will be tracked here.")
|
|
129
|
+
}) : $ && /* @__PURE__ */ S(v, {
|
|
130
|
+
treatment: "glass",
|
|
131
|
+
glow: !0,
|
|
132
|
+
className: "border-border-seam p-5",
|
|
133
|
+
children: [
|
|
134
|
+
/* @__PURE__ */ x("h2", {
|
|
135
|
+
className: "mb-4 text-lg font-medium tracking-tight text-text-primary",
|
|
136
|
+
children: M("ggnomad.provider.onboardedProviders", "Onboarded providers")
|
|
137
|
+
}),
|
|
138
|
+
/* @__PURE__ */ S("div", {
|
|
139
|
+
className: "mb-4 flex flex-col gap-3",
|
|
140
|
+
children: [
|
|
141
|
+
/* @__PURE__ */ S("div", {
|
|
142
|
+
className: "flex flex-wrap items-center gap-2",
|
|
143
|
+
children: [/* @__PURE__ */ S("div", {
|
|
144
|
+
className: "relative w-full sm:w-72",
|
|
93
145
|
children: [
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
146
|
+
/* @__PURE__ */ x(g, {
|
|
147
|
+
className: "pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-text-secondary",
|
|
148
|
+
"aria-hidden": "true"
|
|
149
|
+
}),
|
|
150
|
+
/* @__PURE__ */ x("input", {
|
|
151
|
+
id: "provider-search",
|
|
152
|
+
type: "text",
|
|
153
|
+
value: z,
|
|
154
|
+
onChange: (e) => ae(e.target.value),
|
|
155
|
+
placeholder: M("ggnomad.provider.toolbar.searchPlaceholder", "Search providers"),
|
|
156
|
+
"aria-label": M("ggnomad.provider.toolbar.searchLabel", "Search providers"),
|
|
157
|
+
className: `${E} w-full pl-9 pr-8`
|
|
158
|
+
}),
|
|
159
|
+
z && /* @__PURE__ */ x("button", {
|
|
160
|
+
type: "button",
|
|
161
|
+
onClick: oe,
|
|
162
|
+
"aria-label": M("ggnomad.provider.toolbar.clearSearch", "Clear search"),
|
|
163
|
+
className: "absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary transition-colors hover:text-text-primary",
|
|
164
|
+
children: /* @__PURE__ */ x(_, {
|
|
165
|
+
className: "size-4",
|
|
99
166
|
"aria-hidden": "true"
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
e.contactPhone ? /* @__PURE__ */ x("a", {
|
|
103
|
-
href: `tel:${e.contactPhone}`,
|
|
104
|
-
className: "inline-flex items-center gap-1 text-action-primary-bg hover:underline",
|
|
105
|
-
children: [/* @__PURE__ */ b(f, {
|
|
106
|
-
className: "size-3.5",
|
|
107
|
-
"aria-hidden": "true"
|
|
108
|
-
}), e.contactPhone]
|
|
109
|
-
}) : null,
|
|
110
|
-
e.website ? /* @__PURE__ */ x("a", {
|
|
111
|
-
href: e.website,
|
|
112
|
-
target: "_blank",
|
|
113
|
-
rel: "noreferrer noopener",
|
|
114
|
-
className: "inline-flex items-center gap-1 text-action-primary-bg hover:underline",
|
|
115
|
-
children: [/* @__PURE__ */ b(u, {
|
|
116
|
-
className: "size-3.5",
|
|
117
|
-
"aria-hidden": "true"
|
|
118
|
-
}), S("ggnomad.provider.website", "Website")]
|
|
119
|
-
}) : null
|
|
167
|
+
})
|
|
168
|
+
})
|
|
120
169
|
]
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
170
|
+
}), /* @__PURE__ */ x("select", {
|
|
171
|
+
id: "provider-sort",
|
|
172
|
+
value: G,
|
|
173
|
+
onChange: (e) => se(e.target.value),
|
|
174
|
+
"aria-label": M("ggnomad.provider.toolbar.sortLabel", "Sort providers"),
|
|
175
|
+
className: D,
|
|
176
|
+
children: j.map((e) => /* @__PURE__ */ x("option", {
|
|
177
|
+
value: e.value,
|
|
178
|
+
children: M(e.labelKey, e.labelFallback)
|
|
179
|
+
}, e.value))
|
|
180
|
+
})]
|
|
181
|
+
}),
|
|
182
|
+
/* @__PURE__ */ S("div", {
|
|
183
|
+
className: "flex flex-wrap gap-2",
|
|
184
|
+
role: "group",
|
|
185
|
+
"aria-label": M("ggnomad.provider.toolbar.typeLabel", "Filter by type"),
|
|
186
|
+
children: [/* @__PURE__ */ x("button", {
|
|
187
|
+
type: "button",
|
|
188
|
+
onClick: () => Z(null),
|
|
189
|
+
"aria-pressed": U === null,
|
|
190
|
+
className: O + (U === null ? k : A),
|
|
191
|
+
children: M("ggnomad.provider.toolbar.allTypes", "All")
|
|
192
|
+
}), de.map((e) => {
|
|
193
|
+
let t = U === e.value;
|
|
194
|
+
return /* @__PURE__ */ x("button", {
|
|
195
|
+
type: "button",
|
|
196
|
+
onClick: () => Z(e.value),
|
|
197
|
+
"aria-pressed": t,
|
|
198
|
+
className: O + (t ? k : A),
|
|
199
|
+
children: e.label
|
|
200
|
+
}, e.value);
|
|
201
|
+
})]
|
|
202
|
+
}),
|
|
203
|
+
/* @__PURE__ */ S("p", {
|
|
204
|
+
className: "text-xs text-text-secondary",
|
|
205
|
+
"aria-live": "polite",
|
|
206
|
+
children: [M("ggnomad.provider.toolbar.showing", "Showing {{shown}} of {{total}} providers", {
|
|
207
|
+
shown: $.shown.length,
|
|
208
|
+
total: $.total
|
|
209
|
+
}), $.isFiltered ? M("ggnomad.provider.toolbar.filteredSuffix", " (filtered)") : ""]
|
|
210
|
+
})
|
|
211
|
+
]
|
|
212
|
+
}),
|
|
213
|
+
$.total === 0 ? /* @__PURE__ */ S("div", {
|
|
214
|
+
className: "flex flex-col items-center gap-3 py-12 text-center",
|
|
215
|
+
children: [/* @__PURE__ */ x("p", {
|
|
216
|
+
className: "text-sm text-text-secondary",
|
|
217
|
+
children: M("ggnomad.provider.toolbar.noMatches", "No providers match your filters")
|
|
218
|
+
}), /* @__PURE__ */ x("button", {
|
|
219
|
+
type: "button",
|
|
220
|
+
onClick: ue,
|
|
221
|
+
className: "rounded-lg border border-border-seam bg-bg-surface px-5 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg",
|
|
222
|
+
children: M("ggnomad.provider.toolbar.clearFilters", "Clear filters")
|
|
223
|
+
})]
|
|
224
|
+
}) : /* @__PURE__ */ x("div", {
|
|
225
|
+
className: "grid gap-4",
|
|
226
|
+
style: { gridTemplateColumns: "repeat(auto-fill, minmax(320px, 1fr))" },
|
|
227
|
+
children: $.shown.map((e) => /* @__PURE__ */ x(o, {
|
|
228
|
+
id: e.id,
|
|
229
|
+
name: e.name,
|
|
230
|
+
type: e.type,
|
|
231
|
+
status: e.status,
|
|
232
|
+
description: e.description,
|
|
233
|
+
listingCount: e.listingCount,
|
|
234
|
+
totalRevenue: e.totalRevenue,
|
|
235
|
+
rating: e.rating,
|
|
236
|
+
reviewCount: e.reviewCount,
|
|
237
|
+
contactEmail: e.contactEmail,
|
|
238
|
+
contactPhone: e.contactPhone,
|
|
239
|
+
website: e.website,
|
|
240
|
+
createdAt: e.createdAt
|
|
241
|
+
}, e.id))
|
|
242
|
+
}),
|
|
243
|
+
/* @__PURE__ */ x(n, {
|
|
244
|
+
visible: $.hasMore,
|
|
245
|
+
onLoadMore: () => J((e) => e + 12)
|
|
246
|
+
})
|
|
247
|
+
]
|
|
248
|
+
}),
|
|
249
|
+
/* @__PURE__ */ x(a, {
|
|
250
|
+
open: I,
|
|
251
|
+
onOpenChange: L,
|
|
252
|
+
onCreated: le
|
|
253
|
+
})
|
|
254
|
+
]
|
|
140
255
|
});
|
|
141
256
|
}
|
|
142
257
|
//#endregion
|
|
143
|
-
export {
|
|
258
|
+
export { M as ProviderPage };
|
|
144
259
|
|
|
145
260
|
//# sourceMappingURL=ProviderPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProviderPage.js","names":[],"sources":["../../../src/ggnomad/pages/ProviderPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { Link } from 'react-router';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Briefcase, CalendarPlus, Globe, Mail, Phone, Store } from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_PROVIDERS_QUERY } from '../graphql';\nimport { formatDate } from '../utils/format';\nimport { PageShell } from './PageShell';\nimport { EntityCard, LoadMoreButton, RatingSummary, StatusBadge } from '../components';\n\nexport function ProviderPage() {\n const { t } = useI18n();\n const [limit, setLimit] = useState(12);\n const { data } = useQuery(\n GGNOMAD_PROVIDERS_QUERY as TypedDocumentNode<\n {\n ggnomadProviders: Array<{\n id: string;\n name: string;\n type: string;\n status: string;\n listingCount: number;\n totalRevenue: number;\n rating?: number | null;\n reviewCount: number;\n description?: string | null;\n contactEmail?: string | null;\n contactPhone?: string | null;\n website?: string | null;\n createdAt: string;\n }>;\n },\n { limit: number; offset: number }\n >,\n { variables: { limit, offset: 0 } }\n );\n const providers = data?.ggnomadProviders ?? [];\n\n return (\n <PageShell\n title={t('ggnomad.provider.title', 'Provider Dashboard')}\n titleAccent={t('ggnomad.provider.title.accent', 'Provider')}\n subtitle={t('ggnomad.provider.subtitle', 'B2B provider operations and onboarding metrics')}\n >\n <PagePurpose icon={<Briefcase className=\"size-4\" />}>\n {t(\n 'ggnomad.provider.purpose',\n 'Track the supply side of the marketplace — the hotels, tour operators and venues that publish inventory. Use it to monitor each provider’s status, listing count, revenue and rating, and to spot who needs onboarding attention.'\n )}\n </PagePurpose>\n\n {providers.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={t('ggnomad.provider.emptyTitle', 'No providers onboarded')}\n description={t(\n 'ggnomad.provider.emptyDescription',\n 'Onboarded B2B providers and their performance will be tracked here.'\n )}\n />\n ) : (\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.provider.onboardedProviders', 'Onboarded providers')}\n </h2>\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {providers.map((provider) => (\n <EntityCard\n key={provider.id}\n title={provider.name}\n meta={provider.description ?? provider.type}\n badge={<StatusBadge status={provider.status} live={provider.status === 'ACTIVE'} />}\n footer={\n <div className=\"space-y-2 text-xs\">\n <RatingSummary rating={provider.rating} reviewCount={provider.reviewCount} />\n <dl className=\"grid grid-cols-3 gap-2\">\n <div>\n <dt className=\"text-text-secondary\">\n {t('ggnomad.provider.listings', 'Listings')}\n </dt>\n <dd className=\"mt-0.5 font-medium tabular-nums text-text-primary\">\n {provider.listingCount}\n </dd>\n </div>\n <div>\n <dt className=\"text-text-secondary\">\n {t('ggnomad.provider.revenue', 'Revenue')}\n </dt>\n <dd className=\"mt-0.5 font-medium tabular-nums text-text-primary\">\n ${Number(provider.totalRevenue).toLocaleString()}\n </dd>\n </div>\n <div>\n <dt className=\"text-text-secondary\">\n {t('ggnomad.provider.type', 'Type')}\n </dt>\n <dd className=\"mt-0.5 font-medium text-text-primary\">{provider.type}</dd>\n </div>\n </dl>\n <p className=\"inline-flex items-center gap-1 text-text-secondary\">\n <CalendarPlus className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.provider.joined', 'Joined {{date}}').replace(\n '{{date}}',\n formatDate(provider.createdAt) ?? '—'\n )}\n </p>\n {provider.contactEmail || provider.contactPhone || provider.website ? (\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1\">\n {provider.contactEmail ? (\n <a\n href={`mailto:${provider.contactEmail}`}\n className=\"inline-flex items-center gap-1 text-action-primary-bg hover:underline\"\n >\n <Mail className=\"size-3.5\" aria-hidden=\"true\" />\n {provider.contactEmail}\n </a>\n ) : null}\n {provider.contactPhone ? (\n <a\n href={`tel:${provider.contactPhone}`}\n className=\"inline-flex items-center gap-1 text-action-primary-bg hover:underline\"\n >\n <Phone className=\"size-3.5\" aria-hidden=\"true\" />\n {provider.contactPhone}\n </a>\n ) : null}\n {provider.website ? (\n <a\n href={provider.website}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className=\"inline-flex items-center gap-1 text-action-primary-bg hover:underline\"\n >\n <Globe className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.provider.website', 'Website')}\n </a>\n ) : null}\n </div>\n ) : null}\n <div className=\"mt-1 border-t border-border-seam pt-2\">\n <Link\n to={`${provider.id}/storefront`}\n className=\"inline-flex items-center gap-1 text-xs font-medium text-action-primary-bg hover:underline\"\n >\n <Store className=\"size-3.5\" />\n {t('ggnomad.provider.manageStorefront', 'Manage storefront')}\n </Link>\n </div>\n </div>\n }\n />\n ))}\n </div>\n <LoadMoreButton\n visible={providers.length >= limit}\n onLoadMore={() => setLimit((l) => l + 12)}\n />\n </GlassCard>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProviderPage.js","names":[],"sources":["../../../src/ggnomad/pages/ProviderPage.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useApolloClient } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Briefcase, Plus, Search, X } from 'lucide-react';\nimport {\n CTAOverflowMenu,\n GlassCard,\n IllustratedEmptyState,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_PROVIDERS_QUERY } from '../graphql';\nimport { notify } from '../utils/notify';\nimport { PageShell } from './PageShell';\nimport { CreateProviderDialog, LoadMoreButton, ProviderDashboardCard } from '../components';\nimport { getProviderTypeOptions } from '../utils/providerTypeMeta';\nimport {\n applyProviderControls,\n DEFAULT_PROVIDER_QUERY,\n PROVIDER_PAGE_SIZE,\n type ProviderListQuery,\n type ProviderSort,\n} from '../utils/providerListControls';\n\ninterface Provider {\n id: string;\n name: string;\n type: string;\n status: string;\n listingCount: number;\n totalRevenue: number;\n rating?: number | null;\n reviewCount: number;\n description?: string | null;\n contactEmail?: string | null;\n contactPhone?: string | null;\n website?: string | null;\n createdAt: string;\n}\n\n/** One backend request can return at most MAX_LIMIT (100) rows. */\nconst FETCH_PAGE_SIZE = 100;\n/** Safety stop so a misbehaving backend can never loop this page forever. */\nconst MAX_FETCH_PAGES = 20;\n/** Debounce window for the search box before filters re-apply. */\nconst SEARCH_DEBOUNCE_MS = 250;\n\n// Toolbar control styling — same class families as the create dialog's\n// inputs and LoadMoreButton (bg-*/border-*/text-* tokens), so everything\n// the toolbar uses already renders on this page in light and dark mode.\nconst toolbarInputClass =\n 'rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder-text-secondary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg';\nconst toolbarSelectClass = toolbarInputClass;\nconst chipBaseClass =\n 'rounded-full border border-border-seam px-3 py-1.5 text-sm transition-colors';\nconst chipActiveClass = ' border-border-seam bg-bg-sunken font-medium text-text-primary';\nconst chipIdleClass = ' text-text-secondary hover:bg-bg-sunken';\n\nconst SORT_OPTIONS: Array<{ value: ProviderSort; labelKey: string; labelFallback: string }> = [\n {\n value: 'newest',\n labelKey: 'ggnomad.provider.toolbar.sortNewest',\n labelFallback: 'Newest first',\n },\n {\n value: 'oldest',\n labelKey: 'ggnomad.provider.toolbar.sortOldest',\n labelFallback: 'Oldest first',\n },\n {\n value: 'nameAsc',\n labelKey: 'ggnomad.provider.toolbar.sortNameAsc',\n labelFallback: 'Name A to Z',\n },\n {\n value: 'nameDesc',\n labelKey: 'ggnomad.provider.toolbar.sortNameDesc',\n labelFallback: 'Name Z to A',\n },\n {\n value: 'revenueDesc',\n labelKey: 'ggnomad.provider.toolbar.sortRevenue',\n labelFallback: 'Most revenue',\n },\n];\n\nexport function ProviderPage() {\n const { t } = useI18n();\n const client = useApolloClient();\n const [providers, setProviders] = useState<Provider[] | null>(null);\n const [loadError, setLoadError] = useState(false);\n const [showCreateDialog, setShowCreateDialog] = useState(false);\n\n // Mirror the providers state into a ref so `loadProviders` can read the\n // current value without listing `providers` in its useCallback deps.\n // Without this, every successful setProviders() rebuilds the callback,\n // which retriggers the useEffect that depends on it, which calls\n // loadProviders again — an infinite refetch loop.\n const providersRef = useRef<Provider[] | null>(null);\n useEffect(() => {\n providersRef.current = providers;\n }, [providers]);\n\n const [searchInput, setSearchInput] = useState('');\n const [appliedSearch, setAppliedSearch] = useState('');\n const [typeFilter, setTypeFilter] = useState<string | null>(null);\n const [sort, setSort] = useState<ProviderSort>(DEFAULT_PROVIDER_QUERY.sort);\n const [visibleCount, setVisibleCount] = useState(PROVIDER_PAGE_SIZE);\n const searchTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // The backend caps one request at 100 rows and exposes no filters or\n // totalCount, so load every page up front; the toolbar then filters\n // client-side over the complete list (see providerListControls.ts).\n const loadProviders = useCallback(async () => {\n try {\n const all: Provider[] = [];\n let offset = 0;\n for (let page = 0; page < MAX_FETCH_PAGES; page += 1) {\n const { data } = await client.query({\n query: GGNOMAD_PROVIDERS_QUERY as TypedDocumentNode<\n { ggnomadProviders: Provider[] },\n { limit: number; offset: number }\n >,\n variables: { limit: FETCH_PAGE_SIZE, offset },\n fetchPolicy: 'network-only',\n });\n const batch = data?.ggnomadProviders ?? [];\n all.push(...batch);\n if (batch.length < FETCH_PAGE_SIZE) break;\n offset += FETCH_PAGE_SIZE;\n }\n setProviders(all);\n setLoadError(false);\n } catch {\n // Preserve the existing grid if we already have data — a failed\n // reload after a successful create would otherwise hide the\n // already-loaded providers behind a generic error state even\n // though the data is still in memory. Only escalate to the\n // full-page error when there is no data to fall back to.\n if (providersRef.current === null) setLoadError(true);\n else notify.error(t('ggnomad.provider.reloadError', 'Could not refresh provider list'), '');\n }\n }, [client, t]);\n\n useEffect(() => {\n // Defer one tick so the fetch (and its setState) is async from the\n // effect body — satisfies the no-sync-setState-in-effect rule.\n const timer = setTimeout(() => void loadProviders(), 0);\n return () => clearTimeout(timer);\n }, [loadProviders]);\n\n // Cleanup only: never leave a pending debounce timer mounted.\n useEffect(\n () => () => {\n if (searchTimer.current) clearTimeout(searchTimer.current);\n },\n []\n );\n\n // Debounced search: re-apply filters 250ms after typing stops. Timer is\n // managed from the change handler (not an effect) so typing state stays\n // in the event flow; the applied value resets the grid to page one.\n const handleSearchChange = (value: string) => {\n setSearchInput(value);\n if (searchTimer.current) clearTimeout(searchTimer.current);\n searchTimer.current = setTimeout(() => {\n setAppliedSearch(value);\n setVisibleCount(PROVIDER_PAGE_SIZE);\n }, SEARCH_DEBOUNCE_MS);\n };\n\n const handleClearSearch = () => {\n if (searchTimer.current) clearTimeout(searchTimer.current);\n setSearchInput('');\n setAppliedSearch('');\n setVisibleCount(PROVIDER_PAGE_SIZE);\n };\n\n // Every control change returns the grid to the first page.\n const handleTypeChange = (value: string | null) => {\n setTypeFilter(value);\n setVisibleCount(PROVIDER_PAGE_SIZE);\n };\n\n const handleSortChange = (value: ProviderSort) => {\n setSort(value);\n setVisibleCount(PROVIDER_PAGE_SIZE);\n };\n\n const query: ProviderListQuery = useMemo(\n () => ({ search: appliedSearch, type: typeFilter, sort }),\n [appliedSearch, typeFilter, sort]\n );\n\n const result = useMemo(\n () => (providers === null ? null : applyProviderControls(providers, query, visibleCount)),\n [providers, query, visibleCount]\n );\n\n const openCreateDialog = () => setShowCreateDialog(true);\n const handleCreated = async () => {\n // The dialog already showed a success toast; a quiet reload\n // reconciles the new provider with the server.\n await loadProviders();\n };\n\n const clearFilters = () => {\n if (searchTimer.current) clearTimeout(searchTimer.current);\n setSearchInput('');\n setAppliedSearch('');\n setTypeFilter(null);\n setSort(DEFAULT_PROVIDER_QUERY.sort);\n setVisibleCount(PROVIDER_PAGE_SIZE);\n };\n\n const typeOptions = getProviderTypeOptions(t);\n\n return (\n <PageShell\n title={t('ggnomad.provider.title', 'Provider Dashboard')}\n titleAccent={t('ggnomad.provider.title.accent', 'Provider')}\n subtitle={t('ggnomad.provider.subtitle', 'B2B provider operations and onboarding metrics')}\n actions={\n <CTAOverflowMenu\n primary={{\n label: t('ggnomad.provider.actions.newProvider', 'New provider'),\n onSelect: openCreateDialog,\n icon: <Plus />,\n }}\n actions={[]}\n />\n }\n >\n <PagePurpose icon={<Briefcase className=\"size-4\" />}>\n {t(\n 'ggnomad.provider.purpose',\n 'Track the supply side of the marketplace — the hotels, tour operators and venues that publish inventory. Use it to monitor each provider’s status, listing count, revenue and rating, and to spot who needs onboarding attention.'\n )}\n </PagePurpose>\n\n {loadError ? (\n <p className=\"text-sm text-text-secondary\">\n {t(\n 'ggnomad.provider.loadError',\n 'Providers could not be loaded. Refresh the page to try again.'\n )}\n </p>\n ) : providers === null ? (\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.provider.loading', 'Loading providers…')}\n </p>\n ) : providers.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={t('ggnomad.provider.emptyTitle', 'No providers onboarded')}\n description={t(\n 'ggnomad.provider.emptyDescription',\n 'Onboarded B2B providers and their performance will be tracked here.'\n )}\n />\n ) : (\n result && (\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.provider.onboardedProviders', 'Onboarded providers')}\n </h2>\n\n {/* Toolbar: search / status / sort row, type chips row, count line. */}\n <div className=\"mb-4 flex flex-col gap-3\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <div className=\"relative w-full sm:w-72\">\n <Search\n className=\"pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n id=\"provider-search\"\n type=\"text\"\n value={searchInput}\n onChange={(e) => handleSearchChange(e.target.value)}\n placeholder={t(\n 'ggnomad.provider.toolbar.searchPlaceholder',\n 'Search providers'\n )}\n aria-label={t('ggnomad.provider.toolbar.searchLabel', 'Search providers')}\n className={`${toolbarInputClass} w-full pl-9 pr-8`}\n />\n {searchInput && (\n <button\n type=\"button\"\n onClick={handleClearSearch}\n aria-label={t('ggnomad.provider.toolbar.clearSearch', 'Clear search')}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-text-secondary transition-colors hover:text-text-primary\"\n >\n <X className=\"size-4\" aria-hidden=\"true\" />\n </button>\n )}\n </div>\n\n <select\n id=\"provider-sort\"\n value={sort}\n onChange={(e) => handleSortChange(e.target.value as ProviderSort)}\n aria-label={t('ggnomad.provider.toolbar.sortLabel', 'Sort providers')}\n className={toolbarSelectClass}\n >\n {SORT_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {t(option.labelKey, option.labelFallback)}\n </option>\n ))}\n </select>\n </div>\n\n <div\n className=\"flex flex-wrap gap-2\"\n role=\"group\"\n aria-label={t('ggnomad.provider.toolbar.typeLabel', 'Filter by type')}\n >\n <button\n type=\"button\"\n onClick={() => handleTypeChange(null)}\n aria-pressed={typeFilter === null}\n className={\n chipBaseClass + (typeFilter === null ? chipActiveClass : chipIdleClass)\n }\n >\n {t('ggnomad.provider.toolbar.allTypes', 'All')}\n </button>\n {typeOptions.map((option) => {\n const isActive = typeFilter === option.value;\n return (\n <button\n key={option.value}\n type=\"button\"\n onClick={() => handleTypeChange(option.value)}\n aria-pressed={isActive}\n className={chipBaseClass + (isActive ? chipActiveClass : chipIdleClass)}\n >\n {option.label}\n </button>\n );\n })}\n </div>\n\n <p className=\"text-xs text-text-secondary\" aria-live=\"polite\">\n {t('ggnomad.provider.toolbar.showing', 'Showing {{shown}} of {{total}} providers', {\n shown: result.shown.length,\n total: result.total,\n })}\n {result.isFiltered\n ? t('ggnomad.provider.toolbar.filteredSuffix', ' (filtered)')\n : ''}\n </p>\n </div>\n\n {result.total === 0 ? (\n <div className=\"flex flex-col items-center gap-3 py-12 text-center\">\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.provider.toolbar.noMatches', 'No providers match your filters')}\n </p>\n <button\n type=\"button\"\n onClick={clearFilters}\n className=\"rounded-lg border border-border-seam bg-bg-surface px-5 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg\"\n >\n {t('ggnomad.provider.toolbar.clearFilters', 'Clear filters')}\n </button>\n </div>\n ) : (\n <div\n className=\"grid gap-4\"\n style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))' }}\n >\n {result.shown.map((provider) => (\n <ProviderDashboardCard\n key={provider.id}\n id={provider.id}\n name={provider.name}\n type={provider.type}\n status={provider.status}\n description={provider.description}\n listingCount={provider.listingCount}\n totalRevenue={provider.totalRevenue}\n rating={provider.rating}\n reviewCount={provider.reviewCount}\n contactEmail={provider.contactEmail}\n contactPhone={provider.contactPhone}\n website={provider.website}\n createdAt={provider.createdAt}\n />\n ))}\n </div>\n )}\n\n <LoadMoreButton\n visible={result.hasMore}\n onLoadMore={() => setVisibleCount((count) => count + PROVIDER_PAGE_SIZE)}\n />\n </GlassCard>\n )\n )}\n\n <CreateProviderDialog\n open={showCreateDialog}\n onOpenChange={setShowCreateDialog}\n onCreated={handleCreated}\n />\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
@@ -72,7 +72,7 @@ function A() {
|
|
|
72
72
|
B.data?.ggnomadActivities
|
|
73
73
|
]), U = f(() => {
|
|
74
74
|
let e = A.trim().toLowerCase();
|
|
75
|
-
return H.filter((t) => M !== "ALL" && t.domain !== M ? !1 : e
|
|
75
|
+
return H.filter((t) => M !== "ALL" && t.domain !== M ? !1 : !e || t.haystack.includes(e));
|
|
76
76
|
}, [
|
|
77
77
|
H,
|
|
78
78
|
A,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SearchPage.js","names":[],"sources":["../../../src/ggnomad/pages/SearchPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Activity, Building, Compass, Search, Ticket, X } from 'lucide-react';\nimport { EmphasisPanel, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport {\n GGNOMAD_PROPERTIES_QUERY,\n GGNOMAD_TOURS_QUERY,\n GGNOMAD_EVENTS_QUERY,\n GGNOMAD_ACTIVITIES_QUERY,\n} from '../graphql';\nimport { humanizeEnum } from '../utils/format';\nimport { cn } from '../utils/cn';\nimport { PageShell } from './PageShell';\nimport { EntityCard, LoadMoreButton, RatingSummary, SectionCard, StatCard } from '../components';\n\ntype Domain = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ntype CatalogueRow = {\n id: string;\n name: string;\n description?: string | null;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n images?: string[];\n};\n\ntype PropertyRow = CatalogueRow & { type: string; pricePerNight: number };\ntype TourRow = CatalogueRow & { type: string; price: number; duration: string };\ntype EventRow = CatalogueRow & { category: string; price: number };\ntype ActivityRow = CatalogueRow & { category: string; price: number; duration: string };\n\ntype Result = {\n key: string;\n domain: Domain;\n id: string;\n name: string;\n /** The listing's own category / type label, used both for display and matching. */\n kind: string;\n price: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n image?: string;\n haystack: string;\n /** Route relative to the GGNomad base path. */\n path: string;\n};\n\nconst PAGE_SIZE = 24;\n\nconst DOMAIN_ROUTE: Record<Domain, string> = {\n PROPERTY: '../hospitality',\n TOUR: '../tourism/tours',\n EVENT: '../entertainment',\n ACTIVITY: '../tourism/activities',\n};\n\nfunction toResult(domain: Domain, row: CatalogueRow & { price: number; kind: string }): Result {\n return {\n key: `${domain}-${row.id}`,\n domain,\n id: row.id,\n name: row.name,\n kind: row.kind,\n price: row.price,\n currency: row.currency,\n rating: row.rating,\n reviewCount: row.reviewCount,\n image: row.images?.[0],\n haystack: [row.name, row.kind, row.description ?? ''].join(' ').toLowerCase(),\n path: `${DOMAIN_ROUTE[domain]}/${row.id}`,\n };\n}\n\nexport function SearchPage() {\n const { t } = useI18n();\n const [term, setTerm] = useState('');\n const [domain, setDomain] = useState<Domain | 'ALL'>('ALL');\n const [limit, setLimit] = useState(PAGE_SIZE);\n\n const variables = { limit, offset: 0 };\n const properties = useQuery(\n GGNOMAD_PROPERTIES_QUERY as TypedDocumentNode<\n { ggnomadProperties: PropertyRow[] },\n { limit: number; offset: number }\n >,\n { variables }\n );\n const tours = useQuery(\n GGNOMAD_TOURS_QUERY as TypedDocumentNode<\n { ggnomadTours: TourRow[] },\n { limit: number; offset: number }\n >,\n { variables }\n );\n const events = useQuery(\n GGNOMAD_EVENTS_QUERY as TypedDocumentNode<\n { ggnomadEvents: EventRow[] },\n { limit: number; offset: number }\n >,\n { variables }\n );\n const activities = useQuery(\n GGNOMAD_ACTIVITIES_QUERY as TypedDocumentNode<\n { ggnomadActivities: ActivityRow[] },\n { limit: number; offset: number }\n >,\n { variables }\n );\n\n const loading = properties.loading || tours.loading || events.loading || activities.loading;\n\n const allResults = useMemo<Result[]>(() => {\n const rows: Result[] = [];\n for (const property of properties.data?.ggnomadProperties ?? []) {\n rows.push(\n toResult('PROPERTY', { ...property, price: property.pricePerNight, kind: property.type })\n );\n }\n for (const tour of tours.data?.ggnomadTours ?? []) {\n rows.push(toResult('TOUR', { ...tour, kind: tour.type }));\n }\n for (const event of events.data?.ggnomadEvents ?? []) {\n rows.push(toResult('EVENT', { ...event, kind: event.category }));\n }\n for (const activity of activities.data?.ggnomadActivities ?? []) {\n rows.push(toResult('ACTIVITY', { ...activity, kind: activity.category }));\n }\n return rows;\n }, [\n properties.data?.ggnomadProperties,\n tours.data?.ggnomadTours,\n events.data?.ggnomadEvents,\n activities.data?.ggnomadActivities,\n ]);\n\n const matches = useMemo(() => {\n const needle = term.trim().toLowerCase();\n return allResults.filter((result) => {\n if (domain !== 'ALL' && result.domain !== domain) return false;\n if (!needle) return true;\n return result.haystack.includes(needle);\n });\n }, [allResults, term, domain]);\n\n const countFor = (target: Domain) =>\n allResults.filter(\n (result) =>\n result.domain === target &&\n (!term.trim() || result.haystack.includes(term.trim().toLowerCase()))\n ).length;\n\n const propertyCount = countFor('PROPERTY');\n const tourCount = countFor('TOUR');\n const eventCount = countFor('EVENT');\n const activityCount = countFor('ACTIVITY');\n const total = propertyCount + tourCount + eventCount + activityCount;\n\n // Every domain query asks for the same page size, so any list that came back\n // full may have more behind it.\n const mayHaveMore = [\n properties.data?.ggnomadProperties,\n tours.data?.ggnomadTours,\n events.data?.ggnomadEvents,\n activities.data?.ggnomadActivities,\n ].some((rows) => (rows?.length ?? 0) >= limit);\n\n const domainLabel = (value: Domain): string => {\n switch (value) {\n case 'PROPERTY':\n return t('ggnomad.search.domain.property', 'Property');\n case 'TOUR':\n return t('ggnomad.search.domain.tour', 'Tour');\n case 'EVENT':\n return t('ggnomad.search.domain.event', 'Event');\n case 'ACTIVITY':\n return t('ggnomad.search.domain.activity', 'Activity');\n }\n };\n\n const domainChips: Array<{ value: Domain | 'ALL'; label: string; count: number }> = [\n { value: 'ALL', label: t('ggnomad.search.domain.all', 'All'), count: total },\n {\n value: 'PROPERTY',\n label: t('ggnomad.search.results.properties', 'Properties'),\n count: propertyCount,\n },\n { value: 'TOUR', label: t('ggnomad.search.results.tours', 'Tours'), count: tourCount },\n { value: 'EVENT', label: t('ggnomad.search.results.events', 'Events'), count: eventCount },\n {\n value: 'ACTIVITY',\n label: t('ggnomad.search.results.activities', 'Activities'),\n count: activityCount,\n },\n ];\n\n return (\n <PageShell\n title={t('ggnomad.search.title', 'Global Search')}\n titleAccent={t('ggnomad.search.title.accent', 'Search')}\n subtitle={t('ggnomad.search.subtitle', 'Cross-domain search across all GGNomad inventory')}\n >\n <PagePurpose icon={<Search className=\"size-4\" />}>\n {t(\n 'ggnomad.search.purpose',\n 'One search across the entire GGNomad catalogue — properties, tours, events and activities at once — so you can see everything available for a destination in a single view instead of checking each domain separately.'\n )}\n </PagePurpose>\n\n <div>\n <label htmlFor=\"ggnomad-search-term\" className=\"sr-only\">\n {t('ggnomad.search.inputLabel', 'Search the catalogue')}\n </label>\n <div className=\"relative\">\n <Search\n className=\"pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n id=\"ggnomad-search-term\"\n type=\"search\"\n value={term}\n onChange={(event) => setTerm(event.target.value)}\n placeholder={t(\n 'ggnomad.search.placeholder',\n 'Search names, categories and descriptions…'\n )}\n className=\"w-full rounded-lg border border-border-seam bg-bg-canvas py-2.5 pl-9 pr-10 text-sm text-text-primary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg\"\n />\n {term ? (\n <button\n type=\"button\"\n onClick={() => setTerm('')}\n aria-label={t('ggnomad.search.clear', 'Clear search')}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n >\n <X className=\"size-4\" />\n </button>\n ) : null}\n </div>\n <p className=\"mt-2 text-xs text-text-secondary\">\n {t(\n 'ggnomad.search.scopeNote',\n 'Matches against the catalogue loaded on this page. Use “Load more” to pull further inventory into the search.'\n )}\n </p>\n </div>\n\n <ul className=\"flex flex-wrap gap-2\">\n {domainChips.map((chip) => (\n <li key={chip.value}>\n <button\n type=\"button\"\n aria-pressed={domain === chip.value}\n onClick={() => setDomain(chip.value)}\n className={cn(\n 'rounded-full border px-3 py-1 text-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg',\n domain === chip.value\n ? 'border-action-primary-bg bg-action-primary-bg text-action-primary-text'\n : 'border-border-seam bg-bg-sunken text-text-primary hover:border-border-default'\n )}\n >\n {chip.label}\n <span className=\"ml-1.5 tabular-nums opacity-80\">{chip.count}</span>\n </button>\n </li>\n ))}\n </ul>\n\n <EmphasisPanel>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.search.totalMatches', 'Total matches across all domains')}\n </p>\n <p className=\"mt-1 text-3xl font-semibold tabular-nums text-text-primary\">\n {total.toLocaleString()}\n </p>\n <p className=\"mt-2 text-sm text-text-secondary\">\n {t(\n 'ggnomad.search.totalMatchesNote',\n 'Combined inventory from properties, tours, events and activities — open a domain below to drill in.'\n )}\n </p>\n </EmphasisPanel>\n\n <section className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n label={t('ggnomad.search.results.propertiesFound', 'Properties found')}\n value={propertyCount}\n icon={<Building />}\n />\n <StatCard\n label={t('ggnomad.search.results.toursFound', 'Tours found')}\n value={tourCount}\n icon={<Compass />}\n />\n <StatCard\n label={t('ggnomad.search.results.eventsFound', 'Events found')}\n value={eventCount}\n icon={<Ticket />}\n />\n <StatCard\n label={t('ggnomad.search.results.activitiesFound', 'Activities found')}\n value={activityCount}\n icon={<Activity />}\n />\n </section>\n\n {matches.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title={\n loading\n ? t('ggnomad.search.loadingTitle', 'Searching the catalogue…')\n : t('ggnomad.search.emptyTitle', 'No matching inventory')\n }\n description={\n loading\n ? t(\n 'ggnomad.search.loadingDescription',\n 'Fetching properties, tours, events and activities.'\n )\n : term.trim()\n ? t(\n 'ggnomad.search.noTermMatch',\n 'Nothing matched “{{term}}”. Try a shorter term or another domain.'\n ).replace('{{term}}', term.trim())\n : t(\n 'ggnomad.search.emptyDescription',\n 'Nothing matched across properties, tours, events and activities yet.'\n )\n }\n />\n ) : (\n <SectionCard title={t('ggnomad.search.resultsTitle', 'Results')}>\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {matches.map((result) => (\n <EntityCard\n key={result.key}\n to={result.path}\n image={result.image}\n imageAlt={result.name}\n title={result.name}\n meta={humanizeEnum(result.kind)}\n badge={\n <span className=\"rounded-full bg-bg-surface/90 px-2.5 py-0.5 text-xs font-medium text-text-primary\">\n {domainLabel(result.domain)}\n </span>\n }\n price={`${result.currency} ${Number(result.price).toFixed(2)}`}\n footer={<RatingSummary rating={result.rating} reviewCount={result.reviewCount} />}\n />\n ))}\n </div>\n <LoadMoreButton\n visible={mayHaveMore}\n onLoadMore={() => setLimit((current) => current + PAGE_SIZE)}\n />\n </SectionCard>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SearchPage.js","names":[],"sources":["../../../src/ggnomad/pages/SearchPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Activity, Building, Compass, Search, Ticket, X } from 'lucide-react';\nimport { EmphasisPanel, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport {\n GGNOMAD_PROPERTIES_QUERY,\n GGNOMAD_TOURS_QUERY,\n GGNOMAD_EVENTS_QUERY,\n GGNOMAD_ACTIVITIES_QUERY,\n} from '../graphql';\nimport { humanizeEnum } from '../utils/format';\nimport { cn } from '../utils/cn';\nimport { PageShell } from './PageShell';\nimport { EntityCard, LoadMoreButton, RatingSummary, SectionCard, StatCard } from '../components';\n\ntype Domain = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ntype CatalogueRow = {\n id: string;\n name: string;\n description?: string | null;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n images?: string[];\n};\n\ntype PropertyRow = CatalogueRow & { type: string; pricePerNight: number };\ntype TourRow = CatalogueRow & { type: string; price: number; duration: string };\ntype EventRow = CatalogueRow & { category: string; price: number };\ntype ActivityRow = CatalogueRow & { category: string; price: number; duration: string };\n\ntype Result = {\n key: string;\n domain: Domain;\n id: string;\n name: string;\n /** The listing's own category / type label, used both for display and matching. */\n kind: string;\n price: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n image?: string;\n haystack: string;\n /** Route relative to the GGNomad base path. */\n path: string;\n};\n\nconst PAGE_SIZE = 24;\n\nconst DOMAIN_ROUTE: Record<Domain, string> = {\n PROPERTY: '../hospitality',\n TOUR: '../tourism/tours',\n EVENT: '../entertainment',\n ACTIVITY: '../tourism/activities',\n};\n\nfunction toResult(domain: Domain, row: CatalogueRow & { price: number; kind: string }): Result {\n return {\n key: `${domain}-${row.id}`,\n domain,\n id: row.id,\n name: row.name,\n kind: row.kind,\n price: row.price,\n currency: row.currency,\n rating: row.rating,\n reviewCount: row.reviewCount,\n image: row.images?.[0],\n haystack: [row.name, row.kind, row.description ?? ''].join(' ').toLowerCase(),\n path: `${DOMAIN_ROUTE[domain]}/${row.id}`,\n };\n}\n\nexport function SearchPage() {\n const { t } = useI18n();\n const [term, setTerm] = useState('');\n const [domain, setDomain] = useState<Domain | 'ALL'>('ALL');\n const [limit, setLimit] = useState(PAGE_SIZE);\n\n const variables = { limit, offset: 0 };\n const properties = useQuery(\n GGNOMAD_PROPERTIES_QUERY as TypedDocumentNode<\n { ggnomadProperties: PropertyRow[] },\n { limit: number; offset: number }\n >,\n { variables }\n );\n const tours = useQuery(\n GGNOMAD_TOURS_QUERY as TypedDocumentNode<\n { ggnomadTours: TourRow[] },\n { limit: number; offset: number }\n >,\n { variables }\n );\n const events = useQuery(\n GGNOMAD_EVENTS_QUERY as TypedDocumentNode<\n { ggnomadEvents: EventRow[] },\n { limit: number; offset: number }\n >,\n { variables }\n );\n const activities = useQuery(\n GGNOMAD_ACTIVITIES_QUERY as TypedDocumentNode<\n { ggnomadActivities: ActivityRow[] },\n { limit: number; offset: number }\n >,\n { variables }\n );\n\n const loading = properties.loading || tours.loading || events.loading || activities.loading;\n\n const allResults = useMemo<Result[]>(() => {\n const rows: Result[] = [];\n for (const property of properties.data?.ggnomadProperties ?? []) {\n rows.push(\n toResult('PROPERTY', { ...property, price: property.pricePerNight, kind: property.type })\n );\n }\n for (const tour of tours.data?.ggnomadTours ?? []) {\n rows.push(toResult('TOUR', { ...tour, kind: tour.type }));\n }\n for (const event of events.data?.ggnomadEvents ?? []) {\n rows.push(toResult('EVENT', { ...event, kind: event.category }));\n }\n for (const activity of activities.data?.ggnomadActivities ?? []) {\n rows.push(toResult('ACTIVITY', { ...activity, kind: activity.category }));\n }\n return rows;\n }, [\n properties.data?.ggnomadProperties,\n tours.data?.ggnomadTours,\n events.data?.ggnomadEvents,\n activities.data?.ggnomadActivities,\n ]);\n\n const matches = useMemo(() => {\n const needle = term.trim().toLowerCase();\n return allResults.filter((result) => {\n if (domain !== 'ALL' && result.domain !== domain) return false;\n if (!needle) return true;\n return result.haystack.includes(needle);\n });\n }, [allResults, term, domain]);\n\n const countFor = (target: Domain) =>\n allResults.filter(\n (result) =>\n result.domain === target &&\n (!term.trim() || result.haystack.includes(term.trim().toLowerCase()))\n ).length;\n\n const propertyCount = countFor('PROPERTY');\n const tourCount = countFor('TOUR');\n const eventCount = countFor('EVENT');\n const activityCount = countFor('ACTIVITY');\n const total = propertyCount + tourCount + eventCount + activityCount;\n\n // Every domain query asks for the same page size, so any list that came back\n // full may have more behind it.\n const mayHaveMore = [\n properties.data?.ggnomadProperties,\n tours.data?.ggnomadTours,\n events.data?.ggnomadEvents,\n activities.data?.ggnomadActivities,\n ].some((rows) => (rows?.length ?? 0) >= limit);\n\n const domainLabel = (value: Domain): string => {\n switch (value) {\n case 'PROPERTY':\n return t('ggnomad.search.domain.property', 'Property');\n case 'TOUR':\n return t('ggnomad.search.domain.tour', 'Tour');\n case 'EVENT':\n return t('ggnomad.search.domain.event', 'Event');\n case 'ACTIVITY':\n return t('ggnomad.search.domain.activity', 'Activity');\n }\n };\n\n const domainChips: Array<{ value: Domain | 'ALL'; label: string; count: number }> = [\n { value: 'ALL', label: t('ggnomad.search.domain.all', 'All'), count: total },\n {\n value: 'PROPERTY',\n label: t('ggnomad.search.results.properties', 'Properties'),\n count: propertyCount,\n },\n { value: 'TOUR', label: t('ggnomad.search.results.tours', 'Tours'), count: tourCount },\n { value: 'EVENT', label: t('ggnomad.search.results.events', 'Events'), count: eventCount },\n {\n value: 'ACTIVITY',\n label: t('ggnomad.search.results.activities', 'Activities'),\n count: activityCount,\n },\n ];\n\n return (\n <PageShell\n title={t('ggnomad.search.title', 'Global Search')}\n titleAccent={t('ggnomad.search.title.accent', 'Search')}\n subtitle={t('ggnomad.search.subtitle', 'Cross-domain search across all GGNomad inventory')}\n >\n <PagePurpose icon={<Search className=\"size-4\" />}>\n {t(\n 'ggnomad.search.purpose',\n 'One search across the entire GGNomad catalogue — properties, tours, events and activities at once — so you can see everything available for a destination in a single view instead of checking each domain separately.'\n )}\n </PagePurpose>\n\n <div>\n <label htmlFor=\"ggnomad-search-term\" className=\"sr-only\">\n {t('ggnomad.search.inputLabel', 'Search the catalogue')}\n </label>\n <div className=\"relative\">\n <Search\n className=\"pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n id=\"ggnomad-search-term\"\n type=\"search\"\n value={term}\n onChange={(event) => setTerm(event.target.value)}\n placeholder={t(\n 'ggnomad.search.placeholder',\n 'Search names, categories and descriptions…'\n )}\n className=\"w-full rounded-lg border border-border-seam bg-bg-canvas py-2.5 pl-9 pr-10 text-sm text-text-primary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg\"\n />\n {term ? (\n <button\n type=\"button\"\n onClick={() => setTerm('')}\n aria-label={t('ggnomad.search.clear', 'Clear search')}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n >\n <X className=\"size-4\" />\n </button>\n ) : null}\n </div>\n <p className=\"mt-2 text-xs text-text-secondary\">\n {t(\n 'ggnomad.search.scopeNote',\n 'Matches against the catalogue loaded on this page. Use “Load more” to pull further inventory into the search.'\n )}\n </p>\n </div>\n\n <ul className=\"flex flex-wrap gap-2\">\n {domainChips.map((chip) => (\n <li key={chip.value}>\n <button\n type=\"button\"\n aria-pressed={domain === chip.value}\n onClick={() => setDomain(chip.value)}\n className={cn(\n 'rounded-full border px-3 py-1 text-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg',\n domain === chip.value\n ? 'border-action-primary-bg bg-action-primary-bg text-action-primary-text'\n : 'border-border-seam bg-bg-sunken text-text-primary hover:border-border-default'\n )}\n >\n {chip.label}\n <span className=\"ml-1.5 tabular-nums opacity-80\">{chip.count}</span>\n </button>\n </li>\n ))}\n </ul>\n\n <EmphasisPanel>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.search.totalMatches', 'Total matches across all domains')}\n </p>\n <p className=\"mt-1 text-3xl font-semibold tabular-nums text-text-primary\">\n {total.toLocaleString()}\n </p>\n <p className=\"mt-2 text-sm text-text-secondary\">\n {t(\n 'ggnomad.search.totalMatchesNote',\n 'Combined inventory from properties, tours, events and activities — open a domain below to drill in.'\n )}\n </p>\n </EmphasisPanel>\n\n <section className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n label={t('ggnomad.search.results.propertiesFound', 'Properties found')}\n value={propertyCount}\n icon={<Building />}\n />\n <StatCard\n label={t('ggnomad.search.results.toursFound', 'Tours found')}\n value={tourCount}\n icon={<Compass />}\n />\n <StatCard\n label={t('ggnomad.search.results.eventsFound', 'Events found')}\n value={eventCount}\n icon={<Ticket />}\n />\n <StatCard\n label={t('ggnomad.search.results.activitiesFound', 'Activities found')}\n value={activityCount}\n icon={<Activity />}\n />\n </section>\n\n {matches.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title={\n loading\n ? t('ggnomad.search.loadingTitle', 'Searching the catalogue…')\n : t('ggnomad.search.emptyTitle', 'No matching inventory')\n }\n description={\n loading\n ? t(\n 'ggnomad.search.loadingDescription',\n 'Fetching properties, tours, events and activities.'\n )\n : term.trim()\n ? t(\n 'ggnomad.search.noTermMatch',\n 'Nothing matched “{{term}}”. Try a shorter term or another domain.'\n ).replace('{{term}}', term.trim())\n : t(\n 'ggnomad.search.emptyDescription',\n 'Nothing matched across properties, tours, events and activities yet.'\n )\n }\n />\n ) : (\n <SectionCard title={t('ggnomad.search.resultsTitle', 'Results')}>\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {matches.map((result) => (\n <EntityCard\n key={result.key}\n to={result.path}\n image={result.image}\n imageAlt={result.name}\n title={result.name}\n meta={humanizeEnum(result.kind)}\n badge={\n <span className=\"rounded-full bg-bg-surface/90 px-2.5 py-0.5 text-xs font-medium text-text-primary\">\n {domainLabel(result.domain)}\n </span>\n }\n price={`${result.currency} ${Number(result.price).toFixed(2)}`}\n footer={<RatingSummary rating={result.rating} reviewCount={result.reviewCount} />}\n />\n ))}\n </div>\n <LoadMoreButton\n visible={mayHaveMore}\n onLoadMore={() => setLimit((current) => current + PAGE_SIZE)}\n />\n </SectionCard>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|