@burdenoff/microfe-ggnomad 2026.825.1 → 2026.828.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.
Files changed (98) hide show
  1. package/dist/ggnomad/components/AvailabilityPanel.d.ts +15 -0
  2. package/dist/ggnomad/components/AvailabilityPanel.d.ts.map +1 -0
  3. package/dist/ggnomad/components/AvailabilityPanel.js +91 -0
  4. package/dist/ggnomad/components/AvailabilityPanel.js.map +1 -0
  5. package/dist/ggnomad/components/BookingPanel.d.ts +1 -3
  6. package/dist/ggnomad/components/BookingPanel.d.ts.map +1 -1
  7. package/dist/ggnomad/components/BookingPanel.js +91 -82
  8. package/dist/ggnomad/components/BookingPanel.js.map +1 -1
  9. package/dist/ggnomad/components/EcoAccessPanel.d.ts +17 -0
  10. package/dist/ggnomad/components/EcoAccessPanel.d.ts.map +1 -0
  11. package/dist/ggnomad/components/EcoAccessPanel.js +68 -0
  12. package/dist/ggnomad/components/EcoAccessPanel.js.map +1 -0
  13. package/dist/ggnomad/components/FactGrid.d.ts +18 -0
  14. package/dist/ggnomad/components/FactGrid.d.ts.map +1 -0
  15. package/dist/ggnomad/components/FactGrid.js +29 -0
  16. package/dist/ggnomad/components/FactGrid.js.map +1 -0
  17. package/dist/ggnomad/components/GroupTripPanel.d.ts +22 -0
  18. package/dist/ggnomad/components/GroupTripPanel.d.ts.map +1 -0
  19. package/dist/ggnomad/components/GroupTripPanel.js +350 -0
  20. package/dist/ggnomad/components/GroupTripPanel.js.map +1 -0
  21. package/dist/ggnomad/components/ListingFilters.d.ts +29 -0
  22. package/dist/ggnomad/components/ListingFilters.d.ts.map +1 -0
  23. package/dist/ggnomad/components/ListingFilters.js +164 -0
  24. package/dist/ggnomad/components/ListingFilters.js.map +1 -0
  25. package/dist/ggnomad/components/PhotoGallery.d.ts +15 -0
  26. package/dist/ggnomad/components/PhotoGallery.d.ts.map +1 -0
  27. package/dist/ggnomad/components/PhotoGallery.js +54 -0
  28. package/dist/ggnomad/components/PhotoGallery.js.map +1 -0
  29. package/dist/ggnomad/components/ProviderCard.d.ts +19 -0
  30. package/dist/ggnomad/components/ProviderCard.d.ts.map +1 -0
  31. package/dist/ggnomad/components/ProviderCard.js +66 -0
  32. package/dist/ggnomad/components/ProviderCard.js.map +1 -0
  33. package/dist/ggnomad/components/RatingSummary.d.ts +14 -0
  34. package/dist/ggnomad/components/RatingSummary.d.ts.map +1 -0
  35. package/dist/ggnomad/components/RatingSummary.js +32 -0
  36. package/dist/ggnomad/components/RatingSummary.js.map +1 -0
  37. package/dist/ggnomad/components/TagChips.d.ts +16 -0
  38. package/dist/ggnomad/components/TagChips.d.ts.map +1 -0
  39. package/dist/ggnomad/components/TagChips.js +20 -0
  40. package/dist/ggnomad/components/TagChips.js.map +1 -0
  41. package/dist/ggnomad/components/index.d.ts +13 -0
  42. package/dist/ggnomad/components/index.d.ts.map +1 -1
  43. package/dist/ggnomad/components/index.js +9 -0
  44. package/dist/ggnomad/graphql.d.ts +10 -0
  45. package/dist/ggnomad/graphql.d.ts.map +1 -1
  46. package/dist/ggnomad/graphql.js +308 -20
  47. package/dist/ggnomad/graphql.js.map +1 -1
  48. package/dist/ggnomad/pages/ActivityDetailsPage.d.ts.map +1 -1
  49. package/dist/ggnomad/pages/ActivityDetailsPage.js +174 -86
  50. package/dist/ggnomad/pages/ActivityDetailsPage.js.map +1 -1
  51. package/dist/ggnomad/pages/AnalyticsPage.d.ts.map +1 -1
  52. package/dist/ggnomad/pages/AnalyticsPage.js +109 -77
  53. package/dist/ggnomad/pages/AnalyticsPage.js.map +1 -1
  54. package/dist/ggnomad/pages/BookingsPage.d.ts.map +1 -1
  55. package/dist/ggnomad/pages/BookingsPage.js +188 -137
  56. package/dist/ggnomad/pages/BookingsPage.js.map +1 -1
  57. package/dist/ggnomad/pages/DashboardPage.d.ts.map +1 -1
  58. package/dist/ggnomad/pages/DashboardPage.js +245 -128
  59. package/dist/ggnomad/pages/DashboardPage.js.map +1 -1
  60. package/dist/ggnomad/pages/EntertainmentPage.d.ts.map +1 -1
  61. package/dist/ggnomad/pages/EntertainmentPage.js +115 -53
  62. package/dist/ggnomad/pages/EntertainmentPage.js.map +1 -1
  63. package/dist/ggnomad/pages/EventDetailsPage.d.ts.map +1 -1
  64. package/dist/ggnomad/pages/EventDetailsPage.js +170 -87
  65. package/dist/ggnomad/pages/EventDetailsPage.js.map +1 -1
  66. package/dist/ggnomad/pages/FavoritesPage.d.ts.map +1 -1
  67. package/dist/ggnomad/pages/FavoritesPage.js +201 -84
  68. package/dist/ggnomad/pages/FavoritesPage.js.map +1 -1
  69. package/dist/ggnomad/pages/HospitalityPage.d.ts.map +1 -1
  70. package/dist/ggnomad/pages/HospitalityPage.js +99 -56
  71. package/dist/ggnomad/pages/HospitalityPage.js.map +1 -1
  72. package/dist/ggnomad/pages/PropertyDetailsPage.d.ts.map +1 -1
  73. package/dist/ggnomad/pages/PropertyDetailsPage.js +177 -93
  74. package/dist/ggnomad/pages/PropertyDetailsPage.js.map +1 -1
  75. package/dist/ggnomad/pages/ProviderPage.d.ts.map +1 -1
  76. package/dist/ggnomad/pages/ProviderPage.js +112 -63
  77. package/dist/ggnomad/pages/ProviderPage.js.map +1 -1
  78. package/dist/ggnomad/pages/SearchPage.d.ts.map +1 -1
  79. package/dist/ggnomad/pages/SearchPage.js +236 -65
  80. package/dist/ggnomad/pages/SearchPage.js.map +1 -1
  81. package/dist/ggnomad/pages/TourDetailsPage.d.ts.map +1 -1
  82. package/dist/ggnomad/pages/TourDetailsPage.js +171 -86
  83. package/dist/ggnomad/pages/TourDetailsPage.js.map +1 -1
  84. package/dist/ggnomad/pages/TourismPage.d.ts.map +1 -1
  85. package/dist/ggnomad/pages/TourismPage.js +108 -54
  86. package/dist/ggnomad/pages/TourismPage.js.map +1 -1
  87. package/dist/ggnomad/pages/TravelPage.d.ts.map +1 -1
  88. package/dist/ggnomad/pages/TravelPage.js +406 -43
  89. package/dist/ggnomad/pages/TravelPage.js.map +1 -1
  90. package/dist/ggnomad/pages/TripPlanDetailPage.d.ts.map +1 -1
  91. package/dist/ggnomad/pages/TripPlanDetailPage.js +93 -70
  92. package/dist/ggnomad/pages/TripPlanDetailPage.js.map +1 -1
  93. package/dist/ggnomad/utils/format.d.ts +47 -0
  94. package/dist/ggnomad/utils/format.d.ts.map +1 -0
  95. package/dist/ggnomad/utils/format.js +125 -0
  96. package/dist/ggnomad/utils/format.js.map +1 -0
  97. package/dist/stories/BookingPanel.stories.d.ts.map +1 -1
  98. package/package.json +1 -1
@@ -1,117 +1,234 @@
1
1
  import { GGNOMAD_FAVORITES_QUERY as e, GGNOMAD_FAVORITE_TARGETS_QUERY as t, TOGGLE_GGNOMAD_FAVORITE_MUTATION as n } from "../graphql.js";
2
- import { PageShell as r } from "./PageShell.js";
3
- import { EntityCard as i } from "../components/EntityCard.js";
4
- import { notify as a } from "../utils/notify.js";
2
+ import { humanizeEnum as r } from "../utils/format.js";
3
+ import { PageShell as i } from "./PageShell.js";
4
+ import { cn as a } from "../utils/cn.js";
5
+ import { EntityCard as o } from "../components/EntityCard.js";
6
+ import { notify as s } from "../utils/notify.js";
7
+ import { RatingSummary as c } from "../components/RatingSummary.js";
5
8
  import "../components/index.js";
6
- import { useMemo as o } from "react";
7
- import { Heart as s } from "lucide-react";
8
- import { useI18n as c } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
9
- import { useMutation as l, useQuery as u } from "@apollo/client/react";
10
- import { CTAOverflowMenu as d, GlassCard as f, IllustratedEmptyState as p, PagePurpose as m } from "@burdenoff/fe-libs/ui";
11
- import { jsx as h, jsxs as g } from "react/jsx-runtime";
9
+ import { useMemo as l, useState as u } from "react";
10
+ import { ArrowUpRight as d, Heart as f, HeartOff as p } from "lucide-react";
11
+ import { useI18n as m } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
12
+ import { Link as h } from "react-router";
13
+ import { useMutation as g, useQuery as _ } from "@apollo/client/react";
14
+ import { CTAOverflowMenu as v, GlassCard as y, IllustratedEmptyState as b, PagePurpose as x } from "@burdenoff/fe-libs/ui";
15
+ import { Fragment as S, jsx as C, jsxs as w } from "react/jsx-runtime";
12
16
  //#region src/ggnomad/pages/FavoritesPage.tsx
