@burdenoff/microfe-ggnomad 2026.716.1 → 2026.719.2
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.map +1 -1
- package/dist/ggnomad/GGNomadRoutes.js.map +1 -1
- package/dist/ggnomad/components/BookingPanel.js.map +1 -1
- package/dist/ggnomad/components/DetailHero.js.map +1 -1
- package/dist/ggnomad/components/DetailRow.js.map +1 -1
- package/dist/ggnomad/components/EntityCard.js.map +1 -1
- package/dist/ggnomad/components/ListingEditDialog.js.map +1 -1
- package/dist/ggnomad/components/LoadMoreButton.js.map +1 -1
- package/dist/ggnomad/components/ReviewsSection.js +2 -2
- package/dist/ggnomad/components/ReviewsSection.js.map +1 -1
- package/dist/ggnomad/components/SectionCard.js.map +1 -1
- package/dist/ggnomad/components/StatCard.js.map +1 -1
- package/dist/ggnomad/components/StatusBadge.js.map +1 -1
- package/dist/ggnomad/graphql.js +27 -27
- package/dist/ggnomad/graphql.js.map +1 -1
- package/dist/ggnomad/pages/ActivityDetailsPage.js.map +1 -1
- package/dist/ggnomad/pages/AnalyticsPage.js.map +1 -1
- package/dist/ggnomad/pages/BookingsPage.js +2 -2
- package/dist/ggnomad/pages/BookingsPage.js.map +1 -1
- package/dist/ggnomad/pages/DashboardPage.js +8 -8
- package/dist/ggnomad/pages/DashboardPage.js.map +1 -1
- package/dist/ggnomad/pages/EntertainmentPage.js +2 -2
- 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 +2 -2
- package/dist/ggnomad/pages/HospitalityPage.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/ProviderPage.js +2 -2
- package/dist/ggnomad/pages/ProviderPage.js.map +1 -1
- package/dist/ggnomad/pages/SearchPage.js +8 -8
- package/dist/ggnomad/pages/SearchPage.js.map +1 -1
- package/dist/ggnomad/pages/TourDetailsPage.js.map +1 -1
- package/dist/ggnomad/pages/TourismPage.js +4 -4
- package/dist/ggnomad/pages/TourismPage.js.map +1 -1
- package/dist/ggnomad/pages/TravelPage.js +2 -2
- package/dist/ggnomad/pages/TravelPage.js.map +1 -1
- package/dist/ggnomad/pages/TripPlanDetailPage.js +8 -8
- package/dist/ggnomad/pages/TripPlanDetailPage.js.map +1 -1
- package/dist/ggnomad/pages/TripsPage.js +2 -2
- package/dist/ggnomad/pages/TripsPage.js.map +1 -1
- package/dist/ggnomad/providers/GGNomadProvider.js.map +1 -1
- package/dist/ggnomad/utils/cn.js.map +1 -1
- package/dist/ggnomad/utils/navigation.js.map +1 -1
- package/dist/ggnomad/utils/notify.js.map +1 -1
- package/dist/services/createSandboxAssistantTransport.js.map +1 -1
- package/dist/utils/pageContext.js.map +1 -1
- package/package.json +2 -2
package/dist/ggnomad/graphql.js
CHANGED
|
@@ -18,25 +18,25 @@ var t = e`
|
|
|
18
18
|
}
|
|
19
19
|
`, n = e`
|
|
20
20
|
query GGNomadBookableSample {
|
|
21
|
-
ggnomadProperties(pagination: {
|
|
21
|
+
ggnomadProperties(pagination: { limit: 1, offset: 0 }) {
|
|
22
22
|
id
|
|
23
23
|
name
|
|
24
24
|
pricePerNight
|
|
25
25
|
currency
|
|
26
26
|
}
|
|
27
|
-
ggnomadTours(pagination: {
|
|
27
|
+
ggnomadTours(pagination: { limit: 1, offset: 0 }) {
|
|
28
28
|
id
|
|
29
29
|
name
|
|
30
30
|
price
|
|
31
31
|
currency
|
|
32
32
|
}
|
|
33
|
-
ggnomadEvents(pagination: {
|
|
33
|
+
ggnomadEvents(pagination: { limit: 1, offset: 0 }) {
|
|
34
34
|
id
|
|
35
35
|
name
|
|
36
36
|
price
|
|
37
37
|
currency
|
|
38
38
|
}
|
|
39
|
-
ggnomadActivities(pagination: {
|
|
39
|
+
ggnomadActivities(pagination: { limit: 1, offset: 0 }) {
|
|
40
40
|
id
|
|
41
41
|
name
|
|
42
42
|
price
|
|
@@ -45,26 +45,26 @@ var t = e`
|
|
|
45
45
|
}
|
|
46
46
|
`, r = e`
|
|
47
47
|
query GGNomadFavoriteTargets {
|
|
48
|
-
ggnomadProperties(pagination: {
|
|
48
|
+
ggnomadProperties(pagination: { limit: 100, offset: 0 }) {
|
|
49
49
|
id
|
|
50
50
|
name
|
|
51
51
|
}
|
|
52
|
-
ggnomadTours(pagination: {
|
|
52
|
+
ggnomadTours(pagination: { limit: 100, offset: 0 }) {
|
|
53
53
|
id
|
|
54
54
|
name
|
|
55
55
|
}
|
|
56
|
-
ggnomadEvents(pagination: {
|
|
56
|
+
ggnomadEvents(pagination: { limit: 100, offset: 0 }) {
|
|
57
57
|
id
|
|
58
58
|
name
|
|
59
59
|
}
|
|
60
|
-
ggnomadActivities(pagination: {
|
|
60
|
+
ggnomadActivities(pagination: { limit: 100, offset: 0 }) {
|
|
61
61
|
id
|
|
62
62
|
name
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
`, i = e`
|
|
66
|
-
query GGNomadProviders($
|
|
67
|
-
ggnomadProviders(pagination: {
|
|
66
|
+
query GGNomadProviders($limit: Int, $offset: Int) {
|
|
67
|
+
ggnomadProviders(pagination: { limit: $limit, offset: $offset }) {
|
|
68
68
|
id
|
|
69
69
|
name
|
|
70
70
|
type
|
|
@@ -78,8 +78,8 @@ var t = e`
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
`, a = e`
|
|
81
|
-
query GGNomadProperties($
|
|
82
|
-
ggnomadProperties(pagination: {
|
|
81
|
+
query GGNomadProperties($limit: Int, $offset: Int) {
|
|
82
|
+
ggnomadProperties(pagination: { limit: $limit, offset: $offset }) {
|
|
83
83
|
id
|
|
84
84
|
name
|
|
85
85
|
type
|
|
@@ -135,8 +135,8 @@ e`
|
|
|
135
135
|
}
|
|
136
136
|
`;
|
|
137
137
|
var s = e`
|
|
138
|
-
query GGNomadTours($
|
|
139
|
-
ggnomadTours(pagination: {
|
|
138
|
+
query GGNomadTours($limit: Int, $offset: Int) {
|
|
139
|
+
ggnomadTours(pagination: { limit: $limit, offset: $offset }) {
|
|
140
140
|
id
|
|
141
141
|
name
|
|
142
142
|
description
|
|
@@ -177,8 +177,8 @@ var s = e`
|
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
`, l = e`
|
|
180
|
-
query GGNomadEvents($
|
|
181
|
-
ggnomadEvents(pagination: {
|
|
180
|
+
query GGNomadEvents($limit: Int, $offset: Int) {
|
|
181
|
+
ggnomadEvents(pagination: { limit: $limit, offset: $offset }) {
|
|
182
182
|
id
|
|
183
183
|
name
|
|
184
184
|
description
|
|
@@ -217,8 +217,8 @@ var s = e`
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
`, d = e`
|
|
220
|
-
query GGNomadActivities($
|
|
221
|
-
ggnomadActivities(pagination: {
|
|
220
|
+
query GGNomadActivities($limit: Int, $offset: Int) {
|
|
221
|
+
ggnomadActivities(pagination: { limit: $limit, offset: $offset }) {
|
|
222
222
|
id
|
|
223
223
|
name
|
|
224
224
|
description
|
|
@@ -263,8 +263,8 @@ var s = e`
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
`, p = e`
|
|
266
|
-
query GGNomadTrips($
|
|
267
|
-
ggnomadTrips(pagination: {
|
|
266
|
+
query GGNomadTrips($limit: Int, $offset: Int) {
|
|
267
|
+
ggnomadTrips(pagination: { limit: $limit, offset: $offset }) {
|
|
268
268
|
id
|
|
269
269
|
name
|
|
270
270
|
transportMode
|
|
@@ -283,8 +283,8 @@ var s = e`
|
|
|
283
283
|
}
|
|
284
284
|
}
|
|
285
285
|
`, m = e`
|
|
286
|
-
query GGNomadBookings($
|
|
287
|
-
ggnomadBookings(pagination: {
|
|
286
|
+
query GGNomadBookings($limit: Int, $offset: Int) {
|
|
287
|
+
ggnomadBookings(pagination: { limit: $limit, offset: $offset }) {
|
|
288
288
|
id
|
|
289
289
|
referenceCode
|
|
290
290
|
bookingType
|
|
@@ -337,8 +337,8 @@ var s = e`
|
|
|
337
337
|
}
|
|
338
338
|
}
|
|
339
339
|
`, y = e`
|
|
340
|
-
query GGNomadTripPlans($
|
|
341
|
-
ggnomadTripPlans(pagination: {
|
|
340
|
+
query GGNomadTripPlans($limit: Int, $offset: Int) {
|
|
341
|
+
ggnomadTripPlans(pagination: { limit: $limit, offset: $offset }) {
|
|
342
342
|
id
|
|
343
343
|
name
|
|
344
344
|
description
|
|
@@ -444,13 +444,13 @@ var C = e`
|
|
|
444
444
|
query GGNomadReviews(
|
|
445
445
|
$targetType: GgnomadBookingType!
|
|
446
446
|
$targetId: String!
|
|
447
|
-
$
|
|
448
|
-
$
|
|
447
|
+
$limit: Int
|
|
448
|
+
$offset: Int
|
|
449
449
|
) {
|
|
450
450
|
ggnomadReviews(
|
|
451
451
|
targetType: $targetType
|
|
452
452
|
targetId: $targetId
|
|
453
|
-
pagination: {
|
|
453
|
+
pagination: { limit: $limit, offset: $offset }
|
|
454
454
|
) {
|
|
455
455
|
id
|
|
456
456
|
rating
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graphql.js","names":[],"sources":["../../src/ggnomad/graphql.ts"],"sourcesContent":["import { gql } from '@apollo/client/core';\n\nexport const GGNOMAD_OVERVIEW_QUERY = gql`\n query GGNomadOverview {\n ggnomadOverviewStats {\n users\n listings\n bookings\n monthlyRevenue\n }\n ggnomadAnalytics {\n totalRevenue\n totalBookings\n totalUsers\n conversionRate\n revenueByCategory\n }\n }\n`;\n\n/**\n * One bookable item of each type (properties are priced per night, the rest\n * carry a flat price). Used to seed the Bookings \"create demo booking\" action\n * with a REAL listing id so the mutation passes the backend's\n * assertTargetInWorkspace check instead of failing on a synthetic id.\n */\nexport const GGNOMAD_BOOKABLE_SAMPLE_QUERY = gql`\n query GGNomadBookableSample {\n ggnomadProperties(pagination: { first: 1, skip: 0 }) {\n id\n name\n pricePerNight\n currency\n }\n ggnomadTours(pagination: { first: 1, skip: 0 }) {\n id\n name\n price\n currency\n }\n ggnomadEvents(pagination: { first: 1, skip: 0 }) {\n id\n name\n price\n currency\n }\n ggnomadActivities(pagination: { first: 1, skip: 0 }) {\n id\n name\n price\n currency\n }\n }\n`;\n\n/**\n * id + name for every favouritable listing type, used to (a) resolve the real\n * entity name for a saved favorite (which the backend only exposes as\n * targetType + targetId) and (b) seed the \"toggle demo favorite\" action with a\n * real target id.\n */\nexport const GGNOMAD_FAVORITE_TARGETS_QUERY = gql`\n query GGNomadFavoriteTargets {\n ggnomadProperties(pagination: { first: 100, skip: 0 }) {\n id\n name\n }\n ggnomadTours(pagination: { first: 100, skip: 0 }) {\n id\n name\n }\n ggnomadEvents(pagination: { first: 100, skip: 0 }) {\n id\n name\n }\n ggnomadActivities(pagination: { first: 100, skip: 0 }) {\n id\n name\n }\n }\n`;\n\nexport const GGNOMAD_PROVIDERS_QUERY = gql`\n query GGNomadProviders($first: Int, $skip: Int) {\n ggnomadProviders(pagination: { first: $first, skip: $skip }) {\n id\n name\n type\n status\n listingCount\n totalRevenue\n rating\n reviewCount\n contactEmail\n website\n }\n }\n`;\n\nexport const GGNOMAD_PROPERTIES_QUERY = gql`\n query GGNomadProperties($first: Int, $skip: Int) {\n ggnomadProperties(pagination: { first: $first, skip: $skip }) {\n id\n name\n type\n status\n description\n pricePerNight\n currency\n rating\n reviewCount\n maxGuests\n amenities\n images\n providerId\n }\n }\n`;\n\nexport const GGNOMAD_PROPERTY_QUERY = gql`\n query GGNomadProperty($id: ID!) {\n ggnomadProperty(id: $id) {\n id\n name\n type\n status\n description\n address\n pricePerNight\n currency\n rating\n reviewCount\n bedrooms\n bathrooms\n maxGuests\n checkInTime\n checkOutTime\n amenities\n images\n provider {\n id\n name\n type\n rating\n }\n }\n }\n`;\n\n// Query to get property details for booking - ensures id is included\nexport const GGNOMAD_PROPERTY_FOR_BOOKING_QUERY = gql`\n query GGNomadPropertyForBooking($id: ID!) {\n ggnomadProperty(id: $id) {\n id\n name\n pricePerNight\n currency\n }\n }\n`;\n\nexport const GGNOMAD_TOURS_QUERY = gql`\n query GGNomadTours($first: Int, $skip: Int) {\n ggnomadTours(pagination: { first: $first, skip: $skip }) {\n id\n name\n description\n type\n status\n duration\n difficulty\n price\n currency\n rating\n reviewCount\n highlights\n images\n }\n }\n`;\n\nexport const GGNOMAD_TOUR_QUERY = gql`\n query GGNomadTour($id: ID!) {\n ggnomadTour(id: $id) {\n id\n name\n description\n type\n status\n duration\n difficulty\n price\n currency\n rating\n reviewCount\n highlights\n images\n languages\n guideName\n provider {\n id\n name\n }\n }\n }\n`;\n\nexport const GGNOMAD_EVENTS_QUERY = gql`\n query GGNomadEvents($first: Int, $skip: Int) {\n ggnomadEvents(pagination: { first: $first, skip: $skip }) {\n id\n name\n description\n category\n status\n startsAt\n price\n currency\n rating\n reviewCount\n performers\n images\n }\n }\n`;\n\nexport const GGNOMAD_EVENT_QUERY = gql`\n query GGNomadEvent($id: ID!) {\n ggnomadEvent(id: $id) {\n id\n name\n description\n category\n status\n startsAt\n endsAt\n venue\n price\n currency\n rating\n reviewCount\n performers\n images\n provider {\n id\n name\n }\n }\n }\n`;\n\nexport const GGNOMAD_ACTIVITIES_QUERY = gql`\n query GGNomadActivities($first: Int, $skip: Int) {\n ggnomadActivities(pagination: { first: $first, skip: $skip }) {\n id\n name\n description\n category\n status\n duration\n difficulty\n price\n currency\n rating\n reviewCount\n maxParticipants\n includes\n requirements\n images\n }\n }\n`;\n\nexport const GGNOMAD_ACTIVITY_QUERY = gql`\n query GGNomadActivity($id: ID!) {\n ggnomadActivity(id: $id) {\n id\n name\n description\n category\n status\n location\n duration\n difficulty\n price\n currency\n rating\n reviewCount\n maxParticipants\n includes\n requirements\n safetyInfo\n images\n provider {\n id\n name\n }\n }\n }\n`;\n\nexport const GGNOMAD_TRIPS_QUERY = gql`\n query GGNomadTrips($first: Int, $skip: Int) {\n ggnomadTrips(pagination: { first: $first, skip: $skip }) {\n id\n name\n transportMode\n status\n origin\n destination\n departureAt\n arrivalAt\n isRoundTrip\n carrier\n class\n price\n currency\n passengers\n ecoFriendly\n }\n }\n`;\n\nexport const GGNOMAD_BOOKINGS_QUERY = gql`\n query GGNomadBookings($first: Int, $skip: Int) {\n ggnomadBookings(pagination: { first: $first, skip: $skip }) {\n id\n referenceCode\n bookingType\n status\n itemId\n itemName\n guestName\n guestEmail\n guestPhone\n guests\n checkIn\n checkOut\n eventDate\n totalPrice\n currency\n paymentStatus\n paymentMethod\n specialRequests\n createdAt\n }\n }\n`;\n\nexport const GGNOMAD_FAVORITES_QUERY = gql`\n query GGNomadFavorites {\n ggnomadFavorites\n }\n`;\n\nexport const CREATE_GGNOMAD_BOOKING_MUTATION = gql`\n mutation CreateGGNomadBooking($input: CreateGgnomadBookingInput!) {\n createGgnomadBooking(input: $input) {\n id\n referenceCode\n bookingType\n status\n itemId\n itemName\n guestName\n totalPrice\n currency\n createdAt\n }\n }\n`;\n\nexport const TOGGLE_GGNOMAD_FAVORITE_MUTATION = gql`\n mutation ToggleGGNomadFavorite($input: ToggleGgnomadFavoriteInput!) {\n toggleGgnomadFavorite(input: $input)\n }\n`;\n\nexport const CANCEL_GGNOMAD_BOOKING_MUTATION = gql`\n mutation CancelGGNomadBooking($id: ID!, $reason: String) {\n cancelGgnomadBooking(id: $id, reason: $reason) {\n id\n status\n }\n }\n`;\n\n// Trip Plan Queries\nexport const GGNOMAD_TRIP_PLANS_QUERY = gql`\n query GGNomadTripPlans($first: Int, $skip: Int) {\n ggnomadTripPlans(pagination: { first: $first, skip: $skip }) {\n id\n name\n description\n startDate\n endDate\n travelers\n status\n total\n itemCount\n createdAt\n }\n }\n`;\n\nexport const GGNOMAD_TRIP_PLAN_QUERY = gql`\n query GGNomadTripPlan($id: ID!) {\n ggnomadTripPlan(id: $id) {\n id\n name\n description\n startDate\n endDate\n travelers\n status\n total\n itemCount\n items {\n id\n itemType\n itemId\n itemName\n day\n scheduledAt\n price\n currency\n notes\n }\n createdAt\n updatedAt\n }\n }\n`;\n\n// Trip Plan Mutations\nexport const CREATE_GGNOMAD_TRIP_PLAN_MUTATION = gql`\n mutation CreateGGNomadTripPlan($input: CreateGgnomadTripPlanInput!) {\n createGgnomadTripPlan(input: $input) {\n id\n name\n description\n startDate\n endDate\n travelers\n status\n total\n itemCount\n createdAt\n }\n }\n`;\n\nexport const UPDATE_GGNOMAD_TRIP_PLAN_MUTATION = gql`\n mutation UpdateGGNomadTripPlan($id: ID!, $input: UpdateGgnomadTripPlanInput!) {\n updateGgnomadTripPlan(id: $id, input: $input) {\n id\n name\n description\n startDate\n endDate\n travelers\n status\n total\n itemCount\n updatedAt\n }\n }\n`;\n\nexport const DELETE_GGNOMAD_TRIP_PLAN_MUTATION = gql`\n mutation DeleteGGNomadTripPlan($id: ID!) {\n deleteGgnomadTripPlan(id: $id)\n }\n`;\n\nexport const ADD_GGNOMAD_TRIP_PLAN_ITEM_MUTATION = gql`\n mutation AddGGNomadTripPlanItem($tripPlanId: ID!, $input: AddGgnomadTripPlanItemInput!) {\n addGgnomadTripPlanItem(tripPlanId: $tripPlanId, input: $input) {\n id\n itemType\n itemId\n itemName\n day\n scheduledAt\n price\n currency\n notes\n }\n }\n`;\n\nexport const REMOVE_GGNOMAD_TRIP_PLAN_ITEM_MUTATION = gql`\n mutation RemoveGGNomadTripPlanItem($id: ID!) {\n removeGgnomadTripPlanItem(id: $id)\n }\n`;\n\n// Review Queries\nexport const GGNOMAD_REVIEW_SUMMARY_QUERY = gql`\n query GGNomadReviewSummary($targetType: GgnomadBookingType!, $targetId: String!) {\n ggnomadReviewSummary(targetType: $targetType, targetId: $targetId) {\n average\n count\n distribution\n }\n }\n`;\n\nexport const GGNOMAD_REVIEWS_QUERY = gql`\n query GGNomadReviews(\n $targetType: GgnomadBookingType!\n $targetId: String!\n $first: Int\n $skip: Int\n ) {\n ggnomadReviews(\n targetType: $targetType\n targetId: $targetId\n pagination: { first: $first, skip: $skip }\n ) {\n id\n rating\n title\n content\n images\n isVerified\n helpfulCount\n response\n respondedAt\n createdAt\n }\n }\n`;\n\n// Review Mutations\nexport const CREATE_GGNOMAD_REVIEW_MUTATION = gql`\n mutation CreateGGNomadReview($input: CreateGgnomadReviewInput!) {\n createGgnomadReview(input: $input) {\n id\n rating\n title\n content\n }\n }\n`;\n\nexport const RESPOND_GGNOMAD_REVIEW_MUTATION = gql`\n mutation RespondGGNomadReview($id: ID!, $response: String!) {\n respondGgnomadReview(id: $id, response: $response) {\n id\n response\n respondedAt\n }\n }\n`;\n\n// Listing Edit Mutations\nexport const UPDATE_GGNOMAD_PROPERTY_MUTATION = gql`\n mutation UpdateGGNomadProperty($id: ID!, $input: UpdateGgnomadPropertyInput!) {\n updateGgnomadProperty(id: $id, input: $input) {\n id\n name\n description\n pricePerNight\n status\n images\n }\n }\n`;\n\nexport const UPDATE_GGNOMAD_TOUR_MUTATION = gql`\n mutation UpdateGGNomadTour($id: ID!, $input: UpdateGgnomadTourInput!) {\n updateGgnomadTour(id: $id, input: $input) {\n id\n name\n description\n price\n status\n images\n }\n }\n`;\n\nexport const UPDATE_GGNOMAD_EVENT_MUTATION = gql`\n mutation UpdateGGNomadEvent($id: ID!, $input: UpdateGgnomadEventInput!) {\n updateGgnomadEvent(id: $id, input: $input) {\n id\n name\n description\n price\n status\n images\n }\n }\n`;\n\nexport const UPDATE_GGNOMAD_ACTIVITY_MUTATION = gql`\n mutation UpdateGGNomadActivity($id: ID!, $input: UpdateGgnomadActivityInput!) {\n updateGgnomadActivity(id: $id, input: $input) {\n id\n name\n description\n price\n status\n images\n }\n }\n`;\n"],"mappings":";;AAEA,IAAa,IAAyB,CAAG;;;;;;;;;;;;;;;;GAwB5B,IAAgC,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCnC,IAAiC,CAAG;;;;;;;;;;;;;;;;;;;GAqBpC,IAA0B,CAAG;;;;;;;;;;;;;;;GAiB7B,IAA2B,CAAG;;;;;;;;;;;;;;;;;;GAoB9B,IAAyB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BS,CAAG;;;;;;;;;;AAWrD,IAAa,IAAsB,CAAG;;;;;;;;;;;;;;;;;;GAoBzB,IAAqB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;GA0BxB,IAAuB,CAAG;;;;;;;;;;;;;;;;;GAmB1B,IAAsB,CAAG;;;;;;;;;;;;;;;;;;;;;;;GAyBzB,IAA2B,CAAG;;;;;;;;;;;;;;;;;;;;GAsB9B,IAAyB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;GA4B5B,IAAsB,CAAG;;;;;;;;;;;;;;;;;;;;GAsBzB,IAAyB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;GA0B5B,IAA0B,CAAG;;;;GAM7B,IAAkC,CAAG;;;;;;;;;;;;;;;GAiBrC,IAAmC,CAAG;;;;GAMtC,IAAkC,CAAG;;;;;;;GAUrC,IAA2B,CAAG;;;;;;;;;;;;;;;GAiB9B,IAA0B,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8B7B,IAAoC,CAAG;;;;;;;;;;;;;;;GAiBvC,IAAoC,CAAG;;;;;;;;;;;;;;;;AAiBH,CAAG;;;;;AAMpD,IAAa,IAAsC,CAAG;;;;;;;;;;;;;;GAgBzC,IAAyC,CAAG;;;;GAO5C,IAA+B,CAAG;;;;;;;;GAUlC,IAAwB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;GA2B3B,IAAiC,CAAG;;;;;;;;;;AAWF,CAAG;;;;;;;;;AAWlD,IAAa,IAAmC,CAAG;;;;;;;;;;;GAatC,IAA+B,CAAG;;;;;;;;;;;GAalC,IAAgC,CAAG;;;;;;;;;;;GAanC,IAAmC,CAAG"}
|
|
1
|
+
{"version":3,"file":"graphql.js","names":[],"sources":["../../src/ggnomad/graphql.ts"],"sourcesContent":["import { gql } from '@apollo/client/core';\n\nexport const GGNOMAD_OVERVIEW_QUERY = gql`\n query GGNomadOverview {\n ggnomadOverviewStats {\n users\n listings\n bookings\n monthlyRevenue\n }\n ggnomadAnalytics {\n totalRevenue\n totalBookings\n totalUsers\n conversionRate\n revenueByCategory\n }\n }\n`;\n\n/**\n * One bookable item of each type (properties are priced per night, the rest\n * carry a flat price). Used to seed the Bookings \"create demo booking\" action\n * with a REAL listing id so the mutation passes the backend's\n * assertTargetInWorkspace check instead of failing on a synthetic id.\n */\nexport const GGNOMAD_BOOKABLE_SAMPLE_QUERY = gql`\n query GGNomadBookableSample {\n ggnomadProperties(pagination: { limit: 1, offset: 0 }) {\n id\n name\n pricePerNight\n currency\n }\n ggnomadTours(pagination: { limit: 1, offset: 0 }) {\n id\n name\n price\n currency\n }\n ggnomadEvents(pagination: { limit: 1, offset: 0 }) {\n id\n name\n price\n currency\n }\n ggnomadActivities(pagination: { limit: 1, offset: 0 }) {\n id\n name\n price\n currency\n }\n }\n`;\n\n/**\n * id + name for every favouritable listing type, used to (a) resolve the real\n * entity name for a saved favorite (which the backend only exposes as\n * targetType + targetId) and (b) seed the \"toggle demo favorite\" action with a\n * real target id.\n */\nexport const GGNOMAD_FAVORITE_TARGETS_QUERY = gql`\n query GGNomadFavoriteTargets {\n ggnomadProperties(pagination: { limit: 100, offset: 0 }) {\n id\n name\n }\n ggnomadTours(pagination: { limit: 100, offset: 0 }) {\n id\n name\n }\n ggnomadEvents(pagination: { limit: 100, offset: 0 }) {\n id\n name\n }\n ggnomadActivities(pagination: { limit: 100, offset: 0 }) {\n id\n name\n }\n }\n`;\n\nexport const GGNOMAD_PROVIDERS_QUERY = gql`\n query GGNomadProviders($limit: Int, $offset: Int) {\n ggnomadProviders(pagination: { limit: $limit, offset: $offset }) {\n id\n name\n type\n status\n listingCount\n totalRevenue\n rating\n reviewCount\n contactEmail\n website\n }\n }\n`;\n\nexport const GGNOMAD_PROPERTIES_QUERY = gql`\n query GGNomadProperties($limit: Int, $offset: Int) {\n ggnomadProperties(pagination: { limit: $limit, offset: $offset }) {\n id\n name\n type\n status\n description\n pricePerNight\n currency\n rating\n reviewCount\n maxGuests\n amenities\n images\n providerId\n }\n }\n`;\n\nexport const GGNOMAD_PROPERTY_QUERY = gql`\n query GGNomadProperty($id: ID!) {\n ggnomadProperty(id: $id) {\n id\n name\n type\n status\n description\n address\n pricePerNight\n currency\n rating\n reviewCount\n bedrooms\n bathrooms\n maxGuests\n checkInTime\n checkOutTime\n amenities\n images\n provider {\n id\n name\n type\n rating\n }\n }\n }\n`;\n\n// Query to get property details for booking - ensures id is included\nexport const GGNOMAD_PROPERTY_FOR_BOOKING_QUERY = gql`\n query GGNomadPropertyForBooking($id: ID!) {\n ggnomadProperty(id: $id) {\n id\n name\n pricePerNight\n currency\n }\n }\n`;\n\nexport const GGNOMAD_TOURS_QUERY = gql`\n query GGNomadTours($limit: Int, $offset: Int) {\n ggnomadTours(pagination: { limit: $limit, offset: $offset }) {\n id\n name\n description\n type\n status\n duration\n difficulty\n price\n currency\n rating\n reviewCount\n highlights\n images\n }\n }\n`;\n\nexport const GGNOMAD_TOUR_QUERY = gql`\n query GGNomadTour($id: ID!) {\n ggnomadTour(id: $id) {\n id\n name\n description\n type\n status\n duration\n difficulty\n price\n currency\n rating\n reviewCount\n highlights\n images\n languages\n guideName\n provider {\n id\n name\n }\n }\n }\n`;\n\nexport const GGNOMAD_EVENTS_QUERY = gql`\n query GGNomadEvents($limit: Int, $offset: Int) {\n ggnomadEvents(pagination: { limit: $limit, offset: $offset }) {\n id\n name\n description\n category\n status\n startsAt\n price\n currency\n rating\n reviewCount\n performers\n images\n }\n }\n`;\n\nexport const GGNOMAD_EVENT_QUERY = gql`\n query GGNomadEvent($id: ID!) {\n ggnomadEvent(id: $id) {\n id\n name\n description\n category\n status\n startsAt\n endsAt\n venue\n price\n currency\n rating\n reviewCount\n performers\n images\n provider {\n id\n name\n }\n }\n }\n`;\n\nexport const GGNOMAD_ACTIVITIES_QUERY = gql`\n query GGNomadActivities($limit: Int, $offset: Int) {\n ggnomadActivities(pagination: { limit: $limit, offset: $offset }) {\n id\n name\n description\n category\n status\n duration\n difficulty\n price\n currency\n rating\n reviewCount\n maxParticipants\n includes\n requirements\n images\n }\n }\n`;\n\nexport const GGNOMAD_ACTIVITY_QUERY = gql`\n query GGNomadActivity($id: ID!) {\n ggnomadActivity(id: $id) {\n id\n name\n description\n category\n status\n location\n duration\n difficulty\n price\n currency\n rating\n reviewCount\n maxParticipants\n includes\n requirements\n safetyInfo\n images\n provider {\n id\n name\n }\n }\n }\n`;\n\nexport const GGNOMAD_TRIPS_QUERY = gql`\n query GGNomadTrips($limit: Int, $offset: Int) {\n ggnomadTrips(pagination: { limit: $limit, offset: $offset }) {\n id\n name\n transportMode\n status\n origin\n destination\n departureAt\n arrivalAt\n isRoundTrip\n carrier\n class\n price\n currency\n passengers\n ecoFriendly\n }\n }\n`;\n\nexport const GGNOMAD_BOOKINGS_QUERY = gql`\n query GGNomadBookings($limit: Int, $offset: Int) {\n ggnomadBookings(pagination: { limit: $limit, offset: $offset }) {\n id\n referenceCode\n bookingType\n status\n itemId\n itemName\n guestName\n guestEmail\n guestPhone\n guests\n checkIn\n checkOut\n eventDate\n totalPrice\n currency\n paymentStatus\n paymentMethod\n specialRequests\n createdAt\n }\n }\n`;\n\nexport const GGNOMAD_FAVORITES_QUERY = gql`\n query GGNomadFavorites {\n ggnomadFavorites\n }\n`;\n\nexport const CREATE_GGNOMAD_BOOKING_MUTATION = gql`\n mutation CreateGGNomadBooking($input: CreateGgnomadBookingInput!) {\n createGgnomadBooking(input: $input) {\n id\n referenceCode\n bookingType\n status\n itemId\n itemName\n guestName\n totalPrice\n currency\n createdAt\n }\n }\n`;\n\nexport const TOGGLE_GGNOMAD_FAVORITE_MUTATION = gql`\n mutation ToggleGGNomadFavorite($input: ToggleGgnomadFavoriteInput!) {\n toggleGgnomadFavorite(input: $input)\n }\n`;\n\nexport const CANCEL_GGNOMAD_BOOKING_MUTATION = gql`\n mutation CancelGGNomadBooking($id: ID!, $reason: String) {\n cancelGgnomadBooking(id: $id, reason: $reason) {\n id\n status\n }\n }\n`;\n\n// Trip Plan Queries\nexport const GGNOMAD_TRIP_PLANS_QUERY = gql`\n query GGNomadTripPlans($limit: Int, $offset: Int) {\n ggnomadTripPlans(pagination: { limit: $limit, offset: $offset }) {\n id\n name\n description\n startDate\n endDate\n travelers\n status\n total\n itemCount\n createdAt\n }\n }\n`;\n\nexport const GGNOMAD_TRIP_PLAN_QUERY = gql`\n query GGNomadTripPlan($id: ID!) {\n ggnomadTripPlan(id: $id) {\n id\n name\n description\n startDate\n endDate\n travelers\n status\n total\n itemCount\n items {\n id\n itemType\n itemId\n itemName\n day\n scheduledAt\n price\n currency\n notes\n }\n createdAt\n updatedAt\n }\n }\n`;\n\n// Trip Plan Mutations\nexport const CREATE_GGNOMAD_TRIP_PLAN_MUTATION = gql`\n mutation CreateGGNomadTripPlan($input: CreateGgnomadTripPlanInput!) {\n createGgnomadTripPlan(input: $input) {\n id\n name\n description\n startDate\n endDate\n travelers\n status\n total\n itemCount\n createdAt\n }\n }\n`;\n\nexport const UPDATE_GGNOMAD_TRIP_PLAN_MUTATION = gql`\n mutation UpdateGGNomadTripPlan($id: ID!, $input: UpdateGgnomadTripPlanInput!) {\n updateGgnomadTripPlan(id: $id, input: $input) {\n id\n name\n description\n startDate\n endDate\n travelers\n status\n total\n itemCount\n updatedAt\n }\n }\n`;\n\nexport const DELETE_GGNOMAD_TRIP_PLAN_MUTATION = gql`\n mutation DeleteGGNomadTripPlan($id: ID!) {\n deleteGgnomadTripPlan(id: $id)\n }\n`;\n\nexport const ADD_GGNOMAD_TRIP_PLAN_ITEM_MUTATION = gql`\n mutation AddGGNomadTripPlanItem($tripPlanId: ID!, $input: AddGgnomadTripPlanItemInput!) {\n addGgnomadTripPlanItem(tripPlanId: $tripPlanId, input: $input) {\n id\n itemType\n itemId\n itemName\n day\n scheduledAt\n price\n currency\n notes\n }\n }\n`;\n\nexport const REMOVE_GGNOMAD_TRIP_PLAN_ITEM_MUTATION = gql`\n mutation RemoveGGNomadTripPlanItem($id: ID!) {\n removeGgnomadTripPlanItem(id: $id)\n }\n`;\n\n// Review Queries\nexport const GGNOMAD_REVIEW_SUMMARY_QUERY = gql`\n query GGNomadReviewSummary($targetType: GgnomadBookingType!, $targetId: String!) {\n ggnomadReviewSummary(targetType: $targetType, targetId: $targetId) {\n average\n count\n distribution\n }\n }\n`;\n\nexport const GGNOMAD_REVIEWS_QUERY = gql`\n query GGNomadReviews(\n $targetType: GgnomadBookingType!\n $targetId: String!\n $limit: Int\n $offset: Int\n ) {\n ggnomadReviews(\n targetType: $targetType\n targetId: $targetId\n pagination: { limit: $limit, offset: $offset }\n ) {\n id\n rating\n title\n content\n images\n isVerified\n helpfulCount\n response\n respondedAt\n createdAt\n }\n }\n`;\n\n// Review Mutations\nexport const CREATE_GGNOMAD_REVIEW_MUTATION = gql`\n mutation CreateGGNomadReview($input: CreateGgnomadReviewInput!) {\n createGgnomadReview(input: $input) {\n id\n rating\n title\n content\n }\n }\n`;\n\nexport const RESPOND_GGNOMAD_REVIEW_MUTATION = gql`\n mutation RespondGGNomadReview($id: ID!, $response: String!) {\n respondGgnomadReview(id: $id, response: $response) {\n id\n response\n respondedAt\n }\n }\n`;\n\n// Listing Edit Mutations\nexport const UPDATE_GGNOMAD_PROPERTY_MUTATION = gql`\n mutation UpdateGGNomadProperty($id: ID!, $input: UpdateGgnomadPropertyInput!) {\n updateGgnomadProperty(id: $id, input: $input) {\n id\n name\n description\n pricePerNight\n status\n images\n }\n }\n`;\n\nexport const UPDATE_GGNOMAD_TOUR_MUTATION = gql`\n mutation UpdateGGNomadTour($id: ID!, $input: UpdateGgnomadTourInput!) {\n updateGgnomadTour(id: $id, input: $input) {\n id\n name\n description\n price\n status\n images\n }\n }\n`;\n\nexport const UPDATE_GGNOMAD_EVENT_MUTATION = gql`\n mutation UpdateGGNomadEvent($id: ID!, $input: UpdateGgnomadEventInput!) {\n updateGgnomadEvent(id: $id, input: $input) {\n id\n name\n description\n price\n status\n images\n }\n }\n`;\n\nexport const UPDATE_GGNOMAD_ACTIVITY_MUTATION = gql`\n mutation UpdateGGNomadActivity($id: ID!, $input: UpdateGgnomadActivityInput!) {\n updateGgnomadActivity(id: $id, input: $input) {\n id\n name\n description\n price\n status\n images\n }\n }\n`;\n"],"mappings":";;AAEA,IAAa,IAAyB,CAAG;;;;;;;;;;;;;;;;GAwB5B,IAAgC,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCnC,IAAiC,CAAG;;;;;;;;;;;;;;;;;;;GAqBpC,IAA0B,CAAG;;;;;;;;;;;;;;;GAiB7B,IAA2B,CAAG;;;;;;;;;;;;;;;;;;GAoB9B,IAAyB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BS,CAAG;;;;;;;;;;AAWrD,IAAa,IAAsB,CAAG;;;;;;;;;;;;;;;;;;GAoBzB,IAAqB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;GA0BxB,IAAuB,CAAG;;;;;;;;;;;;;;;;;GAmB1B,IAAsB,CAAG;;;;;;;;;;;;;;;;;;;;;;;GAyBzB,IAA2B,CAAG;;;;;;;;;;;;;;;;;;;;GAsB9B,IAAyB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;GA4B5B,IAAsB,CAAG;;;;;;;;;;;;;;;;;;;;GAsBzB,IAAyB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;GA0B5B,IAA0B,CAAG;;;;GAM7B,IAAkC,CAAG;;;;;;;;;;;;;;;GAiBrC,IAAmC,CAAG;;;;GAMtC,IAAkC,CAAG;;;;;;;GAUrC,IAA2B,CAAG;;;;;;;;;;;;;;;GAiB9B,IAA0B,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8B7B,IAAoC,CAAG;;;;;;;;;;;;;;;GAiBvC,IAAoC,CAAG;;;;;;;;;;;;;;;;AAiBH,CAAG;;;;;AAMpD,IAAa,IAAsC,CAAG;;;;;;;;;;;;;;GAgBzC,IAAyC,CAAG;;;;GAO5C,IAA+B,CAAG;;;;;;;;GAUlC,IAAwB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;GA2B3B,IAAiC,CAAG;;;;;;;;;;AAWF,CAAG;;;;;;;;;AAWlD,IAAa,IAAmC,CAAG;;;;;;;;;;;GAatC,IAA+B,CAAG;;;;;;;;;;;GAalC,IAAgC,CAAG;;;;;;;;;;;GAanC,IAAmC,CAAG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActivityDetailsPage.js","names":[],"sources":["../../../src/ggnomad/pages/ActivityDetailsPage.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 { useNavigate, useParams } from 'react-router-dom';\nimport { Activity, Compass, Pencil, ShieldCheck } from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_ACTIVITY_QUERY } from '../graphql';\nimport { useGGNomad } from '../providers/GGNomadProvider';\nimport { joinPath } from '../utils/navigation';\nimport { PageShell } from './PageShell';\nimport {\n DetailRow,\n SectionCard,\n DetailHero,\n BookingPanel,\n ReviewsSection,\n ListingEditDialog,\n} from '../components';\n\nexport function ActivityDetailsPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const { basePath = '/' } = useGGNomad();\n const { id = '' } = useParams();\n const [editDialogOpen, setEditDialogOpen] = useState(false);\n const { data, refetch } = useQuery(\n GGNOMAD_ACTIVITY_QUERY as TypedDocumentNode<\n {\n ggnomadActivity: {\n id: string;\n name: string;\n description?: string;\n category: string;\n duration: string;\n maxParticipants: number;\n currency: string;\n price: number;\n status?: string;\n safetyInfo?: string;\n images?: string[];\n } | null;\n },\n { id: string }\n >,\n { variables: { id }, skip: !id }\n );\n const activity = data?.ggnomadActivity;\n\n return (\n <PageShell\n title={activity?.name ?? t('ggnomad.activity.fallbackTitle', 'Activity details')}\n subtitle={t('ggnomad.activity.subtitle', 'Safety, inclusions and participation details')}\n >\n {!activity ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.activity.notFoundTitle', 'No activity found')}\n description={t(\n 'ggnomad.activity.notFoundDescription',\n 'This activity is unavailable or may have been removed.'\n )}\n />\n ) : (\n <>\n <DetailHero image={activity.images?.[0]} alt={activity.name} />\n <PagePurpose icon={<Activity className=\"size-4\" />}>\n {t(\n 'ggnomad.activity.purpose',\n 'Everything you need to know before joining this activity — its category, duration, group size, price and the safety policy that applies — so you can be confident it fits your group before you book.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey=\"ggnomad-activity-detail\"\n steps={[\n {\n id: 'browse',\n label: t('ggnomad.activity.next.browse', 'Explore more experiences'),\n description: t(\n 'ggnomad.activity.next.browseDesc',\n 'Return to Tourism to compare tours and activities.'\n ),\n icon: <Compass className=\"size-4\" />,\n onClick: () => navigate(joinPath(basePath, 'tourism')),\n },\n ]}\n />\n\n <div className=\"grid gap-4 lg:grid-cols-3\">\n <div className=\"lg:col-span-2 space-y-4\">\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <div className=\"mb-4 flex items-center justify-between gap-3\">\n <h2 className=\"text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.activity.details', 'Details')}\n </h2>\n <button\n onClick={() => setEditDialogOpen(true)}\n className=\"rounded-md p-2 text-text-secondary hover:bg-action-primary-bg hover:text-action-primary-text\"\n aria-label={t('ggnomad.activity.editListing', 'Edit listing')}\n title={t('ggnomad.activity.editListing', 'Edit listing')}\n >\n <Pencil className=\"size-4\" />\n </button>\n </div>\n <dl className=\"space-y-2 text-sm\">\n <DetailRow\n label={t('ggnomad.activity.category', 'Category')}\n value={activity.category}\n />\n <DetailRow\n label={t('ggnomad.activity.duration', 'Duration')}\n value={activity.duration}\n />\n <DetailRow\n label={t('ggnomad.activity.maxParticipants', 'Max participants')}\n value={String(activity.maxParticipants)}\n />\n <DetailRow\n label={t('ggnomad.activity.price', 'Price')}\n value={`${activity.currency} ${Number(activity.price).toFixed(2)}`}\n />\n </dl>\n </GlassCard>\n <SectionCard title={t('ggnomad.activity.safety', 'Safety information')}>\n <div className=\"flex items-start gap-2 text-sm text-text-primary\">\n <ShieldCheck\n className=\"mt-0.5 size-4 flex-shrink-0 text-status-success-text\"\n aria-hidden=\"true\"\n />\n <p>\n {activity.safetyInfo ??\n t(\n 'ggnomad.activity.defaultSafety',\n 'Standard provider safety policy applies.'\n )}\n </p>\n </div>\n </SectionCard>\n <ReviewsSection targetType=\"ACTIVITY\" targetId={activity.id ?? id} />\n </div>\n <div>\n <BookingPanel\n bookingType=\"ACTIVITY\"\n itemId={activity.id ?? id}\n itemName={activity.name}\n unitPrice={activity.price ?? 0}\n currency={activity.currency ?? 'USD'}\n />\n </div>\n </div>\n\n <ListingEditDialog\n open={editDialogOpen}\n onOpenChange={setEditDialogOpen}\n targetType=\"ACTIVITY\"\n id={activity.id ?? id}\n initial={{\n name: activity.name,\n description: activity.description,\n price: activity.price,\n status: activity.status,\n images: activity.images,\n }}\n onSaved={() => refetch()}\n />\n </>\n )}\n </PageShell>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,IAAsB;CACpC,IAAM,EAAE,SAAM,
|
|
1
|
+
{"version":3,"file":"ActivityDetailsPage.js","names":[],"sources":["../../../src/ggnomad/pages/ActivityDetailsPage.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 { useNavigate, useParams } from 'react-router-dom';\nimport { Activity, Compass, Pencil, ShieldCheck } from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_ACTIVITY_QUERY } from '../graphql';\nimport { useGGNomad } from '../providers/GGNomadProvider';\nimport { joinPath } from '../utils/navigation';\nimport { PageShell } from './PageShell';\nimport {\n DetailRow,\n SectionCard,\n DetailHero,\n BookingPanel,\n ReviewsSection,\n ListingEditDialog,\n} from '../components';\n\nexport function ActivityDetailsPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const { basePath = '/' } = useGGNomad();\n const { id = '' } = useParams();\n const [editDialogOpen, setEditDialogOpen] = useState(false);\n const { data, refetch } = useQuery(\n GGNOMAD_ACTIVITY_QUERY as TypedDocumentNode<\n {\n ggnomadActivity: {\n id: string;\n name: string;\n description?: string;\n category: string;\n duration: string;\n maxParticipants: number;\n currency: string;\n price: number;\n status?: string;\n safetyInfo?: string;\n images?: string[];\n } | null;\n },\n { id: string }\n >,\n { variables: { id }, skip: !id }\n );\n const activity = data?.ggnomadActivity;\n\n return (\n <PageShell\n title={activity?.name ?? t('ggnomad.activity.fallbackTitle', 'Activity details')}\n subtitle={t('ggnomad.activity.subtitle', 'Safety, inclusions and participation details')}\n >\n {!activity ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.activity.notFoundTitle', 'No activity found')}\n description={t(\n 'ggnomad.activity.notFoundDescription',\n 'This activity is unavailable or may have been removed.'\n )}\n />\n ) : (\n <>\n <DetailHero image={activity.images?.[0]} alt={activity.name} />\n <PagePurpose icon={<Activity className=\"size-4\" />}>\n {t(\n 'ggnomad.activity.purpose',\n 'Everything you need to know before joining this activity — its category, duration, group size, price and the safety policy that applies — so you can be confident it fits your group before you book.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey=\"ggnomad-activity-detail\"\n steps={[\n {\n id: 'browse',\n label: t('ggnomad.activity.next.browse', 'Explore more experiences'),\n description: t(\n 'ggnomad.activity.next.browseDesc',\n 'Return to Tourism to compare tours and activities.'\n ),\n icon: <Compass className=\"size-4\" />,\n onClick: () => navigate(joinPath(basePath, 'tourism')),\n },\n ]}\n />\n\n <div className=\"grid gap-4 lg:grid-cols-3\">\n <div className=\"lg:col-span-2 space-y-4\">\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <div className=\"mb-4 flex items-center justify-between gap-3\">\n <h2 className=\"text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.activity.details', 'Details')}\n </h2>\n <button\n onClick={() => setEditDialogOpen(true)}\n className=\"rounded-md p-2 text-text-secondary hover:bg-action-primary-bg hover:text-action-primary-text\"\n aria-label={t('ggnomad.activity.editListing', 'Edit listing')}\n title={t('ggnomad.activity.editListing', 'Edit listing')}\n >\n <Pencil className=\"size-4\" />\n </button>\n </div>\n <dl className=\"space-y-2 text-sm\">\n <DetailRow\n label={t('ggnomad.activity.category', 'Category')}\n value={activity.category}\n />\n <DetailRow\n label={t('ggnomad.activity.duration', 'Duration')}\n value={activity.duration}\n />\n <DetailRow\n label={t('ggnomad.activity.maxParticipants', 'Max participants')}\n value={String(activity.maxParticipants)}\n />\n <DetailRow\n label={t('ggnomad.activity.price', 'Price')}\n value={`${activity.currency} ${Number(activity.price).toFixed(2)}`}\n />\n </dl>\n </GlassCard>\n <SectionCard title={t('ggnomad.activity.safety', 'Safety information')}>\n <div className=\"flex items-start gap-2 text-sm text-text-primary\">\n <ShieldCheck\n className=\"mt-0.5 size-4 flex-shrink-0 text-status-success-text\"\n aria-hidden=\"true\"\n />\n <p>\n {activity.safetyInfo ??\n t(\n 'ggnomad.activity.defaultSafety',\n 'Standard provider safety policy applies.'\n )}\n </p>\n </div>\n </SectionCard>\n <ReviewsSection targetType=\"ACTIVITY\" targetId={activity.id ?? id} />\n </div>\n <div>\n <BookingPanel\n bookingType=\"ACTIVITY\"\n itemId={activity.id ?? id}\n itemName={activity.name}\n unitPrice={activity.price ?? 0}\n currency={activity.currency ?? 'USD'}\n />\n </div>\n </div>\n\n <ListingEditDialog\n open={editDialogOpen}\n onOpenChange={setEditDialogOpen}\n targetType=\"ACTIVITY\"\n id={activity.id ?? id}\n initial={{\n name: activity.name,\n description: activity.description,\n price: activity.price,\n status: activity.status,\n images: activity.images,\n }}\n onSaved={() => refetch()}\n />\n </>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../src/ggnomad/pages/AnalyticsPage.tsx"],"sourcesContent":["import { useMemo } 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 { BarChart3, CalendarCheck, DollarSign, Percent, Users } from 'lucide-react';\nimport { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_OVERVIEW_QUERY } from '../graphql';\nimport { PageShell } from './PageShell';\nimport { SectionCard, StatCard } from '../components';\n\nconst CHART_COLORS = [\n 'var(--color-chart-1)',\n 'var(--color-chart-2)',\n 'var(--color-chart-3)',\n 'var(--color-chart-4)',\n 'var(--color-chart-5)',\n];\n\nconst humanizeCategory = (key: string): string =>\n key\n .replace(/[_-]+/g, ' ')\n .trim()\n .replace(/\\b\\w/g, (char) => char.toUpperCase()) || key;\n\nexport function AnalyticsPage() {\n const { t } = useI18n();\n const { data } = useQuery(\n GGNOMAD_OVERVIEW_QUERY as TypedDocumentNode<\n {\n ggnomadAnalytics: {\n totalRevenue: number;\n totalBookings: number;\n totalUsers: number;\n conversionRate: number;\n revenueByCategory: Record<string, unknown>;\n monthlyTrends?: Array<{ month: string; revenue: number }> | Record<string, number>;\n };\n },\n Record<string, never>\n >\n );\n const analytics = data?.ggnomadAnalytics;\n\n const categories = useMemo(() => {\n const raw = analytics?.revenueByCategory ?? {};\n return Object.entries(raw)\n .map(([key, value]) => ({ label: humanizeCategory(key), value: Number(value) }))\n .filter((entry) => Number.isFinite(entry.value) && entry.value > 0)\n .sort((a, b) => b.value - a.value);\n }, [analytics?.revenueByCategory]);\n\n const maxCategory = categories.length > 0 ? categories[0].value : 0;\n\n const trends = useMemo(() => {\n const raw = analytics?.monthlyTrends;\n if (!raw) return [];\n if (Array.isArray(raw)) {\n return raw\n .map((entry) => ({\n label: entry.month || '',\n value: Number(entry.revenue || 0),\n }))\n .filter((entry) => Number.isFinite(entry.value) && entry.label);\n }\n return Object.entries(raw)\n .map(([key, value]) => ({\n label: key,\n value: Number(value),\n }))\n .filter((entry) => Number.isFinite(entry.value));\n }, [analytics?.monthlyTrends]);\n\n const maxTrend = trends.length > 0 ? Math.max(...trends.map((t) => t.value)) : 0;\n\n return (\n <PageShell\n title={t('ggnomad.analytics.title', 'Analytics')}\n titleAccent={t('ggnomad.analytics.title.accent', 'Analytics')}\n subtitle={t('ggnomad.analytics.subtitle', 'GGNomad performance and growth analytics')}\n >\n <PagePurpose icon={<BarChart3 className=\"size-4\" />}>\n {t(\n 'ggnomad.analytics.purpose',\n 'Understand how the marketplace is performing — total revenue, bookings, active users and conversion rate at a glance, with a category breakdown to see which domains (stays, tours, events) are driving growth.'\n )}\n </PagePurpose>\n\n <EmphasisPanel>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.analytics.totalRevenue', 'Total revenue')}\n </p>\n <p className=\"mt-1 text-3xl font-semibold tabular-nums text-text-primary\">\n ${Number(analytics?.totalRevenue ?? 0).toLocaleString()}\n </p>\n </EmphasisPanel>\n\n <section className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n hero\n label={t('ggnomad.analytics.conversion', 'Conversion')}\n value={`${Number(analytics?.conversionRate ?? 0).toFixed(2)}%`}\n icon={<Percent />}\n />\n <StatCard\n label={t('ggnomad.analytics.revenue', 'Revenue')}\n value={`$${Number(analytics?.totalRevenue ?? 0).toLocaleString()}`}\n icon={<DollarSign />}\n />\n <StatCard\n label={t('ggnomad.analytics.bookings', 'Bookings')}\n value={Number(analytics?.totalBookings ?? 0).toLocaleString()}\n icon={<CalendarCheck />}\n />\n <StatCard\n label={t('ggnomad.analytics.users', 'Users')}\n value={Number(analytics?.totalUsers ?? 0).toLocaleString()}\n icon={<Users />}\n />\n </section>\n\n <SectionCard title={t('ggnomad.analytics.revenueByCategory', 'Revenue by category')}>\n {categories.length === 0 ? (\n <div className=\"flex flex-col items-center gap-2 py-8 text-center\">\n <span className=\"flex size-10 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-text-secondary\">\n <BarChart3 className=\"size-5\" />\n </span>\n <p className=\"text-sm font-medium text-text-primary\">\n {t('ggnomad.analytics.empty.title', 'No analytics yet')}\n </p>\n <p className=\"max-w-sm text-sm text-text-secondary\">\n {t(\n 'ggnomad.analytics.empty.description',\n 'Revenue by category appears here as bookings come in across stays, tours and events.'\n )}\n </p>\n </div>\n ) : (\n <ul className=\"flex flex-col gap-4\">\n {categories.map((category, index) => {\n const pct = maxCategory > 0 ? Math.max((category.value / maxCategory) * 100, 2) : 0;\n return (\n <li key={category.label}>\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"truncate text-sm font-medium text-text-primary\">\n {category.label}\n </span>\n <span className=\"flex-shrink-0 text-sm tabular-nums text-text-secondary\">\n ${category.value.toLocaleString()}\n </span>\n </div>\n <div className=\"mt-1.5 h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full transition-[width] duration-500 ease-out\"\n style={{\n width: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n />\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </SectionCard>\n\n {trends.length > 0 && (\n <SectionCard title={t('ggnomad.analytics.revenueTrend', 'Revenue trend')}>\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex items-end gap-2 overflow-x-auto pb-2\">\n {trends.map((trend, index) => {\n const pct = maxTrend > 0 ? Math.max((trend.value / maxTrend) * 100, 5) : 5;\n return (\n <div key={trend.label} className=\"flex flex-col items-center gap-1\">\n <div className=\"flex h-24 items-end\">\n <div\n className=\"w-8 rounded-t transition-all duration-500 ease-out\"\n style={{\n height: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n title={`${trend.label}: $${trend.value.toLocaleString()}`}\n />\n </div>\n <p className=\"truncate text-xs text-text-secondary\">{trend.label}</p>\n </div>\n );\n })}\n </div>\n <p className=\"text-xs text-text-secondary\">\n {t(\n 'ggnomad.analytics.trendNote',\n 'Revenue trends over time showing performance across all domains.'\n )}\n </p>\n </div>\n </SectionCard>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../src/ggnomad/pages/AnalyticsPage.tsx"],"sourcesContent":["import { useMemo } 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 { BarChart3, CalendarCheck, DollarSign, Percent, Users } from 'lucide-react';\nimport { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_OVERVIEW_QUERY } from '../graphql';\nimport { PageShell } from './PageShell';\nimport { SectionCard, StatCard } from '../components';\n\nconst CHART_COLORS = [\n 'var(--color-chart-1)',\n 'var(--color-chart-2)',\n 'var(--color-chart-3)',\n 'var(--color-chart-4)',\n 'var(--color-chart-5)',\n];\n\nconst humanizeCategory = (key: string): string =>\n key\n .replace(/[_-]+/g, ' ')\n .trim()\n .replace(/\\b\\w/g, (char) => char.toUpperCase()) || key;\n\nexport function AnalyticsPage() {\n const { t } = useI18n();\n const { data } = useQuery(\n GGNOMAD_OVERVIEW_QUERY as TypedDocumentNode<\n {\n ggnomadAnalytics: {\n totalRevenue: number;\n totalBookings: number;\n totalUsers: number;\n conversionRate: number;\n revenueByCategory: Record<string, unknown>;\n monthlyTrends?: Array<{ month: string; revenue: number }> | Record<string, number>;\n };\n },\n Record<string, never>\n >\n );\n const analytics = data?.ggnomadAnalytics;\n\n const categories = useMemo(() => {\n const raw = analytics?.revenueByCategory ?? {};\n return Object.entries(raw)\n .map(([key, value]) => ({ label: humanizeCategory(key), value: Number(value) }))\n .filter((entry) => Number.isFinite(entry.value) && entry.value > 0)\n .sort((a, b) => b.value - a.value);\n }, [analytics?.revenueByCategory]);\n\n const maxCategory = categories.length > 0 ? categories[0].value : 0;\n\n const trends = useMemo(() => {\n const raw = analytics?.monthlyTrends;\n if (!raw) return [];\n if (Array.isArray(raw)) {\n return raw\n .map((entry) => ({\n label: entry.month || '',\n value: Number(entry.revenue || 0),\n }))\n .filter((entry) => Number.isFinite(entry.value) && entry.label);\n }\n return Object.entries(raw)\n .map(([key, value]) => ({\n label: key,\n value: Number(value),\n }))\n .filter((entry) => Number.isFinite(entry.value));\n }, [analytics?.monthlyTrends]);\n\n const maxTrend = trends.length > 0 ? Math.max(...trends.map((t) => t.value)) : 0;\n\n return (\n <PageShell\n title={t('ggnomad.analytics.title', 'Analytics')}\n titleAccent={t('ggnomad.analytics.title.accent', 'Analytics')}\n subtitle={t('ggnomad.analytics.subtitle', 'GGNomad performance and growth analytics')}\n >\n <PagePurpose icon={<BarChart3 className=\"size-4\" />}>\n {t(\n 'ggnomad.analytics.purpose',\n 'Understand how the marketplace is performing — total revenue, bookings, active users and conversion rate at a glance, with a category breakdown to see which domains (stays, tours, events) are driving growth.'\n )}\n </PagePurpose>\n\n <EmphasisPanel>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.analytics.totalRevenue', 'Total revenue')}\n </p>\n <p className=\"mt-1 text-3xl font-semibold tabular-nums text-text-primary\">\n ${Number(analytics?.totalRevenue ?? 0).toLocaleString()}\n </p>\n </EmphasisPanel>\n\n <section className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n hero\n label={t('ggnomad.analytics.conversion', 'Conversion')}\n value={`${Number(analytics?.conversionRate ?? 0).toFixed(2)}%`}\n icon={<Percent />}\n />\n <StatCard\n label={t('ggnomad.analytics.revenue', 'Revenue')}\n value={`$${Number(analytics?.totalRevenue ?? 0).toLocaleString()}`}\n icon={<DollarSign />}\n />\n <StatCard\n label={t('ggnomad.analytics.bookings', 'Bookings')}\n value={Number(analytics?.totalBookings ?? 0).toLocaleString()}\n icon={<CalendarCheck />}\n />\n <StatCard\n label={t('ggnomad.analytics.users', 'Users')}\n value={Number(analytics?.totalUsers ?? 0).toLocaleString()}\n icon={<Users />}\n />\n </section>\n\n <SectionCard title={t('ggnomad.analytics.revenueByCategory', 'Revenue by category')}>\n {categories.length === 0 ? (\n <div className=\"flex flex-col items-center gap-2 py-8 text-center\">\n <span className=\"flex size-10 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-text-secondary\">\n <BarChart3 className=\"size-5\" />\n </span>\n <p className=\"text-sm font-medium text-text-primary\">\n {t('ggnomad.analytics.empty.title', 'No analytics yet')}\n </p>\n <p className=\"max-w-sm text-sm text-text-secondary\">\n {t(\n 'ggnomad.analytics.empty.description',\n 'Revenue by category appears here as bookings come in across stays, tours and events.'\n )}\n </p>\n </div>\n ) : (\n <ul className=\"flex flex-col gap-4\">\n {categories.map((category, index) => {\n const pct = maxCategory > 0 ? Math.max((category.value / maxCategory) * 100, 2) : 0;\n return (\n <li key={category.label}>\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"truncate text-sm font-medium text-text-primary\">\n {category.label}\n </span>\n <span className=\"flex-shrink-0 text-sm tabular-nums text-text-secondary\">\n ${category.value.toLocaleString()}\n </span>\n </div>\n <div className=\"mt-1.5 h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full transition-[width] duration-500 ease-out\"\n style={{\n width: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n />\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </SectionCard>\n\n {trends.length > 0 && (\n <SectionCard title={t('ggnomad.analytics.revenueTrend', 'Revenue trend')}>\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex items-end gap-2 overflow-x-auto pb-2\">\n {trends.map((trend, index) => {\n const pct = maxTrend > 0 ? Math.max((trend.value / maxTrend) * 100, 5) : 5;\n return (\n <div key={trend.label} className=\"flex flex-col items-center gap-1\">\n <div className=\"flex h-24 items-end\">\n <div\n className=\"w-8 rounded-t transition-all duration-500 ease-out\"\n style={{\n height: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n title={`${trend.label}: $${trend.value.toLocaleString()}`}\n />\n </div>\n <p className=\"truncate text-xs text-text-secondary\">{trend.label}</p>\n </div>\n );\n })}\n </div>\n <p className=\"text-xs text-text-secondary\">\n {t(\n 'ggnomad.analytics.trendNote',\n 'Revenue trends over time showing performance across all domains.'\n )}\n </p>\n </div>\n </SectionCard>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
@@ -14,8 +14,8 @@ import { jsx as N, jsxs as P } from "react/jsx-runtime";
|
|
|
14
14
|
//#region src/ggnomad/pages/BookingsPage.tsx
|
|
15
15
|
function F() {
|
|
16
16
|
let { t: F } = g(), I = _(), [L, R] = l(null), { data: z, refetch: B } = y(r, { variables: {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
limit: 20,
|
|
18
|
+
offset: 0
|
|
19
19
|
} }), { data: V } = y(n), [H, { loading: U }] = v(t), [W, { loading: G }] = v(e), K = c(() => z?.ggnomadBookings ?? [], [z?.ggnomadBookings]), q = c(() => {
|
|
20
20
|
let e = {
|
|
21
21
|
CONFIRMED: 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BookingsPage.js","names":[],"sources":["../../../src/ggnomad/pages/BookingsPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { CalendarCheck, Compass, Hotel, Plus, Ticket, XCircle } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n CTAOverflowMenu,\n GlassCard,\n IllustratedEmptyState,\n NextSteps,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n CANCEL_GGNOMAD_BOOKING_MUTATION,\n CREATE_GGNOMAD_BOOKING_MUTATION,\n GGNOMAD_BOOKABLE_SAMPLE_QUERY,\n GGNOMAD_BOOKINGS_QUERY,\n} from '../graphql';\nimport { PageShell } from './PageShell';\nimport { EntityCard, StatusBadge } from '../components';\nimport { notify } from '../utils/notify';\n\ntype BookingType = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ntype PricedListing = { id: string; name: string; price: number; currency: string };\n\ntype CancelTarget = { id: string; referenceCode: string };\n\nexport function BookingsPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const [cancelTarget, setCancelTarget] = useState<CancelTarget | null>(null);\n\n const { data, refetch } = useQuery(\n GGNOMAD_BOOKINGS_QUERY as TypedDocumentNode<\n {\n ggnomadBookings: Array<{\n id: string;\n referenceCode: string;\n itemName: string;\n status: string;\n totalPrice: number;\n currency: string;\n }>;\n },\n { first: number; skip: number }\n >,\n { variables: { first: 20, skip: 0 } }\n );\n\n // One real listing of each type so the demo booking targets an id that\n // actually exists in the workspace (the backend rejects synthetic ids).\n const { data: sampleData } = useQuery(\n GGNOMAD_BOOKABLE_SAMPLE_QUERY as TypedDocumentNode<\n {\n ggnomadProperties: Array<{\n id: string;\n name: string;\n pricePerNight: number;\n currency: string;\n }>;\n ggnomadTours: Array<{ id: string; name: string; price: number; currency: string }>;\n ggnomadEvents: Array<{ id: string; name: string; price: number; currency: string }>;\n ggnomadActivities: Array<{ id: string; name: string; price: number; currency: string }>;\n },\n Record<string, never>\n >\n );\n\n const [createBooking, { loading: creating }] = useMutation(\n CREATE_GGNOMAD_BOOKING_MUTATION as TypedDocumentNode<\n { createGgnomadBooking: { id: string; referenceCode: string; itemName: string } },\n {\n input: {\n bookingType: BookingType;\n itemId: string;\n itemName: string;\n guestName: string;\n guestEmail: string;\n guests: number;\n totalPrice: number;\n currency: string;\n paymentMethod: string;\n };\n }\n >\n );\n\n const [cancelBooking, { loading: cancelling }] = useMutation(\n CANCEL_GGNOMAD_BOOKING_MUTATION as TypedDocumentNode<\n { cancelGgnomadBooking: { id: string; status: string } },\n { id: string; reason: string }\n >\n );\n\n const bookings = useMemo(() => data?.ggnomadBookings ?? [], [data?.ggnomadBookings]);\n\n const statusCounts = useMemo(() => {\n const counts: Record<string, number> = {\n CONFIRMED: 0,\n CHECKED_IN: 0,\n COMPLETED: 0,\n CANCELLED: 0,\n };\n bookings.forEach((booking) => {\n const status = booking.status?.toUpperCase();\n if (status && status in counts) {\n counts[status] += 1;\n }\n });\n return counts;\n }, [bookings]);\n\n /** First available real listing, in priority order, mapped to a booking type. */\n const resolveDemoTarget = (): { type: BookingType; listing: PricedListing } | null => {\n const property = sampleData?.ggnomadProperties?.[0];\n if (property) {\n return {\n type: 'PROPERTY',\n listing: {\n id: property.id,\n name: property.name,\n price: property.pricePerNight,\n currency: property.currency,\n },\n };\n }\n const tour = sampleData?.ggnomadTours?.[0];\n if (tour) return { type: 'TOUR', listing: tour };\n const event = sampleData?.ggnomadEvents?.[0];\n if (event) return { type: 'EVENT', listing: event };\n const activity = sampleData?.ggnomadActivities?.[0];\n if (activity) return { type: 'ACTIVITY', listing: activity };\n return null;\n };\n\n const handleCreateDemoBooking = async () => {\n const target = resolveDemoTarget();\n if (!target) {\n notify.warning(\n t('ggnomad.bookings.toast.noListingsTitle', 'No listings available to book yet'),\n t(\n 'ggnomad.bookings.toast.noListingsBody',\n 'Add a property, tour, event or activity first, then create a booking against it.'\n )\n );\n return;\n }\n const { type, listing } = target;\n try {\n const result = await createBooking({\n variables: {\n input: {\n bookingType: type,\n itemId: listing.id,\n itemName: listing.name,\n guestName: t('ggnomad.bookings.demo.guestName', 'Demo Guest'),\n guestEmail: 'demo@burdenoff.com',\n guests: 2,\n totalPrice: Number(listing.price ?? 0),\n currency: listing.currency ?? 'USD',\n paymentMethod: 'card',\n },\n },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.bookings.toast.createErrorTitle', 'Could not create booking'),\n result.error.message\n );\n return;\n }\n const created = result.data?.createGgnomadBooking;\n notify.success(\n t('ggnomad.bookings.toast.createSuccessTitle', 'Booking confirmed'),\n created\n ? t('ggnomad.bookings.toast.createSuccessBody', '{{ref}} · {{item}}')\n .replace('{{ref}}', created.referenceCode)\n .replace('{{item}}', created.itemName)\n : listing.name\n );\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.bookings.toast.createErrorTitle', 'Could not create booking'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const confirmCancel = async () => {\n if (!cancelTarget) return;\n try {\n const result = await cancelBooking({\n variables: {\n id: cancelTarget.id,\n reason: t('ggnomad.bookings.actions.cancelReason', 'Cancelled via UI'),\n },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.bookings.toast.cancelErrorTitle', 'Could not cancel booking'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.bookings.toast.cancelSuccessTitle', 'Booking cancelled'),\n cancelTarget.referenceCode\n );\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.bookings.toast.cancelErrorTitle', 'Could not cancel booking'),\n err instanceof Error ? err.message : String(err)\n );\n } finally {\n setCancelTarget(null);\n }\n };\n\n return (\n <PageShell\n title={t('ggnomad.bookings.title', 'Bookings')}\n titleAccent={t('ggnomad.bookings.title.accent', 'Bookings')}\n subtitle={t(\n 'ggnomad.bookings.subtitle',\n 'End-to-end booking operations with real API mutations'\n )}\n actions={\n <CTAOverflowMenu\n primary={{\n label: t('ggnomad.bookings.actions.createDemoBooking', 'Create demo booking'),\n onSelect: () => void handleCreateDemoBooking(),\n disabled: creating,\n icon: <Plus />,\n }}\n actions={[]}\n />\n }\n >\n <PagePurpose icon={<CalendarCheck className=\"size-4\" />}>\n {t(\n 'ggnomad.bookings.purpose',\n 'Your single place to manage every reservation across travel, stays, tours and events. Track each booking’s status and reference code in real time, and cancel a reservation when plans change — all backed by live API mutations.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey=\"ggnomad-bookings\"\n steps={[\n {\n id: 'book-stay',\n label: t('ggnomad.bookings.next.stay', 'Book a place to stay'),\n description: t(\n 'ggnomad.bookings.next.stayDesc',\n 'Browse hospitality listings and reserve a property.'\n ),\n icon: <Hotel className=\"size-4\" />,\n onClick: () => navigate('../hospitality'),\n },\n {\n id: 'book-tour',\n label: t('ggnomad.bookings.next.tour', 'Add a tour or activity'),\n description: t(\n 'ggnomad.bookings.next.tourDesc',\n 'Pick a curated experience from Tourism.'\n ),\n icon: <Compass className=\"size-4\" />,\n onClick: () => navigate('../tourism'),\n },\n {\n id: 'book-event',\n label: t('ggnomad.bookings.next.event', 'Grab event tickets'),\n description: t(\n 'ggnomad.bookings.next.eventDesc',\n 'Find live events to round out the itinerary.'\n ),\n icon: <Ticket className=\"size-4\" />,\n onClick: () => navigate('../entertainment'),\n },\n ]}\n />\n\n {bookings.length > 0 && (\n <div className=\"grid gap-3 sm:grid-cols-4\">\n {[\n {\n status: 'CONFIRMED',\n label: t('ggnomad.bookings.pipeline.confirmed', 'Confirmed'),\n icon: CalendarCheck,\n },\n {\n status: 'CHECKED_IN',\n label: t('ggnomad.bookings.pipeline.checkedIn', 'Checked-in'),\n icon: Hotel,\n },\n {\n status: 'COMPLETED',\n label: t('ggnomad.bookings.pipeline.completed', 'Completed'),\n icon: Ticket,\n },\n {\n status: 'CANCELLED',\n label: t('ggnomad.bookings.pipeline.cancelled', 'Cancelled'),\n icon: XCircle,\n },\n ].map((stage) => {\n const Icon = stage.icon;\n const count = statusCounts[stage.status];\n return (\n <div\n key={stage.status}\n className=\"flex flex-col items-center gap-2 rounded-lg border border-border-seam bg-bg-surface/40 p-3 text-center\"\n >\n <div className=\"flex size-8 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <Icon className=\"size-4\" />\n </div>\n <p className=\"text-xs font-medium text-text-secondary\">{stage.label}</p>\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">{count}</p>\n </div>\n );\n })}\n </div>\n )}\n\n {bookings.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.bookings.emptyTitle', 'No bookings yet')}\n description={t(\n 'ggnomad.bookings.emptyDescription',\n 'Create your first booking to see it tracked here in real time.'\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.bookings.activeBookings', 'Your bookings')}\n </h2>\n <div className=\"grid gap-3 sm:grid-cols-2\">\n {bookings.map((booking) => (\n <EntityCard\n key={booking.id}\n title={booking.referenceCode}\n meta={booking.itemName}\n price={`${booking.currency} ${Number(booking.totalPrice).toFixed(2)}`}\n badge={\n <StatusBadge status={booking.status} live={booking.status !== 'CANCELLED'} />\n }\n footer={\n booking.status !== 'CANCELLED' ? (\n <CTAOverflowMenu\n align=\"end\"\n actions={[\n {\n label: t('ggnomad.bookings.actions.cancel', 'Cancel booking'),\n onSelect: () =>\n setCancelTarget({\n id: booking.id,\n referenceCode: booking.referenceCode,\n }),\n intent: 'destructive',\n icon: <XCircle />,\n },\n ]}\n />\n ) : null\n }\n />\n ))}\n </div>\n </GlassCard>\n )}\n\n <AlertDialog\n open={cancelTarget !== null}\n onOpenChange={(open) => {\n if (!open) setCancelTarget(null);\n }}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.bookings.cancelDialog.title', 'Cancel this booking?')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.bookings.cancelDialog.body',\n 'This releases reservation {{ref}} and cannot be undone.'\n ).replace('{{ref}}', cancelTarget?.referenceCode ?? '')}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={cancelling}>\n {t('ggnomad.bookings.cancelDialog.keep', 'Keep booking')}\n </AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-action-danger-bg text-action-danger-text hover:bg-action-danger-bgHover\"\n disabled={cancelling}\n onClick={(event) => {\n event.preventDefault();\n void confirmCancel();\n }}\n >\n {cancelling\n ? t('ggnomad.bookings.cancelDialog.cancelling', 'Cancelling…')\n : t('ggnomad.bookings.cancelDialog.confirm', 'Cancel booking')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </PageShell>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAsCA,SAAgB,IAAe;CAC7B,IAAM,EAAE,SAAM,GAAS,EACjB,IAAW,GAAa,EACxB,CAAC,GAAc,KAAmB,EAA8B,KAAK,EAErE,EAAE,SAAM,eAAY,EACxB,GAaA,EAAE,WAAW;EAAE,OAAO;EAAI,MAAM;EAAG,EAAE,CACtC,EAIK,EAAE,MAAM,MAAe,EAC3B,EAcD,EAEK,CAAC,GAAe,EAAE,SAAS,OAAc,EAC7C,EAgBD,EAEK,CAAC,GAAe,EAAE,SAAS,OAAgB,EAC/C,EAID,EAEK,IAAW,QAAc,GAAM,mBAAmB,EAAE,EAAE,CAAC,GAAM,gBAAgB,CAAC,EAE9E,IAAe,QAAc;EACjC,IAAM,IAAiC;GACrC,WAAW;GACX,YAAY;GACZ,WAAW;GACX,WAAW;GACZ;AAOD,SANA,EAAS,SAAS,MAAY;GAC5B,IAAM,IAAS,EAAQ,QAAQ,aAAa;AAC5C,GAAI,KAAU,KAAU,MACtB,EAAO,MAAW;IAEpB,EACK;IACN,CAAC,EAAS,CAAC,EAGR,UAAgF;EACpF,IAAM,IAAW,GAAY,oBAAoB;AACjD,MAAI,EACF,QAAO;GACL,MAAM;GACN,SAAS;IACP,IAAI,EAAS;IACb,MAAM,EAAS;IACf,OAAO,EAAS;IAChB,UAAU,EAAS;IACpB;GACF;EAEH,IAAM,IAAO,GAAY,eAAe;AACxC,MAAI,EAAM,QAAO;GAAE,MAAM;GAAQ,SAAS;GAAM;EAChD,IAAM,IAAQ,GAAY,gBAAgB;AAC1C,MAAI,EAAO,QAAO;GAAE,MAAM;GAAS,SAAS;GAAO;EACnD,IAAM,IAAW,GAAY,oBAAoB;AAEjD,SADI,IAAiB;GAAE,MAAM;GAAY,SAAS;GAAU,GACrD;IAGH,IAA0B,YAAY;EAC1C,IAAM,IAAS,GAAmB;AAClC,MAAI,CAAC,GAAQ;AACX,KAAO,QACL,EAAE,0CAA0C,oCAAoC,EAChF,EACE,yCACA,mFACD,CACF;AACD;;EAEF,IAAM,EAAE,SAAM,eAAY;AAC1B,MAAI;GACF,IAAM,IAAS,MAAM,EAAc,EACjC,WAAW,EACT,OAAO;IACL,aAAa;IACb,QAAQ,EAAQ;IAChB,UAAU,EAAQ;IAClB,WAAW,EAAE,mCAAmC,aAAa;IAC7D,YAAY;IACZ,QAAQ;IACR,YAAY,OAAO,EAAQ,SAAS,EAAE;IACtC,UAAU,EAAQ,YAAY;IAC9B,eAAe;IAChB,EACF,EACF,CAAC;AACF,OAAI,EAAO,OAAO;AAChB,MAAO,MACL,EAAE,2CAA2C,2BAA2B,EACxE,EAAO,MAAM,QACd;AACD;;GAEF,IAAM,IAAU,EAAO,MAAM;AAS7B,GARA,EAAO,QACL,EAAE,6CAA6C,oBAAoB,EACnE,IACI,EAAE,4CAA4C,qBAAqB,CAChE,QAAQ,WAAW,EAAQ,cAAc,CACzC,QAAQ,YAAY,EAAQ,SAAS,GACxC,EAAQ,KACb,EACD,MAAM,GAAS;WACR,GAAK;AACZ,KAAO,MACL,EAAE,2CAA2C,2BAA2B,EACxE,aAAe,QAAQ,EAAI,UAAU,OAAO,EAAI,CACjD;;IAIC,IAAgB,YAAY;AAC3B,QACL,KAAI;GACF,IAAM,IAAS,MAAM,EAAc,EACjC,WAAW;IACT,IAAI,EAAa;IACjB,QAAQ,EAAE,yCAAyC,mBAAmB;IACvE,EACF,CAAC;AACF,OAAI,EAAO,OAAO;AAChB,MAAO,MACL,EAAE,2CAA2C,2BAA2B,EACxE,EAAO,MAAM,QACd;AACD;;AAMF,GAJA,EAAO,QACL,EAAE,6CAA6C,oBAAoB,EACnE,EAAa,cACd,EACD,MAAM,GAAS;WACR,GAAK;AACZ,KAAO,MACL,EAAE,2CAA2C,2BAA2B,EACxE,aAAe,QAAQ,EAAI,UAAU,OAAO,EAAI,CACjD;YACO;AACR,KAAgB,KAAK;;;AAIzB,QACE,kBAAC,GAAD;EACE,OAAO,EAAE,0BAA0B,WAAW;EAC9C,aAAa,EAAE,iCAAiC,WAAW;EAC3D,UAAU,EACR,6BACA,wDACD;EACD,SACE,kBAAC,GAAD;GACE,SAAS;IACP,OAAO,EAAE,8CAA8C,sBAAsB;IAC7E,gBAAgB,KAAK,GAAyB;IAC9C,UAAU;IACV,MAAM,kBAAC,GAAD,EAAQ,CAAA;IACf;GACD,SAAS,EAAE;GACX,CAAA;YAhBN;GAmBE,kBAAC,GAAD;IAAa,MAAM,kBAAC,GAAD,EAAe,WAAU,UAAW,CAAA;cACpD,EACC,4BACA,oOACD;IACW,CAAA;GAEd,kBAAC,GAAD;IACE,YAAW;IACX,OAAO;KACL;MACE,IAAI;MACJ,OAAO,EAAE,8BAA8B,uBAAuB;MAC9D,aAAa,EACX,kCACA,sDACD;MACD,MAAM,kBAAC,GAAD,EAAO,WAAU,UAAW,CAAA;MAClC,eAAe,EAAS,iBAAiB;MAC1C;KACD;MACE,IAAI;MACJ,OAAO,EAAE,8BAA8B,yBAAyB;MAChE,aAAa,EACX,kCACA,0CACD;MACD,MAAM,kBAAC,GAAD,EAAS,WAAU,UAAW,CAAA;MACpC,eAAe,EAAS,aAAa;MACtC;KACD;MACE,IAAI;MACJ,OAAO,EAAE,+BAA+B,qBAAqB;MAC7D,aAAa,EACX,mCACA,+CACD;MACD,MAAM,kBAAC,GAAD,EAAQ,WAAU,UAAW,CAAA;MACnC,eAAe,EAAS,mBAAmB;MAC5C;KACF;IACD,CAAA;GAED,EAAS,SAAS,KACjB,kBAAC,OAAD;IAAK,WAAU;cACZ;KACC;MACE,QAAQ;MACR,OAAO,EAAE,uCAAuC,YAAY;MAC5D,MAAM;MACP;KACD;MACE,QAAQ;MACR,OAAO,EAAE,uCAAuC,aAAa;MAC7D,MAAM;MACP;KACD;MACE,QAAQ;MACR,OAAO,EAAE,uCAAuC,YAAY;MAC5D,MAAM;MACP;KACD;MACE,QAAQ;MACR,OAAO,EAAE,uCAAuC,YAAY;MAC5D,MAAM;MACP;KACF,CAAC,KAAK,MAAU;KACf,IAAM,IAAO,EAAM,MACb,IAAQ,EAAa,EAAM;AACjC,YACE,kBAAC,OAAD;MAEE,WAAU;gBAFZ;OAIE,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA;QACvB,CAAA;OACN,kBAAC,KAAD;QAAG,WAAU;kBAA2C,EAAM;QAAU,CAAA;OACxE,kBAAC,KAAD;QAAG,WAAU;kBAAwD;QAAU,CAAA;OAC3E;QARC,EAAM,OAQP;MAER;IACE,CAAA;GAGP,EAAS,WAAW,IACnB,kBAAC,GAAD;IACE,cAAa;IACb,OAAO,EAAE,+BAA+B,kBAAkB;IAC1D,aAAa,EACX,qCACA,iEACD;IACD,CAAA,GAEF,kBAAC,GAAD;IAAW,WAAU;IAAQ,MAAA;IAAK,WAAU;cAA5C,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAE,mCAAmC,gBAAgB;KACnD,CAAA,EACL,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAS,KAAK,MACb,kBAAC,GAAD;MAEE,OAAO,EAAQ;MACf,MAAM,EAAQ;MACd,OAAO,GAAG,EAAQ,SAAS,GAAG,OAAO,EAAQ,WAAW,CAAC,QAAQ,EAAE;MACnE,OACE,kBAAC,GAAD;OAAa,QAAQ,EAAQ;OAAQ,MAAM,EAAQ,WAAW;OAAe,CAAA;MAE/E,QACE,EAAQ,WAAW,cAgBf,OAfF,kBAAC,GAAD;OACE,OAAM;OACN,SAAS,CACP;QACE,OAAO,EAAE,mCAAmC,iBAAiB;QAC7D,gBACE,EAAgB;SACd,IAAI,EAAQ;SACZ,eAAe,EAAQ;SACxB,CAAC;QACJ,QAAQ;QACR,MAAM,kBAAC,GAAD,EAAW,CAAA;QAClB,CACF;OACD,CAAA;MAGN,EA1BK,EAAQ,GA0Bb,CACF;KACE,CAAA,CACI;;GAGd,kBAAC,GAAD;IACE,MAAM,MAAiB;IACvB,eAAe,MAAS;AACtB,KAAK,KAAM,EAAgB,KAAK;;cAGlC,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACG,EAAE,uCAAuC,uBAAuB,EAChD,CAAA,EACnB,kBAAC,GAAD,EAAA,UACG,EACC,sCACA,0DACD,CAAC,QAAQ,WAAW,GAAc,iBAAiB,GAAG,EAChC,CAAA,CACP,EAAA,CAAA,EACpB,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;KAAmB,UAAU;eAC1B,EAAE,sCAAsC,eAAe;KACtC,CAAA,EACpB,kBAAC,GAAD;KACE,WAAU;KACV,UAAU;KACV,UAAU,MAAU;AAEb,MADL,EAAM,gBAAgB,EACjB,GAAe;;eAGrB,IACG,EAAE,4CAA4C,cAAc,GAC5D,EAAE,yCAAyC,iBAAiB;KAC9C,CAAA,CACF,EAAA,CAAA,CACD,EAAA,CAAA;IACT,CAAA;GACJ"}
|
|
1
|
+
{"version":3,"file":"BookingsPage.js","names":[],"sources":["../../../src/ggnomad/pages/BookingsPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { CalendarCheck, Compass, Hotel, Plus, Ticket, XCircle } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n CTAOverflowMenu,\n GlassCard,\n IllustratedEmptyState,\n NextSteps,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n CANCEL_GGNOMAD_BOOKING_MUTATION,\n CREATE_GGNOMAD_BOOKING_MUTATION,\n GGNOMAD_BOOKABLE_SAMPLE_QUERY,\n GGNOMAD_BOOKINGS_QUERY,\n} from '../graphql';\nimport { PageShell } from './PageShell';\nimport { EntityCard, StatusBadge } from '../components';\nimport { notify } from '../utils/notify';\n\ntype BookingType = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ntype PricedListing = { id: string; name: string; price: number; currency: string };\n\ntype CancelTarget = { id: string; referenceCode: string };\n\nexport function BookingsPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const [cancelTarget, setCancelTarget] = useState<CancelTarget | null>(null);\n\n const { data, refetch } = useQuery(\n GGNOMAD_BOOKINGS_QUERY as TypedDocumentNode<\n {\n ggnomadBookings: Array<{\n id: string;\n referenceCode: string;\n itemName: string;\n status: string;\n totalPrice: number;\n currency: string;\n }>;\n },\n { limit: number; offset: number }\n >,\n { variables: { limit: 20, offset: 0 } }\n );\n\n // One real listing of each type so the demo booking targets an id that\n // actually exists in the workspace (the backend rejects synthetic ids).\n const { data: sampleData } = useQuery(\n GGNOMAD_BOOKABLE_SAMPLE_QUERY as TypedDocumentNode<\n {\n ggnomadProperties: Array<{\n id: string;\n name: string;\n pricePerNight: number;\n currency: string;\n }>;\n ggnomadTours: Array<{ id: string; name: string; price: number; currency: string }>;\n ggnomadEvents: Array<{ id: string; name: string; price: number; currency: string }>;\n ggnomadActivities: Array<{ id: string; name: string; price: number; currency: string }>;\n },\n Record<string, never>\n >\n );\n\n const [createBooking, { loading: creating }] = useMutation(\n CREATE_GGNOMAD_BOOKING_MUTATION as TypedDocumentNode<\n { createGgnomadBooking: { id: string; referenceCode: string; itemName: string } },\n {\n input: {\n bookingType: BookingType;\n itemId: string;\n itemName: string;\n guestName: string;\n guestEmail: string;\n guests: number;\n totalPrice: number;\n currency: string;\n paymentMethod: string;\n };\n }\n >\n );\n\n const [cancelBooking, { loading: cancelling }] = useMutation(\n CANCEL_GGNOMAD_BOOKING_MUTATION as TypedDocumentNode<\n { cancelGgnomadBooking: { id: string; status: string } },\n { id: string; reason: string }\n >\n );\n\n const bookings = useMemo(() => data?.ggnomadBookings ?? [], [data?.ggnomadBookings]);\n\n const statusCounts = useMemo(() => {\n const counts: Record<string, number> = {\n CONFIRMED: 0,\n CHECKED_IN: 0,\n COMPLETED: 0,\n CANCELLED: 0,\n };\n bookings.forEach((booking) => {\n const status = booking.status?.toUpperCase();\n if (status && status in counts) {\n counts[status] += 1;\n }\n });\n return counts;\n }, [bookings]);\n\n /** First available real listing, in priority order, mapped to a booking type. */\n const resolveDemoTarget = (): { type: BookingType; listing: PricedListing } | null => {\n const property = sampleData?.ggnomadProperties?.[0];\n if (property) {\n return {\n type: 'PROPERTY',\n listing: {\n id: property.id,\n name: property.name,\n price: property.pricePerNight,\n currency: property.currency,\n },\n };\n }\n const tour = sampleData?.ggnomadTours?.[0];\n if (tour) return { type: 'TOUR', listing: tour };\n const event = sampleData?.ggnomadEvents?.[0];\n if (event) return { type: 'EVENT', listing: event };\n const activity = sampleData?.ggnomadActivities?.[0];\n if (activity) return { type: 'ACTIVITY', listing: activity };\n return null;\n };\n\n const handleCreateDemoBooking = async () => {\n const target = resolveDemoTarget();\n if (!target) {\n notify.warning(\n t('ggnomad.bookings.toast.noListingsTitle', 'No listings available to book yet'),\n t(\n 'ggnomad.bookings.toast.noListingsBody',\n 'Add a property, tour, event or activity first, then create a booking against it.'\n )\n );\n return;\n }\n const { type, listing } = target;\n try {\n const result = await createBooking({\n variables: {\n input: {\n bookingType: type,\n itemId: listing.id,\n itemName: listing.name,\n guestName: t('ggnomad.bookings.demo.guestName', 'Demo Guest'),\n guestEmail: 'demo@burdenoff.com',\n guests: 2,\n totalPrice: Number(listing.price ?? 0),\n currency: listing.currency ?? 'USD',\n paymentMethod: 'card',\n },\n },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.bookings.toast.createErrorTitle', 'Could not create booking'),\n result.error.message\n );\n return;\n }\n const created = result.data?.createGgnomadBooking;\n notify.success(\n t('ggnomad.bookings.toast.createSuccessTitle', 'Booking confirmed'),\n created\n ? t('ggnomad.bookings.toast.createSuccessBody', '{{ref}} · {{item}}')\n .replace('{{ref}}', created.referenceCode)\n .replace('{{item}}', created.itemName)\n : listing.name\n );\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.bookings.toast.createErrorTitle', 'Could not create booking'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const confirmCancel = async () => {\n if (!cancelTarget) return;\n try {\n const result = await cancelBooking({\n variables: {\n id: cancelTarget.id,\n reason: t('ggnomad.bookings.actions.cancelReason', 'Cancelled via UI'),\n },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.bookings.toast.cancelErrorTitle', 'Could not cancel booking'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.bookings.toast.cancelSuccessTitle', 'Booking cancelled'),\n cancelTarget.referenceCode\n );\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.bookings.toast.cancelErrorTitle', 'Could not cancel booking'),\n err instanceof Error ? err.message : String(err)\n );\n } finally {\n setCancelTarget(null);\n }\n };\n\n return (\n <PageShell\n title={t('ggnomad.bookings.title', 'Bookings')}\n titleAccent={t('ggnomad.bookings.title.accent', 'Bookings')}\n subtitle={t(\n 'ggnomad.bookings.subtitle',\n 'End-to-end booking operations with real API mutations'\n )}\n actions={\n <CTAOverflowMenu\n primary={{\n label: t('ggnomad.bookings.actions.createDemoBooking', 'Create demo booking'),\n onSelect: () => void handleCreateDemoBooking(),\n disabled: creating,\n icon: <Plus />,\n }}\n actions={[]}\n />\n }\n >\n <PagePurpose icon={<CalendarCheck className=\"size-4\" />}>\n {t(\n 'ggnomad.bookings.purpose',\n 'Your single place to manage every reservation across travel, stays, tours and events. Track each booking’s status and reference code in real time, and cancel a reservation when plans change — all backed by live API mutations.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey=\"ggnomad-bookings\"\n steps={[\n {\n id: 'book-stay',\n label: t('ggnomad.bookings.next.stay', 'Book a place to stay'),\n description: t(\n 'ggnomad.bookings.next.stayDesc',\n 'Browse hospitality listings and reserve a property.'\n ),\n icon: <Hotel className=\"size-4\" />,\n onClick: () => navigate('../hospitality'),\n },\n {\n id: 'book-tour',\n label: t('ggnomad.bookings.next.tour', 'Add a tour or activity'),\n description: t(\n 'ggnomad.bookings.next.tourDesc',\n 'Pick a curated experience from Tourism.'\n ),\n icon: <Compass className=\"size-4\" />,\n onClick: () => navigate('../tourism'),\n },\n {\n id: 'book-event',\n label: t('ggnomad.bookings.next.event', 'Grab event tickets'),\n description: t(\n 'ggnomad.bookings.next.eventDesc',\n 'Find live events to round out the itinerary.'\n ),\n icon: <Ticket className=\"size-4\" />,\n onClick: () => navigate('../entertainment'),\n },\n ]}\n />\n\n {bookings.length > 0 && (\n <div className=\"grid gap-3 sm:grid-cols-4\">\n {[\n {\n status: 'CONFIRMED',\n label: t('ggnomad.bookings.pipeline.confirmed', 'Confirmed'),\n icon: CalendarCheck,\n },\n {\n status: 'CHECKED_IN',\n label: t('ggnomad.bookings.pipeline.checkedIn', 'Checked-in'),\n icon: Hotel,\n },\n {\n status: 'COMPLETED',\n label: t('ggnomad.bookings.pipeline.completed', 'Completed'),\n icon: Ticket,\n },\n {\n status: 'CANCELLED',\n label: t('ggnomad.bookings.pipeline.cancelled', 'Cancelled'),\n icon: XCircle,\n },\n ].map((stage) => {\n const Icon = stage.icon;\n const count = statusCounts[stage.status];\n return (\n <div\n key={stage.status}\n className=\"flex flex-col items-center gap-2 rounded-lg border border-border-seam bg-bg-surface/40 p-3 text-center\"\n >\n <div className=\"flex size-8 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <Icon className=\"size-4\" />\n </div>\n <p className=\"text-xs font-medium text-text-secondary\">{stage.label}</p>\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">{count}</p>\n </div>\n );\n })}\n </div>\n )}\n\n {bookings.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.bookings.emptyTitle', 'No bookings yet')}\n description={t(\n 'ggnomad.bookings.emptyDescription',\n 'Create your first booking to see it tracked here in real time.'\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.bookings.activeBookings', 'Your bookings')}\n </h2>\n <div className=\"grid gap-3 sm:grid-cols-2\">\n {bookings.map((booking) => (\n <EntityCard\n key={booking.id}\n title={booking.referenceCode}\n meta={booking.itemName}\n price={`${booking.currency} ${Number(booking.totalPrice).toFixed(2)}`}\n badge={\n <StatusBadge status={booking.status} live={booking.status !== 'CANCELLED'} />\n }\n footer={\n booking.status !== 'CANCELLED' ? (\n <CTAOverflowMenu\n align=\"end\"\n actions={[\n {\n label: t('ggnomad.bookings.actions.cancel', 'Cancel booking'),\n onSelect: () =>\n setCancelTarget({\n id: booking.id,\n referenceCode: booking.referenceCode,\n }),\n intent: 'destructive',\n icon: <XCircle />,\n },\n ]}\n />\n ) : null\n }\n />\n ))}\n </div>\n </GlassCard>\n )}\n\n <AlertDialog\n open={cancelTarget !== null}\n onOpenChange={(open) => {\n if (!open) setCancelTarget(null);\n }}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.bookings.cancelDialog.title', 'Cancel this booking?')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.bookings.cancelDialog.body',\n 'This releases reservation {{ref}} and cannot be undone.'\n ).replace('{{ref}}', cancelTarget?.referenceCode ?? '')}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={cancelling}>\n {t('ggnomad.bookings.cancelDialog.keep', 'Keep booking')}\n </AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-action-danger-bg text-action-danger-text hover:bg-action-danger-bgHover\"\n disabled={cancelling}\n onClick={(event) => {\n event.preventDefault();\n void confirmCancel();\n }}\n >\n {cancelling\n ? t('ggnomad.bookings.cancelDialog.cancelling', 'Cancelling…')\n : t('ggnomad.bookings.cancelDialog.confirm', 'Cancel booking')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
@@ -21,17 +21,17 @@ var E = [
|
|
|
21
21
|
], D = (e) => e.replace(/[_-]+/g, " ").trim().replace(/\b\w/g, (e) => e.toUpperCase()) || e;
|
|
22
22
|
function O() {
|
|
23
23
|
let { t: O } = _(), k = v(), A = y(n), j = y(e, { variables: {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
limit: 5,
|
|
25
|
+
offset: 0
|
|
26
26
|
} }), M = y(r, { variables: {
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
limit: 4,
|
|
28
|
+
offset: 0
|
|
29
29
|
} }), N = y(i, { variables: {
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
limit: 4,
|
|
31
|
+
offset: 0
|
|
32
32
|
} }), P = y(t, { variables: {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
limit: 4,
|
|
34
|
+
offset: 0
|
|
35
35
|
} }), F = A.data?.ggnomadOverviewStats, I = A.data?.ggnomadAnalytics, L = j.data?.ggnomadBookings ?? [], R = l(() => {
|
|
36
36
|
let e = I?.revenueByCategory ?? {};
|
|
37
37
|
return Object.entries(e).map(([e, t]) => ({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/ggnomad/pages/DashboardPage.tsx"],"sourcesContent":["import { useMemo } 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 { Building, CalendarCheck, Compass, DollarSign, Hotel, Search, Users } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n EmphasisPanel,\n IllustratedEmptyState,\n NextSteps,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n GGNOMAD_OVERVIEW_QUERY,\n GGNOMAD_BOOKINGS_QUERY,\n GGNOMAD_PROPERTIES_QUERY,\n GGNOMAD_TOURS_QUERY,\n GGNOMAD_EVENTS_QUERY,\n} from '../graphql';\nimport { PageShell } from './PageShell';\nimport { SectionCard, StatCard, StatusBadge } from '../components';\n\nconst CHART_COLORS = [\n 'var(--color-chart-1)',\n 'var(--color-chart-2)',\n 'var(--color-chart-3)',\n 'var(--color-chart-4)',\n 'var(--color-chart-5)',\n];\n\nconst humanizeCategory = (key: string): string =>\n key\n .replace(/[_-]+/g, ' ')\n .trim()\n .replace(/\\b\\w/g, (char) => char.toUpperCase()) || key;\n\nexport function DashboardPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const overview = useQuery(\n GGNOMAD_OVERVIEW_QUERY as TypedDocumentNode<\n {\n ggnomadOverviewStats: {\n users: number;\n listings: number;\n bookings: number;\n monthlyRevenue: number;\n };\n ggnomadAnalytics: {\n totalRevenue: number;\n totalBookings: number;\n totalUsers: number;\n conversionRate: number;\n revenueByCategory: Record<string, unknown>;\n };\n },\n Record<string, never>\n >\n );\n const bookings = useQuery(\n GGNOMAD_BOOKINGS_QUERY as TypedDocumentNode<\n {\n ggnomadBookings: Array<{\n id: string;\n referenceCode: string;\n itemName: string;\n status: string;\n }>;\n },\n { first: number; skip: number }\n >,\n {\n variables: { first: 5, skip: 0 },\n }\n );\n const properties = useQuery(\n GGNOMAD_PROPERTIES_QUERY as TypedDocumentNode<\n { ggnomadProperties: Array<{ id: string }> },\n { first: number; skip: number }\n >,\n { variables: { first: 4, skip: 0 } }\n );\n const tours = useQuery(\n GGNOMAD_TOURS_QUERY as TypedDocumentNode<\n { ggnomadTours: Array<{ id: string }> },\n { first: number; skip: number }\n >,\n {\n variables: { first: 4, skip: 0 },\n }\n );\n const events = useQuery(\n GGNOMAD_EVENTS_QUERY as TypedDocumentNode<\n { ggnomadEvents: Array<{ id: string }> },\n { first: number; skip: number }\n >,\n {\n variables: { first: 4, skip: 0 },\n }\n );\n\n const stats = overview.data?.ggnomadOverviewStats;\n const analytics = overview.data?.ggnomadAnalytics;\n const recentBookings = bookings.data?.ggnomadBookings ?? [];\n\n const categories = useMemo(() => {\n const raw = analytics?.revenueByCategory ?? {};\n return Object.entries(raw)\n .map(([key, value]) => ({ label: humanizeCategory(key), value: Number(value) }))\n .filter((entry) => Number.isFinite(entry.value) && entry.value > 0)\n .sort((a, b) => b.value - a.value);\n }, [analytics?.revenueByCategory]);\n\n const maxCategory = categories.length > 0 ? categories[0].value : 0;\n\n return (\n <PageShell\n hero\n title={t('ggnomad.dashboard.title', 'GGNomad Dashboard')}\n titleAccent={t('ggnomad.dashboard.title.accent', 'GGNomad')}\n subtitle={t(\n 'ggnomad.dashboard.subtitle',\n 'Unified travel, hospitality, tourism and entertainment operations'\n )}\n >\n <PagePurpose icon={<Compass className=\"size-4\" />}>\n {t(\n 'ggnomad.dashboard.purpose',\n 'Your command centre for the whole GGNomad marketplace — see live revenue, users, listings and bookings across travel, hospitality, tourism and entertainment in one place, and jump straight into the area you want to work on.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey=\"ggnomad-dashboard\"\n steps={[\n {\n id: 'search',\n label: t('ggnomad.dashboard.next.search', 'Search the catalogue'),\n description: t(\n 'ggnomad.dashboard.next.searchDesc',\n 'Find inventory across every domain at once.'\n ),\n icon: <Search className=\"size-4\" />,\n onClick: () => navigate('search'),\n },\n {\n id: 'stays',\n label: t('ggnomad.dashboard.next.stays', 'Browse places to stay'),\n description: t(\n 'ggnomad.dashboard.next.staysDesc',\n 'Explore hospitality listings and open a property.'\n ),\n icon: <Hotel className=\"size-4\" />,\n onClick: () => navigate('hospitality'),\n },\n {\n id: 'tourism',\n label: t('ggnomad.dashboard.next.tourism', 'Discover tours & activities'),\n description: t(\n 'ggnomad.dashboard.next.tourismDesc',\n 'Plan what to do at each destination.'\n ),\n icon: <Compass className=\"size-4\" />,\n onClick: () => navigate('tourism'),\n },\n {\n id: 'bookings',\n label: t('ggnomad.dashboard.next.bookings', 'Manage bookings'),\n description: t(\n 'ggnomad.dashboard.next.bookingsDesc',\n 'Track and cancel reservations in real time.'\n ),\n icon: <CalendarCheck className=\"size-4\" />,\n onClick: () => navigate('bookings'),\n },\n ]}\n />\n\n <section className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n hero\n label={t('ggnomad.dashboard.stats.monthlyRevenue', 'Monthly Revenue')}\n value={`$${Number(stats?.monthlyRevenue ?? 0).toLocaleString()}`}\n icon={<DollarSign />}\n />\n <StatCard\n label={t('ggnomad.dashboard.stats.users', 'Users')}\n value={Number(stats?.users ?? 0).toLocaleString()}\n icon={<Users />}\n />\n <StatCard\n label={t('ggnomad.dashboard.stats.listings', 'Listings')}\n value={Number(stats?.listings ?? 0).toLocaleString()}\n icon={<Building />}\n />\n <StatCard\n label={t('ggnomad.dashboard.stats.bookings', 'Bookings')}\n value={Number(stats?.bookings ?? 0).toLocaleString()}\n icon={<CalendarCheck />}\n />\n </section>\n\n <EmphasisPanel>\n <div className=\"grid gap-4 sm:grid-cols-3\">\n <div>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.dashboard.inventory.properties', 'Properties')}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums text-text-primary\">\n {properties.data?.ggnomadProperties?.length ?? 0}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.dashboard.inventory.tours', 'Tours')}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums text-text-primary\">\n {tours.data?.ggnomadTours?.length ?? 0}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.dashboard.inventory.events', 'Events')}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums text-text-primary\">\n {events.data?.ggnomadEvents?.length ?? 0}\n </p>\n </div>\n </div>\n <p className=\"mt-4 text-sm text-text-secondary\">\n {t(\n 'ggnomad.dashboard.inventory.liveDataNotice',\n 'Data is live from `wspace-ggnomad-svc` GraphQL APIs.'\n )}\n </p>\n </EmphasisPanel>\n\n <SectionCard title={t('ggnomad.dashboard.sections.recentBookings', 'Recent bookings')}>\n {recentBookings.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.dashboard.recentBookings.emptyTitle', 'No recent bookings')}\n description={t(\n 'ggnomad.dashboard.recentBookings.emptyDescription',\n 'New bookings across travel, hospitality and tourism will appear here.'\n )}\n />\n ) : (\n <ul className=\"space-y-2 text-sm\">\n {recentBookings.map((booking) => (\n <li\n key={booking.id}\n className=\"flex items-center justify-between gap-3 rounded-lg border border-border-seam bg-bg-surface/40 p-3\"\n >\n <span className=\"min-w-0 truncate text-text-primary\">\n {booking.referenceCode} — {booking.itemName}\n </span>\n <StatusBadge status={booking.status} />\n </li>\n ))}\n </ul>\n )}\n </SectionCard>\n\n <SectionCard title={t('ggnomad.dashboard.sections.revenueByCategory', 'Revenue by category')}>\n {categories.length === 0 ? (\n <div className=\"flex flex-col items-center gap-2 py-8 text-center\">\n <span className=\"flex size-10 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-text-secondary\">\n <DollarSign className=\"size-5\" />\n </span>\n <p className=\"text-sm font-medium text-text-primary\">\n {t('ggnomad.dashboard.empty.title', 'Revenue data coming soon')}\n </p>\n <p className=\"max-w-sm text-sm text-text-secondary\">\n {t(\n 'ggnomad.dashboard.empty.description',\n 'Revenue by category appears here as bookings come in across stays, tours and events.'\n )}\n </p>\n </div>\n ) : (\n <ul className=\"flex flex-col gap-4\">\n {categories.map((category, index) => {\n const pct = maxCategory > 0 ? Math.max((category.value / maxCategory) * 100, 2) : 0;\n return (\n <li key={category.label}>\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"truncate text-sm font-medium text-text-primary\">\n {category.label}\n </span>\n <span className=\"flex-shrink-0 text-sm tabular-nums text-text-secondary\">\n ${category.value.toLocaleString()}\n </span>\n </div>\n <div className=\"mt-1.5 h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full transition-[width] duration-500 ease-out\"\n style={{\n width: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n />\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </SectionCard>\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/ggnomad/pages/DashboardPage.tsx"],"sourcesContent":["import { useMemo } 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 { Building, CalendarCheck, Compass, DollarSign, Hotel, Search, Users } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n EmphasisPanel,\n IllustratedEmptyState,\n NextSteps,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n GGNOMAD_OVERVIEW_QUERY,\n GGNOMAD_BOOKINGS_QUERY,\n GGNOMAD_PROPERTIES_QUERY,\n GGNOMAD_TOURS_QUERY,\n GGNOMAD_EVENTS_QUERY,\n} from '../graphql';\nimport { PageShell } from './PageShell';\nimport { SectionCard, StatCard, StatusBadge } from '../components';\n\nconst CHART_COLORS = [\n 'var(--color-chart-1)',\n 'var(--color-chart-2)',\n 'var(--color-chart-3)',\n 'var(--color-chart-4)',\n 'var(--color-chart-5)',\n];\n\nconst humanizeCategory = (key: string): string =>\n key\n .replace(/[_-]+/g, ' ')\n .trim()\n .replace(/\\b\\w/g, (char) => char.toUpperCase()) || key;\n\nexport function DashboardPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const overview = useQuery(\n GGNOMAD_OVERVIEW_QUERY as TypedDocumentNode<\n {\n ggnomadOverviewStats: {\n users: number;\n listings: number;\n bookings: number;\n monthlyRevenue: number;\n };\n ggnomadAnalytics: {\n totalRevenue: number;\n totalBookings: number;\n totalUsers: number;\n conversionRate: number;\n revenueByCategory: Record<string, unknown>;\n };\n },\n Record<string, never>\n >\n );\n const bookings = useQuery(\n GGNOMAD_BOOKINGS_QUERY as TypedDocumentNode<\n {\n ggnomadBookings: Array<{\n id: string;\n referenceCode: string;\n itemName: string;\n status: string;\n }>;\n },\n { limit: number; offset: number }\n >,\n {\n variables: { limit: 5, offset: 0 },\n }\n );\n const properties = useQuery(\n GGNOMAD_PROPERTIES_QUERY as TypedDocumentNode<\n { ggnomadProperties: Array<{ id: string }> },\n { limit: number; offset: number }\n >,\n { variables: { limit: 4, offset: 0 } }\n );\n const tours = useQuery(\n GGNOMAD_TOURS_QUERY as TypedDocumentNode<\n { ggnomadTours: Array<{ id: string }> },\n { limit: number; offset: number }\n >,\n {\n variables: { limit: 4, offset: 0 },\n }\n );\n const events = useQuery(\n GGNOMAD_EVENTS_QUERY as TypedDocumentNode<\n { ggnomadEvents: Array<{ id: string }> },\n { limit: number; offset: number }\n >,\n {\n variables: { limit: 4, offset: 0 },\n }\n );\n\n const stats = overview.data?.ggnomadOverviewStats;\n const analytics = overview.data?.ggnomadAnalytics;\n const recentBookings = bookings.data?.ggnomadBookings ?? [];\n\n const categories = useMemo(() => {\n const raw = analytics?.revenueByCategory ?? {};\n return Object.entries(raw)\n .map(([key, value]) => ({ label: humanizeCategory(key), value: Number(value) }))\n .filter((entry) => Number.isFinite(entry.value) && entry.value > 0)\n .sort((a, b) => b.value - a.value);\n }, [analytics?.revenueByCategory]);\n\n const maxCategory = categories.length > 0 ? categories[0].value : 0;\n\n return (\n <PageShell\n hero\n title={t('ggnomad.dashboard.title', 'GGNomad Dashboard')}\n titleAccent={t('ggnomad.dashboard.title.accent', 'GGNomad')}\n subtitle={t(\n 'ggnomad.dashboard.subtitle',\n 'Unified travel, hospitality, tourism and entertainment operations'\n )}\n >\n <PagePurpose icon={<Compass className=\"size-4\" />}>\n {t(\n 'ggnomad.dashboard.purpose',\n 'Your command centre for the whole GGNomad marketplace — see live revenue, users, listings and bookings across travel, hospitality, tourism and entertainment in one place, and jump straight into the area you want to work on.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey=\"ggnomad-dashboard\"\n steps={[\n {\n id: 'search',\n label: t('ggnomad.dashboard.next.search', 'Search the catalogue'),\n description: t(\n 'ggnomad.dashboard.next.searchDesc',\n 'Find inventory across every domain at once.'\n ),\n icon: <Search className=\"size-4\" />,\n onClick: () => navigate('search'),\n },\n {\n id: 'stays',\n label: t('ggnomad.dashboard.next.stays', 'Browse places to stay'),\n description: t(\n 'ggnomad.dashboard.next.staysDesc',\n 'Explore hospitality listings and open a property.'\n ),\n icon: <Hotel className=\"size-4\" />,\n onClick: () => navigate('hospitality'),\n },\n {\n id: 'tourism',\n label: t('ggnomad.dashboard.next.tourism', 'Discover tours & activities'),\n description: t(\n 'ggnomad.dashboard.next.tourismDesc',\n 'Plan what to do at each destination.'\n ),\n icon: <Compass className=\"size-4\" />,\n onClick: () => navigate('tourism'),\n },\n {\n id: 'bookings',\n label: t('ggnomad.dashboard.next.bookings', 'Manage bookings'),\n description: t(\n 'ggnomad.dashboard.next.bookingsDesc',\n 'Track and cancel reservations in real time.'\n ),\n icon: <CalendarCheck className=\"size-4\" />,\n onClick: () => navigate('bookings'),\n },\n ]}\n />\n\n <section className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n hero\n label={t('ggnomad.dashboard.stats.monthlyRevenue', 'Monthly Revenue')}\n value={`$${Number(stats?.monthlyRevenue ?? 0).toLocaleString()}`}\n icon={<DollarSign />}\n />\n <StatCard\n label={t('ggnomad.dashboard.stats.users', 'Users')}\n value={Number(stats?.users ?? 0).toLocaleString()}\n icon={<Users />}\n />\n <StatCard\n label={t('ggnomad.dashboard.stats.listings', 'Listings')}\n value={Number(stats?.listings ?? 0).toLocaleString()}\n icon={<Building />}\n />\n <StatCard\n label={t('ggnomad.dashboard.stats.bookings', 'Bookings')}\n value={Number(stats?.bookings ?? 0).toLocaleString()}\n icon={<CalendarCheck />}\n />\n </section>\n\n <EmphasisPanel>\n <div className=\"grid gap-4 sm:grid-cols-3\">\n <div>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.dashboard.inventory.properties', 'Properties')}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums text-text-primary\">\n {properties.data?.ggnomadProperties?.length ?? 0}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.dashboard.inventory.tours', 'Tours')}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums text-text-primary\">\n {tours.data?.ggnomadTours?.length ?? 0}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.dashboard.inventory.events', 'Events')}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums text-text-primary\">\n {events.data?.ggnomadEvents?.length ?? 0}\n </p>\n </div>\n </div>\n <p className=\"mt-4 text-sm text-text-secondary\">\n {t(\n 'ggnomad.dashboard.inventory.liveDataNotice',\n 'Data is live from `wspace-ggnomad-svc` GraphQL APIs.'\n )}\n </p>\n </EmphasisPanel>\n\n <SectionCard title={t('ggnomad.dashboard.sections.recentBookings', 'Recent bookings')}>\n {recentBookings.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.dashboard.recentBookings.emptyTitle', 'No recent bookings')}\n description={t(\n 'ggnomad.dashboard.recentBookings.emptyDescription',\n 'New bookings across travel, hospitality and tourism will appear here.'\n )}\n />\n ) : (\n <ul className=\"space-y-2 text-sm\">\n {recentBookings.map((booking) => (\n <li\n key={booking.id}\n className=\"flex items-center justify-between gap-3 rounded-lg border border-border-seam bg-bg-surface/40 p-3\"\n >\n <span className=\"min-w-0 truncate text-text-primary\">\n {booking.referenceCode} — {booking.itemName}\n </span>\n <StatusBadge status={booking.status} />\n </li>\n ))}\n </ul>\n )}\n </SectionCard>\n\n <SectionCard title={t('ggnomad.dashboard.sections.revenueByCategory', 'Revenue by category')}>\n {categories.length === 0 ? (\n <div className=\"flex flex-col items-center gap-2 py-8 text-center\">\n <span className=\"flex size-10 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-text-secondary\">\n <DollarSign className=\"size-5\" />\n </span>\n <p className=\"text-sm font-medium text-text-primary\">\n {t('ggnomad.dashboard.empty.title', 'Revenue data coming soon')}\n </p>\n <p className=\"max-w-sm text-sm text-text-secondary\">\n {t(\n 'ggnomad.dashboard.empty.description',\n 'Revenue by category appears here as bookings come in across stays, tours and events.'\n )}\n </p>\n </div>\n ) : (\n <ul className=\"flex flex-col gap-4\">\n {categories.map((category, index) => {\n const pct = maxCategory > 0 ? Math.max((category.value / maxCategory) * 100, 2) : 0;\n return (\n <li key={category.label}>\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"truncate text-sm font-medium text-text-primary\">\n {category.label}\n </span>\n <span className=\"flex-shrink-0 text-sm tabular-nums text-text-secondary\">\n ${category.value.toLocaleString()}\n </span>\n </div>\n <div className=\"mt-1.5 h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full transition-[width] duration-500 ease-out\"\n style={{\n width: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n />\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </SectionCard>\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
@@ -12,8 +12,8 @@ import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
|
12
12
|
//#region src/ggnomad/pages/EntertainmentPage.tsx
|
|
13
13
|
function p() {
|
|
14
14
|
let { t: p } = o(), [m, h] = i(12), { data: g } = s(e, { variables: {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
limit: m,
|
|
16
|
+
offset: 0
|
|
17
17
|
} }), _ = g?.ggnomadEvents ?? [];
|
|
18
18
|
return /* @__PURE__ */ f(t, {
|
|
19
19
|
title: p("ggnomad.entertainment.title", "Entertainment"),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EntertainmentPage.js","names":[],"sources":["../../../src/ggnomad/pages/EntertainmentPage.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 { Ticket } from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_EVENTS_QUERY } from '../graphql';\nimport { PageShell } from './PageShell';\nimport { EntityCard, LoadMoreButton } from '../components';\n\nexport function EntertainmentPage() {\n const { t } = useI18n();\n const [limit, setLimit] = useState(12);\n const { data } = useQuery(\n GGNOMAD_EVENTS_QUERY as TypedDocumentNode<\n {\n ggnomadEvents: Array<{\n id: string;\n name: string;\n category: string;\n startsAt: string;\n price: number;\n currency: string;\n images?: string[];\n }>;\n },\n {
|
|
1
|
+
{"version":3,"file":"EntertainmentPage.js","names":[],"sources":["../../../src/ggnomad/pages/EntertainmentPage.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 { Ticket } from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_EVENTS_QUERY } from '../graphql';\nimport { PageShell } from './PageShell';\nimport { EntityCard, LoadMoreButton } from '../components';\n\nexport function EntertainmentPage() {\n const { t } = useI18n();\n const [limit, setLimit] = useState(12);\n const { data } = useQuery(\n GGNOMAD_EVENTS_QUERY as TypedDocumentNode<\n {\n ggnomadEvents: Array<{\n id: string;\n name: string;\n category: string;\n startsAt: string;\n price: number;\n currency: string;\n images?: string[];\n }>;\n },\n { limit: number; offset: number }\n >,\n { variables: { limit, offset: 0 } }\n );\n const events = data?.ggnomadEvents ?? [];\n\n return (\n <PageShell\n title={t('ggnomad.entertainment.title', 'Entertainment')}\n titleAccent={t('ggnomad.entertainment.title.accent', 'Entertainment')}\n subtitle={t('ggnomad.entertainment.subtitle', 'Events and live experiences')}\n >\n <PagePurpose icon={<Ticket className=\"size-4\" />}>\n {t(\n 'ggnomad.entertainment.purpose',\n 'Find live events and experiences — concerts, shows and local happenings. Open an event to see its lineup, venue and timing, then book tickets for your trip dates.'\n )}\n </PagePurpose>\n\n {events.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.entertainment.emptyTitle', 'No events scheduled')}\n description={t(\n 'ggnomad.entertainment.emptyDescription',\n 'Upcoming events and live experiences will be listed 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.entertainment.upcomingEvents', 'Upcoming events')}\n </h2>\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {events.map((event) => (\n <EntityCard\n key={event.id}\n to={event.id}\n image={event.images?.[0]}\n imageAlt={event.name}\n title={event.name}\n meta={event.category}\n price={`${event.currency} ${Number(event.price).toFixed(2)}`}\n footer={new Date(event.startsAt).toLocaleString()}\n />\n ))}\n </div>\n <LoadMoreButton\n visible={events.length >= limit}\n onLoadMore={() => setLimit((l) => l + 12)}\n />\n </GlassCard>\n )}\n </PageShell>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAWA,SAAgB,IAAoB;CAClC,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,IAAS,GAAM,iBAAiB,CAAC;CAEvC,OACE,kBAAC,GAAD;EACE,OAAO,EAAE,+BAA+B,eAAe;EACvD,aAAa,EAAE,sCAAsC,eAAe;EACpE,UAAU,EAAE,kCAAkC,6BAA6B;YAH7E,CAKE,kBAAC,GAAD;GAAa,MAAM,kBAAC,GAAD,EAAQ,WAAU,SAAU,CAAA;aAC5C,EACC,iCACA,oKACF;EACW,CAAA,GAEZ,EAAO,WAAW,IACjB,kBAAC,GAAD;GACE,cAAa;GACb,OAAO,EAAE,oCAAoC,qBAAqB;GAClE,aAAa,EACX,0CACA,2DACF;EACD,CAAA,IAED,kBAAC,GAAD;GAAW,WAAU;GAAQ,MAAA;GAAK,WAAU;aAA5C;IACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAE,wCAAwC,iBAAiB;IAC1D,CAAA;IACJ,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAO,KAAK,MACX,kBAAC,GAAD;MAEE,IAAI,EAAM;MACV,OAAO,EAAM,SAAS;MACtB,UAAU,EAAM;MAChB,OAAO,EAAM;MACb,MAAM,EAAM;MACZ,OAAO,GAAG,EAAM,SAAS,GAAG,OAAO,EAAM,KAAK,EAAE,QAAQ,CAAC;MACzD,QAAQ,IAAI,KAAK,EAAM,QAAQ,EAAE,eAAe;KACjD,GARM,EAAM,EAQZ,CACF;IACE,CAAA;IACL,kBAAC,GAAD;KACE,SAAS,EAAO,UAAU;KAC1B,kBAAkB,GAAU,MAAM,IAAI,EAAE;IACzC,CAAA;GACQ;IAEJ;;AAEf"}
|