13
- function _() {
14
- let { t: _ } = c(), v = (e) => {
17
+ var T = [
18
+ "PROPERTY",
19
+ "TOUR",
20
+ "EVENT",
21
+ "ACTIVITY"
22
+ ];
23
+ function E() {
24
+ let { t: E } = m(), [D, O] = u("ALL"), k = (e) => {
15
25
  switch (e.toUpperCase()) {
16
- case "PROPERTY": return _("ggnomad.favorites.type.property", "Property");
17
- case "TOUR": return _("ggnomad.favorites.type.tour", "Tour");
18
- case "EVENT": return _("ggnomad.favorites.type.event", "Event");
19
- case "ACTIVITY": return _("ggnomad.favorites.type.activity", "Activity");
20
- case "TRIP": return _("ggnomad.favorites.type.trip", "Trip");
21
- default: return _("ggnomad.favorites.type.saved", "Saved item");
26
+ case "PROPERTY": return E("ggnomad.favorites.type.property", "Property");
27
+ case "TOUR": return E("ggnomad.favorites.type.tour", "Tour");
28
+ case "EVENT": return E("ggnomad.favorites.type.event", "Event");
29
+ case "ACTIVITY": return E("ggnomad.favorites.type.activity", "Activity");
30
+ case "TRIP": return E("ggnomad.favorites.type.trip", "Trip");
31
+ default: return E("ggnomad.favorites.type.saved", "Saved item");
22
32
  }
23
- }, { data: y, refetch: b } = u(e), { data: x } = u(t), [S] = l(n), C = o(() => {
33
+ }, { data: A, refetch: j } = _(e), { data: M } = _(t), [N, { loading: P }] = g(n), F = l(() => {
24
34
  let e = /* @__PURE__ */ new Map();
25
- for (let t of [
26
- ...x?.ggnomadProperties ?? [],
27
- ...x?.ggnomadTours ?? [],
28
- ...x?.ggnomadEvents ?? [],
29
- ...x?.ggnomadActivities ?? []
30
- ]) e.set(t.id, t.name);
35
+ for (let t of M?.ggnomadProperties ?? []) e.set(`PROPERTY:${t.id}`, {
36
+ id: t.id,
37
+ name: t.name,
38
+ kind: t.type,
39
+ price: t.pricePerNight,
40
+ currency: t.currency,
41
+ rating: t.rating,
42
+ reviewCount: t.reviewCount,
43
+ images: t.images,
44
+ path: `../hospitality/${t.id}`
45
+ });
46
+ for (let t of M?.ggnomadTours ?? []) e.set(`TOUR:${t.id}`, {
47
+ id: t.id,
48
+ name: t.name,
49
+ kind: t.type,
50
+ price: t.price,
51
+ currency: t.currency,
52
+ rating: t.rating,
53
+ reviewCount: t.reviewCount,
54
+ images: t.images,
55
+ path: `../tourism/tours/${t.id}`
56
+ });
57
+ for (let t of M?.ggnomadEvents ?? []) e.set(`EVENT:${t.id}`, {
58
+ id: t.id,
59
+ name: t.name,
60
+ kind: t.category,
61
+ price: t.price,
62
+ currency: t.currency,
63
+ rating: t.rating,
64
+ reviewCount: t.reviewCount,
65
+ images: t.images,
66
+ path: `../entertainment/${t.id}`
67
+ });
68
+ for (let t of M?.ggnomadActivities ?? []) e.set(`ACTIVITY:${t.id}`, {
69
+ id: t.id,
70
+ name: t.name,
71
+ kind: t.category,
72
+ price: t.price,
73
+ currency: t.currency,
74
+ rating: t.rating,
75
+ reviewCount: t.reviewCount,
76
+ images: t.images,
77
+ path: `../tourism/activities/${t.id}`
78
+ });
79
+ for (let t of M?.ggnomadTrips ?? []) e.set(`TRIP:${t.id}`, {
80
+ id: t.id,
81
+ name: t.name,
82
+ kind: t.transportMode,
83
+ price: t.price,
84
+ currency: t.currency
85
+ });
31
86
  return e;
32
- }, [x]), w = y?.ggnomadFavorites ?? [], T = () => {
33
- let e = x?.ggnomadProperties?.[0];
34
- if (e) return {
35
- type: "PROPERTY",
36
- listing: e
37
- };
38
- let t = x?.ggnomadTours?.[0];
39
- if (t) return {
40
- type: "TOUR",
41
- listing: t
42
- };
43
- let n = x?.ggnomadEvents?.[0];
44
- if (n) return {
45
- type: "EVENT",
46
- listing: n
47
- };
48
- let r = x?.ggnomadActivities?.[0];
49
- return r ? {
50
- type: "ACTIVITY",
51
- listing: r
52
- } : null;
53
- }, E = async () => {
54
- let e = T();
55
- if (!e) {
56
- a.warning(_("ggnomad.favorites.toast.noListingsTitle", "Nothing to favorite yet"), _("ggnomad.favorites.toast.noListingsBody", "Add a property, tour, event or activity first, then save it to your favorites."));
57
- return;
87
+ }, [M]), I = l(() => A?.ggnomadFavorites ?? [], [A?.ggnomadFavorites]), L = l(() => {
88
+ let e = /* @__PURE__ */ new Map();
89
+ for (let t of I) {
90
+ let n = t.targetType.toUpperCase();
91
+ e.set(n, (e.get(n) ?? 0) + 1);
58
92
  }
59
- let { type: t, listing: n } = e;
93
+ return e;
94
+ }, [I]), R = I.filter((e) => D === "ALL" || e.targetType.toUpperCase() === D), z = [{
95
+ value: "ALL",
96
+ label: E("ggnomad.favorites.filter.all", "All"),
97
+ count: I.length
98
+ }, ...[
99
+ "PROPERTY",
100
+ "TOUR",
101
+ "EVENT",
102
+ "ACTIVITY",
103
+ "TRIP"
104
+ ].map((e) => ({
105
+ value: e,
106
+ label: k(e),
107
+ count: L.get(e) ?? 0
108
+ }))], B = async (e, t, n) => {
60
109
  try {
61
- let e = await S({ variables: { input: {
62
- targetType: t,
63
- targetId: n.id
110
+ let r = await N({ variables: { input: {
111
+ targetType: e,
112
+ targetId: t
64
113
  } } });
65
- if (e.error) {
66
- a.error(_("ggnomad.favorites.toast.errorTitle", "Could not update favorite"), e.error.message);
114
+ if (r.error) {
115
+ s.error(E("ggnomad.favorites.toast.errorTitle", "Could not update favorite"), r.error.message);
67
116
  return;
68
117
  }
69
- let r = e.data?.toggleGgnomadFavorite === !0;
70
- a.success(r ? _("ggnomad.favorites.toast.added", "Added to favorites") : _("ggnomad.favorites.toast.removed", "Removed from favorites"), n.name), await b();
118
+ let i = r.data?.toggleGgnomadFavorite === !0;
119
+ s.success(i ? E("ggnomad.favorites.toast.added", "Added to favorites") : E("ggnomad.favorites.toast.removed", "Removed from favorites"), n), await j();
71
120
  } catch (e) {
72
- a.error(_("ggnomad.favorites.toast.errorTitle", "Could not update favorite"), e instanceof Error ? e.message : String(e));
121
+ s.error(E("ggnomad.favorites.toast.errorTitle", "Could not update favorite"), e instanceof Error ? e.message : String(e));
122
+ }
123
+ }, V = () => {
124
+ for (let e of T) {
125
+ let t = [...F.entries()].find(([t]) => t.startsWith(`${e}:`));
126
+ if (t) return {
127
+ type: e,
128
+ listing: t[1]
129
+ };
130
+ }
131
+ return null;
132
+ }, H = async () => {
133
+ let e = V();
134
+ if (!e) {
135
+ s.warning(E("ggnomad.favorites.toast.noListingsTitle", "Nothing to favorite yet"), E("ggnomad.favorites.toast.noListingsBody", "Add a property, tour, event or activity first, then save it to your favorites."));
136
+ return;
73
137
  }
138
+ await B(e.type, e.listing.id, e.listing.name);
74
139
  };
75
- return /* @__PURE__ */ g(r, {
76
- title: _("ggnomad.favorites.title", "Favorites"),
77
- titleAccent: _("ggnomad.favorites.title.accent", "Favorites"),
78
- subtitle: _("ggnomad.favorites.subtitle", "Social ecosystem: saved entities across GGNomad domains"),
79
- actions: /* @__PURE__ */ h(d, {
140
+ return /* @__PURE__ */ w(i, {
141
+ title: E("ggnomad.favorites.title", "Favorites"),
142
+ titleAccent: E("ggnomad.favorites.title.accent", "Favorites"),
143
+ subtitle: E("ggnomad.favorites.subtitle", "Social ecosystem: saved entities across GGNomad domains"),
144
+ actions: /* @__PURE__ */ C(v, {
80
145
  primary: {
81
- label: _("ggnomad.favorites.actions.toggleDemo", "Toggle demo favorite"),
82
- onSelect: () => void E(),
83
- icon: /* @__PURE__ */ h(s, {})
146
+ label: E("ggnomad.favorites.actions.toggleDemo", "Toggle demo favorite"),
147
+ onSelect: () => void H(),
148
+ disabled: P,
149
+ icon: /* @__PURE__ */ C(f, {})
84
150
  },
85
151
  actions: []
86
152
  }),
87
- children: [/* @__PURE__ */ h(m, {
88
- icon: /* @__PURE__ */ h(s, { className: "size-4" }),
89
- children: _("ggnomad.favorites.purpose", "A personal shortlist that spans every GGNomad domain — save properties, tours, events and trips you’re considering so you can compare and come back to them before you book.")
90
- }), w.length === 0 ? /* @__PURE__ */ h(p, {
153
+ children: [/* @__PURE__ */ C(x, {
154
+ icon: /* @__PURE__ */ C(f, { className: "size-4" }),
155
+ children: E("ggnomad.favorites.purpose", "A personal shortlist that spans every GGNomad domain — save properties, tours, events and trips you’re considering so you can compare and come back to them before you book.")
156
+ }), I.length === 0 ? /* @__PURE__ */ C(b, {
91
157
  illustration: "empty-notifications",
92
- title: _("ggnomad.favorites.emptyTitle", "No favorites saved"),
93
- description: _("ggnomad.favorites.emptyDescription", "Items you save across travel, hospitality and tourism will collect here.")
94
- }) : /* @__PURE__ */ g(f, {
158
+ title: E("ggnomad.favorites.emptyTitle", "No favorites saved"),
159
+ description: E("ggnomad.favorites.emptyDescription", "Items you save across travel, hospitality and tourism will collect here.")
160
+ }) : /* @__PURE__ */ w(S, { children: [/* @__PURE__ */ C("ul", {
161
+ className: "flex flex-wrap gap-2",
162
+ children: z.map((e) => /* @__PURE__ */ C("li", { children: /* @__PURE__ */ w("button", {
163
+ type: "button",
164
+ "aria-pressed": D === e.value,
165
+ onClick: () => O(e.value),
166
+ className: a("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", D === e.value ? "border-action-primary-bg bg-action-primary-bg text-action-primary-text" : "border-border-seam bg-bg-sunken text-text-primary hover:border-border-default"),
167
+ children: [e.label, /* @__PURE__ */ C("span", {
168
+ className: "ml-1.5 tabular-nums opacity-80",
169
+ children: e.count
170
+ })]
171
+ }) }, e.value))
172
+ }), /* @__PURE__ */ w(y, {
95
173
  treatment: "glass",
96
174
  glow: !0,
97
175
  className: "border-border-seam p-5",
98
- children: [/* @__PURE__ */ h("h2", {
176
+ children: [/* @__PURE__ */ C("h2", {
99
177
  className: "mb-4 text-lg font-medium tracking-tight text-text-primary",
100
- children: _("ggnomad.favorites.savedItems", "Saved items")
101
- }), /* @__PURE__ */ h("div", {
178
+ children: E("ggnomad.favorites.savedItems", "Saved items")
179
+ }), R.length === 0 ? /* @__PURE__ */ C("p", {
180
+ className: "py-6 text-center text-sm text-text-secondary",
181
+ children: E("ggnomad.favorites.noneOfType", "Nothing saved in this category yet.")
182
+ }) : /* @__PURE__ */ C("div", {
102
183
  className: "grid gap-3 sm:grid-cols-2 xl:grid-cols-3",
103
- children: w.map((e) => {
104
- let t = C.get(e.targetId);
105
- return /* @__PURE__ */ h(i, {
106
- title: t ?? v(e.targetType),
107
- meta: t ? v(e.targetType) : _("ggnomad.favorites.savedListing", "Saved listing")
184
+ children: R.map((e) => {
185
+ let t = e.targetType.toUpperCase(), n = F.get(`${t}:${e.targetId}`), i = T.includes(t);
186
+ return /* @__PURE__ */ C(o, {
187
+ image: n?.images?.[0],
188
+ imageAlt: n?.name,
189
+ title: n?.name ?? k(e.targetType),
190
+ meta: n?.kind ? `${k(e.targetType)} · ${r(n.kind)}` : k(e.targetType),
191
+ badge: /* @__PURE__ */ C("span", {
192
+ className: "rounded-full bg-bg-surface/90 px-2.5 py-0.5 text-xs font-medium text-text-primary",
193
+ children: k(e.targetType)
194
+ }),
195
+ price: n?.price !== void 0 && n.currency ? `${n.currency} ${Number(n.price).toFixed(2)}` : void 0,
196
+ footer: /* @__PURE__ */ w("div", {
197
+ className: "space-y-2",
198
+ children: [n ? /* @__PURE__ */ C(c, {
199
+ rating: n.rating,
200
+ reviewCount: n.reviewCount
201
+ }) : /* @__PURE__ */ C("span", {
202
+ className: "text-sm text-text-secondary",
203
+ children: E("ggnomad.favorites.savedListing", "Saved listing")
204
+ }), /* @__PURE__ */ w("div", {
205
+ className: "flex flex-wrap items-center justify-between gap-2",
206
+ children: [n?.path ? /* @__PURE__ */ w(h, {
207
+ to: n.path,
208
+ className: "inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium text-action-primary-bg hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg",
209
+ children: [/* @__PURE__ */ C(d, {
210
+ className: "size-3.5",
211
+ "aria-hidden": "true"
212
+ }), E("ggnomad.favorites.view", "View details")]
213
+ }) : /* @__PURE__ */ C("span", {}), i ? /* @__PURE__ */ w("button", {
214
+ type: "button",
215
+ disabled: P,
216
+ onClick: () => void B(t, e.targetId, n?.name ?? k(e.targetType)),
217
+ className: "inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium text-text-secondary hover:bg-bg-sunken hover:text-status-error-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg disabled:opacity-60",
218
+ children: [/* @__PURE__ */ C(p, {
219
+ className: "size-3.5",
220
+ "aria-hidden": "true"
221
+ }), E("ggnomad.favorites.remove", "Remove")]
222
+ }) : null]
223
+ })]
224
+ })
108
225
  }, e.id);
109
226
  })
110
227
  })]
111
- })]
228
+ })] })]
112
229
  });
113
230
  }
114
231
  //#endregion
115
- export { _ as FavoritesPage };
232
+ export { E as FavoritesPage };
116
233
 
117
234
  //# sourceMappingURL=FavoritesPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"FavoritesPage.js","names":[],"sources":["../../../src/ggnomad/pages/FavoritesPage.tsx"],"sourcesContent":["import { useMemo } 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 { Heart } from 'lucide-react';\nimport {\n CTAOverflowMenu,\n GlassCard,\n IllustratedEmptyState,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n GGNOMAD_FAVORITES_QUERY,\n GGNOMAD_FAVORITE_TARGETS_QUERY,\n TOGGLE_GGNOMAD_FAVORITE_MUTATION,\n} from '../graphql';\nimport { PageShell } from './PageShell';\nimport { EntityCard } from '../components';\nimport { notify } from '../utils/notify';\n\ntype FavoriteType = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ntype Listing = { id: string; name: string };\n\nexport function FavoritesPage() {\n const { t } = useI18n();\n\n const typeLabel = (type: string): string => {\n switch (type.toUpperCase()) {\n case 'PROPERTY':\n return t('ggnomad.favorites.type.property', 'Property');\n case 'TOUR':\n return t('ggnomad.favorites.type.tour', 'Tour');\n case 'EVENT':\n return t('ggnomad.favorites.type.event', 'Event');\n case 'ACTIVITY':\n return t('ggnomad.favorites.type.activity', 'Activity');\n case 'TRIP':\n return t('ggnomad.favorites.type.trip', 'Trip');\n default:\n return t('ggnomad.favorites.type.saved', 'Saved item');\n }\n };\n\n const { data, refetch } = useQuery(\n GGNOMAD_FAVORITES_QUERY as TypedDocumentNode<\n { ggnomadFavorites: Array<{ id: string; targetType: string; targetId: string }> },\n Record<string, never>\n >\n );\n\n // Real listing names so a saved favorite reads as its entity name instead of\n // a raw enum + UUID (the backend only stores targetType + targetId).\n const { data: targetsData } = useQuery(\n GGNOMAD_FAVORITE_TARGETS_QUERY as TypedDocumentNode<\n {\n ggnomadProperties: Listing[];\n ggnomadTours: Listing[];\n ggnomadEvents: Listing[];\n ggnomadActivities: Listing[];\n },\n Record<string, never>\n >\n );\n\n const [toggleFavorite] = useMutation(\n TOGGLE_GGNOMAD_FAVORITE_MUTATION as TypedDocumentNode<\n { toggleGgnomadFavorite: boolean },\n { input: { targetType: FavoriteType; targetId: string } }\n >\n );\n\n const nameById = useMemo(() => {\n const map = new Map<string, string>();\n for (const listing of [\n ...(targetsData?.ggnomadProperties ?? []),\n ...(targetsData?.ggnomadTours ?? []),\n ...(targetsData?.ggnomadEvents ?? []),\n ...(targetsData?.ggnomadActivities ?? []),\n ]) {\n map.set(listing.id, listing.name);\n }\n return map;\n }, [targetsData]);\n\n const favorites = (data?.ggnomadFavorites ?? []) as Array<{\n id: string;\n targetType: string;\n targetId: string;\n }>;\n\n /** First available real listing, in priority order, mapped to a favorite type. */\n const resolveDemoTarget = (): { type: FavoriteType; listing: Listing } | null => {\n const property = targetsData?.ggnomadProperties?.[0];\n if (property) return { type: 'PROPERTY', listing: property };\n const tour = targetsData?.ggnomadTours?.[0];\n if (tour) return { type: 'TOUR', listing: tour };\n const event = targetsData?.ggnomadEvents?.[0];\n if (event) return { type: 'EVENT', listing: event };\n const activity = targetsData?.ggnomadActivities?.[0];\n if (activity) return { type: 'ACTIVITY', listing: activity };\n return null;\n };\n\n const handleToggleDemo = async () => {\n const target = resolveDemoTarget();\n if (!target) {\n notify.warning(\n t('ggnomad.favorites.toast.noListingsTitle', 'Nothing to favorite yet'),\n t(\n 'ggnomad.favorites.toast.noListingsBody',\n 'Add a property, tour, event or activity first, then save it to your favorites.'\n )\n );\n return;\n }\n const { type, listing } = target;\n try {\n const result = await toggleFavorite({\n variables: { input: { targetType: type, targetId: listing.id } },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.favorites.toast.errorTitle', 'Could not update favorite'),\n result.error.message\n );\n return;\n }\n const added = result.data?.toggleGgnomadFavorite === true;\n notify.success(\n added\n ? t('ggnomad.favorites.toast.added', 'Added to favorites')\n : t('ggnomad.favorites.toast.removed', 'Removed from favorites'),\n listing.name\n );\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.favorites.toast.errorTitle', 'Could not update favorite'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n return (\n <PageShell\n title={t('ggnomad.favorites.title', 'Favorites')}\n titleAccent={t('ggnomad.favorites.title.accent', 'Favorites')}\n subtitle={t(\n 'ggnomad.favorites.subtitle',\n 'Social ecosystem: saved entities across GGNomad domains'\n )}\n actions={\n <CTAOverflowMenu\n primary={{\n label: t('ggnomad.favorites.actions.toggleDemo', 'Toggle demo favorite'),\n onSelect: () => void handleToggleDemo(),\n icon: <Heart />,\n }}\n actions={[]}\n />\n }\n >\n <PagePurpose icon={<Heart className=\"size-4\" />}>\n {t(\n 'ggnomad.favorites.purpose',\n 'A personal shortlist that spans every GGNomad domain — save properties, tours, events and trips you’re considering so you can compare and come back to them before you book.'\n )}\n </PagePurpose>\n\n {favorites.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-notifications\"\n title={t('ggnomad.favorites.emptyTitle', 'No favorites saved')}\n description={t(\n 'ggnomad.favorites.emptyDescription',\n 'Items you save across travel, hospitality and tourism will collect 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.favorites.savedItems', 'Saved items')}\n </h2>\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {favorites.map((favorite) => {\n const resolvedName = nameById.get(favorite.targetId);\n return (\n <EntityCard\n key={favorite.id}\n title={resolvedName ?? typeLabel(favorite.targetType)}\n meta={\n resolvedName\n ? typeLabel(favorite.targetType)\n : t('ggnomad.favorites.savedListing', 'Saved listing')\n }\n />\n );\n })}\n </div>\n </GlassCard>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"FavoritesPage.js","names":[],"sources":["../../../src/ggnomad/pages/FavoritesPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useMutation, 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 { ArrowUpRight, Heart, HeartOff } from 'lucide-react';\nimport {\n CTAOverflowMenu,\n GlassCard,\n IllustratedEmptyState,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n GGNOMAD_FAVORITES_QUERY,\n GGNOMAD_FAVORITE_TARGETS_QUERY,\n TOGGLE_GGNOMAD_FAVORITE_MUTATION,\n} from '../graphql';\nimport { humanizeEnum } from '../utils/format';\nimport { cn } from '../utils/cn';\nimport { PageShell } from './PageShell';\nimport { EntityCard, RatingSummary } from '../components';\nimport { notify } from '../utils/notify';\n\ntype FavoriteType = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY' | 'TRIP';\n\n/** A favouritable listing, normalised across the five target types. */\ntype Target = {\n id: string;\n name: string;\n kind?: string;\n price?: number;\n currency?: string;\n rating?: number | null;\n reviewCount?: number;\n images?: string[];\n /** Route to the listing's detail page, relative to this page. Trips have none. */\n path?: string;\n};\n\ntype Favorite = {\n id: string;\n targetType: string;\n targetId: string;\n createdAt?: string;\n};\n\nconst TOGGLEABLE_TYPES: FavoriteType[] = ['PROPERTY', 'TOUR', 'EVENT', 'ACTIVITY'];\n\nexport function FavoritesPage() {\n const { t } = useI18n();\n const [typeFilter, setTypeFilter] = useState<FavoriteType | 'ALL'>('ALL');\n\n const typeLabel = (type: string): string => {\n switch (type.toUpperCase()) {\n case 'PROPERTY':\n return t('ggnomad.favorites.type.property', 'Property');\n case 'TOUR':\n return t('ggnomad.favorites.type.tour', 'Tour');\n case 'EVENT':\n return t('ggnomad.favorites.type.event', 'Event');\n case 'ACTIVITY':\n return t('ggnomad.favorites.type.activity', 'Activity');\n case 'TRIP':\n return t('ggnomad.favorites.type.trip', 'Trip');\n default:\n return t('ggnomad.favorites.type.saved', 'Saved item');\n }\n };\n\n const { data, refetch } = useQuery(\n GGNOMAD_FAVORITES_QUERY as TypedDocumentNode<\n { ggnomadFavorites: Favorite[] },\n Record<string, never>\n >\n );\n\n // The backend stores a favorite as targetType + targetId only, so the listing\n // catalogue is joined client-side to show the real name, cover photo, price and\n // rating instead of a bare enum + UUID.\n const { data: targetsData } = useQuery(\n GGNOMAD_FAVORITE_TARGETS_QUERY as TypedDocumentNode<\n {\n ggnomadProperties: Array<{\n id: string;\n name: string;\n type: string;\n pricePerNight: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n images: string[];\n }>;\n ggnomadTours: Array<{\n id: string;\n name: string;\n type: string;\n price: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n images: string[];\n }>;\n ggnomadEvents: Array<{\n id: string;\n name: string;\n category: string;\n price: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n images: string[];\n }>;\n ggnomadActivities: Array<{\n id: string;\n name: string;\n category: string;\n price: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n images: string[];\n }>;\n ggnomadTrips: Array<{\n id: string;\n name: string;\n transportMode: string;\n price: number;\n currency: string;\n }>;\n },\n Record<string, never>\n >\n );\n\n const [toggleFavorite, { loading: toggling }] = useMutation(\n TOGGLE_GGNOMAD_FAVORITE_MUTATION as TypedDocumentNode<\n { toggleGgnomadFavorite: boolean },\n { input: { targetType: FavoriteType; targetId: string } }\n >\n );\n\n const targetsByType = useMemo(() => {\n const map = new Map<string, Target>();\n for (const property of targetsData?.ggnomadProperties ?? []) {\n map.set(`PROPERTY:${property.id}`, {\n id: property.id,\n name: property.name,\n kind: property.type,\n price: property.pricePerNight,\n currency: property.currency,\n rating: property.rating,\n reviewCount: property.reviewCount,\n images: property.images,\n path: `../hospitality/${property.id}`,\n });\n }\n for (const tour of targetsData?.ggnomadTours ?? []) {\n map.set(`TOUR:${tour.id}`, {\n id: tour.id,\n name: tour.name,\n kind: tour.type,\n price: tour.price,\n currency: tour.currency,\n rating: tour.rating,\n reviewCount: tour.reviewCount,\n images: tour.images,\n path: `../tourism/tours/${tour.id}`,\n });\n }\n for (const event of targetsData?.ggnomadEvents ?? []) {\n map.set(`EVENT:${event.id}`, {\n id: event.id,\n name: event.name,\n kind: event.category,\n price: event.price,\n currency: event.currency,\n rating: event.rating,\n reviewCount: event.reviewCount,\n images: event.images,\n path: `../entertainment/${event.id}`,\n });\n }\n for (const activity of targetsData?.ggnomadActivities ?? []) {\n map.set(`ACTIVITY:${activity.id}`, {\n id: activity.id,\n name: activity.name,\n kind: activity.category,\n price: activity.price,\n currency: activity.currency,\n rating: activity.rating,\n reviewCount: activity.reviewCount,\n images: activity.images,\n path: `../tourism/activities/${activity.id}`,\n });\n }\n for (const trip of targetsData?.ggnomadTrips ?? []) {\n map.set(`TRIP:${trip.id}`, {\n id: trip.id,\n name: trip.name,\n kind: trip.transportMode,\n price: trip.price,\n currency: trip.currency,\n });\n }\n return map;\n }, [targetsData]);\n\n const favorites = useMemo(() => data?.ggnomadFavorites ?? [], [data?.ggnomadFavorites]);\n\n const counts = useMemo(() => {\n const tally = new Map<string, number>();\n for (const favorite of favorites) {\n const key = favorite.targetType.toUpperCase();\n tally.set(key, (tally.get(key) ?? 0) + 1);\n }\n return tally;\n }, [favorites]);\n\n const visible = favorites.filter(\n (favorite) => typeFilter === 'ALL' || favorite.targetType.toUpperCase() === typeFilter\n );\n\n const chips: Array<{ value: FavoriteType | 'ALL'; label: string; count: number }> = [\n { value: 'ALL', label: t('ggnomad.favorites.filter.all', 'All'), count: favorites.length },\n ...(['PROPERTY', 'TOUR', 'EVENT', 'ACTIVITY', 'TRIP'] as FavoriteType[]).map((type) => ({\n value: type,\n label: typeLabel(type),\n count: counts.get(type) ?? 0,\n })),\n ];\n\n const runToggle = async (type: FavoriteType, targetId: string, label: string) => {\n try {\n const result = await toggleFavorite({\n variables: { input: { targetType: type, targetId } },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.favorites.toast.errorTitle', 'Could not update favorite'),\n result.error.message\n );\n return;\n }\n const added = result.data?.toggleGgnomadFavorite === true;\n notify.success(\n added\n ? t('ggnomad.favorites.toast.added', 'Added to favorites')\n : t('ggnomad.favorites.toast.removed', 'Removed from favorites'),\n label\n );\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.favorites.toast.errorTitle', 'Could not update favorite'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n /** First available real listing, in priority order, mapped to a favorite type. */\n const resolveDemoTarget = (): { type: FavoriteType; listing: Target } | null => {\n for (const type of TOGGLEABLE_TYPES) {\n const match = [...targetsByType.entries()].find(([key]) => key.startsWith(`${type}:`));\n if (match) return { type, listing: match[1] };\n }\n return null;\n };\n\n const handleToggleDemo = async () => {\n const target = resolveDemoTarget();\n if (!target) {\n notify.warning(\n t('ggnomad.favorites.toast.noListingsTitle', 'Nothing to favorite yet'),\n t(\n 'ggnomad.favorites.toast.noListingsBody',\n 'Add a property, tour, event or activity first, then save it to your favorites.'\n )\n );\n return;\n }\n await runToggle(target.type, target.listing.id, target.listing.name);\n };\n\n return (\n <PageShell\n title={t('ggnomad.favorites.title', 'Favorites')}\n titleAccent={t('ggnomad.favorites.title.accent', 'Favorites')}\n subtitle={t(\n 'ggnomad.favorites.subtitle',\n 'Social ecosystem: saved entities across GGNomad domains'\n )}\n actions={\n <CTAOverflowMenu\n primary={{\n label: t('ggnomad.favorites.actions.toggleDemo', 'Toggle demo favorite'),\n onSelect: () => void handleToggleDemo(),\n disabled: toggling,\n icon: <Heart />,\n }}\n actions={[]}\n />\n }\n >\n <PagePurpose icon={<Heart className=\"size-4\" />}>\n {t(\n 'ggnomad.favorites.purpose',\n 'A personal shortlist that spans every GGNomad domain — save properties, tours, events and trips you’re considering so you can compare and come back to them before you book.'\n )}\n </PagePurpose>\n\n {favorites.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-notifications\"\n title={t('ggnomad.favorites.emptyTitle', 'No favorites saved')}\n description={t(\n 'ggnomad.favorites.emptyDescription',\n 'Items you save across travel, hospitality and tourism will collect here.'\n )}\n />\n ) : (\n <>\n <ul className=\"flex flex-wrap gap-2\">\n {chips.map((chip) => (\n <li key={chip.value}>\n <button\n type=\"button\"\n aria-pressed={typeFilter === chip.value}\n onClick={() => setTypeFilter(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 typeFilter === 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 <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.favorites.savedItems', 'Saved items')}\n </h2>\n {visible.length === 0 ? (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n {t('ggnomad.favorites.noneOfType', 'Nothing saved in this category yet.')}\n </p>\n ) : (\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {visible.map((favorite) => {\n const type = favorite.targetType.toUpperCase();\n const target = targetsByType.get(`${type}:${favorite.targetId}`);\n const removable = (TOGGLEABLE_TYPES as string[]).includes(type);\n return (\n <EntityCard\n key={favorite.id}\n image={target?.images?.[0]}\n imageAlt={target?.name}\n title={target?.name ?? typeLabel(favorite.targetType)}\n meta={\n target?.kind\n ? `${typeLabel(favorite.targetType)} · ${humanizeEnum(target.kind)}`\n : typeLabel(favorite.targetType)\n }\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 {typeLabel(favorite.targetType)}\n </span>\n }\n price={\n target?.price !== undefined && target.currency\n ? `${target.currency} ${Number(target.price).toFixed(2)}`\n : undefined\n }\n footer={\n <div className=\"space-y-2\">\n {target ? (\n <RatingSummary\n rating={target.rating}\n reviewCount={target.reviewCount}\n />\n ) : (\n <span className=\"text-sm text-text-secondary\">\n {t('ggnomad.favorites.savedListing', 'Saved listing')}\n </span>\n )}\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n {target?.path ? (\n <Link\n to={target.path}\n className=\"inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium text-action-primary-bg hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg\"\n >\n <ArrowUpRight className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.favorites.view', 'View details')}\n </Link>\n ) : (\n <span />\n )}\n {removable ? (\n <button\n type=\"button\"\n disabled={toggling}\n onClick={() =>\n void runToggle(\n type as FavoriteType,\n favorite.targetId,\n target?.name ?? typeLabel(favorite.targetType)\n )\n }\n className=\"inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium text-text-secondary hover:bg-bg-sunken hover:text-status-error-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg disabled:opacity-60\"\n >\n <HeartOff className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.favorites.remove', 'Remove')}\n </button>\n ) : null}\n </div>\n </div>\n }\n />\n );\n })}\n </div>\n )}\n </GlassCard>\n </>\n )}\n </PageShell>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA+CA,IAAM,IAAmC;CAAC;CAAY;CAAQ;CAAS;AAAU;AAEjF,SAAgB,IAAgB;CAC9B,IAAM,EAAE,SAAM,EAAQ,GAChB,CAAC,GAAY,KAAiB,EAA+B,KAAK,GAElE,KAAa,MAAyB;EAC1C,QAAQ,EAAK,YAAY,GAAzB;GACE,KAAK,YACH,OAAO,EAAE,mCAAmC,UAAU;GACxD,KAAK,QACH,OAAO,EAAE,+BAA+B,MAAM;GAChD,KAAK,SACH,OAAO,EAAE,gCAAgC,OAAO;GAClD,KAAK,YACH,OAAO,EAAE,mCAAmC,UAAU;GACxD,KAAK,QACH,OAAO,EAAE,+BAA+B,MAAM;GAChD,SACE,OAAO,EAAE,gCAAgC,YAAY;EACzD;CACF,GAEM,EAAE,SAAM,eAAY,EACxB,CAIF,GAKM,EAAE,MAAM,MAAgB,EAC5B,CAoDF,GAEM,CAAC,GAAgB,EAAE,SAAS,OAAc,EAC9C,CAIF,GAEM,IAAgB,QAAc;EAClC,IAAM,oBAAM,IAAI,IAAoB;EACpC,KAAK,IAAM,KAAY,GAAa,qBAAqB,CAAC,GACxD,EAAI,IAAI,YAAY,EAAS,MAAM;GACjC,IAAI,EAAS;GACb,MAAM,EAAS;GACf,MAAM,EAAS;GACf,OAAO,EAAS;GAChB,UAAU,EAAS;GACnB,QAAQ,EAAS;GACjB,aAAa,EAAS;GACtB,QAAQ,EAAS;GACjB,MAAM,kBAAkB,EAAS;EACnC,CAAC;EAEH,KAAK,IAAM,KAAQ,GAAa,gBAAgB,CAAC,GAC/C,EAAI,IAAI,QAAQ,EAAK,MAAM;GACzB,IAAI,EAAK;GACT,MAAM,EAAK;GACX,MAAM,EAAK;GACX,OAAO,EAAK;GACZ,UAAU,EAAK;GACf,QAAQ,EAAK;GACb,aAAa,EAAK;GAClB,QAAQ,EAAK;GACb,MAAM,oBAAoB,EAAK;EACjC,CAAC;EAEH,KAAK,IAAM,KAAS,GAAa,iBAAiB,CAAC,GACjD,EAAI,IAAI,SAAS,EAAM,MAAM;GAC3B,IAAI,EAAM;GACV,MAAM,EAAM;GACZ,MAAM,EAAM;GACZ,OAAO,EAAM;GACb,UAAU,EAAM;GAChB,QAAQ,EAAM;GACd,aAAa,EAAM;GACnB,QAAQ,EAAM;GACd,MAAM,oBAAoB,EAAM;EAClC,CAAC;EAEH,KAAK,IAAM,KAAY,GAAa,qBAAqB,CAAC,GACxD,EAAI,IAAI,YAAY,EAAS,MAAM;GACjC,IAAI,EAAS;GACb,MAAM,EAAS;GACf,MAAM,EAAS;GACf,OAAO,EAAS;GAChB,UAAU,EAAS;GACnB,QAAQ,EAAS;GACjB,aAAa,EAAS;GACtB,QAAQ,EAAS;GACjB,MAAM,yBAAyB,EAAS;EAC1C,CAAC;EAEH,KAAK,IAAM,KAAQ,GAAa,gBAAgB,CAAC,GAC/C,EAAI,IAAI,QAAQ,EAAK,MAAM;GACzB,IAAI,EAAK;GACT,MAAM,EAAK;GACX,MAAM,EAAK;GACX,OAAO,EAAK;GACZ,UAAU,EAAK;EACjB,CAAC;EAEH,OAAO;CACT,GAAG,CAAC,CAAW,CAAC,GAEV,IAAY,QAAc,GAAM,oBAAoB,CAAC,GAAG,CAAC,GAAM,gBAAgB,CAAC,GAEhF,IAAS,QAAc;EAC3B,IAAM,oBAAQ,IAAI,IAAoB;EACtC,KAAK,IAAM,KAAY,GAAW;GAChC,IAAM,IAAM,EAAS,WAAW,YAAY;GAC5C,EAAM,IAAI,IAAM,EAAM,IAAI,CAAG,KAAK,KAAK,CAAC;EAC1C;EACA,OAAO;CACT,GAAG,CAAC,CAAS,CAAC,GAER,IAAU,EAAU,QACvB,MAAa,MAAe,SAAS,EAAS,WAAW,YAAY,MAAM,CAC9E,GAEM,IAA8E,CAClF;EAAE,OAAO;EAAO,OAAO,EAAE,gCAAgC,KAAK;EAAG,OAAO,EAAU;CAAO,GACzF,GAAI;EAAC;EAAY;EAAQ;EAAS;EAAY;CAAM,EAAqB,KAAK,OAAU;EACtF,OAAO;EACP,OAAO,EAAU,CAAI;EACrB,OAAO,EAAO,IAAI,CAAI,KAAK;CAC7B,EAAE,CACJ,GAEM,IAAY,OAAO,GAAoB,GAAkB,MAAkB;EAC/E,IAAI;GACF,IAAM,IAAS,MAAM,EAAe,EAClC,WAAW,EAAE,OAAO;IAAE,YAAY;IAAM;GAAS,EAAE,EACrD,CAAC;GACD,IAAI,EAAO,OAAO;IAChB,EAAO,MACL,EAAE,sCAAsC,2BAA2B,GACnE,EAAO,MAAM,OACf;IACA;GACF;GACA,IAAM,IAAQ,EAAO,MAAM,0BAA0B;GAOrD,AANA,EAAO,QACL,IACI,EAAE,iCAAiC,oBAAoB,IACvD,EAAE,mCAAmC,wBAAwB,GACjE,CACF,GACA,MAAM,EAAQ;EAChB,SAAS,GAAK;GACZ,EAAO,MACL,EAAE,sCAAsC,2BAA2B,GACnE,aAAe,QAAQ,EAAI,UAAU,OAAO,CAAG,CACjD;EACF;CACF,GAGM,UAA0E;EAC9E,KAAK,IAAM,KAAQ,GAAkB;GACnC,IAAM,IAAQ,CAAC,GAAG,EAAc,QAAQ,CAAC,EAAE,MAAM,CAAC,OAAS,EAAI,WAAW,GAAG,EAAK,EAAE,CAAC;GACrF,IAAI,GAAO,OAAO;IAAE;IAAM,SAAS,EAAM;GAAG;EAC9C;EACA,OAAO;CACT,GAEM,IAAmB,YAAY;EACnC,IAAM,IAAS,EAAkB;EACjC,IAAI,CAAC,GAAQ;GACX,EAAO,QACL,EAAE,2CAA2C,yBAAyB,GACtE,EACE,0CACA,gFACF,CACF;GACA;EACF;EACA,MAAM,EAAU,EAAO,MAAM,EAAO,QAAQ,IAAI,EAAO,QAAQ,IAAI;CACrE;CAEA,OACE,kBAAC,GAAD;EACE,OAAO,EAAE,2BAA2B,WAAW;EAC/C,aAAa,EAAE,kCAAkC,WAAW;EAC5D,UAAU,EACR,8BACA,yDACF;EACA,SACE,kBAAC,GAAD;GACE,SAAS;IACP,OAAO,EAAE,wCAAwC,sBAAsB;IACvE,gBAAgB,KAAK,EAAiB;IACtC,UAAU;IACV,MAAM,kBAAC,GAAD,CAAQ,CAAA;GAChB;GACA,SAAS,CAAC;EACX,CAAA;YAhBL,CAmBE,kBAAC,GAAD;GAAa,MAAM,kBAAC,GAAD,EAAO,WAAU,SAAU,CAAA;aAC3C,EACC,6BACA,8KACF;EACW,CAAA,GAEZ,EAAU,WAAW,IACpB,kBAAC,GAAD;GACE,cAAa;GACb,OAAO,EAAE,gCAAgC,oBAAoB;GAC7D,aAAa,EACX,sCACA,0EACF;EACD,CAAA,IAED,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,MAAD;GAAI,WAAU;aACX,EAAM,KAAK,MACV,kBAAC,MAAD,EAAA,UACE,kBAAC,UAAD;IACE,MAAK;IACL,gBAAc,MAAe,EAAK;IAClC,eAAe,EAAc,EAAK,KAAK;IACvC,WAAW,EACT,gLACA,MAAe,EAAK,QAChB,2EACA,+EACN;cATF,CAWG,EAAK,OACN,kBAAC,QAAD;KAAM,WAAU;eAAkC,EAAK;IAAY,CAAA,CAC7D;MACN,GAfK,EAAK,KAeV,CACL;EACC,CAAA,GAEJ,kBAAC,GAAD;GAAW,WAAU;GAAQ,MAAA;GAAK,WAAU;aAA5C,CACE,kBAAC,MAAD;IAAI,WAAU;cACX,EAAE,gCAAgC,aAAa;GAC9C,CAAA,GACH,EAAQ,WAAW,IAClB,kBAAC,KAAD;IAAG,WAAU;cACV,EAAE,gCAAgC,qCAAqC;GACvE,CAAA,IAEH,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAQ,KAAK,MAAa;KACzB,IAAM,IAAO,EAAS,WAAW,YAAY,GACvC,IAAS,EAAc,IAAI,GAAG,EAAK,GAAG,EAAS,UAAU,GACzD,IAAa,EAA8B,SAAS,CAAI;KAC9D,OACE,kBAAC,GAAD;MAEE,OAAO,GAAQ,SAAS;MACxB,UAAU,GAAQ;MAClB,OAAO,GAAQ,QAAQ,EAAU,EAAS,UAAU;MACpD,MACE,GAAQ,OACJ,GAAG,EAAU,EAAS,UAAU,EAAE,KAAK,EAAa,EAAO,IAAI,MAC/D,EAAU,EAAS,UAAU;MAEnC,OACE,kBAAC,QAAD;OAAM,WAAU;iBACb,EAAU,EAAS,UAAU;MAC1B,CAAA;MAER,OACE,GAAQ,UAAU,KAAA,KAAa,EAAO,WAClC,GAAG,EAAO,SAAS,GAAG,OAAO,EAAO,KAAK,EAAE,QAAQ,CAAC,MACpD,KAAA;MAEN,QACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACG,IACC,kBAAC,GAAD;QACE,QAAQ,EAAO;QACf,aAAa,EAAO;OACrB,CAAA,IAED,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAE,kCAAkC,eAAe;OAChD,CAAA,GAER,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACG,GAAQ,OACP,kBAAC,GAAD;SACE,IAAI,EAAO;SACX,WAAU;mBAFZ,CAIE,kBAAC,GAAD;UAAc,WAAU;UAAW,eAAY;SAAQ,CAAA,GACtD,EAAE,0BAA0B,cAAc,CACvC;aAEN,kBAAC,QAAD,CAAO,CAAA,GAER,IACC,kBAAC,UAAD;SACE,MAAK;SACL,UAAU;SACV,eACE,KAAK,EACH,GACA,EAAS,UACT,GAAQ,QAAQ,EAAU,EAAS,UAAU,CAC/C;SAEF,WAAU;mBAVZ,CAYE,kBAAC,GAAD;UAAU,WAAU;UAAW,eAAY;SAAQ,CAAA,GAClD,EAAE,4BAA4B,QAAQ,CACjC;aACN,IACD;SACF;;KAER,GA/DM,EAAS,EA+Df;IAEL,CAAC;GACE,CAAA,CAEE;IACX,EAAA,CAAA,CAEK;;AAEf"}
@@ -1 +1 @@
1
- {"version":3,"file":"HospitalityPage.d.ts","sourceRoot":"","sources":["../../../src/ggnomad/pages/HospitalityPage.tsx"],"names":[],"mappings":"AAWA,wBAAgB,eAAe,gCAkF9B"}
1
+ {"version":3,"file":"HospitalityPage.d.ts","sourceRoot":"","sources":["../../../src/ggnomad/pages/HospitalityPage.tsx"],"names":[],"mappings":"AAiCA,wBAAgB,eAAe,gCA6I9B"}
@@ -2,66 +2,109 @@ import { GGNOMAD_PROPERTIES_QUERY as e } from "../graphql.js";
2
2
  import { PageShell as t } from "./PageShell.js";
3
3
  import { EntityCard as n } from "../components/EntityCard.js";
4
4
  import { LoadMoreButton as r } from "../components/LoadMoreButton.js";
5
+ import { RatingSummary as i } from "../components/RatingSummary.js";
6
+ import { EMPTY_LISTING_FILTERS as a, ListingFilters as o, hasActiveListingFilters as s } from "../components/ListingFilters.js";
5
7
  import "../components/index.js";
6
- import { useState as i } from "react";
7
- import { Hotel as a, Star as o } from "lucide-react";
8
- import { useI18n as s } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
9
- import { useQuery as c } from "@apollo/client/react";
10
- import { GlassCard as l, IllustratedEmptyState as u, PagePurpose as d } from "@burdenoff/fe-libs/ui";
11
- import { jsx as f, jsxs as p } from "react/jsx-runtime";
8
+ import { useState as c } from "react";
9
+ import { Hotel as l, Leaf as u, Users as d, Zap as f } from "lucide-react";
10
+ import { useI18n as p } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
11
+ import { useQuery as m } from "@apollo/client/react";
12
+ import { GlassCard as h, IllustratedEmptyState as g, PagePurpose as _ } from "@burdenoff/fe-libs/ui";
13
+ import { jsx as v, jsxs as y } from "react/jsx-runtime";
12
14
  //#region src/ggnomad/pages/HospitalityPage.tsx
13
- function m() {
14
- let { t: m } = s(), [h, g] = i(12), { data: _ } = c(e, { variables: {
15
- limit: h,
16
- offset: 0
17
- } }), v = _?.ggnomadProperties ?? [];
18
- return /* @__PURE__ */ p(t, {
19
- title: m("ggnomad.hospitality.title", "Hospitality"),
20
- titleAccent: m("ggnomad.hospitality.title.accent", "Hospitality"),
21
- subtitle: m("ggnomad.hospitality.subtitle", "Property discovery powered by live GraphQL data"),
22
- children: [/* @__PURE__ */ f(d, {
23
- icon: /* @__PURE__ */ f(a, { className: "size-4" }),
24
- children: m("ggnomad.hospitality.purpose", "Discover places to stay — hotels, homestays and short-stay rentals. Open any property to review its nightly price, guest capacity and rating, then create a booking for the dates you need.")
25
- }), v.length === 0 ? /* @__PURE__ */ f(u, {
26
- illustration: "empty-data",
27
- title: m("ggnomad.hospitality.emptyTitle", "No properties available"),
28
- description: m("ggnomad.hospitality.emptyDescription", "Properties published by providers will be discoverable here.")
29
- }) : /* @__PURE__ */ p(l, {
30
- treatment: "glass",
31
- glow: !0,
32
- className: "border-border-seam p-5",
33
- children: [
34
- /* @__PURE__ */ f("h2", {
35
- className: "mb-4 text-lg font-medium tracking-tight text-text-primary",
36
- children: m("ggnomad.hospitality.featuredStays", "Featured stays")
37
- }),
38
- /* @__PURE__ */ f("div", {
39
- className: "grid gap-3 sm:grid-cols-2 xl:grid-cols-3",
40
- children: v.map((e) => /* @__PURE__ */ f(n, {
41
- to: e.id,
42
- image: e.images?.[0],
43
- imageAlt: e.name,
44
- title: e.name,
45
- meta: e.type,
46
- price: `${e.currency} ${Number(e.pricePerNight).toFixed(2)} ${m("ggnomad.hospitality.card.perNight", "per night")}`,
47
- footer: /* @__PURE__ */ p("span", {
48
- className: "inline-flex items-center gap-1 tabular-nums",
49
- children: [/* @__PURE__ */ f(o, {
50
- className: "size-3.5 text-status-warning-text",
51
- "aria-hidden": "true"
52
- }), e.rating ?? m("ggnomad.hospitality.card.noRating", "N/A")]
53
- })
54
- }, e.id))
55
- }),
56
- /* @__PURE__ */ f(r, {
57
- visible: v.length >= h,
58
- onLoadMore: () => g((e) => e + 12)
59
- })
60
- ]
61
- })]
15
+ function b() {
16
+ let { t: b } = p(), [x, S] = c(12), [C, w] = c(a), { data: T, loading: E, error: D } = m(e, { variables: {
17
+ limit: x,
18
+ offset: 0,
19
+ status: C.status,
20
+ minSustainabilityScore: C.minSustainabilityScore,
21
+ hasAccessibilityFeatures: C.accessibilityFeatures.length > 0 ? C.accessibilityFeatures : void 0
22
+ } }), O = T?.ggnomadProperties ?? [], k = s(C);
23
+ return /* @__PURE__ */ y(t, {
24
+ title: b("ggnomad.hospitality.title", "Hospitality"),
25
+ titleAccent: b("ggnomad.hospitality.title.accent", "Hospitality"),
26
+ subtitle: b("ggnomad.hospitality.subtitle", "Property discovery powered by live GraphQL data"),
27
+ children: [
28
+ /* @__PURE__ */ v(_, {
29
+ icon: /* @__PURE__ */ v(l, { className: "size-4" }),
30
+ children: b("ggnomad.hospitality.purpose", "Discover places to stay — hotels, homestays and short-stay rentals. Open any property to review its nightly price, guest capacity and rating, then create a booking for the dates you need.")
31
+ }),
32
+ /* @__PURE__ */ v(o, {
33
+ idPrefix: "ggnomad-hospitality",
34
+ value: C,
35
+ onChange: (e) => {
36
+ S(12), w(e);
37
+ },
38
+ resultCount: O.length
39
+ }),
40
+ D ? /* @__PURE__ */ v(g, {
41
+ illustration: "empty-data",
42
+ title: b("ggnomad.hospitality.errorTitle", "Could not load properties"),
43
+ description: D.message
44
+ }) : O.length === 0 ? /* @__PURE__ */ v(g, {
45
+ illustration: "empty-data",
46
+ title: E ? b("ggnomad.hospitality.loadingTitle", "Loading properties…") : k ? b("ggnomad.hospitality.noMatchTitle", "No properties match these filters") : b("ggnomad.hospitality.emptyTitle", "No properties available"),
47
+ description: E ? b("ggnomad.hospitality.loadingDescription", "Fetching the live property catalogue.") : k ? b("ggnomad.hospitality.noMatchDescription", "Try clearing a filter — status, sustainability score and accessibility all narrow the catalogue.") : b("ggnomad.hospitality.emptyDescription", "Properties published by providers will be discoverable here.")
48
+ }) : /* @__PURE__ */ y(h, {
49
+ treatment: "glass",
50
+ glow: !0,
51
+ className: "border-border-seam p-5",
52
+ children: [
53
+ /* @__PURE__ */ v("h2", {
54
+ className: "mb-4 text-lg font-medium tracking-tight text-text-primary",
55
+ children: b("ggnomad.hospitality.featuredStays", "Featured stays")
56
+ }),
57
+ /* @__PURE__ */ v("div", {
58
+ className: "grid gap-3 sm:grid-cols-2 xl:grid-cols-3",
59
+ children: O.map((e) => /* @__PURE__ */ v(n, {
60
+ to: e.id,
61
+ image: e.images?.[0],
62
+ imageAlt: e.name,
63
+ title: e.name,
64
+ meta: e.type,
65
+ badge: e.instantBook ? /* @__PURE__ */ y("span", {
66
+ className: "inline-flex items-center gap-1 rounded-full bg-status-success-bg-subtle px-2.5 py-0.5 text-xs font-medium text-status-success-text",
67
+ children: [/* @__PURE__ */ v(f, {
68
+ className: "size-3",
69
+ "aria-hidden": "true"
70
+ }), b("ggnomad.listing.instantBook", "Instant book")]
71
+ }) : null,
72
+ price: `${e.currency} ${Number(e.pricePerNight).toFixed(2)} ${b("ggnomad.hospitality.card.perNight", "per night")}`,
73
+ footer: /* @__PURE__ */ y("div", {
74
+ className: "flex flex-wrap items-center gap-x-3 gap-y-1",
75
+ children: [
76
+ /* @__PURE__ */ v(i, {
77
+ rating: e.rating,
78
+ reviewCount: e.reviewCount
79
+ }),
80
+ /* @__PURE__ */ y("span", {
81
+ className: "inline-flex items-center gap-1 tabular-nums",
82
+ children: [/* @__PURE__ */ v(d, {
83
+ className: "size-3.5",
84
+ "aria-hidden": "true"
85
+ }), b("ggnomad.hospitality.card.guests", "Up to {{n}}").replace("{{n}}", String(e.maxGuests))]
86
+ }),
87
+ e.sustainabilityScore > 0 ? /* @__PURE__ */ y("span", {
88
+ className: "inline-flex items-center gap-1 tabular-nums text-status-success-text",
89
+ children: [/* @__PURE__ */ v(u, {
90
+ className: "size-3.5",
91
+ "aria-hidden": "true"
92
+ }), e.sustainabilityScore]
93
+ }) : null
94
+ ]
95
+ })
96
+ }, e.id))
97
+ }),
98
+ /* @__PURE__ */ v(r, {
99
+ visible: O.length >= x,
100
+ onLoadMore: () => S((e) => e + 12)
101
+ })
102
+ ]
103
+ })
104
+ ]
62
105
  });
63
106
  }
64
107
  //#endregion
65
- export { m as HospitalityPage };
108
+ export { b as HospitalityPage };
66
109
 
67
110
  //# sourceMappingURL=HospitalityPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"HospitalityPage.js","names":[],"sources":["../../../src/ggnomad/pages/HospitalityPage.tsx"],"sourcesContent":["import { 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 { Hotel, Star } from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_PROPERTIES_QUERY } from '../graphql';\nimport { PageShell } from './PageShell';\nimport { EntityCard, LoadMoreButton } from '../components';\n\nexport function HospitalityPage() {\n const { t } = useI18n();\n const [limit, setLimit] = useState(12);\n const { data } = useQuery(\n GGNOMAD_PROPERTIES_QUERY as TypedDocumentNode<\n {\n ggnomadProperties: Array<{\n id: string;\n name: string;\n type: string;\n pricePerNight: number;\n currency: string;\n rating?: number;\n images?: string[];\n }>;\n },\n { limit: number; offset: number }\n >,\n { variables: { limit, offset: 0 } }\n );\n const properties = data?.ggnomadProperties ?? [];\n\n return (\n <PageShell\n title={t('ggnomad.hospitality.title', 'Hospitality')}\n titleAccent={t('ggnomad.hospitality.title.accent', 'Hospitality')}\n subtitle={t(\n 'ggnomad.hospitality.subtitle',\n 'Property discovery powered by live GraphQL data'\n )}\n >\n <PagePurpose icon={<Hotel className=\"size-4\" />}>\n {t(\n 'ggnomad.hospitality.purpose',\n 'Discover places to stay — hotels, homestays and short-stay rentals. Open any property to review its nightly price, guest capacity and rating, then create a booking for the dates you need.'\n )}\n </PagePurpose>\n\n {properties.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.hospitality.emptyTitle', 'No properties available')}\n description={t(\n 'ggnomad.hospitality.emptyDescription',\n 'Properties published by providers will be discoverable 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.hospitality.featuredStays', 'Featured stays')}\n </h2>\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {properties.map((property) => (\n <EntityCard\n key={property.id}\n to={property.id}\n image={property.images?.[0]}\n imageAlt={property.name}\n title={property.name}\n meta={property.type}\n price={`${property.currency} ${Number(property.pricePerNight).toFixed(2)} ${t(\n 'ggnomad.hospitality.card.perNight',\n 'per night'\n )}`}\n footer={\n <span className=\"inline-flex items-center gap-1 tabular-nums\">\n <Star className=\"size-3.5 text-status-warning-text\" aria-hidden=\"true\" />\n {property.rating ?? t('ggnomad.hospitality.card.noRating', 'N/A')}\n </span>\n }\n />\n ))}\n </div>\n <LoadMoreButton\n visible={properties.length >= limit}\n onLoadMore={() => setLimit((l) => l + 12)}\n />\n </GlassCard>\n )}\n </PageShell>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAWA,SAAgB,IAAkB;CAChC,IAAM,EAAE,SAAM,EAAQ,GAChB,CAAC,GAAO,KAAY,EAAS,EAAE,GAC/B,EAAE,YAAS,EACf,GAcA,EAAE,WAAW;EAAE;EAAO,QAAQ;CAAE,EAAE,CACpC,GACM,IAAa,GAAM,qBAAqB,CAAC;CAE/C,OACE,kBAAC,GAAD;EACE,OAAO,EAAE,6BAA6B,aAAa;EACnD,aAAa,EAAE,oCAAoC,aAAa;EAChE,UAAU,EACR,gCACA,iDACF;YANF,CAQE,kBAAC,GAAD;GAAa,MAAM,kBAAC,GAAD,EAAO,WAAU,SAAU,CAAA;aAC3C,EACC,+BACA,6LACF;EACW,CAAA,GAEZ,EAAW,WAAW,IACrB,kBAAC,GAAD;GACE,cAAa;GACb,OAAO,EAAE,kCAAkC,yBAAyB;GACpE,aAAa,EACX,wCACA,8DACF;EACD,CAAA,IAED,kBAAC,GAAD;GAAW,WAAU;GAAQ,MAAA;GAAK,WAAU;aAA5C;IACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAE,qCAAqC,gBAAgB;IACtD,CAAA;IACJ,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAW,KAAK,MACf,kBAAC,GAAD;MAEE,IAAI,EAAS;MACb,OAAO,EAAS,SAAS;MACzB,UAAU,EAAS;MACnB,OAAO,EAAS;MAChB,MAAM,EAAS;MACf,OAAO,GAAG,EAAS,SAAS,GAAG,OAAO,EAAS,aAAa,EAAE,QAAQ,CAAC,EAAE,GAAG,EAC1E,qCACA,WACF;MACA,QACE,kBAAC,QAAD;OAAM,WAAU;iBAAhB,CACE,kBAAC,GAAD;QAAM,WAAU;QAAoC,eAAY;OAAQ,CAAA,GACvE,EAAS,UAAU,EAAE,qCAAqC,KAAK,CAC5D;;KAET,GAhBM,EAAS,EAgBf,CACF;IACE,CAAA;IACL,kBAAC,GAAD;KACE,SAAS,EAAW,UAAU;KAC9B,kBAAkB,GAAU,MAAM,IAAI,EAAE;IACzC,CAAA;GACQ;IAEJ;;AAEf"}
1
+ {"version":3,"file":"HospitalityPage.js","names":[],"sources":["../../../src/ggnomad/pages/HospitalityPage.tsx"],"sourcesContent":["import { 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 { Hotel, Leaf, Users, Zap } from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_PROPERTIES_QUERY } from '../graphql';\nimport { PageShell } from './PageShell';\nimport {\n EntityCard,\n EMPTY_LISTING_FILTERS,\n ListingFilters,\n LoadMoreButton,\n RatingSummary,\n hasActiveListingFilters,\n} from '../components';\nimport type { ListingFilterState } from '../components';\n\ntype PropertyRow = {\n id: string;\n name: string;\n type: string;\n pricePerNight: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n maxGuests: number;\n images?: string[];\n instantBook: boolean;\n sustainabilityScore: number;\n};\n\nexport function HospitalityPage() {\n const { t } = useI18n();\n const [limit, setLimit] = useState(12);\n const [filters, setFilters] = useState<ListingFilterState>(EMPTY_LISTING_FILTERS);\n\n const { data, loading, error } = useQuery(\n GGNOMAD_PROPERTIES_QUERY as TypedDocumentNode<\n { ggnomadProperties: PropertyRow[] },\n {\n limit: number;\n offset: number;\n status?: string;\n minSustainabilityScore?: number;\n hasAccessibilityFeatures?: string[];\n }\n >,\n {\n variables: {\n limit,\n offset: 0,\n status: filters.status,\n minSustainabilityScore: filters.minSustainabilityScore,\n hasAccessibilityFeatures:\n filters.accessibilityFeatures.length > 0 ? filters.accessibilityFeatures : undefined,\n },\n }\n );\n const properties = data?.ggnomadProperties ?? [];\n const filtered = hasActiveListingFilters(filters);\n\n return (\n <PageShell\n title={t('ggnomad.hospitality.title', 'Hospitality')}\n titleAccent={t('ggnomad.hospitality.title.accent', 'Hospitality')}\n subtitle={t(\n 'ggnomad.hospitality.subtitle',\n 'Property discovery powered by live GraphQL data'\n )}\n >\n <PagePurpose icon={<Hotel className=\"size-4\" />}>\n {t(\n 'ggnomad.hospitality.purpose',\n 'Discover places to stay — hotels, homestays and short-stay rentals. Open any property to review its nightly price, guest capacity and rating, then create a booking for the dates you need.'\n )}\n </PagePurpose>\n\n <ListingFilters\n idPrefix=\"ggnomad-hospitality\"\n value={filters}\n onChange={(next) => {\n setLimit(12);\n setFilters(next);\n }}\n resultCount={properties.length}\n />\n\n {error ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.hospitality.errorTitle', 'Could not load properties')}\n description={error.message}\n />\n ) : properties.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={\n loading\n ? t('ggnomad.hospitality.loadingTitle', 'Loading properties…')\n : filtered\n ? t('ggnomad.hospitality.noMatchTitle', 'No properties match these filters')\n : t('ggnomad.hospitality.emptyTitle', 'No properties available')\n }\n description={\n loading\n ? t('ggnomad.hospitality.loadingDescription', 'Fetching the live property catalogue.')\n : filtered\n ? t(\n 'ggnomad.hospitality.noMatchDescription',\n 'Try clearing a filter — status, sustainability score and accessibility all narrow the catalogue.'\n )\n : t(\n 'ggnomad.hospitality.emptyDescription',\n 'Properties published by providers will be discoverable here.'\n )\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.hospitality.featuredStays', 'Featured stays')}\n </h2>\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {properties.map((property) => (\n <EntityCard\n key={property.id}\n to={property.id}\n image={property.images?.[0]}\n imageAlt={property.name}\n title={property.name}\n meta={property.type}\n badge={\n property.instantBook ? (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-success-bg-subtle px-2.5 py-0.5 text-xs font-medium text-status-success-text\">\n <Zap className=\"size-3\" aria-hidden=\"true\" />\n {t('ggnomad.listing.instantBook', 'Instant book')}\n </span>\n ) : null\n }\n price={`${property.currency} ${Number(property.pricePerNight).toFixed(2)} ${t(\n 'ggnomad.hospitality.card.perNight',\n 'per night'\n )}`}\n footer={\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1\">\n <RatingSummary rating={property.rating} reviewCount={property.reviewCount} />\n <span className=\"inline-flex items-center gap-1 tabular-nums\">\n <Users className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.hospitality.card.guests', 'Up to {{n}}').replace(\n '{{n}}',\n String(property.maxGuests)\n )}\n </span>\n {property.sustainabilityScore > 0 ? (\n <span className=\"inline-flex items-center gap-1 tabular-nums text-status-success-text\">\n <Leaf className=\"size-3.5\" aria-hidden=\"true\" />\n {property.sustainabilityScore}\n </span>\n ) : null}\n </div>\n }\n />\n ))}\n </div>\n <LoadMoreButton\n visible={properties.length >= limit}\n onLoadMore={() => setLimit((l) => l + 12)}\n />\n </GlassCard>\n )}\n </PageShell>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAiCA,SAAgB,IAAkB;CAChC,IAAM,EAAE,SAAM,EAAQ,GAChB,CAAC,GAAO,KAAY,EAAS,EAAE,GAC/B,CAAC,GAAS,KAAc,EAA6B,CAAqB,GAE1E,EAAE,SAAM,YAAS,aAAU,EAC/B,GAUA,EACE,WAAW;EACT;EACA,QAAQ;EACR,QAAQ,EAAQ;EAChB,wBAAwB,EAAQ;EAChC,0BACE,EAAQ,sBAAsB,SAAS,IAAI,EAAQ,wBAAwB,KAAA;CAC/E,EACF,CACF,GACM,IAAa,GAAM,qBAAqB,CAAC,GACzC,IAAW,EAAwB,CAAO;CAEhD,OACE,kBAAC,GAAD;EACE,OAAO,EAAE,6BAA6B,aAAa;EACnD,aAAa,EAAE,oCAAoC,aAAa;EAChE,UAAU,EACR,gCACA,iDACF;YANF;GAQE,kBAAC,GAAD;IAAa,MAAM,kBAAC,GAAD,EAAO,WAAU,SAAU,CAAA;cAC3C,EACC,+BACA,6LACF;GACW,CAAA;GAEb,kBAAC,GAAD;IACE,UAAS;IACT,OAAO;IACP,WAAW,MAAS;KAElB,AADA,EAAS,EAAE,GACX,EAAW,CAAI;IACjB;IACA,aAAa,EAAW;GACzB,CAAA;GAEA,IACC,kBAAC,GAAD;IACE,cAAa;IACb,OAAO,EAAE,kCAAkC,2BAA2B;IACtE,aAAa,EAAM;GACpB,CAAA,IACC,EAAW,WAAW,IACxB,kBAAC,GAAD;IACE,cAAa;IACb,OACE,IACI,EAAE,oCAAoC,qBAAqB,IAC3D,IACE,EAAE,oCAAoC,mCAAmC,IACzE,EAAE,kCAAkC,yBAAyB;IAErE,aACE,IACI,EAAE,0CAA0C,uCAAuC,IACnF,IACE,EACE,0CACA,kGACF,IACA,EACE,wCACA,8DACF;GAET,CAAA,IAED,kBAAC,GAAD;IAAW,WAAU;IAAQ,MAAA;IAAK,WAAU;cAA5C;KACE,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAE,qCAAqC,gBAAgB;KACtD,CAAA;KACJ,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAW,KAAK,MACf,kBAAC,GAAD;OAEE,IAAI,EAAS;OACb,OAAO,EAAS,SAAS;OACzB,UAAU,EAAS;OACnB,OAAO,EAAS;OAChB,MAAM,EAAS;OACf,OACE,EAAS,cACP,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACE,kBAAC,GAAD;SAAK,WAAU;SAAS,eAAY;QAAQ,CAAA,GAC3C,EAAE,+BAA+B,cAAc,CAC5C;YACJ;OAEN,OAAO,GAAG,EAAS,SAAS,GAAG,OAAO,EAAS,aAAa,EAAE,QAAQ,CAAC,EAAE,GAAG,EAC1E,qCACA,WACF;OACA,QACE,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,GAAD;UAAe,QAAQ,EAAS;UAAQ,aAAa,EAAS;SAAc,CAAA;SAC5E,kBAAC,QAAD;UAAM,WAAU;oBAAhB,CACE,kBAAC,GAAD;WAAO,WAAU;WAAW,eAAY;UAAQ,CAAA,GAC/C,EAAE,mCAAmC,aAAa,EAAE,QACnD,SACA,OAAO,EAAS,SAAS,CAC3B,CACI;;SACL,EAAS,sBAAsB,IAC9B,kBAAC,QAAD;UAAM,WAAU;oBAAhB,CACE,kBAAC,GAAD;WAAM,WAAU;WAAW,eAAY;UAAQ,CAAA,GAC9C,EAAS,mBACN;cACJ;QACD;;MAER,GApCM,EAAS,EAoCf,CACF;KACE,CAAA;KACL,kBAAC,GAAD;MACE,SAAS,EAAW,UAAU;MAC9B,kBAAkB,GAAU,MAAM,IAAI,EAAE;KACzC,CAAA;IACQ;;EAEJ;;AAEf"}
@@ -1 +1 @@
1
- {"version":3,"file":"PropertyDetailsPage.d.ts","sourceRoot":"","sources":["../../../src/ggnomad/pages/PropertyDetailsPage.tsx"],"names":[],"mappings":"AAqBA,wBAAgB,mBAAmB,gCAoJlC"}
1
+ {"version":3,"file":"PropertyDetailsPage.d.ts","sourceRoot":"","sources":["../../../src/ggnomad/pages/PropertyDetailsPage.tsx"],"names":[],"mappings":"AAiEA,wBAAgB,mBAAmB,gCAsOlC"}