@burdenoff/microfe-ggnomad 2026.1016.1 → 2026.1029.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ggnomad/GGNomadRoot.js +4 -2
- package/dist/ggnomad/GGNomadRoot.js.map +1 -1
- package/dist/ggnomad/GGNomadRoutes.js.map +1 -1
- package/dist/ggnomad/components/AuditTrailDialog.js.map +1 -1
- package/dist/ggnomad/components/AvailabilityPanel.js.map +1 -1
- package/dist/ggnomad/components/BookingPanel.js +6 -6
- package/dist/ggnomad/components/BookingPanel.js.map +1 -1
- package/dist/ggnomad/components/CreateProviderDialog.js.map +1 -1
- package/dist/ggnomad/components/CreateTravelPolicyDialog.js.map +1 -1
- package/dist/ggnomad/components/GroupTripPanel.js.map +1 -1
- package/dist/ggnomad/components/ListingEditDialog.js.map +1 -1
- package/dist/ggnomad/components/ListingFilters.js.map +1 -1
- package/dist/ggnomad/components/PhotoGallery.js.map +1 -1
- package/dist/ggnomad/components/ProviderCard.js.map +1 -1
- package/dist/ggnomad/components/ProviderDashboardCard.js.map +1 -1
- package/dist/ggnomad/components/RatingSummary.js.map +1 -1
- package/dist/ggnomad/components/RedeemTicketPanel.js.map +1 -1
- package/dist/ggnomad/components/ReviewsSection.js.map +1 -1
- package/dist/ggnomad/components/StatusBadge.js.map +1 -1
- package/dist/ggnomad/components/TicketDialog.js +1 -1
- package/dist/ggnomad/components/TicketDialog.js.map +1 -1
- package/dist/ggnomad/components/index.js +26 -25
- package/dist/ggnomad/index.js +3 -2
- package/dist/ggnomad/pages/ActivityDetailsPage.js.map +1 -1
- package/dist/ggnomad/pages/AnalyticsPage.js.map +1 -1
- package/dist/ggnomad/pages/BookingsPage.js.map +1 -1
- package/dist/ggnomad/pages/DashboardPage.js.map +1 -1
- package/dist/ggnomad/pages/EntertainmentPage.js.map +1 -1
- package/dist/ggnomad/pages/EventDetailsPage.js.map +1 -1
- package/dist/ggnomad/pages/FavoritesPage.js.map +1 -1
- package/dist/ggnomad/pages/HospitalityPage.js.map +1 -1
- package/dist/ggnomad/pages/OperationsQueuePage.js.map +1 -1
- package/dist/ggnomad/pages/PageShell.js.map +1 -1
- package/dist/ggnomad/pages/PropertyDetailsPage.js.map +1 -1
- package/dist/ggnomad/pages/ProviderAnalyticsPage.js.map +1 -1
- package/dist/ggnomad/pages/SearchPage.js +1 -1
- package/dist/ggnomad/pages/SearchPage.js.map +1 -1
- package/dist/ggnomad/pages/StorefrontPage.js.map +1 -1
- package/dist/ggnomad/pages/StorefrontPublicPage.js.map +1 -1
- package/dist/ggnomad/pages/TourDetailsPage.js.map +1 -1
- package/dist/ggnomad/pages/TourismPage.js.map +1 -1
- package/dist/ggnomad/pages/TravelPage.js +1 -1
- package/dist/ggnomad/pages/TravelPage.js.map +1 -1
- package/dist/ggnomad/pages/TravelPolicyPage.js.map +1 -1
- package/dist/ggnomad/pages/TripPlanDetailPage.js.map +1 -1
- package/dist/ggnomad/pages/TripsPage.js.map +1 -1
- package/dist/ggnomad/providers/GGNomadProvider.js.map +1 -1
- package/dist/ggnomad/utils/format.js.map +1 -1
- package/dist/ggnomad/utils/notify.js +1 -1
- package/dist/ggnomad/utils/providerListControls.js +1 -1
- package/dist/ggnomad/utils/providerListControls.js.map +1 -1
- package/dist/ggnomad/utils/providerTypeMeta.js.map +1 -1
- package/dist/utils/pageContext.js +1 -1
- package/dist/utils/pageContext.js.map +1 -1
- package/package.json +18 -17
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TripPlanDetailPage.js","names":[],"sources":["../../../src/ggnomad/pages/TripPlanDetailPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ChevronDown, Plus, Users, X } from 'lucide-react';\nimport { useNavigate, useParams } from 'react-router';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n GlassCard,\n IllustratedEmptyState,\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n ADD_GGNOMAD_TRIP_PLAN_ITEM_MUTATION,\n CREATE_GGNOMAD_BOOKING_MUTATION,\n GGNOMAD_ACTIVITIES_QUERY,\n GGNOMAD_EVENTS_QUERY,\n GGNOMAD_PROPERTIES_QUERY,\n GGNOMAD_TOURS_QUERY,\n GGNOMAD_TRIP_PLAN_QUERY,\n REMOVE_GGNOMAD_TRIP_PLAN_ITEM_MUTATION,\n UPDATE_GGNOMAD_TRIP_PLAN_MUTATION,\n} from '../graphql';\nimport { PageShell } from './PageShell';\nimport { GroupTripPanel } from '../components';\nimport { notify } from '../utils/notify';\nimport { useGGNomad } from '../providers/GGNomadProvider';\nimport { joinPath } from '../utils/navigation';\n\ntype ItemType = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ninterface TripPlanItem {\n id: string;\n itemType: ItemType;\n itemId: string;\n itemName: string;\n day?: number;\n scheduledAt?: string;\n price: number;\n currency: string;\n notes?: string;\n}\n\ninterface AddingItem {\n itemType: ItemType;\n itemId: string;\n itemName: string;\n price: number;\n currency: string;\n}\n\nexport function TripPlanDetailPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const { basePath = '/' } = useGGNomad();\n const { id = '' } = useParams();\n const [addingItem, setAddingItem] = useState<AddingItem | null>(null);\n const [showCheckout, setShowCheckout] = useState(false);\n const [removingItemId, setRemovingItemId] = useState<string | null>(null);\n const [activeTab, setActiveTab] = useState('itinerary');\n\n const { data, refetch } = useQuery(\n GGNOMAD_TRIP_PLAN_QUERY as TypedDocumentNode<\n {\n ggnomadTripPlan: {\n id: string;\n name: string;\n description?: string;\n startDate?: string;\n endDate?: string;\n travelers: number;\n status: string;\n total: number;\n itemCount: number;\n items: TripPlanItem[];\n createdAt: string;\n updatedAt: string;\n } | null;\n },\n { id: string }\n >,\n { variables: { id }, skip: !id }\n );\n\n const tripPlan = data?.ggnomadTripPlan;\n\n const { data: propertiesData } = useQuery(\n GGNOMAD_PROPERTIES_QUERY as TypedDocumentNode<\n {\n ggnomadProperties: Array<{\n id: string;\n name: string;\n pricePerNight: number;\n currency: string;\n }>;\n },\n { limit: number; offset: number }\n >,\n { variables: { limit: 20, offset: 0 } }\n );\n\n const { data: toursData } = useQuery(\n GGNOMAD_TOURS_QUERY as TypedDocumentNode<\n {\n ggnomadTours: Array<{\n id: string;\n name: string;\n price: number;\n currency: string;\n }>;\n },\n { limit: number; offset: number }\n >,\n { variables: { limit: 20, offset: 0 } }\n );\n\n const { data: eventsData } = useQuery(\n GGNOMAD_EVENTS_QUERY as TypedDocumentNode<\n {\n ggnomadEvents: Array<{\n id: string;\n name: string;\n price: number;\n currency: string;\n }>;\n },\n { limit: number; offset: number }\n >,\n { variables: { limit: 20, offset: 0 } }\n );\n\n const { data: activitiesData } = useQuery(\n GGNOMAD_ACTIVITIES_QUERY as TypedDocumentNode<\n {\n ggnomadActivities: Array<{\n id: string;\n name: string;\n price: number;\n currency: string;\n }>;\n },\n { limit: number; offset: number }\n >,\n { variables: { limit: 20, offset: 0 } }\n );\n\n const [addItem, { loading: addingItemMutation }] = useMutation(\n ADD_GGNOMAD_TRIP_PLAN_ITEM_MUTATION as TypedDocumentNode<\n {\n addGgnomadTripPlanItem: TripPlanItem;\n },\n {\n tripPlanId: string;\n input: {\n itemType: ItemType;\n itemId: string;\n itemName: string;\n price: number;\n currency: string;\n };\n }\n >\n );\n\n const [removeItem, { loading: removingItemMutation }] = useMutation(\n REMOVE_GGNOMAD_TRIP_PLAN_ITEM_MUTATION as TypedDocumentNode<\n { removeGgnomadTripPlanItem: boolean },\n { id: string }\n >\n );\n\n const [updateStatus] = useMutation(\n UPDATE_GGNOMAD_TRIP_PLAN_MUTATION as TypedDocumentNode<\n {\n updateGgnomadTripPlan: {\n id: string;\n status: string;\n };\n },\n {\n id: string;\n input: {\n status: string;\n };\n }\n >\n );\n\n const [createBooking] = useMutation(\n CREATE_GGNOMAD_BOOKING_MUTATION as TypedDocumentNode<\n {\n createGgnomadBooking: {\n id: string;\n referenceCode: string;\n itemName: string;\n };\n },\n {\n input: {\n bookingType: ItemType;\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 handleAddItem = async () => {\n if (!addingItem || !tripPlan) return;\n\n try {\n const result = await addItem({\n variables: {\n tripPlanId: tripPlan.id,\n input: {\n itemType: addingItem.itemType,\n itemId: addingItem.itemId,\n itemName: addingItem.itemName,\n price: addingItem.price,\n currency: addingItem.currency,\n },\n },\n });\n\n if (result.error) {\n notify.error(\n t('ggnomad.tripPlan.toast.addItemErrorTitle', 'Could not add item'),\n result.error.message\n );\n return;\n }\n\n notify.success(\n t('ggnomad.tripPlan.toast.addItemSuccessTitle', 'Item added'),\n addingItem.itemName\n );\n\n setAddingItem(null);\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.tripPlan.toast.addItemErrorTitle', 'Could not add item'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const handleRemoveItem = async () => {\n if (!removingItemId) return;\n\n try {\n const result = await removeItem({\n variables: { id: removingItemId },\n });\n\n if (result.error) {\n notify.error(\n t('ggnomad.tripPlan.toast.removeItemErrorTitle', 'Could not remove item'),\n result.error.message\n );\n return;\n }\n\n notify.success(t('ggnomad.tripPlan.toast.removeItemSuccessTitle', 'Item removed'), '');\n\n setRemovingItemId(null);\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.tripPlan.toast.removeItemErrorTitle', 'Could not remove item'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const handleCheckout = async () => {\n if (!tripPlan) return;\n\n try {\n for (const item of tripPlan.items) {\n await createBooking({\n variables: {\n input: {\n bookingType: item.itemType,\n itemId: item.itemId,\n itemName: item.itemName,\n guestName: 'Guest',\n guestEmail: 'guest@burdenoff.com',\n guests: tripPlan.travelers,\n totalPrice: item.price,\n currency: item.currency,\n paymentMethod: 'card',\n },\n },\n });\n }\n\n await updateStatus({\n variables: {\n id: tripPlan.id,\n input: { status: 'BOOKED' },\n },\n });\n\n notify.success(\n t('ggnomad.tripPlan.toast.checkoutSuccessTitle', 'Trip booked!'),\n t('ggnomad.tripPlan.toast.checkoutSuccessBody', 'All items have been booked')\n );\n\n await refetch();\n setShowCheckout(false);\n navigate(joinPath(basePath, 'bookings'));\n } catch (err) {\n notify.error(\n t('ggnomad.tripPlan.toast.checkoutErrorTitle', 'Booking failed'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n if (!tripPlan) {\n return (\n <PageShell\n title={t('ggnomad.tripPlan.loadingTitle', 'Trip Plan')}\n subtitle={t('ggnomad.tripPlan.loadingSubtitle', 'Loading trip plan')}\n >\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.tripPlan.notFoundTitle', 'Trip plan not found')}\n description={t(\n 'ggnomad.tripPlan.notFoundDescription',\n 'This trip may have been deleted or is unavailable.'\n )}\n />\n </PageShell>\n );\n }\n\n const itemsByDay: Record<number, TripPlanItem[]> = {};\n tripPlan.items.forEach((item) => {\n const day = item.day ?? 0;\n if (!itemsByDay[day]) {\n itemsByDay[day] = [];\n }\n itemsByDay[day].push(item);\n });\n\n const sortedDays: number[] = Object.keys(itemsByDay)\n .map((d) => Number(d))\n .sort((a: number, b: number) => a - b);\n\n const properties = propertiesData?.ggnomadProperties ?? [];\n const tours = toursData?.ggnomadTours ?? [];\n const events = eventsData?.ggnomadEvents ?? [];\n const activities = activitiesData?.ggnomadActivities ?? [];\n\n return (\n <PageShell\n title={tripPlan.name}\n subtitle={t('ggnomad.tripPlan.subtitle', 'Build your complete itinerary')}\n >\n <Tabs value={activeTab} onValueChange={setActiveTab} className=\"w-full\">\n <TabsList className=\"mb-6 w-full justify-start\">\n <TabsTrigger value=\"itinerary\">\n {t('ggnomad.tripPlan.tabs.itinerary', 'Itinerary')}\n </TabsTrigger>\n <TabsTrigger value=\"timeline\">\n {t('ggnomad.tripPlan.tabs.timeline', 'Timeline')}\n </TabsTrigger>\n <TabsTrigger value=\"group\">{t('ggnomad.tripPlan.tabs.group', 'Group')}</TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"itinerary\" className=\"space-y-6\">\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-4\">\n {/* Left Rail - Add Items */}\n <div className=\"space-y-4\">\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-4\">\n <h3 className=\"mb-3 flex items-center gap-2 font-medium text-text-primary\">\n <Plus className=\"size-4\" />\n {t('ggnomad.tripPlan.addToTrip', 'Add to trip')}\n </h3>\n\n <div className=\"space-y-3\">\n {/* Stays */}\n <details className=\"group\">\n <summary className=\"flex cursor-pointer items-center justify-between rounded-md bg-bg-canvas-inverted p-2 text-sm font-medium text-text-primary hover:bg-opacity-80\">\n {t('ggnomad.tripPlan.stays', 'Stays')}\n <ChevronDown className=\"size-4 transition-transform group-open:rotate-180\" />\n </summary>\n <div className=\"mt-2 space-y-2\">\n {properties.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n {t('ggnomad.tripPlan.noStaysAvailable', 'No stays available')}\n </p>\n ) : (\n properties.map((property) => (\n <button\n key={property.id}\n onClick={() =>\n setAddingItem({\n itemType: 'PROPERTY',\n itemId: property.id,\n itemName: property.name,\n price: property.pricePerNight,\n currency: property.currency,\n })\n }\n className=\"w-full rounded-md border border-border-seam bg-bg-canvas p-2 text-left text-xs text-text-primary hover:bg-action-primary-bg hover:text-action-primary-text\"\n >\n <div className=\"font-medium\">{property.name}</div>\n <div className=\"text-text-secondary\">\n {property.currency} {property.pricePerNight.toFixed(2)}\n </div>\n </button>\n ))\n )}\n </div>\n </details>\n\n {/* Tours */}\n <details className=\"group\">\n <summary className=\"flex cursor-pointer items-center justify-between rounded-md bg-bg-canvas-inverted p-2 text-sm font-medium text-text-primary hover:bg-opacity-80\">\n {t('ggnomad.tripPlan.tours', 'Tours')}\n <ChevronDown className=\"size-4 transition-transform group-open:rotate-180\" />\n </summary>\n <div className=\"mt-2 space-y-2\">\n {tours.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n {t('ggnomad.tripPlan.noToursAvailable', 'No tours available')}\n </p>\n ) : (\n tours.map((tour) => (\n <button\n key={tour.id}\n onClick={() =>\n setAddingItem({\n itemType: 'TOUR',\n itemId: tour.id,\n itemName: tour.name,\n price: tour.price,\n currency: tour.currency,\n })\n }\n className=\"w-full rounded-md border border-border-seam bg-bg-canvas p-2 text-left text-xs text-text-primary hover:bg-action-primary-bg hover:text-action-primary-text\"\n >\n <div className=\"font-medium\">{tour.name}</div>\n <div className=\"text-text-secondary\">\n {tour.currency} {tour.price.toFixed(2)}\n </div>\n </button>\n ))\n )}\n </div>\n </details>\n\n {/* Events */}\n <details className=\"group\">\n <summary className=\"flex cursor-pointer items-center justify-between rounded-md bg-bg-canvas-inverted p-2 text-sm font-medium text-text-primary hover:bg-opacity-80\">\n {t('ggnomad.tripPlan.events', 'Events')}\n <ChevronDown className=\"size-4 transition-transform group-open:rotate-180\" />\n </summary>\n <div className=\"mt-2 space-y-2\">\n {events.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n {t('ggnomad.tripPlan.noEventsAvailable', 'No events available')}\n </p>\n ) : (\n events.map((event) => (\n <button\n key={event.id}\n onClick={() =>\n setAddingItem({\n itemType: 'EVENT',\n itemId: event.id,\n itemName: event.name,\n price: event.price,\n currency: event.currency,\n })\n }\n className=\"w-full rounded-md border border-border-seam bg-bg-canvas p-2 text-left text-xs text-text-primary hover:bg-action-primary-bg hover:text-action-primary-text\"\n >\n <div className=\"font-medium\">{event.name}</div>\n <div className=\"text-text-secondary\">\n {event.currency} {event.price.toFixed(2)}\n </div>\n </button>\n ))\n )}\n </div>\n </details>\n\n {/* Activities */}\n <details className=\"group\">\n <summary className=\"flex cursor-pointer items-center justify-between rounded-md bg-bg-canvas-inverted p-2 text-sm font-medium text-text-primary hover:bg-opacity-80\">\n {t('ggnomad.tripPlan.activities', 'Activities')}\n <ChevronDown className=\"size-4 transition-transform group-open:rotate-180\" />\n </summary>\n <div className=\"mt-2 space-y-2\">\n {activities.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n {t('ggnomad.tripPlan.noActivitiesAvailable', 'No activities available')}\n </p>\n ) : (\n activities.map((activity) => (\n <button\n key={activity.id}\n onClick={() =>\n setAddingItem({\n itemType: 'ACTIVITY',\n itemId: activity.id,\n itemName: activity.name,\n price: activity.price,\n currency: activity.currency,\n })\n }\n className=\"w-full rounded-md border border-border-seam bg-bg-canvas p-2 text-left text-xs text-text-primary hover:bg-action-primary-bg hover:text-action-primary-text\"\n >\n <div className=\"font-medium\">{activity.name}</div>\n <div className=\"text-text-secondary\">\n {activity.currency} {activity.price.toFixed(2)}\n </div>\n </button>\n ))\n )}\n </div>\n </details>\n </div>\n </GlassCard>\n </div>\n\n {/* Center - Itinerary Items */}\n <div className=\"space-y-4 lg:col-span-2\">\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.tripPlan.itineraryHeader', 'Your itinerary')}\n </h2>\n\n {tripPlan.items.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {t(\n 'ggnomad.tripPlan.noItemsYet',\n 'No items added yet. Start adding stays, tours, events and activities from the left.'\n )}\n </p>\n ) : (\n <div className=\"space-y-6\">\n {sortedDays.map((day: number) => (\n <div key={String(day)}>\n <h3 className=\"mb-2 text-sm font-medium text-text-secondary\">\n {day === 0\n ? t('ggnomad.tripPlan.unscheduled', 'Unscheduled')\n : t('ggnomad.tripPlan.day', 'Day {{day}}').replace(\n '{{day}}',\n String(day)\n )}\n </h3>\n <div className=\"space-y-2\">\n {(itemsByDay[day] || []).map((item: TripPlanItem) => (\n <div\n key={item.id}\n className=\"flex items-start justify-between rounded-md border border-border-seam bg-bg-canvas p-3\"\n >\n <div className=\"flex-1\">\n <p className=\"font-medium text-text-primary\">{item.itemName}</p>\n <p className=\"text-xs text-text-secondary\">\n {item.itemType.charAt(0) + item.itemType.slice(1).toLowerCase()}\n </p>\n <p className=\"mt-1 font-semibold text-status-success-text\">\n {item.currency} {item.price.toFixed(2)}\n </p>\n </div>\n <button\n onClick={() => setRemovingItemId(item.id)}\n className=\"ml-2 rounded-md p-1 text-text-secondary hover:bg-action-danger-bg hover:text-action-danger-text\"\n aria-label={t('ggnomad.tripPlan.removeItem', 'Remove item')}\n >\n <X className=\"size-4\" />\n </button>\n </div>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n </GlassCard>\n </div>\n\n {/* Right - Trip Summary */}\n <div className=\"space-y-4\">\n <GlassCard treatment=\"glass\" glow className=\"sticky top-6 border-border-seam p-5\">\n <h3 className=\"mb-4 font-medium text-text-primary\">\n {t('ggnomad.tripPlan.tripTotal', 'Trip total')}\n </h3>\n\n <div className=\"space-y-3 border-b border-border-seam pb-4\">\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">\n {t('ggnomad.tripPlan.items', 'Items')}\n </span>\n <span className=\"font-medium text-text-primary\">{tripPlan.itemCount}</span>\n </div>\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">\n {t('ggnomad.tripPlan.travelers', 'Travelers')}\n </span>\n <span className=\"font-medium text-text-primary flex items-center gap-1\">\n <Users className=\"size-3\" />\n {tripPlan.travelers}\n </span>\n </div>\n </div>\n\n <div className=\"my-4\">\n <div className=\"text-2xl font-bold text-text-primary\">\n USD {tripPlan.total.toFixed(2)}\n </div>\n {tripPlan.travelers > 0 && (\n <div className=\"mt-3 rounded-md border border-border-seam bg-bg-canvas/50 p-3\">\n <p className=\"text-xs font-medium text-text-secondary\">\n {t('ggnomad.tripPlan.costSplit', 'Split {{count}} ways').replace(\n '{{count}}',\n String(tripPlan.travelers)\n )}\n </p>\n <p className=\"mt-1 text-lg font-semibold text-action-primary-text\">\n USD {(tripPlan.total / tripPlan.travelers).toFixed(2)} each\n </p>\n <p className=\"mt-2 text-xs text-text-secondary\">\n {t(\n 'ggnomad.tripPlan.costSplitNote',\n 'This trip plan is shared with everyone in your workspace.'\n )}\n </p>\n </div>\n )}\n </div>\n\n <div className=\"space-y-2\">\n <button\n onClick={() => setShowCheckout(true)}\n disabled={tripPlan.items.length === 0}\n className=\"w-full rounded-md bg-action-primary-bg px-4 py-2 font-medium text-action-primary-text hover:bg-action-primary-bgHover disabled:opacity-50\"\n >\n {t('ggnomad.tripPlan.checkout', 'Checkout')}\n </button>\n\n <div className=\"flex items-center gap-2\">\n <select\n value={tripPlan.status}\n onChange={async (e) => {\n await updateStatus({\n variables: {\n id: tripPlan.id,\n input: { status: e.target.value },\n },\n });\n await refetch();\n }}\n className=\"flex-1 rounded-md border border-border-seam bg-bg-canvas px-2 py-1 text-sm text-text-primary\"\n >\n <option value=\"DRAFT\">Draft</option>\n <option value=\"PLANNED\">Planned</option>\n <option value=\"BOOKED\">Booked</option>\n <option value=\"CONFIRMED\">Confirmed</option>\n <option value=\"COMPLETED\">Completed</option>\n <option value=\"CANCELLED\">Cancelled</option>\n </select>\n </div>\n </div>\n </GlassCard>\n </div>\n </div>\n </TabsContent>\n\n <TabsContent value=\"timeline\" className=\"space-y-4\">\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.tripPlan.timeline', 'Trip timeline')}\n </h2>\n\n <div className=\"space-y-6\">\n {/* Plan Status Milestone */}\n <div className=\"flex gap-4\">\n <div className=\"flex flex-col items-center\">\n <div className=\"size-3 rounded-full bg-status-info-bg\" />\n <div className=\"my-2 h-8 w-0.5 bg-border-seam\" />\n </div>\n <div>\n <h3 className=\"font-medium text-text-primary\">\n {t('ggnomad.tripPlan.milestone.started', 'Trip started')}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t('ggnomad.tripPlan.milestone.startedDesc', 'Created your itinerary')}\n </p>\n </div>\n </div>\n\n {/* Items Timeline */}\n {tripPlan.items.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.tripPlan.noItemsInTimeline', 'No items in your timeline yet')}\n </p>\n ) : (\n tripPlan.items.map((item, index) => (\n <div key={item.id} className=\"flex gap-4\">\n <div className=\"flex flex-col items-center\">\n <div\n className={`size-3 rounded-full ${\n tripPlan.status === 'BOOKED' || tripPlan.status === 'COMPLETED'\n ? 'bg-status-success-bg'\n : 'bg-status-warning-bg'\n }`}\n />\n {index < tripPlan.items.length - 1 && (\n <div className=\"my-2 h-8 w-0.5 bg-border-seam\" />\n )}\n </div>\n <div>\n <h3 className=\"font-medium text-text-primary\">{item.itemName}</h3>\n <p className=\"text-xs text-text-secondary\">\n {item.itemType.charAt(0) + item.itemType.slice(1).toLowerCase()}\n {item.day && ` · Day ${item.day}`}\n </p>\n <p className=\"mt-1 text-sm font-semibold text-status-success-text\">\n {item.currency} {item.price.toFixed(2)}\n </p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {tripPlan.status === 'BOOKED' || tripPlan.status === 'COMPLETED'\n ? t('ggnomad.tripPlan.milestone.booked', 'Booked')\n : t('ggnomad.tripPlan.milestone.planned', 'Planned')}\n </p>\n </div>\n </div>\n ))\n )}\n\n {/* Final Status */}\n {tripPlan.items.length > 0 && (\n <>\n <div className=\"flex gap-4\">\n <div className=\"flex flex-col items-center\">\n <div\n className={`size-3 rounded-full ${\n tripPlan.status === 'COMPLETED'\n ? 'bg-status-success-bg'\n : 'bg-status-secondary-bg'\n }`}\n />\n </div>\n <div>\n <h3 className=\"font-medium text-text-primary\">\n {tripPlan.status === 'COMPLETED'\n ? t('ggnomad.tripPlan.milestone.completed', 'Trip completed')\n : t('ggnomad.tripPlan.milestone.confirming', 'Awaiting completion')}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {tripPlan.status === 'COMPLETED'\n ? t('ggnomad.tripPlan.milestone.completedDesc', 'Your trip is complete')\n : t(\n 'ggnomad.tripPlan.milestone.confirmingDesc',\n 'Update status as your trip progresses'\n )}\n </p>\n </div>\n </div>\n </>\n )}\n </div>\n </GlassCard>\n </TabsContent>\n\n <TabsContent value=\"group\" className=\"space-y-4\">\n <GroupTripPanel\n tripPlanId={tripPlan.id}\n tripPlanName={tripPlan.name}\n items={tripPlan.items.map((item) => ({\n id: item.id,\n itemName: item.itemName,\n price: item.price,\n currency: item.currency,\n }))}\n />\n </TabsContent>\n </Tabs>\n\n {/* Add Item Dialog */}\n <AlertDialog open={addingItem !== null} onOpenChange={(open) => !open && setAddingItem(null)}>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.tripPlan.addItemDialog.title', 'Add to itinerary?')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {addingItem && (\n <>\n {addingItem.itemName}\n {' · '}\n {addingItem.currency} {addingItem.price.toFixed(2)}\n </>\n )}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={addingItemMutation}>\n {t('ggnomad.tripPlan.addItemDialog.cancel', 'Cancel')}\n </AlertDialogCancel>\n <AlertDialogAction\n disabled={addingItemMutation}\n onClick={(event) => {\n event.preventDefault();\n void handleAddItem();\n }}\n >\n {addingItemMutation\n ? t('ggnomad.tripPlan.addItemDialog.adding', 'Adding…')\n : t('ggnomad.tripPlan.addItemDialog.add', 'Add item')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n\n {/* Remove Item Dialog */}\n <AlertDialog\n open={removingItemId !== null}\n onOpenChange={(open) => !open && setRemovingItemId(null)}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.tripPlan.removeItemDialog.title', 'Remove item from trip?')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.tripPlan.removeItemDialog.description',\n 'This item will be removed from your itinerary.'\n )}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={removingItemMutation}>\n {t('ggnomad.tripPlan.removeItemDialog.cancel', 'Keep item')}\n </AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-action-danger-bg text-action-danger-text hover:bg-action-danger-bgHover\"\n disabled={removingItemMutation}\n onClick={(event) => {\n event.preventDefault();\n void handleRemoveItem();\n }}\n >\n {removingItemMutation\n ? t('ggnomad.tripPlan.removeItemDialog.removing', 'Removing…')\n : t('ggnomad.tripPlan.removeItemDialog.remove', 'Remove item')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n\n {/* Checkout Dialog */}\n <AlertDialog open={showCheckout} onOpenChange={setShowCheckout}>\n <AlertDialogContent className=\"max-w-2xl\">\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.tripPlan.checkoutDialog.title', 'Review and book')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.tripPlan.checkoutDialog.description',\n 'Review your items and confirm to book everything at once.'\n )}\n </AlertDialogDescription>\n </AlertDialogHeader>\n\n <div className=\"space-y-4 max-h-96 overflow-y-auto\">\n {tripPlan.items.map((item) => (\n <div\n key={item.id}\n className=\"flex items-center justify-between rounded-md border border-border-seam bg-bg-canvas p-3\"\n >\n <div>\n <p className=\"font-medium text-text-primary\">{item.itemName}</p>\n <p className=\"text-xs text-text-secondary\">\n {item.itemType.charAt(0) + item.itemType.slice(1).toLowerCase()}\n </p>\n </div>\n <p className=\"font-semibold text-text-primary\">\n {item.currency} {item.price.toFixed(2)}\n </p>\n </div>\n ))}\n\n <div className=\"space-y-2 border-t border-border-seam pt-4\">\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-text-secondary\">Subtotal</span>\n <span className=\"text-text-primary\">USD {tripPlan.total.toFixed(2)}</span>\n </div>\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-text-secondary\">Taxes (10%)</span>\n <span className=\"text-text-primary\">USD {(tripPlan.total * 0.1).toFixed(2)}</span>\n </div>\n <div className=\"flex justify-between border-t border-border-seam pt-2 text-lg font-bold\">\n <span className=\"text-text-primary\">Total</span>\n <span className=\"text-status-success-text\">\n USD {(tripPlan.total * 1.1).toFixed(2)}\n </span>\n </div>\n </div>\n </div>\n\n <AlertDialogFooter>\n <AlertDialogCancel disabled={false}>\n {t('ggnomad.tripPlan.checkoutDialog.cancel', 'Cancel')}\n </AlertDialogCancel>\n <AlertDialogAction\n onClick={(event) => {\n event.preventDefault();\n void handleCheckout();\n }}\n >\n {t('ggnomad.tripPlan.checkoutDialog.confirm', 'Confirm & book all')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TripPlanDetailPage.js","names":[],"sources":["../../../src/ggnomad/pages/TripPlanDetailPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ChevronDown, Plus, Users, X } from 'lucide-react';\nimport { useNavigate, useParams } from 'react-router';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n GlassCard,\n IllustratedEmptyState,\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n ADD_GGNOMAD_TRIP_PLAN_ITEM_MUTATION,\n CREATE_GGNOMAD_BOOKING_MUTATION,\n GGNOMAD_ACTIVITIES_QUERY,\n GGNOMAD_EVENTS_QUERY,\n GGNOMAD_PROPERTIES_QUERY,\n GGNOMAD_TOURS_QUERY,\n GGNOMAD_TRIP_PLAN_QUERY,\n REMOVE_GGNOMAD_TRIP_PLAN_ITEM_MUTATION,\n UPDATE_GGNOMAD_TRIP_PLAN_MUTATION,\n} from '../graphql';\nimport { PageShell } from './PageShell';\nimport { GroupTripPanel } from '../components';\nimport { notify } from '../utils/notify';\nimport { useGGNomad } from '../providers/GGNomadProvider';\nimport { joinPath } from '../utils/navigation';\n\ntype ItemType = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ninterface TripPlanItem {\n id: string;\n itemType: ItemType;\n itemId: string;\n itemName: string;\n day?: number;\n scheduledAt?: string;\n price: number;\n currency: string;\n notes?: string;\n}\n\ninterface AddingItem {\n itemType: ItemType;\n itemId: string;\n itemName: string;\n price: number;\n currency: string;\n}\n\nexport function TripPlanDetailPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const { basePath = '/' } = useGGNomad();\n const { id = '' } = useParams();\n const [addingItem, setAddingItem] = useState<AddingItem | null>(null);\n const [showCheckout, setShowCheckout] = useState(false);\n const [removingItemId, setRemovingItemId] = useState<string | null>(null);\n const [activeTab, setActiveTab] = useState('itinerary');\n\n const { data, refetch } = useQuery(\n GGNOMAD_TRIP_PLAN_QUERY as TypedDocumentNode<\n {\n ggnomadTripPlan: {\n id: string;\n name: string;\n description?: string;\n startDate?: string;\n endDate?: string;\n travelers: number;\n status: string;\n total: number;\n itemCount: number;\n items: TripPlanItem[];\n createdAt: string;\n updatedAt: string;\n } | null;\n },\n { id: string }\n >,\n { variables: { id }, skip: !id }\n );\n\n const tripPlan = data?.ggnomadTripPlan;\n\n const { data: propertiesData } = useQuery(\n GGNOMAD_PROPERTIES_QUERY as TypedDocumentNode<\n {\n ggnomadProperties: Array<{\n id: string;\n name: string;\n pricePerNight: number;\n currency: string;\n }>;\n },\n { limit: number; offset: number }\n >,\n { variables: { limit: 20, offset: 0 } }\n );\n\n const { data: toursData } = useQuery(\n GGNOMAD_TOURS_QUERY as TypedDocumentNode<\n {\n ggnomadTours: Array<{\n id: string;\n name: string;\n price: number;\n currency: string;\n }>;\n },\n { limit: number; offset: number }\n >,\n { variables: { limit: 20, offset: 0 } }\n );\n\n const { data: eventsData } = useQuery(\n GGNOMAD_EVENTS_QUERY as TypedDocumentNode<\n {\n ggnomadEvents: Array<{\n id: string;\n name: string;\n price: number;\n currency: string;\n }>;\n },\n { limit: number; offset: number }\n >,\n { variables: { limit: 20, offset: 0 } }\n );\n\n const { data: activitiesData } = useQuery(\n GGNOMAD_ACTIVITIES_QUERY as TypedDocumentNode<\n {\n ggnomadActivities: Array<{\n id: string;\n name: string;\n price: number;\n currency: string;\n }>;\n },\n { limit: number; offset: number }\n >,\n { variables: { limit: 20, offset: 0 } }\n );\n\n const [addItem, { loading: addingItemMutation }] = useMutation(\n ADD_GGNOMAD_TRIP_PLAN_ITEM_MUTATION as TypedDocumentNode<\n {\n addGgnomadTripPlanItem: TripPlanItem;\n },\n {\n tripPlanId: string;\n input: {\n itemType: ItemType;\n itemId: string;\n itemName: string;\n price: number;\n currency: string;\n };\n }\n >\n );\n\n const [removeItem, { loading: removingItemMutation }] = useMutation(\n REMOVE_GGNOMAD_TRIP_PLAN_ITEM_MUTATION as TypedDocumentNode<\n { removeGgnomadTripPlanItem: boolean },\n { id: string }\n >\n );\n\n const [updateStatus] = useMutation(\n UPDATE_GGNOMAD_TRIP_PLAN_MUTATION as TypedDocumentNode<\n {\n updateGgnomadTripPlan: {\n id: string;\n status: string;\n };\n },\n {\n id: string;\n input: {\n status: string;\n };\n }\n >\n );\n\n const [createBooking] = useMutation(\n CREATE_GGNOMAD_BOOKING_MUTATION as TypedDocumentNode<\n {\n createGgnomadBooking: {\n id: string;\n referenceCode: string;\n itemName: string;\n };\n },\n {\n input: {\n bookingType: ItemType;\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 handleAddItem = async () => {\n if (!addingItem || !tripPlan) return;\n\n try {\n const result = await addItem({\n variables: {\n tripPlanId: tripPlan.id,\n input: {\n itemType: addingItem.itemType,\n itemId: addingItem.itemId,\n itemName: addingItem.itemName,\n price: addingItem.price,\n currency: addingItem.currency,\n },\n },\n });\n\n if (result.error) {\n notify.error(\n t('ggnomad.tripPlan.toast.addItemErrorTitle', 'Could not add item'),\n result.error.message\n );\n return;\n }\n\n notify.success(\n t('ggnomad.tripPlan.toast.addItemSuccessTitle', 'Item added'),\n addingItem.itemName\n );\n\n setAddingItem(null);\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.tripPlan.toast.addItemErrorTitle', 'Could not add item'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const handleRemoveItem = async () => {\n if (!removingItemId) return;\n\n try {\n const result = await removeItem({\n variables: { id: removingItemId },\n });\n\n if (result.error) {\n notify.error(\n t('ggnomad.tripPlan.toast.removeItemErrorTitle', 'Could not remove item'),\n result.error.message\n );\n return;\n }\n\n notify.success(t('ggnomad.tripPlan.toast.removeItemSuccessTitle', 'Item removed'), '');\n\n setRemovingItemId(null);\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.tripPlan.toast.removeItemErrorTitle', 'Could not remove item'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const handleCheckout = async () => {\n if (!tripPlan) return;\n\n try {\n for (const item of tripPlan.items) {\n await createBooking({\n variables: {\n input: {\n bookingType: item.itemType,\n itemId: item.itemId,\n itemName: item.itemName,\n guestName: 'Guest',\n guestEmail: 'guest@burdenoff.com',\n guests: tripPlan.travelers,\n totalPrice: item.price,\n currency: item.currency,\n paymentMethod: 'card',\n },\n },\n });\n }\n\n await updateStatus({\n variables: {\n id: tripPlan.id,\n input: { status: 'BOOKED' },\n },\n });\n\n notify.success(\n t('ggnomad.tripPlan.toast.checkoutSuccessTitle', 'Trip booked!'),\n t('ggnomad.tripPlan.toast.checkoutSuccessBody', 'All items have been booked')\n );\n\n await refetch();\n setShowCheckout(false);\n navigate(joinPath(basePath, 'bookings'));\n } catch (err) {\n notify.error(\n t('ggnomad.tripPlan.toast.checkoutErrorTitle', 'Booking failed'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n if (!tripPlan) {\n return (\n <PageShell\n title={t('ggnomad.tripPlan.loadingTitle', 'Trip Plan')}\n subtitle={t('ggnomad.tripPlan.loadingSubtitle', 'Loading trip plan')}\n >\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.tripPlan.notFoundTitle', 'Trip plan not found')}\n description={t(\n 'ggnomad.tripPlan.notFoundDescription',\n 'This trip may have been deleted or is unavailable.'\n )}\n />\n </PageShell>\n );\n }\n\n const itemsByDay: Record<number, TripPlanItem[]> = {};\n tripPlan.items.forEach((item) => {\n const day = item.day ?? 0;\n if (!itemsByDay[day]) {\n itemsByDay[day] = [];\n }\n itemsByDay[day].push(item);\n });\n\n const sortedDays: number[] = Object.keys(itemsByDay)\n .map((d) => Number(d))\n .sort((a: number, b: number) => a - b);\n\n const properties = propertiesData?.ggnomadProperties ?? [];\n const tours = toursData?.ggnomadTours ?? [];\n const events = eventsData?.ggnomadEvents ?? [];\n const activities = activitiesData?.ggnomadActivities ?? [];\n\n return (\n <PageShell\n title={tripPlan.name}\n subtitle={t('ggnomad.tripPlan.subtitle', 'Build your complete itinerary')}\n >\n <Tabs value={activeTab} onValueChange={setActiveTab} className=\"w-full\">\n <TabsList className=\"mb-6 w-full justify-start\">\n <TabsTrigger value=\"itinerary\">\n {t('ggnomad.tripPlan.tabs.itinerary', 'Itinerary')}\n </TabsTrigger>\n <TabsTrigger value=\"timeline\">\n {t('ggnomad.tripPlan.tabs.timeline', 'Timeline')}\n </TabsTrigger>\n <TabsTrigger value=\"group\">{t('ggnomad.tripPlan.tabs.group', 'Group')}</TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"itinerary\" className=\"space-y-6\">\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-4\">\n {/* Left Rail - Add Items */}\n <div className=\"space-y-4\">\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-4\">\n <h3 className=\"mb-3 flex items-center gap-2 font-medium text-text-primary\">\n <Plus className=\"size-4\" />\n {t('ggnomad.tripPlan.addToTrip', 'Add to trip')}\n </h3>\n\n <div className=\"space-y-3\">\n {/* Stays */}\n <details className=\"group\">\n <summary className=\"flex cursor-pointer items-center justify-between rounded-md bg-bg-canvas-inverted p-2 text-sm font-medium text-text-primary hover:bg-opacity-80\">\n {t('ggnomad.tripPlan.stays', 'Stays')}\n <ChevronDown className=\"size-4 transition-transform group-open:rotate-180\" />\n </summary>\n <div className=\"mt-2 space-y-2\">\n {properties.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n {t('ggnomad.tripPlan.noStaysAvailable', 'No stays available')}\n </p>\n ) : (\n properties.map((property) => (\n <button\n key={property.id}\n onClick={() =>\n setAddingItem({\n itemType: 'PROPERTY',\n itemId: property.id,\n itemName: property.name,\n price: property.pricePerNight,\n currency: property.currency,\n })\n }\n className=\"w-full rounded-md border border-border-seam bg-bg-canvas p-2 text-left text-xs text-text-primary hover:bg-action-primary-bg hover:text-action-primary-text\"\n >\n <div className=\"font-medium\">{property.name}</div>\n <div className=\"text-text-secondary\">\n {property.currency} {property.pricePerNight.toFixed(2)}\n </div>\n </button>\n ))\n )}\n </div>\n </details>\n\n {/* Tours */}\n <details className=\"group\">\n <summary className=\"flex cursor-pointer items-center justify-between rounded-md bg-bg-canvas-inverted p-2 text-sm font-medium text-text-primary hover:bg-opacity-80\">\n {t('ggnomad.tripPlan.tours', 'Tours')}\n <ChevronDown className=\"size-4 transition-transform group-open:rotate-180\" />\n </summary>\n <div className=\"mt-2 space-y-2\">\n {tours.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n {t('ggnomad.tripPlan.noToursAvailable', 'No tours available')}\n </p>\n ) : (\n tours.map((tour) => (\n <button\n key={tour.id}\n onClick={() =>\n setAddingItem({\n itemType: 'TOUR',\n itemId: tour.id,\n itemName: tour.name,\n price: tour.price,\n currency: tour.currency,\n })\n }\n className=\"w-full rounded-md border border-border-seam bg-bg-canvas p-2 text-left text-xs text-text-primary hover:bg-action-primary-bg hover:text-action-primary-text\"\n >\n <div className=\"font-medium\">{tour.name}</div>\n <div className=\"text-text-secondary\">\n {tour.currency} {tour.price.toFixed(2)}\n </div>\n </button>\n ))\n )}\n </div>\n </details>\n\n {/* Events */}\n <details className=\"group\">\n <summary className=\"flex cursor-pointer items-center justify-between rounded-md bg-bg-canvas-inverted p-2 text-sm font-medium text-text-primary hover:bg-opacity-80\">\n {t('ggnomad.tripPlan.events', 'Events')}\n <ChevronDown className=\"size-4 transition-transform group-open:rotate-180\" />\n </summary>\n <div className=\"mt-2 space-y-2\">\n {events.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n {t('ggnomad.tripPlan.noEventsAvailable', 'No events available')}\n </p>\n ) : (\n events.map((event) => (\n <button\n key={event.id}\n onClick={() =>\n setAddingItem({\n itemType: 'EVENT',\n itemId: event.id,\n itemName: event.name,\n price: event.price,\n currency: event.currency,\n })\n }\n className=\"w-full rounded-md border border-border-seam bg-bg-canvas p-2 text-left text-xs text-text-primary hover:bg-action-primary-bg hover:text-action-primary-text\"\n >\n <div className=\"font-medium\">{event.name}</div>\n <div className=\"text-text-secondary\">\n {event.currency} {event.price.toFixed(2)}\n </div>\n </button>\n ))\n )}\n </div>\n </details>\n\n {/* Activities */}\n <details className=\"group\">\n <summary className=\"flex cursor-pointer items-center justify-between rounded-md bg-bg-canvas-inverted p-2 text-sm font-medium text-text-primary hover:bg-opacity-80\">\n {t('ggnomad.tripPlan.activities', 'Activities')}\n <ChevronDown className=\"size-4 transition-transform group-open:rotate-180\" />\n </summary>\n <div className=\"mt-2 space-y-2\">\n {activities.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n {t('ggnomad.tripPlan.noActivitiesAvailable', 'No activities available')}\n </p>\n ) : (\n activities.map((activity) => (\n <button\n key={activity.id}\n onClick={() =>\n setAddingItem({\n itemType: 'ACTIVITY',\n itemId: activity.id,\n itemName: activity.name,\n price: activity.price,\n currency: activity.currency,\n })\n }\n className=\"w-full rounded-md border border-border-seam bg-bg-canvas p-2 text-left text-xs text-text-primary hover:bg-action-primary-bg hover:text-action-primary-text\"\n >\n <div className=\"font-medium\">{activity.name}</div>\n <div className=\"text-text-secondary\">\n {activity.currency} {activity.price.toFixed(2)}\n </div>\n </button>\n ))\n )}\n </div>\n </details>\n </div>\n </GlassCard>\n </div>\n\n {/* Center - Itinerary Items */}\n <div className=\"space-y-4 lg:col-span-2\">\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.tripPlan.itineraryHeader', 'Your itinerary')}\n </h2>\n\n {tripPlan.items.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {t(\n 'ggnomad.tripPlan.noItemsYet',\n 'No items added yet. Start adding stays, tours, events and activities from the left.'\n )}\n </p>\n ) : (\n <div className=\"space-y-6\">\n {sortedDays.map((day: number) => (\n <div key={String(day)}>\n <h3 className=\"mb-2 text-sm font-medium text-text-secondary\">\n {day === 0\n ? t('ggnomad.tripPlan.unscheduled', 'Unscheduled')\n : t('ggnomad.tripPlan.day', 'Day {{day}}').replace(\n '{{day}}',\n String(day)\n )}\n </h3>\n <div className=\"space-y-2\">\n {(itemsByDay[day] || []).map((item: TripPlanItem) => (\n <div\n key={item.id}\n className=\"flex items-start justify-between rounded-md border border-border-seam bg-bg-canvas p-3\"\n >\n <div className=\"flex-1\">\n <p className=\"font-medium text-text-primary\">{item.itemName}</p>\n <p className=\"text-xs text-text-secondary\">\n {item.itemType.charAt(0) + item.itemType.slice(1).toLowerCase()}\n </p>\n <p className=\"mt-1 font-semibold text-status-success-text\">\n {item.currency} {item.price.toFixed(2)}\n </p>\n </div>\n <button\n onClick={() => setRemovingItemId(item.id)}\n className=\"ml-2 rounded-md p-1 text-text-secondary hover:bg-action-danger-bg hover:text-action-danger-text\"\n aria-label={t('ggnomad.tripPlan.removeItem', 'Remove item')}\n >\n <X className=\"size-4\" />\n </button>\n </div>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n </GlassCard>\n </div>\n\n {/* Right - Trip Summary */}\n <div className=\"space-y-4\">\n <GlassCard treatment=\"glass\" glow className=\"sticky top-6 border-border-seam p-5\">\n <h3 className=\"mb-4 font-medium text-text-primary\">\n {t('ggnomad.tripPlan.tripTotal', 'Trip total')}\n </h3>\n\n <div className=\"space-y-3 border-b border-border-seam pb-4\">\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">\n {t('ggnomad.tripPlan.items', 'Items')}\n </span>\n <span className=\"font-medium text-text-primary\">{tripPlan.itemCount}</span>\n </div>\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">\n {t('ggnomad.tripPlan.travelers', 'Travelers')}\n </span>\n <span className=\"font-medium text-text-primary flex items-center gap-1\">\n <Users className=\"size-3\" />\n {tripPlan.travelers}\n </span>\n </div>\n </div>\n\n <div className=\"my-4\">\n <div className=\"text-2xl font-bold text-text-primary\">\n USD {tripPlan.total.toFixed(2)}\n </div>\n {tripPlan.travelers > 0 && (\n <div className=\"mt-3 rounded-md border border-border-seam bg-bg-canvas/50 p-3\">\n <p className=\"text-xs font-medium text-text-secondary\">\n {t('ggnomad.tripPlan.costSplit', 'Split {{count}} ways').replace(\n '{{count}}',\n String(tripPlan.travelers)\n )}\n </p>\n <p className=\"mt-1 text-lg font-semibold text-action-primary-text\">\n USD {(tripPlan.total / tripPlan.travelers).toFixed(2)} each\n </p>\n <p className=\"mt-2 text-xs text-text-secondary\">\n {t(\n 'ggnomad.tripPlan.costSplitNote',\n 'This trip plan is shared with everyone in your workspace.'\n )}\n </p>\n </div>\n )}\n </div>\n\n <div className=\"space-y-2\">\n <button\n onClick={() => setShowCheckout(true)}\n disabled={tripPlan.items.length === 0}\n className=\"w-full rounded-md bg-action-primary-bg px-4 py-2 font-medium text-action-primary-text hover:bg-action-primary-bgHover disabled:opacity-50\"\n >\n {t('ggnomad.tripPlan.checkout', 'Checkout')}\n </button>\n\n <div className=\"flex items-center gap-2\">\n <select\n value={tripPlan.status}\n onChange={async (e) => {\n await updateStatus({\n variables: {\n id: tripPlan.id,\n input: { status: e.target.value },\n },\n });\n await refetch();\n }}\n className=\"flex-1 rounded-md border border-border-seam bg-bg-canvas px-2 py-1 text-sm text-text-primary\"\n >\n <option value=\"DRAFT\">Draft</option>\n <option value=\"PLANNED\">Planned</option>\n <option value=\"BOOKED\">Booked</option>\n <option value=\"CONFIRMED\">Confirmed</option>\n <option value=\"COMPLETED\">Completed</option>\n <option value=\"CANCELLED\">Cancelled</option>\n </select>\n </div>\n </div>\n </GlassCard>\n </div>\n </div>\n </TabsContent>\n\n <TabsContent value=\"timeline\" className=\"space-y-4\">\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.tripPlan.timeline', 'Trip timeline')}\n </h2>\n\n <div className=\"space-y-6\">\n {/* Plan Status Milestone */}\n <div className=\"flex gap-4\">\n <div className=\"flex flex-col items-center\">\n <div className=\"size-3 rounded-full bg-status-info-bg\" />\n <div className=\"my-2 h-8 w-0.5 bg-border-seam\" />\n </div>\n <div>\n <h3 className=\"font-medium text-text-primary\">\n {t('ggnomad.tripPlan.milestone.started', 'Trip started')}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t('ggnomad.tripPlan.milestone.startedDesc', 'Created your itinerary')}\n </p>\n </div>\n </div>\n\n {/* Items Timeline */}\n {tripPlan.items.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.tripPlan.noItemsInTimeline', 'No items in your timeline yet')}\n </p>\n ) : (\n tripPlan.items.map((item, index) => (\n <div key={item.id} className=\"flex gap-4\">\n <div className=\"flex flex-col items-center\">\n <div\n className={`size-3 rounded-full ${\n tripPlan.status === 'BOOKED' || tripPlan.status === 'COMPLETED'\n ? 'bg-status-success-bg'\n : 'bg-status-warning-bg'\n }`}\n />\n {index < tripPlan.items.length - 1 && (\n <div className=\"my-2 h-8 w-0.5 bg-border-seam\" />\n )}\n </div>\n <div>\n <h3 className=\"font-medium text-text-primary\">{item.itemName}</h3>\n <p className=\"text-xs text-text-secondary\">\n {item.itemType.charAt(0) + item.itemType.slice(1).toLowerCase()}\n {item.day && ` · Day ${item.day}`}\n </p>\n <p className=\"mt-1 text-sm font-semibold text-status-success-text\">\n {item.currency} {item.price.toFixed(2)}\n </p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {tripPlan.status === 'BOOKED' || tripPlan.status === 'COMPLETED'\n ? t('ggnomad.tripPlan.milestone.booked', 'Booked')\n : t('ggnomad.tripPlan.milestone.planned', 'Planned')}\n </p>\n </div>\n </div>\n ))\n )}\n\n {/* Final Status */}\n {tripPlan.items.length > 0 && (\n <>\n <div className=\"flex gap-4\">\n <div className=\"flex flex-col items-center\">\n <div\n className={`size-3 rounded-full ${\n tripPlan.status === 'COMPLETED'\n ? 'bg-status-success-bg'\n : 'bg-status-secondary-bg'\n }`}\n />\n </div>\n <div>\n <h3 className=\"font-medium text-text-primary\">\n {tripPlan.status === 'COMPLETED'\n ? t('ggnomad.tripPlan.milestone.completed', 'Trip completed')\n : t('ggnomad.tripPlan.milestone.confirming', 'Awaiting completion')}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {tripPlan.status === 'COMPLETED'\n ? t('ggnomad.tripPlan.milestone.completedDesc', 'Your trip is complete')\n : t(\n 'ggnomad.tripPlan.milestone.confirmingDesc',\n 'Update status as your trip progresses'\n )}\n </p>\n </div>\n </div>\n </>\n )}\n </div>\n </GlassCard>\n </TabsContent>\n\n <TabsContent value=\"group\" className=\"space-y-4\">\n <GroupTripPanel\n tripPlanId={tripPlan.id}\n tripPlanName={tripPlan.name}\n items={tripPlan.items.map((item) => ({\n id: item.id,\n itemName: item.itemName,\n price: item.price,\n currency: item.currency,\n }))}\n />\n </TabsContent>\n </Tabs>\n\n {/* Add Item Dialog */}\n <AlertDialog open={addingItem !== null} onOpenChange={(open) => !open && setAddingItem(null)}>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.tripPlan.addItemDialog.title', 'Add to itinerary?')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {addingItem && (\n <>\n {addingItem.itemName}\n {' · '}\n {addingItem.currency} {addingItem.price.toFixed(2)}\n </>\n )}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={addingItemMutation}>\n {t('ggnomad.tripPlan.addItemDialog.cancel', 'Cancel')}\n </AlertDialogCancel>\n <AlertDialogAction\n disabled={addingItemMutation}\n onClick={(event) => {\n event.preventDefault();\n void handleAddItem();\n }}\n >\n {addingItemMutation\n ? t('ggnomad.tripPlan.addItemDialog.adding', 'Adding…')\n : t('ggnomad.tripPlan.addItemDialog.add', 'Add item')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n\n {/* Remove Item Dialog */}\n <AlertDialog\n open={removingItemId !== null}\n onOpenChange={(open) => !open && setRemovingItemId(null)}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.tripPlan.removeItemDialog.title', 'Remove item from trip?')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.tripPlan.removeItemDialog.description',\n 'This item will be removed from your itinerary.'\n )}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={removingItemMutation}>\n {t('ggnomad.tripPlan.removeItemDialog.cancel', 'Keep item')}\n </AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-action-danger-bg text-action-danger-text hover:bg-action-danger-bgHover\"\n disabled={removingItemMutation}\n onClick={(event) => {\n event.preventDefault();\n void handleRemoveItem();\n }}\n >\n {removingItemMutation\n ? t('ggnomad.tripPlan.removeItemDialog.removing', 'Removing…')\n : t('ggnomad.tripPlan.removeItemDialog.remove', 'Remove item')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n\n {/* Checkout Dialog */}\n <AlertDialog open={showCheckout} onOpenChange={setShowCheckout}>\n <AlertDialogContent className=\"max-w-2xl\">\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.tripPlan.checkoutDialog.title', 'Review and book')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.tripPlan.checkoutDialog.description',\n 'Review your items and confirm to book everything at once.'\n )}\n </AlertDialogDescription>\n </AlertDialogHeader>\n\n <div className=\"space-y-4 max-h-96 overflow-y-auto\">\n {tripPlan.items.map((item) => (\n <div\n key={item.id}\n className=\"flex items-center justify-between rounded-md border border-border-seam bg-bg-canvas p-3\"\n >\n <div>\n <p className=\"font-medium text-text-primary\">{item.itemName}</p>\n <p className=\"text-xs text-text-secondary\">\n {item.itemType.charAt(0) + item.itemType.slice(1).toLowerCase()}\n </p>\n </div>\n <p className=\"font-semibold text-text-primary\">\n {item.currency} {item.price.toFixed(2)}\n </p>\n </div>\n ))}\n\n <div className=\"space-y-2 border-t border-border-seam pt-4\">\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-text-secondary\">Subtotal</span>\n <span className=\"text-text-primary\">USD {tripPlan.total.toFixed(2)}</span>\n </div>\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-text-secondary\">Taxes (10%)</span>\n <span className=\"text-text-primary\">USD {(tripPlan.total * 0.1).toFixed(2)}</span>\n </div>\n <div className=\"flex justify-between border-t border-border-seam pt-2 text-lg font-bold\">\n <span className=\"text-text-primary\">Total</span>\n <span className=\"text-status-success-text\">\n USD {(tripPlan.total * 1.1).toFixed(2)}\n </span>\n </div>\n </div>\n </div>\n\n <AlertDialogFooter>\n <AlertDialogCancel disabled={false}>\n {t('ggnomad.tripPlan.checkoutDialog.cancel', 'Cancel')}\n </AlertDialogCancel>\n <AlertDialogAction\n onClick={(event) => {\n event.preventDefault();\n void handleCheckout();\n }}\n >\n {t('ggnomad.tripPlan.checkoutDialog.confirm', 'Confirm & book all')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TripsPage.js","names":[],"sources":["../../../src/ggnomad/pages/TripsPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Calendar, Map, Plus } from 'lucide-react';\nimport { useNavigate } from 'react-router';\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 { CREATE_GGNOMAD_TRIP_PLAN_MUTATION, GGNOMAD_TRIP_PLANS_QUERY } from '../graphql';\nimport { PageShell } from './PageShell';\nimport { EntityCard, StatusBadge } from '../components';\nimport { notify } from '../utils/notify';\n\ninterface NewTripFormState {\n name: string;\n startDate: string;\n endDate: string;\n}\n\nconst defaultFormState: NewTripFormState = {\n name: '',\n startDate: '',\n endDate: '',\n};\n\nexport function TripsPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const [showNewTripDialog, setShowNewTripDialog] = useState(false);\n const [formState, setFormState] = useState<NewTripFormState>(defaultFormState);\n\n const { data, refetch } = useQuery(\n GGNOMAD_TRIP_PLANS_QUERY as TypedDocumentNode<\n {\n ggnomadTripPlans: Array<{\n id: string;\n name: string;\n description?: string;\n startDate?: string;\n endDate?: string;\n travelers: number;\n status: string;\n total: number;\n itemCount: number;\n createdAt: string;\n }>;\n },\n { limit: number; offset: number }\n >,\n { variables: { limit: 50, offset: 0 } }\n );\n\n const [createTripPlan, { loading: creating }] = useMutation(\n CREATE_GGNOMAD_TRIP_PLAN_MUTATION as TypedDocumentNode<\n {\n createGgnomadTripPlan: {\n id: string;\n name: string;\n startDate?: string;\n endDate?: string;\n };\n },\n {\n input: {\n name: string;\n description?: string;\n startDate?: string;\n endDate?: string;\n travelers?: number;\n };\n }\n >\n );\n\n const trips = data?.ggnomadTripPlans ?? [];\n\n const handleCreateTrip = async () => {\n if (!formState.name.trim()) {\n notify.warning(\n t('ggnomad.trips.toast.validationTitle', 'Required field missing'),\n t('ggnomad.trips.toast.nameRequired', 'Please enter a trip name')\n );\n return;\n }\n\n try {\n const result = await createTripPlan({\n variables: {\n input: {\n name: formState.name.trim(),\n startDate: formState.startDate\n ? new Date(formState.startDate).toISOString()\n : undefined,\n endDate: formState.endDate ? new Date(formState.endDate).toISOString() : undefined,\n travelers: 1,\n },\n },\n });\n\n if (result.error) {\n notify.error(\n t('ggnomad.trips.toast.createErrorTitle', 'Could not create trip'),\n result.error.message\n );\n return;\n }\n\n const created = result.data?.createGgnomadTripPlan;\n if (!created) return;\n\n notify.success(t('ggnomad.trips.toast.createSuccessTitle', 'Trip created'), created.name);\n\n setShowNewTripDialog(false);\n setFormState(defaultFormState);\n await refetch();\n navigate(`trips/${created.id}`);\n } catch (err) {\n notify.error(\n t('ggnomad.trips.toast.createErrorTitle', 'Could not create trip'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const formatDate = (dateStr?: string) => {\n if (!dateStr) return '';\n const date = new Date(dateStr);\n return date.toLocaleDateString('en-US', {\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n });\n };\n\n return (\n <PageShell\n title={t('ggnomad.trips.title', 'Trips')}\n titleAccent={t('ggnomad.trips.title.accent', 'Trips')}\n subtitle={t(\n 'ggnomad.trips.subtitle',\n 'Plan complete itineraries with real trip planning API mutations'\n )}\n actions={\n <CTAOverflowMenu\n primary={{\n label: t('ggnomad.trips.actions.newTrip', 'New trip'),\n onSelect: () => setShowNewTripDialog(true),\n disabled: creating,\n icon: <Plus />,\n }}\n actions={[]}\n />\n }\n >\n <PagePurpose icon={<Map className=\"size-4\" />}>\n {t(\n 'ggnomad.trips.purpose',\n 'Organize every aspect of your journey — build itineraries day by day, add activities and accommodations, manage your trip status, and book everything at once. All your trips in one place.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey=\"ggnomad-trips\"\n steps={[\n {\n id: 'add-stays',\n label: t('ggnomad.trips.next.stays', 'Add places to stay'),\n description: t(\n 'ggnomad.trips.next.staysDesc',\n 'Browse accommodations and add them to your itinerary.'\n ),\n icon: <Calendar className=\"size-4\" />,\n onClick: () => navigate('../hospitality'),\n },\n {\n id: 'build-activities',\n label: t('ggnomad.trips.next.activities', 'Plan activities'),\n description: t(\n 'ggnomad.trips.next.activitiesDesc',\n 'Mix in tours, events, and activities by day.'\n ),\n icon: <Map className=\"size-4\" />,\n onClick: () => navigate('../tourism'),\n },\n ]}\n />\n\n {trips.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.trips.emptyTitle', 'No trips yet')}\n description={t(\n 'ggnomad.trips.emptyDescription',\n 'Create your first trip to start building an itinerary.'\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.trips.yourTrips', 'Your trips')}\n </h2>\n <div className=\"grid gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {trips.map((trip) => (\n <EntityCard\n key={trip.id}\n to={`trips/${trip.id}`}\n title={trip.name}\n meta={`${trip.travelers} ${\n trip.travelers === 1\n ? t('ggnomad.trips.card.traveler', 'traveler')\n : t('ggnomad.trips.card.travelers', 'travelers')\n } · ${trip.itemCount} ${t('ggnomad.trips.card.items', 'items')}`}\n price={trip.total > 0 ? `USD ${trip.total.toFixed(2)}` : ''}\n badge={<StatusBadge status={trip.status} live={trip.status !== 'CANCELLED'} />}\n footer={\n trip.startDate && trip.endDate\n ? `${formatDate(trip.startDate)} – ${formatDate(trip.endDate)}`\n : t('ggnomad.trips.card.noDates', 'No dates set')\n }\n />\n ))}\n </div>\n </GlassCard>\n )}\n\n <AlertDialog open={showNewTripDialog} onOpenChange={setShowNewTripDialog}>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.trips.newDialog.title', 'Create a new trip')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.trips.newDialog.description',\n 'Name your trip and set the dates to get started building your itinerary.'\n )}\n </AlertDialogDescription>\n </AlertDialogHeader>\n\n <div className=\"space-y-4\">\n <div>\n <label htmlFor=\"trip-name\" className=\"block text-sm font-medium text-text-primary\">\n {t('ggnomad.trips.newDialog.tripName', 'Trip name')}\n </label>\n <input\n id=\"trip-name\"\n type=\"text\"\n className=\"mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder-text-secondary focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary\"\n placeholder={t(\n 'ggnomad.trips.newDialog.tripNamePlaceholder',\n 'e.g. Summer Europe Adventure'\n )}\n value={formState.name}\n onChange={(e) => setFormState((prev) => ({ ...prev, name: e.target.value }))}\n />\n </div>\n\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label\n htmlFor=\"trip-start-date\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.trips.newDialog.startDate', 'Start date')}\n </label>\n <input\n id=\"trip-start-date\"\n type=\"date\"\n className=\"mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary\"\n value={formState.startDate}\n onChange={(e) => setFormState((prev) => ({ ...prev, startDate: e.target.value }))}\n />\n </div>\n <div>\n <label\n htmlFor=\"trip-end-date\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.trips.newDialog.endDate', 'End date')}\n </label>\n <input\n id=\"trip-end-date\"\n type=\"date\"\n className=\"mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary\"\n value={formState.endDate}\n onChange={(e) => setFormState((prev) => ({ ...prev, endDate: e.target.value }))}\n />\n </div>\n </div>\n </div>\n\n <AlertDialogFooter>\n <AlertDialogCancel disabled={creating}>\n {t('ggnomad.trips.newDialog.cancel', 'Cancel')}\n </AlertDialogCancel>\n <AlertDialogAction\n disabled={creating || !formState.name.trim()}\n onClick={(event) => {\n event.preventDefault();\n void handleCreateTrip();\n }}\n >\n {creating\n ? t('ggnomad.trips.newDialog.creating', 'Creating…')\n : t('ggnomad.trips.newDialog.create', 'Create trip')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TripsPage.js","names":[],"sources":["../../../src/ggnomad/pages/TripsPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Calendar, Map, Plus } from 'lucide-react';\nimport { useNavigate } from 'react-router';\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 { CREATE_GGNOMAD_TRIP_PLAN_MUTATION, GGNOMAD_TRIP_PLANS_QUERY } from '../graphql';\nimport { PageShell } from './PageShell';\nimport { EntityCard, StatusBadge } from '../components';\nimport { notify } from '../utils/notify';\n\ninterface NewTripFormState {\n name: string;\n startDate: string;\n endDate: string;\n}\n\nconst defaultFormState: NewTripFormState = {\n name: '',\n startDate: '',\n endDate: '',\n};\n\nexport function TripsPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const [showNewTripDialog, setShowNewTripDialog] = useState(false);\n const [formState, setFormState] = useState<NewTripFormState>(defaultFormState);\n\n const { data, refetch } = useQuery(\n GGNOMAD_TRIP_PLANS_QUERY as TypedDocumentNode<\n {\n ggnomadTripPlans: Array<{\n id: string;\n name: string;\n description?: string;\n startDate?: string;\n endDate?: string;\n travelers: number;\n status: string;\n total: number;\n itemCount: number;\n createdAt: string;\n }>;\n },\n { limit: number; offset: number }\n >,\n { variables: { limit: 50, offset: 0 } }\n );\n\n const [createTripPlan, { loading: creating }] = useMutation(\n CREATE_GGNOMAD_TRIP_PLAN_MUTATION as TypedDocumentNode<\n {\n createGgnomadTripPlan: {\n id: string;\n name: string;\n startDate?: string;\n endDate?: string;\n };\n },\n {\n input: {\n name: string;\n description?: string;\n startDate?: string;\n endDate?: string;\n travelers?: number;\n };\n }\n >\n );\n\n const trips = data?.ggnomadTripPlans ?? [];\n\n const handleCreateTrip = async () => {\n if (!formState.name.trim()) {\n notify.warning(\n t('ggnomad.trips.toast.validationTitle', 'Required field missing'),\n t('ggnomad.trips.toast.nameRequired', 'Please enter a trip name')\n );\n return;\n }\n\n try {\n const result = await createTripPlan({\n variables: {\n input: {\n name: formState.name.trim(),\n startDate: formState.startDate\n ? new Date(formState.startDate).toISOString()\n : undefined,\n endDate: formState.endDate ? new Date(formState.endDate).toISOString() : undefined,\n travelers: 1,\n },\n },\n });\n\n if (result.error) {\n notify.error(\n t('ggnomad.trips.toast.createErrorTitle', 'Could not create trip'),\n result.error.message\n );\n return;\n }\n\n const created = result.data?.createGgnomadTripPlan;\n if (!created) return;\n\n notify.success(t('ggnomad.trips.toast.createSuccessTitle', 'Trip created'), created.name);\n\n setShowNewTripDialog(false);\n setFormState(defaultFormState);\n await refetch();\n navigate(`trips/${created.id}`);\n } catch (err) {\n notify.error(\n t('ggnomad.trips.toast.createErrorTitle', 'Could not create trip'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const formatDate = (dateStr?: string) => {\n if (!dateStr) return '';\n const date = new Date(dateStr);\n return date.toLocaleDateString('en-US', {\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n });\n };\n\n return (\n <PageShell\n title={t('ggnomad.trips.title', 'Trips')}\n titleAccent={t('ggnomad.trips.title.accent', 'Trips')}\n subtitle={t(\n 'ggnomad.trips.subtitle',\n 'Plan complete itineraries with real trip planning API mutations'\n )}\n actions={\n <CTAOverflowMenu\n primary={{\n label: t('ggnomad.trips.actions.newTrip', 'New trip'),\n onSelect: () => setShowNewTripDialog(true),\n disabled: creating,\n icon: <Plus />,\n }}\n actions={[]}\n />\n }\n >\n <PagePurpose icon={<Map className=\"size-4\" />}>\n {t(\n 'ggnomad.trips.purpose',\n 'Organize every aspect of your journey — build itineraries day by day, add activities and accommodations, manage your trip status, and book everything at once. All your trips in one place.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey=\"ggnomad-trips\"\n steps={[\n {\n id: 'add-stays',\n label: t('ggnomad.trips.next.stays', 'Add places to stay'),\n description: t(\n 'ggnomad.trips.next.staysDesc',\n 'Browse accommodations and add them to your itinerary.'\n ),\n icon: <Calendar className=\"size-4\" />,\n onClick: () => navigate('../hospitality'),\n },\n {\n id: 'build-activities',\n label: t('ggnomad.trips.next.activities', 'Plan activities'),\n description: t(\n 'ggnomad.trips.next.activitiesDesc',\n 'Mix in tours, events, and activities by day.'\n ),\n icon: <Map className=\"size-4\" />,\n onClick: () => navigate('../tourism'),\n },\n ]}\n />\n\n {trips.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.trips.emptyTitle', 'No trips yet')}\n description={t(\n 'ggnomad.trips.emptyDescription',\n 'Create your first trip to start building an itinerary.'\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.trips.yourTrips', 'Your trips')}\n </h2>\n <div className=\"grid gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {trips.map((trip) => (\n <EntityCard\n key={trip.id}\n to={`trips/${trip.id}`}\n title={trip.name}\n meta={`${trip.travelers} ${\n trip.travelers === 1\n ? t('ggnomad.trips.card.traveler', 'traveler')\n : t('ggnomad.trips.card.travelers', 'travelers')\n } · ${trip.itemCount} ${t('ggnomad.trips.card.items', 'items')}`}\n price={trip.total > 0 ? `USD ${trip.total.toFixed(2)}` : ''}\n badge={<StatusBadge status={trip.status} live={trip.status !== 'CANCELLED'} />}\n footer={\n trip.startDate && trip.endDate\n ? `${formatDate(trip.startDate)} – ${formatDate(trip.endDate)}`\n : t('ggnomad.trips.card.noDates', 'No dates set')\n }\n />\n ))}\n </div>\n </GlassCard>\n )}\n\n <AlertDialog open={showNewTripDialog} onOpenChange={setShowNewTripDialog}>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.trips.newDialog.title', 'Create a new trip')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.trips.newDialog.description',\n 'Name your trip and set the dates to get started building your itinerary.'\n )}\n </AlertDialogDescription>\n </AlertDialogHeader>\n\n <div className=\"space-y-4\">\n <div>\n <label htmlFor=\"trip-name\" className=\"block text-sm font-medium text-text-primary\">\n {t('ggnomad.trips.newDialog.tripName', 'Trip name')}\n </label>\n <input\n id=\"trip-name\"\n type=\"text\"\n className=\"mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder-text-secondary focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary\"\n placeholder={t(\n 'ggnomad.trips.newDialog.tripNamePlaceholder',\n 'e.g. Summer Europe Adventure'\n )}\n value={formState.name}\n onChange={(e) => setFormState((prev) => ({ ...prev, name: e.target.value }))}\n />\n </div>\n\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label\n htmlFor=\"trip-start-date\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.trips.newDialog.startDate', 'Start date')}\n </label>\n <input\n id=\"trip-start-date\"\n type=\"date\"\n className=\"mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary\"\n value={formState.startDate}\n onChange={(e) => setFormState((prev) => ({ ...prev, startDate: e.target.value }))}\n />\n </div>\n <div>\n <label\n htmlFor=\"trip-end-date\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.trips.newDialog.endDate', 'End date')}\n </label>\n <input\n id=\"trip-end-date\"\n type=\"date\"\n className=\"mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary\"\n value={formState.endDate}\n onChange={(e) => setFormState((prev) => ({ ...prev, endDate: e.target.value }))}\n />\n </div>\n </div>\n </div>\n\n <AlertDialogFooter>\n <AlertDialogCancel disabled={creating}>\n {t('ggnomad.trips.newDialog.cancel', 'Cancel')}\n </AlertDialogCancel>\n <AlertDialogAction\n disabled={creating || !formState.name.trim()}\n onClick={(event) => {\n event.preventDefault();\n void handleCreateTrip();\n }}\n >\n {creating\n ? t('ggnomad.trips.newDialog.creating', 'Creating…')\n : t('ggnomad.trips.newDialog.create', 'Create trip')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GGNomadProvider.js","names":[],"sources":["../../../src/ggnomad/providers/GGNomadProvider.tsx"],"sourcesContent":["import type { FC, PropsWithChildren } from 'react';\nimport { createContext, useContext, useMemo } from 'react';\nimport { MemoryRouter } from 'react-router';\nimport { ApolloProvider } from '@apollo/client/react';\nimport { ApolloClient, ApolloLink, HttpLink, InMemoryCache, from } from '@apollo/client/core';\nimport { onError } from '@apollo/client/link/error';\nimport { setContext } from '@apollo/client/link/context';\n\nimport type { GGNomadRootProps } from '../types';\n\ntype GGNomadContextValue = GGNomadRootProps;\n\nconst GGNomadContext = createContext<GGNomadContextValue | null>(null);\n\nexport const useGGNomad = (): GGNomadRootProps => {\n const context = useContext(GGNomadContext);\n if (!context) {\n throw new Error('useGGNomad must be used within GGNomadProvider');\n }\n return context;\n};\n\nexport const GGNomadProvider: FC<PropsWithChildren<GGNomadRootProps>> = ({\n children,\n ...props\n}) => {\n const { apiGatewayUrl, authToken, workspaceToken, onAuthError, basePath } = props;\n\n const client = useMemo(() => {\n const endpoint = `${(apiGatewayUrl ?? '').replace(/\\/$/, '')}/workspaces/graphql`;\n\n const errorLink = onError(({ error }) => {\n const graphQLErrors =\n 'errors' in (error ?? {})\n ? (error as { errors?: Array<{ extensions?: { code?: string } }> }).errors\n : undefined;\n const networkError =\n 'cause' in (error ?? {}) ? (error as { cause?: { statusCode?: number } }).cause : undefined;\n\n const authGraphError = graphQLErrors?.some((graphQLError) =>\n ['UNAUTHENTICATED', 'FORBIDDEN'].includes(String(graphQLError.extensions?.code ?? ''))\n );\n const authNetworkError =\n typeof networkError === 'object' && networkError !== null && 'statusCode' in networkError\n ? [401, 403].includes(Number((networkError as { statusCode?: number }).statusCode))\n : false;\n\n if (authGraphError || authNetworkError) {\n onAuthError?.();\n }\n });\n\n const authLink = setContext((_, prevContext) => ({\n headers: {\n ...(prevContext.headers as Record<string, string> | undefined),\n ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),\n ...(workspaceToken ? { 'X-Workspace-Authorization': `Bearer ${workspaceToken}` } : {}),\n },\n }));\n\n const httpLink = new HttpLink({\n uri: endpoint,\n credentials: 'include',\n });\n\n return new ApolloClient({\n cache: new InMemoryCache(),\n link: from([errorLink as ApolloLink, authLink, httpLink]),\n defaultOptions: {\n watchQuery: { fetchPolicy: 'cache-and-network', errorPolicy: 'all' },\n query: { fetchPolicy: 'network-only', errorPolicy: 'all' },\n mutate: { errorPolicy: 'all' },\n },\n });\n }, [apiGatewayUrl, authToken, workspaceToken, onAuthError]);\n\n const isIntegrated = !!basePath;\n const contextProvider = (\n <GGNomadContext.Provider value={props}>{children}</GGNomadContext.Provider>\n );\n\n const content = isIntegrated ? (\n contextProvider\n ) : (\n <ApolloProvider client={client}>{contextProvider}</ApolloProvider>\n );\n\n if (isIntegrated) {\n return content;\n }\n\n return <MemoryRouter>{content}</MemoryRouter>;\n};\n"],"mappings":";;;;;;;;AAYA,IAAM,IAAiB,EAA0C,IAAI,GAExD,UAAqC;CAChD,IAAM,IAAU,EAAW,CAAc;CACzC,IAAI,CAAC,GACH,MAAU,MAAM,gDAAgD;CAElE,OAAO;AACT,GAEa,KAA4D,EACvE,aACA,GAAG,QACC;CACJ,IAAM,EAAE,kBAAe,cAAW,mBAAgB,gBAAa,gBAAa,GAEtE,IAAS,QAAc;EAC3B,IAAM,IAAW,IAAI,KAAiB,
|
|
1
|
+
{"version":3,"file":"GGNomadProvider.js","names":[],"sources":["../../../src/ggnomad/providers/GGNomadProvider.tsx"],"sourcesContent":["import type { FC, PropsWithChildren } from 'react';\nimport { createContext, useContext, useMemo } from 'react';\nimport { MemoryRouter } from 'react-router';\nimport { ApolloProvider } from '@apollo/client/react';\nimport { ApolloClient, ApolloLink, HttpLink, InMemoryCache, from } from '@apollo/client/core';\nimport { onError } from '@apollo/client/link/error';\nimport { setContext } from '@apollo/client/link/context';\n\nimport type { GGNomadRootProps } from '../types';\n\ntype GGNomadContextValue = GGNomadRootProps;\n\nconst GGNomadContext = createContext<GGNomadContextValue | null>(null);\n\nexport const useGGNomad = (): GGNomadRootProps => {\n const context = useContext(GGNomadContext);\n if (!context) {\n throw new Error('useGGNomad must be used within GGNomadProvider');\n }\n return context;\n};\n\nexport const GGNomadProvider: FC<PropsWithChildren<GGNomadRootProps>> = ({\n children,\n ...props\n}) => {\n const { apiGatewayUrl, authToken, workspaceToken, onAuthError, basePath } = props;\n\n const client = useMemo(() => {\n const endpoint = `${(apiGatewayUrl ?? '').replace(/\\/$/, '')}/workspaces/graphql`;\n\n const errorLink = onError(({ error }) => {\n const graphQLErrors =\n 'errors' in (error ?? {})\n ? (error as { errors?: Array<{ extensions?: { code?: string } }> }).errors\n : undefined;\n const networkError =\n 'cause' in (error ?? {}) ? (error as { cause?: { statusCode?: number } }).cause : undefined;\n\n const authGraphError = graphQLErrors?.some((graphQLError) =>\n ['UNAUTHENTICATED', 'FORBIDDEN'].includes(String(graphQLError.extensions?.code ?? ''))\n );\n const authNetworkError =\n typeof networkError === 'object' && networkError !== null && 'statusCode' in networkError\n ? [401, 403].includes(Number((networkError as { statusCode?: number }).statusCode))\n : false;\n\n if (authGraphError || authNetworkError) {\n onAuthError?.();\n }\n });\n\n const authLink = setContext((_, prevContext) => ({\n headers: {\n ...(prevContext.headers as Record<string, string> | undefined),\n ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),\n ...(workspaceToken ? { 'X-Workspace-Authorization': `Bearer ${workspaceToken}` } : {}),\n },\n }));\n\n const httpLink = new HttpLink({\n uri: endpoint,\n credentials: 'include',\n });\n\n return new ApolloClient({\n cache: new InMemoryCache(),\n link: from([errorLink as ApolloLink, authLink, httpLink]),\n defaultOptions: {\n watchQuery: { fetchPolicy: 'cache-and-network', errorPolicy: 'all' },\n query: { fetchPolicy: 'network-only', errorPolicy: 'all' },\n mutate: { errorPolicy: 'all' },\n },\n });\n }, [apiGatewayUrl, authToken, workspaceToken, onAuthError]);\n\n const isIntegrated = !!basePath;\n const contextProvider = (\n <GGNomadContext.Provider value={props}>{children}</GGNomadContext.Provider>\n );\n\n const content = isIntegrated ? (\n contextProvider\n ) : (\n <ApolloProvider client={client}>{contextProvider}</ApolloProvider>\n );\n\n if (isIntegrated) {\n return content;\n }\n\n return <MemoryRouter>{content}</MemoryRouter>;\n};\n"],"mappings":";;;;;;;;AAYA,IAAM,IAAiB,EAA0C,IAAI,GAExD,UAAqC;CAChD,IAAM,IAAU,EAAW,CAAc;CACzC,IAAI,CAAC,GACH,MAAU,MAAM,gDAAgD;CAElE,OAAO;AACT,GAEa,KAA4D,EACvE,aACA,GAAG,QACC;CACJ,IAAM,EAAE,kBAAe,cAAW,mBAAgB,gBAAa,gBAAa,GAEtE,IAAS,QAAc;EAC3B,IAAM,IAAW,IAAI,KAAiB,GAAA,CAAI,QAAQ,OAAO,EAAE,EAAE,sBAEvD,IAAY,GAAS,EAAE,eAAY;GACvC,IAAM,IACJ,aAAa,KAAS,CAAC,KAClB,EAAiE,SAClE,KAAA,GACA,IACJ,YAAY,KAAS,CAAC,KAAM,EAA8C,QAAQ,KAAA,GAE9E,IAAiB,GAAe,MAAM,MAC1C,CAAC,mBAAmB,WAAW,CAAC,CAAC,SAAS,OAAO,EAAa,YAAY,QAAQ,EAAE,CAAC,CACvF,GACM,IACJ,OAAO,KAAiB,YAAY,KAAyB,gBAAgB,IACzE,CAAC,KAAK,GAAG,CAAC,CAAC,SAAS,OAAQ,EAAyC,UAAU,CAAC,IAChF;GAEN,CAAI,KAAkB,MACpB,IAAc;EAElB,CAAC,GAEK,IAAW,GAAY,GAAG,OAAiB,EAC/C,SAAS;GACP,GAAI,EAAY;GAChB,GAAI,IAAY,EAAE,eAAe,UAAU,IAAY,IAAI,CAAC;GAC5D,GAAI,IAAiB,EAAE,6BAA6B,UAAU,IAAiB,IAAI,CAAC;EACtF,EACF,EAAE,GAEI,IAAW,IAAI,EAAS;GAC5B,KAAK;GACL,aAAa;EACf,CAAC;EAED,OAAO,IAAI,EAAa;GACtB,OAAO,IAAI,EAAc;GACzB,MAAM,EAAK;IAAC;IAAyB;IAAU;GAAQ,CAAC;GACxD,gBAAgB;IACd,YAAY;KAAE,aAAa;KAAqB,aAAa;IAAM;IACnE,OAAO;KAAE,aAAa;KAAgB,aAAa;IAAM;IACzD,QAAQ,EAAE,aAAa,MAAM;GAC/B;EACF,CAAC;CACH,GAAG;EAAC;EAAe;EAAW;EAAgB;CAAW,CAAC,GAEpD,IAAe,CAAC,CAAC,GACjB,IACJ,kBAAC,EAAe,UAAhB;EAAyB,OAAO;EAAQ;CAAkC,CAAA,GAGtE,IAAU,IACd,IAEA,kBAAC,GAAD;EAAwB;YAAS;CAAgC,CAAA;CAOnE,OAJI,IACK,IAGF,kBAAC,GAAD,EAAA,UAAe,EAAsB,CAAA;AAC9C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.js","names":[],"sources":["../../../src/ggnomad/utils/format.ts"],"sourcesContent":["/**\n * Presentation helpers shared across GGNomad pages.\n *\n * Several GGNomad fields are `JSON!` on the service schema (`GgnomadProperty.address`,\n * `GgnomadTour.location`, `GgnomadEvent.venue`, `GgnomadTrip.origin`/`destination`,\n * `GgnomadTrip.stops`, `GgnomadAnalytics.monthlyTrends`/`topDestinations`) with no\n * enforced shape — the reference transport provider writes `{ name, code }`, listing\n * mutations pass whatever the caller supplied, and an unset column is `{}`. These\n * helpers therefore read defensively: anything they cannot resolve to a real scalar is\n * dropped rather than guessed, so a page never presents an invented value as fact.\n */\n\n/** `PENDING_REVIEW` / `step_free_entrance` → `Pending review` / `Step free entrance`. */\nexport function humanizeEnum(value: string): string {\n const spaced = value.replace(/[_-]+/g, ' ').trim().toLowerCase();\n if (!spaced) return value;\n return spaced.charAt(0).toUpperCase() + spaced.slice(1);\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** Scalar entries of a JSON object, in declaration order, with empty values dropped. */\nexport function jsonFacts(value: unknown): Array<{ key: string; label: string; value: string }> {\n if (!isPlainObject(value)) return [];\n const facts: Array<{ key: string; label: string; value: string }> = [];\n for (const [key, raw] of Object.entries(value)) {\n if (typeof raw === 'string') {\n const trimmed = raw.trim();\n if (trimmed) facts.push({ key, label: humanizeEnum(key), value: trimmed });\n } else if (typeof raw === 'number' && Number.isFinite(raw)) {\n facts.push({ key, label: humanizeEnum(key), value: String(raw) });\n } else if (typeof raw === 'boolean') {\n facts.push({ key, label: humanizeEnum(key), value: raw ? 'Yes' : 'No' });\n }\n }\n return facts;\n}\n\nconst LABEL_KEYS = ['name', 'label', 'city', 'title', 'address', 'code'];\n\n/**\n * A single human label for a JSON location-ish value: the first recognised naming key,\n * else the first scalar entry, else null (never a stringified object).\n */\nexport function jsonLabel(value: unknown): string | null {\n if (typeof value === 'string') return value.trim() || null;\n const facts = jsonFacts(value);\n if (facts.length === 0) return null;\n for (const key of LABEL_KEYS) {\n const match = facts.find((fact) => fact.key.toLowerCase() === key);\n if (match) return match.value;\n }\n return facts[0].value;\n}\n\n/** Number of entries in a JSON array field (e.g. `GgnomadTrip.stops`), or null. */\nexport function jsonArrayLength(value: unknown): number | null {\n return Array.isArray(value) ? value.length : null;\n}\n\nexport function formatMoney(amount: number, currency: string): string {\n return `${currency} ${Number(amount).toFixed(2)}`;\n}\n\n/**\n * Currency-formatted number with compact notation at >=10,000. Used by\n * the Provider Dashboard card so large revenues don't break the stats\n * row. The full unformatted amount is returned in a `title` attribute\n * via the second tuple element when callers need the exact value.\n *\n * `GgnomadProvider` does not currently expose a `currency` field in the\n * GraphQL schema, so callers pass `'USD'` as the default until the\n * backend adds the field. Keeping the parameter preserves the call site\n * for when it does.\n *\n * Returns `{ display, exact }` so the card can render `$98.5K` and\n * keep the full `$98,450` in the tooltip.\n */\nexport function formatRevenue(\n amount: number,\n currency = 'USD'\n): { display: string; exact: string } {\n const safe = Number.isFinite(amount) ? amount : 0;\n const useCompact = Math.abs(safe) >= 10_000;\n const formatter = new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency,\n notation: useCompact ? 'compact' : 'standard',\n maximumFractionDigits: useCompact ? 1 : 0,\n });\n const exact = new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency,\n maximumFractionDigits: 0,\n }).format(safe);\n return { display: formatter.format(safe), exact };\n}\n\nexport function formatDate(value?: string | null): string | null {\n if (!value) return null;\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return null;\n return date.toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' });\n}\n\nexport function formatDateTime(value?: string | null): string | null {\n if (!value) return null;\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return null;\n return date.toLocaleString(undefined, {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n });\n}\n\nexport function formatTimeOnly(value?: string | null): string | null {\n if (!value) return null;\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return null;\n return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });\n}\n\n/** `95` → `1h 35m`. */\nexport function formatDurationMinutes(minutes: number): string {\n const safe = Math.max(0, Math.round(minutes));\n const hours = Math.floor(safe / 60);\n const rest = safe % 60;\n if (hours === 0) return `${rest}m`;\n if (rest === 0) return `${hours}h`;\n return `${hours}h ${rest}m`;\n}\n\n/** Whole nights between two `<input type=\"date\">` values; 0 when unusable. */\nexport function nightsBetween(checkIn: string, checkOut: string): number {\n if (!checkIn || !checkOut) return 0;\n const from = new Date(checkIn).getTime();\n const to = new Date(checkOut).getTime();\n if (Number.isNaN(from) || Number.isNaN(to) || to <= from) return 0;\n return Math.round((to - from) / 86_400_000);\n}\n\n/**\n * Reads a `label → count` series out of an unshaped JSON field such as\n * `GgnomadAnalytics.topDestinations`, which the service passes straight through from\n * the latest analytics snapshot. Accepts either an array of objects (a string-valued\n * key is the label, the first numeric key is the count) or a flat `{ label: count }`\n * map. Entries that yield neither a label nor a finite number are dropped, so a\n * snapshot written in an unexpected shape renders as \"no data\" rather than as noise.\n */\nexport function parseNamedCounts(value: unknown): Array<{ label: string; value: number }> {\n const out: Array<{ label: string; value: number }> = [];\n\n if (Array.isArray(value)) {\n for (const entry of value) {\n if (!isPlainObject(entry)) continue;\n let label: string | null = null;\n let count: number | null = null;\n for (const key of LABEL_KEYS) {\n const candidate = entry[key];\n if (typeof candidate === 'string' && candidate.trim()) {\n label = candidate.trim();\n break;\n }\n }\n if (label === null) {\n const firstString = Object.values(entry).find(\n (item): item is string => typeof item === 'string' && item.trim().length > 0\n );\n label = firstString?.trim() ?? null;\n }\n for (const item of Object.values(entry)) {\n if (typeof item === 'number' && Number.isFinite(item)) {\n count = item;\n break;\n }\n }\n if (label !== null && count !== null) out.push({ label, value: count });\n }\n return out;\n }\n\n if (isPlainObject(value)) {\n for (const [key, item] of Object.entries(value)) {\n if (typeof item === 'number' && Number.isFinite(item)) {\n out.push({ label: humanizeEnum(key), value: item });\n }\n }\n }\n\n return out;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"format.js","names":[],"sources":["../../../src/ggnomad/utils/format.ts"],"sourcesContent":["/**\n * Presentation helpers shared across GGNomad pages.\n *\n * Several GGNomad fields are `JSON!` on the service schema (`GgnomadProperty.address`,\n * `GgnomadTour.location`, `GgnomadEvent.venue`, `GgnomadTrip.origin`/`destination`,\n * `GgnomadTrip.stops`, `GgnomadAnalytics.monthlyTrends`/`topDestinations`) with no\n * enforced shape — the reference transport provider writes `{ name, code }`, listing\n * mutations pass whatever the caller supplied, and an unset column is `{}`. These\n * helpers therefore read defensively: anything they cannot resolve to a real scalar is\n * dropped rather than guessed, so a page never presents an invented value as fact.\n */\n\n/** `PENDING_REVIEW` / `step_free_entrance` → `Pending review` / `Step free entrance`. */\nexport function humanizeEnum(value: string): string {\n const spaced = value.replace(/[_-]+/g, ' ').trim().toLowerCase();\n if (!spaced) return value;\n return spaced.charAt(0).toUpperCase() + spaced.slice(1);\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** Scalar entries of a JSON object, in declaration order, with empty values dropped. */\nexport function jsonFacts(value: unknown): Array<{ key: string; label: string; value: string }> {\n if (!isPlainObject(value)) return [];\n const facts: Array<{ key: string; label: string; value: string }> = [];\n for (const [key, raw] of Object.entries(value)) {\n if (typeof raw === 'string') {\n const trimmed = raw.trim();\n if (trimmed) facts.push({ key, label: humanizeEnum(key), value: trimmed });\n } else if (typeof raw === 'number' && Number.isFinite(raw)) {\n facts.push({ key, label: humanizeEnum(key), value: String(raw) });\n } else if (typeof raw === 'boolean') {\n facts.push({ key, label: humanizeEnum(key), value: raw ? 'Yes' : 'No' });\n }\n }\n return facts;\n}\n\nconst LABEL_KEYS = ['name', 'label', 'city', 'title', 'address', 'code'];\n\n/**\n * A single human label for a JSON location-ish value: the first recognised naming key,\n * else the first scalar entry, else null (never a stringified object).\n */\nexport function jsonLabel(value: unknown): string | null {\n if (typeof value === 'string') return value.trim() || null;\n const facts = jsonFacts(value);\n if (facts.length === 0) return null;\n for (const key of LABEL_KEYS) {\n const match = facts.find((fact) => fact.key.toLowerCase() === key);\n if (match) return match.value;\n }\n return facts[0].value;\n}\n\n/** Number of entries in a JSON array field (e.g. `GgnomadTrip.stops`), or null. */\nexport function jsonArrayLength(value: unknown): number | null {\n return Array.isArray(value) ? value.length : null;\n}\n\nexport function formatMoney(amount: number, currency: string): string {\n return `${currency} ${Number(amount).toFixed(2)}`;\n}\n\n/**\n * Currency-formatted number with compact notation at >=10,000. Used by\n * the Provider Dashboard card so large revenues don't break the stats\n * row. The full unformatted amount is returned in a `title` attribute\n * via the second tuple element when callers need the exact value.\n *\n * `GgnomadProvider` does not currently expose a `currency` field in the\n * GraphQL schema, so callers pass `'USD'` as the default until the\n * backend adds the field. Keeping the parameter preserves the call site\n * for when it does.\n *\n * Returns `{ display, exact }` so the card can render `$98.5K` and\n * keep the full `$98,450` in the tooltip.\n */\nexport function formatRevenue(\n amount: number,\n currency = 'USD'\n): { display: string; exact: string } {\n const safe = Number.isFinite(amount) ? amount : 0;\n const useCompact = Math.abs(safe) >= 10_000;\n const formatter = new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency,\n notation: useCompact ? 'compact' : 'standard',\n maximumFractionDigits: useCompact ? 1 : 0,\n });\n const exact = new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency,\n maximumFractionDigits: 0,\n }).format(safe);\n return { display: formatter.format(safe), exact };\n}\n\nexport function formatDate(value?: string | null): string | null {\n if (!value) return null;\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return null;\n return date.toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' });\n}\n\nexport function formatDateTime(value?: string | null): string | null {\n if (!value) return null;\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return null;\n return date.toLocaleString(undefined, {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n });\n}\n\nexport function formatTimeOnly(value?: string | null): string | null {\n if (!value) return null;\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return null;\n return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });\n}\n\n/** `95` → `1h 35m`. */\nexport function formatDurationMinutes(minutes: number): string {\n const safe = Math.max(0, Math.round(minutes));\n const hours = Math.floor(safe / 60);\n const rest = safe % 60;\n if (hours === 0) return `${rest}m`;\n if (rest === 0) return `${hours}h`;\n return `${hours}h ${rest}m`;\n}\n\n/** Whole nights between two `<input type=\"date\">` values; 0 when unusable. */\nexport function nightsBetween(checkIn: string, checkOut: string): number {\n if (!checkIn || !checkOut) return 0;\n const from = new Date(checkIn).getTime();\n const to = new Date(checkOut).getTime();\n if (Number.isNaN(from) || Number.isNaN(to) || to <= from) return 0;\n return Math.round((to - from) / 86_400_000);\n}\n\n/**\n * Reads a `label → count` series out of an unshaped JSON field such as\n * `GgnomadAnalytics.topDestinations`, which the service passes straight through from\n * the latest analytics snapshot. Accepts either an array of objects (a string-valued\n * key is the label, the first numeric key is the count) or a flat `{ label: count }`\n * map. Entries that yield neither a label nor a finite number are dropped, so a\n * snapshot written in an unexpected shape renders as \"no data\" rather than as noise.\n */\nexport function parseNamedCounts(value: unknown): Array<{ label: string; value: number }> {\n const out: Array<{ label: string; value: number }> = [];\n\n if (Array.isArray(value)) {\n for (const entry of value) {\n if (!isPlainObject(entry)) continue;\n let label: string | null = null;\n let count: number | null = null;\n for (const key of LABEL_KEYS) {\n const candidate = entry[key];\n if (typeof candidate === 'string' && candidate.trim()) {\n label = candidate.trim();\n break;\n }\n }\n if (label === null) {\n const firstString = Object.values(entry).find(\n (item): item is string => typeof item === 'string' && item.trim().length > 0\n );\n label = firstString?.trim() ?? null;\n }\n for (const item of Object.values(entry)) {\n if (typeof item === 'number' && Number.isFinite(item)) {\n count = item;\n break;\n }\n }\n if (label !== null && count !== null) out.push({ label, value: count });\n }\n return out;\n }\n\n if (isPlainObject(value)) {\n for (const [key, item] of Object.entries(value)) {\n if (typeof item === 'number' && Number.isFinite(item)) {\n out.push({ label: humanizeEnum(key), value: item });\n }\n }\n }\n\n return out;\n}\n"],"mappings":"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"}
|
|
@@ -45,6 +45,6 @@ function a(e, n, r) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
//#endregion
|
|
48
|
-
export { e as DEFAULT_PROVIDER_QUERY, a as applyProviderControls };
|
|
48
|
+
export { e as DEFAULT_PROVIDER_QUERY, a as applyProviderControls, t as isProviderQueryFiltered };
|
|
49
49
|
|
|
50
50
|
//# sourceMappingURL=providerListControls.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"providerListControls.js","names":[],"sources":["../../../src/ggnomad/utils/providerListControls.ts"],"sourcesContent":["/**\n * Pure filter / sort / paging logic for the Provider Dashboard toolbar\n * (BOFF-8177 follow-up). Kept out of the component so the behavior is\n * trivially exercisable and the page stays presentational. No React\n * imports here.\n *\n * The backend `ggnomadProviders` query exposes offset paging only — no\n * filter args and no totalCount — and caps one request at MAX_LIMIT=100\n * rows. Callers therefore fetch EVERY page up front and run the toolbar\n * controls over the full loaded list here. That is correct for a fleet\n * of hundreds; past ~a thousand providers the backend should grow filter\n * args + totalCount and this logic moves server-side.\n */\n\n/** Number of provider cards shown before \"Load more\" offers the next batch. */\nexport const PROVIDER_PAGE_SIZE = 12;\n\nexport type ProviderSort = 'newest' | 'oldest' | 'nameAsc' | 'nameDesc' | 'revenueDesc';\n\n/** The minimum shape the controls need from a provider row. */\nexport interface ProviderListItem {\n id: string;\n name: string;\n type: string;\n status: string;\n totalRevenue: number;\n createdAt: string;\n}\n\nexport interface ProviderListQuery {\n /** Case-insensitive substring matched anywhere in the provider name. */\n search: string;\n /** Provider type enum value; null = all types. */\n type: string | null;\n sort: ProviderSort;\n}\n\n/** Toolbar rest state — also what \"Clear filters\" returns to. */\nexport const DEFAULT_PROVIDER_QUERY: ProviderListQuery = {\n search: '',\n type: null,\n sort: 'newest',\n};\n\nexport interface ProviderControlsResult<T> {\n /** Filtered + sorted slice, cut to `visibleCount`. */\n shown: T[];\n /** Rows matching the current filters (before the visible-count cut). */\n total: number;\n /** True when search/type/status differ from their defaults. */\n isFiltered: boolean;\n /** True when matching rows exist beyond what `shown` contains. */\n hasMore: boolean;\n}\n\n/** A query counts as \"filtered\" when search/type are non-default; sort never does. */\nexport function isProviderQueryFiltered(query: ProviderListQuery): boolean {\n return query.search.trim() !== '' || query.type !== null;\n}\n\nfunction timestamp(value: string): number {\n const parsed = Date.parse(value);\n return Number.isFinite(parsed) ? parsed : 0;\n}\n\nfunction revenue(value: number): number {\n return Number.isFinite(value) ? value : 0;\n}\n\nfunction sortItems<T extends ProviderListItem>(items: readonly T[], sort: ProviderSort): T[] {\n const copy = [...items];\n switch (sort) {\n case 'oldest':\n copy.sort((a, b) => timestamp(a.createdAt) - timestamp(b.createdAt));\n break;\n case 'nameAsc':\n copy.sort((a, b) => a.name.localeCompare(b.name));\n break;\n case 'nameDesc':\n copy.sort((a, b) => b.name.localeCompare(a.name));\n break;\n case 'revenueDesc':\n copy.sort((a, b) => revenue(b.totalRevenue) - revenue(a.totalRevenue));\n break;\n case 'newest':\n default:\n // Mirrors the backend's orderBy: { createdAt: 'desc' } default.\n copy.sort((a, b) => timestamp(b.createdAt) - timestamp(a.createdAt));\n break;\n }\n return copy;\n}\n\n/**\n * Apply the toolbar search/type/status filters and sort, then cut the\n * result to the visible page. `items` MUST be the complete loaded list —\n * callers page through every backend batch first (see the module note).\n */\nexport function applyProviderControls<T extends ProviderListItem>(\n items: readonly T[],\n query: ProviderListQuery,\n visibleCount: number\n): ProviderControlsResult<T> {\n const needle = query.search.trim().toLowerCase();\n const filtered = items.filter((item) => {\n if (needle && !item.name.toLowerCase().includes(needle)) return false;\n if (query.type !== null && item.type !== query.type) return false;\n return true;\n });\n\n const sorted = sortItems(filtered, query.sort);\n const cap = Math.max(1, Math.floor(visibleCount));\n return {\n shown: sorted.slice(0, cap),\n total: sorted.length,\n isFiltered: isProviderQueryFiltered(query),\n hasMore: sorted.length > cap,\n };\n}\n"],"mappings":";AAsCA,IAAa,IAA4C;CACvD,QAAQ;CACR,MAAM;CACN,MAAM;AACR;AAcA,SAAgB,EAAwB,GAAmC;CACzE,OAAO,EAAM,OAAO,KAAK,MAAM,MAAM,EAAM,SAAS;AACtD;AAEA,SAAS,EAAU,GAAuB;CACxC,IAAM,IAAS,KAAK,MAAM,CAAK;CAC/B,OAAO,OAAO,SAAS,CAAM,IAAI,IAAS;AAC5C;AAEA,SAAS,EAAQ,GAAuB;CACtC,OAAO,OAAO,SAAS,CAAK,IAAI,IAAQ;AAC1C;AAEA,SAAS,EAAsC,GAAqB,GAAyB;CAC3F,IAAM,IAAO,CAAC,GAAG,CAAK;CACtB,QAAQ,GAAR;EACE,KAAK;GACH,EAAK,MAAM,GAAG,MAAM,EAAU,EAAE,SAAS,IAAI,EAAU,EAAE,SAAS,CAAC;GACnE;EACF,KAAK;GACH,EAAK,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;GAChD;EACF,KAAK;GACH,EAAK,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;GAChD;EACF,KAAK;GACH,EAAK,MAAM,GAAG,MAAM,EAAQ,EAAE,YAAY,IAAI,EAAQ,EAAE,YAAY,CAAC;GACrE;EAEF;GAEE,EAAK,MAAM,GAAG,MAAM,EAAU,EAAE,SAAS,IAAI,EAAU,EAAE,SAAS,CAAC;GACnE;CACJ;CACA,OAAO;AACT;AAOA,SAAgB,EACd,GACA,GACA,GAC2B;CAC3B,IAAM,IAAS,EAAM,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"providerListControls.js","names":[],"sources":["../../../src/ggnomad/utils/providerListControls.ts"],"sourcesContent":["/**\n * Pure filter / sort / paging logic for the Provider Dashboard toolbar\n * (BOFF-8177 follow-up). Kept out of the component so the behavior is\n * trivially exercisable and the page stays presentational. No React\n * imports here.\n *\n * The backend `ggnomadProviders` query exposes offset paging only — no\n * filter args and no totalCount — and caps one request at MAX_LIMIT=100\n * rows. Callers therefore fetch EVERY page up front and run the toolbar\n * controls over the full loaded list here. That is correct for a fleet\n * of hundreds; past ~a thousand providers the backend should grow filter\n * args + totalCount and this logic moves server-side.\n */\n\n/** Number of provider cards shown before \"Load more\" offers the next batch. */\nexport const PROVIDER_PAGE_SIZE = 12;\n\nexport type ProviderSort = 'newest' | 'oldest' | 'nameAsc' | 'nameDesc' | 'revenueDesc';\n\n/** The minimum shape the controls need from a provider row. */\nexport interface ProviderListItem {\n id: string;\n name: string;\n type: string;\n status: string;\n totalRevenue: number;\n createdAt: string;\n}\n\nexport interface ProviderListQuery {\n /** Case-insensitive substring matched anywhere in the provider name. */\n search: string;\n /** Provider type enum value; null = all types. */\n type: string | null;\n sort: ProviderSort;\n}\n\n/** Toolbar rest state — also what \"Clear filters\" returns to. */\nexport const DEFAULT_PROVIDER_QUERY: ProviderListQuery = {\n search: '',\n type: null,\n sort: 'newest',\n};\n\nexport interface ProviderControlsResult<T> {\n /** Filtered + sorted slice, cut to `visibleCount`. */\n shown: T[];\n /** Rows matching the current filters (before the visible-count cut). */\n total: number;\n /** True when search/type/status differ from their defaults. */\n isFiltered: boolean;\n /** True when matching rows exist beyond what `shown` contains. */\n hasMore: boolean;\n}\n\n/** A query counts as \"filtered\" when search/type are non-default; sort never does. */\nexport function isProviderQueryFiltered(query: ProviderListQuery): boolean {\n return query.search.trim() !== '' || query.type !== null;\n}\n\nfunction timestamp(value: string): number {\n const parsed = Date.parse(value);\n return Number.isFinite(parsed) ? parsed : 0;\n}\n\nfunction revenue(value: number): number {\n return Number.isFinite(value) ? value : 0;\n}\n\nfunction sortItems<T extends ProviderListItem>(items: readonly T[], sort: ProviderSort): T[] {\n const copy = [...items];\n switch (sort) {\n case 'oldest':\n copy.sort((a, b) => timestamp(a.createdAt) - timestamp(b.createdAt));\n break;\n case 'nameAsc':\n copy.sort((a, b) => a.name.localeCompare(b.name));\n break;\n case 'nameDesc':\n copy.sort((a, b) => b.name.localeCompare(a.name));\n break;\n case 'revenueDesc':\n copy.sort((a, b) => revenue(b.totalRevenue) - revenue(a.totalRevenue));\n break;\n case 'newest':\n default:\n // Mirrors the backend's orderBy: { createdAt: 'desc' } default.\n copy.sort((a, b) => timestamp(b.createdAt) - timestamp(a.createdAt));\n break;\n }\n return copy;\n}\n\n/**\n * Apply the toolbar search/type/status filters and sort, then cut the\n * result to the visible page. `items` MUST be the complete loaded list —\n * callers page through every backend batch first (see the module note).\n */\nexport function applyProviderControls<T extends ProviderListItem>(\n items: readonly T[],\n query: ProviderListQuery,\n visibleCount: number\n): ProviderControlsResult<T> {\n const needle = query.search.trim().toLowerCase();\n const filtered = items.filter((item) => {\n if (needle && !item.name.toLowerCase().includes(needle)) return false;\n if (query.type !== null && item.type !== query.type) return false;\n return true;\n });\n\n const sorted = sortItems(filtered, query.sort);\n const cap = Math.max(1, Math.floor(visibleCount));\n return {\n shown: sorted.slice(0, cap),\n total: sorted.length,\n isFiltered: isProviderQueryFiltered(query),\n hasMore: sorted.length > cap,\n };\n}\n"],"mappings":";AAsCA,IAAa,IAA4C;CACvD,QAAQ;CACR,MAAM;CACN,MAAM;AACR;AAcA,SAAgB,EAAwB,GAAmC;CACzE,OAAO,EAAM,OAAO,KAAK,MAAM,MAAM,EAAM,SAAS;AACtD;AAEA,SAAS,EAAU,GAAuB;CACxC,IAAM,IAAS,KAAK,MAAM,CAAK;CAC/B,OAAO,OAAO,SAAS,CAAM,IAAI,IAAS;AAC5C;AAEA,SAAS,EAAQ,GAAuB;CACtC,OAAO,OAAO,SAAS,CAAK,IAAI,IAAQ;AAC1C;AAEA,SAAS,EAAsC,GAAqB,GAAyB;CAC3F,IAAM,IAAO,CAAC,GAAG,CAAK;CACtB,QAAQ,GAAR;EACE,KAAK;GACH,EAAK,MAAM,GAAG,MAAM,EAAU,EAAE,SAAS,IAAI,EAAU,EAAE,SAAS,CAAC;GACnE;EACF,KAAK;GACH,EAAK,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;GAChD;EACF,KAAK;GACH,EAAK,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;GAChD;EACF,KAAK;GACH,EAAK,MAAM,GAAG,MAAM,EAAQ,EAAE,YAAY,IAAI,EAAQ,EAAE,YAAY,CAAC;GACrE;EAEF;GAEE,EAAK,MAAM,GAAG,MAAM,EAAU,EAAE,SAAS,IAAI,EAAU,EAAE,SAAS,CAAC;GACnE;CACJ;CACA,OAAO;AACT;AAOA,SAAgB,EACd,GACA,GACA,GAC2B;CAC3B,IAAM,IAAS,EAAM,OAAO,KAAK,CAAC,CAAC,YAAY,GAOzC,IAAS,EANE,EAAM,QAAQ,MAE7B,EADI,KAAU,CAAC,EAAK,KAAK,YAAY,CAAC,CAAC,SAAS,CAAM,KAClD,EAAM,SAAS,QAAQ,EAAK,SAAS,EAAM,KAIxB,GAAU,EAAM,IAAI,GACvC,IAAM,KAAK,IAAI,GAAG,KAAK,MAAM,CAAY,CAAC;CAChD,OAAO;EACL,OAAO,EAAO,MAAM,GAAG,CAAG;EAC1B,OAAO,EAAO;EACd,YAAY,EAAwB,CAAK;EACzC,SAAS,EAAO,SAAS;CAC3B;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"providerTypeMeta.js","names":[],"sources":["../../../src/ggnomad/utils/providerTypeMeta.ts"],"sourcesContent":["/**\n * Shared metadata for the GgnomadProviderType enum + provider visual\n * helpers.\n *\n * The single source of truth for the translation key + English fallback\n * for each provider type label, reused by both ProviderDashboardCard.tsx\n * (rendering) and CreateProviderDialog.tsx (form dropdown) so the wording\n * cannot drift between the two surfaces.\n *\n * If the backend ever adds a new value to GgnomadProviderType, add a\n * row here — anything unlisted falls back to a sentence-cased version of\n * the enum (\"HOTEL_OPERATOR\" -> \"Hotel operator\").\n *\n * Type-specific icons were intentionally removed: a single type\n * (\"Transport provider\") can cover buses, taxis, ferries and airlines,\n * so one icon per type is wrong. The card's banner is tinted by type\n * instead (see bannerTintFor) so the tint works as a type indicator.\n */\n\n/**\n * Local enum declaration. The published supergraph on Hive CDN still\n * declares `GgnomadProvider.type` as `String`, so the codegen-derived\n * types in src/generated/wspace-types.ts don't carry this enum yet. The\n * backend change (BOFF-8207) adds the enum to ggnomad.gql; once it ships\n * to alpha, the next codegen run will export this enum from\n * wspace-types and the local fallback below becomes a no-op. Keep the\n * two declarations in sync.\n */\nexport type GgnomadProviderType =\n | 'HOTEL_OPERATOR'\n | 'TOUR_OPERATOR'\n | 'ACTIVITY_PROVIDER'\n | 'TRANSPORT_PROVIDER'\n | 'EVENT_ORGANIZER';\n\n/**\n * Per-type translation keys + English fallbacks. The dropdown/filter chips\n * show these labels, and the same keys are reused by CreateProviderDialog.\n * If you add a value to GgnomadProviderType, add a key here and (eventually)\n * a translation in the locale files; the fallback covers English-only\n * locales.\n */\nconst KNOWN_LABEL_KEYS: Record<GgnomadProviderType, { key: string; fallback: string }> = {\n HOTEL_OPERATOR: { key: 'ggnomad.provider.type.hotelOperator', fallback: 'Hotel operator' },\n TOUR_OPERATOR: { key: 'ggnomad.provider.type.tourOperator', fallback: 'Tour operator' },\n ACTIVITY_PROVIDER: {\n key: 'ggnomad.provider.type.activityProvider',\n fallback: 'Activity provider',\n },\n TRANSPORT_PROVIDER: {\n key: 'ggnomad.provider.type.transportProvider',\n fallback: 'Transport provider',\n },\n EVENT_ORGANIZER: { key: 'ggnomad.provider.type.eventOrganizer', fallback: 'Event organizer' },\n};\n\nconst ORDER: GgnomadProviderType[] = [\n 'HOTEL_OPERATOR',\n 'TOUR_OPERATOR',\n 'ACTIVITY_PROVIDER',\n 'TRANSPORT_PROVIDER',\n 'EVENT_ORGANIZER',\n];\n\n/**\n * Build the dropdown options. `translate` should be the `t` from `useI18n()` in\n * the calling component so each label resolves in the active locale; the\n * fallback string is used otherwise (or when the translator is omitted).\n */\nexport function getProviderTypeOptions(\n translate?: (key: string, fallback: string) => string\n): Array<{\n value: GgnomadProviderType;\n label: string;\n}> {\n return ORDER.map((value) => {\n const { key, fallback } = KNOWN_LABEL_KEYS[value];\n return { value, label: translate ? translate(key, fallback) : fallback };\n });\n}\n\n/** Human-facing label for a provider type, with a safe fallback. */\nexport function labelForProviderType(\n type: string,\n translate?: (key: string, fallback: string) => string\n): string {\n if (type in KNOWN_LABEL_KEYS) {\n const { key, fallback } = KNOWN_LABEL_KEYS[type as GgnomadProviderType];\n return translate ? translate(key, fallback) : fallback;\n }\n return enumToSentence(type);\n}\n\n/** \"HOTEL_OPERATOR\" -> \"Hotel operator\", \"OTHER\" -> \"Other\". */\nfunction enumToSentence(value: string): string {\n const spaced = value.replace(/_+/g, ' ').trim().toLowerCase();\n if (!spaced) return value;\n return spaced.charAt(0).toUpperCase() + spaced.slice(1);\n}\n\n/**\n * Two-letter initials for the avatar fallback.\n *\n * - \"Indigo\" -> \"IN\" (first two letters of the single word)\n * - \"Blue Sky\" -> \"BS\" (first letter of each of the first two words)\n * - \"A\" -> \"A\" (single character preserved as-is)\n * - \"\" -> \"??\" (sentinel for empty names)\n */\nexport function initialsFor(name: string): string {\n const cleaned = name.trim();\n if (!cleaned) return '??';\n\n const words = cleaned.split(/\\s+/).filter(Boolean);\n let result: string;\n if (words.length >= 2) {\n result = (words[0].charAt(0) + words[1].charAt(0)).toUpperCase();\n } else {\n result = words[0].slice(0, 2).toUpperCase();\n }\n return result || '??';\n}\n\n/** \"alpine\" -> \"Alpine\", \"SUSPENDED\" -> \"Suspended\". Lowercases the input\n * first so the function normalizes backend enum values (which arrive\n * in UPPER_SNAKE_CASE) to a sentence-case display label. */\nexport function sentenceCase(value: string): string {\n const trimmed = value.trim();\n if (!trimmed) return '';\n const lower = trimmed.toLowerCase();\n return lower.charAt(0).toUpperCase() + lower.slice(1);\n}\n\n/** \"blue sky adventures\" -> \"Blue Sky Adventures\". Title-cases each word,\n * leaving small connector words (\"of\", \"the\", \"and\", \"a\", \"an\", \"in\", \"on\",\n * \"for\", \"to\") lowercase unless they are the first word. Used to display\n * user-entered provider names. */\nconst TITLE_CASE_SMALL_WORDS = new Set([\n 'a',\n 'an',\n 'and',\n 'as',\n 'at',\n 'but',\n 'by',\n 'for',\n 'in',\n 'nor',\n 'of',\n 'on',\n 'or',\n 'so',\n 'the',\n 'to',\n 'up',\n 'yet',\n]);\n\nexport function titleCase(value: string): string {\n const trimmed = value.trim();\n if (!trimmed) return '';\n // Preserve acronyms and proper nouns the user has already chosen the\n // casing for (\"McDonald's\", \"DHL Express\"): only short-circuit on\n // *mixed-case* input. Pure all-caps input like \"ALPINE TREKS CO\" is\n // still title-cased so the user gets \"Alpine Treks Co\".\n if (/[A-Z]/.test(trimmed.slice(1)) && /[a-z]/.test(trimmed)) return trimmed;\n const words = trimmed.split(/\\s+/);\n return words\n .map((word, index) => {\n const lower = word.toLowerCase();\n if (index > 0 && TITLE_CASE_SMALL_WORDS.has(lower)) return lower;\n if (word.length === 0) return word;\n return lower.charAt(0).toUpperCase() + lower.slice(1);\n })\n .join(' ');\n}\n\n/**\n * Banner tint as a CSS `style` object. The banner band uses the hue\n * at 16% alpha over the card surface so the band reads as colored in\n * both light and dark mode; the avatar initials use\n * `var(--color-text-primary)` so contrast holds against the card\n * surface in both themes. (Previously the per-type hue was used\n * directly for the initials, which was too low-contrast for some\n * chart-N slots in light mode.)\n *\n * Structurally compatible with React.CSSProperties — we don't import\n * React here to keep this util free of UI dependencies.\n *\n * Implementation note: colors are applied via inline `style` (not\n * Tailwind classes) because the host shell's Tailwind v4 build does\n * not emit utility classes for the theme's custom palette\n * (`bg-chart-N`, `bg-accent-*`, ...). The tokens resolve against the\n * active theme so contrast holds in light and dark mode without us\n * hard-coding hex values per theme.\n */\nexport interface BannerTintStyle {\n backgroundColor: string;\n color: string;\n}\n\n/** Build the style object for a hue token. */\nfunction tint(token: string): BannerTintStyle {\n return {\n backgroundColor: `color-mix(in srgb, ${token} 16%, transparent)`,\n color: 'var(--color-text-primary)',\n };\n}\n\n/**\n * Fixed banner hue per provider type. Color is keyed by type (not by\n * id), so every provider of a given type reads the same — the tint\n * becomes a type indicator at a glance. No green (reserved for the\n * \"Active\" status) and no red (reserved for errors).\n */\n/**\n * Chart-token slot per provider type. The actual hue is theme-dependent\n * (Forest/Ocean/Sunset/HC themes ship different chart-1..12 palettes), so\n * the card's banner reads as a type indicator via the design system rather\n * than a fixed hex. Light/dark contrast is the theme tokens' responsibility.\n *\n * Slots 3..7 are used so slots 1/2 stay free for brand accents and chart-\n * positive stays free for status success. If you add a new provider type,\n * pick the next free slot from this range; chart-8..12 stay reserved for\n * later additions.\n */\nconst TYPE_TINT_VAR: Record<GgnomadProviderType, string> = {\n HOTEL_OPERATOR: 'var(--color-chart-3)',\n TOUR_OPERATOR: 'var(--color-chart-4)',\n ACTIVITY_PROVIDER: 'var(--color-chart-5)',\n TRANSPORT_PROVIDER: 'var(--color-chart-6)',\n EVENT_ORGANIZER: 'var(--color-chart-7)',\n};\n\n/** Fallback for types outside the known enum (incl. future additions). */\nconst FALLBACK_TINT_VAR = 'var(--color-border-seam)';\n\n/** Banner tint for a provider type; unknown types get neutral slate. */\nexport function bannerTintFor(type: string): BannerTintStyle {\n // `in` guard mirrors labelForProviderType so an arbitrary string never\n // indexes the enum-keyed record without a check.\n const token = type in TYPE_TINT_VAR ? TYPE_TINT_VAR[type as GgnomadProviderType] : undefined;\n return tint(token ?? FALLBACK_TINT_VAR);\n}\n"],"mappings":";AA0CA,IAAM,IAAmF;CACvF,gBAAgB;EAAE,KAAK;EAAuC,UAAU;CAAiB;CACzF,eAAe;EAAE,KAAK;EAAsC,UAAU;CAAgB;CACtF,mBAAmB;EACjB,KAAK;EACL,UAAU;CACZ;CACA,oBAAoB;EAClB,KAAK;EACL,UAAU;CACZ;CACA,iBAAiB;EAAE,KAAK;EAAwC,UAAU;CAAkB;AAC9F,GAEM,IAA+B;CACnC;CACA;CACA;CACA;CACA;AACF;AAOA,SAAgB,EACd,GAIC;CACD,OAAO,EAAM,KAAK,MAAU;EAC1B,IAAM,EAAE,QAAK,gBAAa,EAAiB;EAC3C,OAAO;GAAE;GAAO,OAAO,IAAY,EAAU,GAAK,CAAQ,IAAI;EAAS;CACzE,CAAC;AACH;AAGA,SAAgB,EACd,GACA,GACQ;CACR,IAAI,KAAQ,GAAkB;EAC5B,IAAM,EAAE,QAAK,gBAAa,EAAiB;EAC3C,OAAO,IAAY,EAAU,GAAK,CAAQ,IAAI;CAChD;CACA,OAAO,EAAe,CAAI;AAC5B;AAGA,SAAS,EAAe,GAAuB;CAC7C,IAAM,IAAS,EAAM,QAAQ,OAAO,GAAG,EAAE,KAAK,EAAE,YAAY;CAE5D,OADK,IACE,EAAO,OAAO,CAAC,EAAE,YAAY,IAAI,EAAO,MAAM,CAAC,IADlC;AAEtB;AAUA,SAAgB,EAAY,GAAsB;CAChD,IAAM,IAAU,EAAK,KAAK;CAC1B,IAAI,CAAC,GAAS,OAAO;CAErB,IAAM,IAAQ,EAAQ,MAAM,KAAK,EAAE,OAAO,OAAO,GAC7C;CAMJ,OALA,AAGE,IAHE,EAAM,UAAU,KACR,EAAM,GAAG,OAAO,CAAC,IAAI,EAAM,GAAG,OAAO,CAAC,GAAG,YAAY,IAEtD,EAAM,GAAG,MAAM,GAAG,CAAC,EAAE,YAAY,GAErC,KAAU;AACnB;AAKA,SAAgB,EAAa,GAAuB;CAClD,IAAM,IAAU,EAAM,KAAK;CAC3B,IAAI,CAAC,GAAS,OAAO;CACrB,IAAM,IAAQ,EAAQ,YAAY;CAClC,OAAO,EAAM,OAAO,CAAC,EAAE,YAAY,IAAI,EAAM,MAAM,CAAC;AACtD;AAuEA,SAAS,EAAK,GAAgC;CAC5C,OAAO;EACL,iBAAiB,sBAAsB,EAAM;EAC7C,OAAO;CACT;AACF;AAmBA,IAAM,IAAqD;CACzD,gBAAgB;CAChB,eAAe;CACf,mBAAmB;CACnB,oBAAoB;CACpB,iBAAiB;AACnB,GAGM,IAAoB;AAG1B,SAAgB,EAAc,GAA+B;CAI3D,OAAO,GADO,KAAQ,IAAgB,EAAc,KAA+B,KAAA,MAC9D,CAAiB;AACxC"}
|
|
1
|
+
{"version":3,"file":"providerTypeMeta.js","names":[],"sources":["../../../src/ggnomad/utils/providerTypeMeta.ts"],"sourcesContent":["/**\n * Shared metadata for the GgnomadProviderType enum + provider visual\n * helpers.\n *\n * The single source of truth for the translation key + English fallback\n * for each provider type label, reused by both ProviderDashboardCard.tsx\n * (rendering) and CreateProviderDialog.tsx (form dropdown) so the wording\n * cannot drift between the two surfaces.\n *\n * If the backend ever adds a new value to GgnomadProviderType, add a\n * row here — anything unlisted falls back to a sentence-cased version of\n * the enum (\"HOTEL_OPERATOR\" -> \"Hotel operator\").\n *\n * Type-specific icons were intentionally removed: a single type\n * (\"Transport provider\") can cover buses, taxis, ferries and airlines,\n * so one icon per type is wrong. The card's banner is tinted by type\n * instead (see bannerTintFor) so the tint works as a type indicator.\n */\n\n/**\n * Local enum declaration. The published supergraph on Hive CDN still\n * declares `GgnomadProvider.type` as `String`, so the codegen-derived\n * types in src/generated/wspace-types.ts don't carry this enum yet. The\n * backend change (BOFF-8207) adds the enum to ggnomad.gql; once it ships\n * to alpha, the next codegen run will export this enum from\n * wspace-types and the local fallback below becomes a no-op. Keep the\n * two declarations in sync.\n */\nexport type GgnomadProviderType =\n | 'HOTEL_OPERATOR'\n | 'TOUR_OPERATOR'\n | 'ACTIVITY_PROVIDER'\n | 'TRANSPORT_PROVIDER'\n | 'EVENT_ORGANIZER';\n\n/**\n * Per-type translation keys + English fallbacks. The dropdown/filter chips\n * show these labels, and the same keys are reused by CreateProviderDialog.\n * If you add a value to GgnomadProviderType, add a key here and (eventually)\n * a translation in the locale files; the fallback covers English-only\n * locales.\n */\nconst KNOWN_LABEL_KEYS: Record<GgnomadProviderType, { key: string; fallback: string }> = {\n HOTEL_OPERATOR: { key: 'ggnomad.provider.type.hotelOperator', fallback: 'Hotel operator' },\n TOUR_OPERATOR: { key: 'ggnomad.provider.type.tourOperator', fallback: 'Tour operator' },\n ACTIVITY_PROVIDER: {\n key: 'ggnomad.provider.type.activityProvider',\n fallback: 'Activity provider',\n },\n TRANSPORT_PROVIDER: {\n key: 'ggnomad.provider.type.transportProvider',\n fallback: 'Transport provider',\n },\n EVENT_ORGANIZER: { key: 'ggnomad.provider.type.eventOrganizer', fallback: 'Event organizer' },\n};\n\nconst ORDER: GgnomadProviderType[] = [\n 'HOTEL_OPERATOR',\n 'TOUR_OPERATOR',\n 'ACTIVITY_PROVIDER',\n 'TRANSPORT_PROVIDER',\n 'EVENT_ORGANIZER',\n];\n\n/**\n * Build the dropdown options. `translate` should be the `t` from `useI18n()` in\n * the calling component so each label resolves in the active locale; the\n * fallback string is used otherwise (or when the translator is omitted).\n */\nexport function getProviderTypeOptions(\n translate?: (key: string, fallback: string) => string\n): Array<{\n value: GgnomadProviderType;\n label: string;\n}> {\n return ORDER.map((value) => {\n const { key, fallback } = KNOWN_LABEL_KEYS[value];\n return { value, label: translate ? translate(key, fallback) : fallback };\n });\n}\n\n/** Human-facing label for a provider type, with a safe fallback. */\nexport function labelForProviderType(\n type: string,\n translate?: (key: string, fallback: string) => string\n): string {\n if (type in KNOWN_LABEL_KEYS) {\n const { key, fallback } = KNOWN_LABEL_KEYS[type as GgnomadProviderType];\n return translate ? translate(key, fallback) : fallback;\n }\n return enumToSentence(type);\n}\n\n/** \"HOTEL_OPERATOR\" -> \"Hotel operator\", \"OTHER\" -> \"Other\". */\nfunction enumToSentence(value: string): string {\n const spaced = value.replace(/_+/g, ' ').trim().toLowerCase();\n if (!spaced) return value;\n return spaced.charAt(0).toUpperCase() + spaced.slice(1);\n}\n\n/**\n * Two-letter initials for the avatar fallback.\n *\n * - \"Indigo\" -> \"IN\" (first two letters of the single word)\n * - \"Blue Sky\" -> \"BS\" (first letter of each of the first two words)\n * - \"A\" -> \"A\" (single character preserved as-is)\n * - \"\" -> \"??\" (sentinel for empty names)\n */\nexport function initialsFor(name: string): string {\n const cleaned = name.trim();\n if (!cleaned) return '??';\n\n const words = cleaned.split(/\\s+/).filter(Boolean);\n let result: string;\n if (words.length >= 2) {\n result = (words[0].charAt(0) + words[1].charAt(0)).toUpperCase();\n } else {\n result = words[0].slice(0, 2).toUpperCase();\n }\n return result || '??';\n}\n\n/** \"alpine\" -> \"Alpine\", \"SUSPENDED\" -> \"Suspended\". Lowercases the input\n * first so the function normalizes backend enum values (which arrive\n * in UPPER_SNAKE_CASE) to a sentence-case display label. */\nexport function sentenceCase(value: string): string {\n const trimmed = value.trim();\n if (!trimmed) return '';\n const lower = trimmed.toLowerCase();\n return lower.charAt(0).toUpperCase() + lower.slice(1);\n}\n\n/** \"blue sky adventures\" -> \"Blue Sky Adventures\". Title-cases each word,\n * leaving small connector words (\"of\", \"the\", \"and\", \"a\", \"an\", \"in\", \"on\",\n * \"for\", \"to\") lowercase unless they are the first word. Used to display\n * user-entered provider names. */\nconst TITLE_CASE_SMALL_WORDS = new Set([\n 'a',\n 'an',\n 'and',\n 'as',\n 'at',\n 'but',\n 'by',\n 'for',\n 'in',\n 'nor',\n 'of',\n 'on',\n 'or',\n 'so',\n 'the',\n 'to',\n 'up',\n 'yet',\n]);\n\nexport function titleCase(value: string): string {\n const trimmed = value.trim();\n if (!trimmed) return '';\n // Preserve acronyms and proper nouns the user has already chosen the\n // casing for (\"McDonald's\", \"DHL Express\"): only short-circuit on\n // *mixed-case* input. Pure all-caps input like \"ALPINE TREKS CO\" is\n // still title-cased so the user gets \"Alpine Treks Co\".\n if (/[A-Z]/.test(trimmed.slice(1)) && /[a-z]/.test(trimmed)) return trimmed;\n const words = trimmed.split(/\\s+/);\n return words\n .map((word, index) => {\n const lower = word.toLowerCase();\n if (index > 0 && TITLE_CASE_SMALL_WORDS.has(lower)) return lower;\n if (word.length === 0) return word;\n return lower.charAt(0).toUpperCase() + lower.slice(1);\n })\n .join(' ');\n}\n\n/**\n * Banner tint as a CSS `style` object. The banner band uses the hue\n * at 16% alpha over the card surface so the band reads as colored in\n * both light and dark mode; the avatar initials use\n * `var(--color-text-primary)` so contrast holds against the card\n * surface in both themes. (Previously the per-type hue was used\n * directly for the initials, which was too low-contrast for some\n * chart-N slots in light mode.)\n *\n * Structurally compatible with React.CSSProperties — we don't import\n * React here to keep this util free of UI dependencies.\n *\n * Implementation note: colors are applied via inline `style` (not\n * Tailwind classes) because the host shell's Tailwind v4 build does\n * not emit utility classes for the theme's custom palette\n * (`bg-chart-N`, `bg-accent-*`, ...). The tokens resolve against the\n * active theme so contrast holds in light and dark mode without us\n * hard-coding hex values per theme.\n */\nexport interface BannerTintStyle {\n backgroundColor: string;\n color: string;\n}\n\n/** Build the style object for a hue token. */\nfunction tint(token: string): BannerTintStyle {\n return {\n backgroundColor: `color-mix(in srgb, ${token} 16%, transparent)`,\n color: 'var(--color-text-primary)',\n };\n}\n\n/**\n * Fixed banner hue per provider type. Color is keyed by type (not by\n * id), so every provider of a given type reads the same — the tint\n * becomes a type indicator at a glance. No green (reserved for the\n * \"Active\" status) and no red (reserved for errors).\n */\n/**\n * Chart-token slot per provider type. The actual hue is theme-dependent\n * (Forest/Ocean/Sunset/HC themes ship different chart-1..12 palettes), so\n * the card's banner reads as a type indicator via the design system rather\n * than a fixed hex. Light/dark contrast is the theme tokens' responsibility.\n *\n * Slots 3..7 are used so slots 1/2 stay free for brand accents and chart-\n * positive stays free for status success. If you add a new provider type,\n * pick the next free slot from this range; chart-8..12 stay reserved for\n * later additions.\n */\nconst TYPE_TINT_VAR: Record<GgnomadProviderType, string> = {\n HOTEL_OPERATOR: 'var(--color-chart-3)',\n TOUR_OPERATOR: 'var(--color-chart-4)',\n ACTIVITY_PROVIDER: 'var(--color-chart-5)',\n TRANSPORT_PROVIDER: 'var(--color-chart-6)',\n EVENT_ORGANIZER: 'var(--color-chart-7)',\n};\n\n/** Fallback for types outside the known enum (incl. future additions). */\nconst FALLBACK_TINT_VAR = 'var(--color-border-seam)';\n\n/** Banner tint for a provider type; unknown types get neutral slate. */\nexport function bannerTintFor(type: string): BannerTintStyle {\n // `in` guard mirrors labelForProviderType so an arbitrary string never\n // indexes the enum-keyed record without a check.\n const token = type in TYPE_TINT_VAR ? TYPE_TINT_VAR[type as GgnomadProviderType] : undefined;\n return tint(token ?? FALLBACK_TINT_VAR);\n}\n"],"mappings":";AA0CA,IAAM,IAAmF;CACvF,gBAAgB;EAAE,KAAK;EAAuC,UAAU;CAAiB;CACzF,eAAe;EAAE,KAAK;EAAsC,UAAU;CAAgB;CACtF,mBAAmB;EACjB,KAAK;EACL,UAAU;CACZ;CACA,oBAAoB;EAClB,KAAK;EACL,UAAU;CACZ;CACA,iBAAiB;EAAE,KAAK;EAAwC,UAAU;CAAkB;AAC9F,GAEM,IAA+B;CACnC;CACA;CACA;CACA;CACA;AACF;AAOA,SAAgB,EACd,GAIC;CACD,OAAO,EAAM,KAAK,MAAU;EAC1B,IAAM,EAAE,QAAK,gBAAa,EAAiB;EAC3C,OAAO;GAAE;GAAO,OAAO,IAAY,EAAU,GAAK,CAAQ,IAAI;EAAS;CACzE,CAAC;AACH;AAGA,SAAgB,EACd,GACA,GACQ;CACR,IAAI,KAAQ,GAAkB;EAC5B,IAAM,EAAE,QAAK,gBAAa,EAAiB;EAC3C,OAAO,IAAY,EAAU,GAAK,CAAQ,IAAI;CAChD;CACA,OAAO,EAAe,CAAI;AAC5B;AAGA,SAAS,EAAe,GAAuB;CAC7C,IAAM,IAAS,EAAM,QAAQ,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,YAAY;CAE5D,OADK,IACE,EAAO,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,EAAO,MAAM,CAAC,IADlC;AAEtB;AAUA,SAAgB,EAAY,GAAsB;CAChD,IAAM,IAAU,EAAK,KAAK;CAC1B,IAAI,CAAC,GAAS,OAAO;CAErB,IAAM,IAAQ,EAAQ,MAAM,KAAK,CAAC,CAAC,OAAO,OAAO,GAC7C;CAMJ,OALA,AAGE,IAHE,EAAM,UAAU,KACR,EAAM,EAAE,CAAC,OAAO,CAAC,IAAI,EAAM,EAAE,CAAC,OAAO,CAAC,EAAA,CAAG,YAAY,IAEtD,EAAM,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,YAAY,GAErC,KAAU;AACnB;AAKA,SAAgB,EAAa,GAAuB;CAClD,IAAM,IAAU,EAAM,KAAK;CAC3B,IAAI,CAAC,GAAS,OAAO;CACrB,IAAM,IAAQ,EAAQ,YAAY;CAClC,OAAO,EAAM,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,EAAM,MAAM,CAAC;AACtD;AAuEA,SAAS,EAAK,GAAgC;CAC5C,OAAO;EACL,iBAAiB,sBAAsB,EAAM;EAC7C,OAAO;CACT;AACF;AAmBA,IAAM,IAAqD;CACzD,gBAAgB;CAChB,eAAe;CACf,mBAAmB;CACnB,oBAAoB;CACpB,iBAAiB;AACnB,GAGM,IAAoB;AAG1B,SAAgB,EAAc,GAA+B;CAI3D,OAAO,GADO,KAAQ,IAAgB,EAAc,KAA+B,KAAA,MAC9D,CAAiB;AACxC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pageContext.js","names":[],"sources":["../../src/utils/pageContext.ts"],"sourcesContent":["/**\n * Page context gathering for the GGNomad AI assistant.\n *\n * Captures the user's current browser context (URL, route, entity) so the\n * floating assistant can answer \"this page\" / \"current\" questions and run\n * operations scoped to the screen the user is looking at.\n */\n\nexport interface PageContext {\n currentUrl: string;\n currentPage: string;\n entityType?: string;\n entityId?: string;\n platform: string;\n version: string;\n viewportSize: string;\n userAgent: string;\n timestamp: string;\n}\n\ninterface ConversationContextShape {\n entityType?: string;\n entityId?: string;\n activeTab?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport function gatherPageContext(): PageContext {\n const url = window.location.href;\n const pathSegments = window.location.pathname.split('/').filter(Boolean);\n const currentPage = pathSegments.slice(0, 3).join('/') || 'home';\n const entity = resolveGGNomadConversationContext(\n window.location.pathname,\n window.location.search\n );\n\n return {\n currentUrl: url,\n currentPage,\n entityType: entity.entityType,\n entityId: entity.entityId,\n platform: 'ggnomad',\n version: import.meta.env.VITE_APP_VERSION ?? '2026.420.95',\n viewportSize: `${window.innerWidth}x${window.innerHeight}`,\n userAgent: navigator.userAgent,\n timestamp: new Date().toISOString(),\n };\n}\n\nfunction matchPath(pattern: RegExp, pathname: string): RegExpMatchArray | null {\n return pathname.match(pattern);\n}\n\nexport function resolveGGNomadConversationContext(\n pathname: string,\n search: string\n): ConversationContextShape {\n const params = new URLSearchParams(search);\n\n const directMatches: Array<[RegExp, string]> = [\n [/^\\/ggnomad\\/hospitality\\/([^/]+)$/, 'GGNOMAD_PROPERTY'],\n [/^\\/ggnomad\\/tourism\\/tours\\/([^/]+)$/, 'GGNOMAD_TOUR'],\n [/^\\/ggnomad\\/tourism\\/activities\\/([^/]+)$/, 'GGNOMAD_ACTIVITY'],\n [/^\\/ggnomad\\/entertainment\\/([^/]+)$/, 'GGNOMAD_EVENT'],\n [/^\\/ggnomad\\/trips\\/([^/]+)$/, 'GGNOMAD_TRIP_PLAN'],\n ];\n\n for (const [pattern, entityType] of directMatches) {\n const match = matchPath(pattern, pathname);\n if (match) {\n return {\n entityType,\n entityId: match[1],\n metadata: { routePattern: pattern.source },\n };\n }\n }\n\n const activeTab =\n params.get('tab') ??\n (() => {\n const segments = pathname.split('/').filter(Boolean);\n return segments.length > 2 ? segments[segments.length - 1] : undefined;\n })();\n\n return {\n activeTab: activeTab ?? undefined,\n metadata: {\n ...(activeTab ? { activeTab } : {}),\n ...(params.get('q') ? { searchQuery: params.get('q') } : {}),\n },\n };\n}\n"],"mappings":";AA2BA,SAAgB,IAAiC;CAC/C,IAAM,IAAM,OAAO,SAAS,MAEtB,IADe,OAAO,SAAS,SAAS,MAAM,GAAG,
|
|
1
|
+
{"version":3,"file":"pageContext.js","names":[],"sources":["../../src/utils/pageContext.ts"],"sourcesContent":["/**\n * Page context gathering for the GGNomad AI assistant.\n *\n * Captures the user's current browser context (URL, route, entity) so the\n * floating assistant can answer \"this page\" / \"current\" questions and run\n * operations scoped to the screen the user is looking at.\n */\n\nexport interface PageContext {\n currentUrl: string;\n currentPage: string;\n entityType?: string;\n entityId?: string;\n platform: string;\n version: string;\n viewportSize: string;\n userAgent: string;\n timestamp: string;\n}\n\ninterface ConversationContextShape {\n entityType?: string;\n entityId?: string;\n activeTab?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport function gatherPageContext(): PageContext {\n const url = window.location.href;\n const pathSegments = window.location.pathname.split('/').filter(Boolean);\n const currentPage = pathSegments.slice(0, 3).join('/') || 'home';\n const entity = resolveGGNomadConversationContext(\n window.location.pathname,\n window.location.search\n );\n\n return {\n currentUrl: url,\n currentPage,\n entityType: entity.entityType,\n entityId: entity.entityId,\n platform: 'ggnomad',\n version: import.meta.env.VITE_APP_VERSION ?? '2026.420.95',\n viewportSize: `${window.innerWidth}x${window.innerHeight}`,\n userAgent: navigator.userAgent,\n timestamp: new Date().toISOString(),\n };\n}\n\nfunction matchPath(pattern: RegExp, pathname: string): RegExpMatchArray | null {\n return pathname.match(pattern);\n}\n\nexport function resolveGGNomadConversationContext(\n pathname: string,\n search: string\n): ConversationContextShape {\n const params = new URLSearchParams(search);\n\n const directMatches: Array<[RegExp, string]> = [\n [/^\\/ggnomad\\/hospitality\\/([^/]+)$/, 'GGNOMAD_PROPERTY'],\n [/^\\/ggnomad\\/tourism\\/tours\\/([^/]+)$/, 'GGNOMAD_TOUR'],\n [/^\\/ggnomad\\/tourism\\/activities\\/([^/]+)$/, 'GGNOMAD_ACTIVITY'],\n [/^\\/ggnomad\\/entertainment\\/([^/]+)$/, 'GGNOMAD_EVENT'],\n [/^\\/ggnomad\\/trips\\/([^/]+)$/, 'GGNOMAD_TRIP_PLAN'],\n ];\n\n for (const [pattern, entityType] of directMatches) {\n const match = matchPath(pattern, pathname);\n if (match) {\n return {\n entityType,\n entityId: match[1],\n metadata: { routePattern: pattern.source },\n };\n }\n }\n\n const activeTab =\n params.get('tab') ??\n (() => {\n const segments = pathname.split('/').filter(Boolean);\n return segments.length > 2 ? segments[segments.length - 1] : undefined;\n })();\n\n return {\n activeTab: activeTab ?? undefined,\n metadata: {\n ...(activeTab ? { activeTab } : {}),\n ...(params.get('q') ? { searchQuery: params.get('q') } : {}),\n },\n };\n}\n"],"mappings":";AA2BA,SAAgB,IAAiC;CAC/C,IAAM,IAAM,OAAO,SAAS,MAEtB,IADe,OAAO,SAAS,SAAS,MAAM,GAAG,CAAC,CAAC,OAAO,OAC5C,CAAA,CAAa,MAAM,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,KAAK,QACpD,IAAS,EACb,OAAO,SAAS,UAChB,OAAO,SAAS,MAClB;CAEA,OAAO;EACL,YAAY;EACZ;EACA,YAAY,EAAO;EACnB,UAAU,EAAO;EACjB,UAAU;EACV,SAA6C;EAC7C,cAAc,GAAG,OAAO,WAAW,GAAG,OAAO;EAC7C,WAAW,UAAU;EACrB,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;CACpC;AACF;AAEA,SAAS,EAAU,GAAiB,GAA2C;CAC7E,OAAO,EAAS,MAAM,CAAO;AAC/B;AAEA,SAAgB,EACd,GACA,GAC0B;CAC1B,IAAM,IAAS,IAAI,gBAAgB,CAAM;CAUzC,KAAK,IAAM,CAAC,GAAS,MAAe;EAPlC,CAAC,qCAAqC,kBAAkB;EACxD,CAAC,wCAAwC,cAAc;EACvD,CAAC,6CAA6C,kBAAkB;EAChE,CAAC,uCAAuC,eAAe;EACvD,CAAC,+BAA+B,mBAAmB;CAGjB,GAAe;EACjD,IAAM,IAAQ,EAAU,GAAS,CAAQ;EACzC,IAAI,GACF,OAAO;GACL;GACA,UAAU,EAAM;GAChB,UAAU,EAAE,cAAc,EAAQ,OAAO;EAC3C;CAEJ;CAEA,IAAM,IACJ,EAAO,IAAI,KAAK,YACT;EACL,IAAM,IAAW,EAAS,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO;EACnD,OAAO,EAAS,SAAS,IAAI,EAAS,EAAS,SAAS,KAAK,KAAA;CAC/D,EAAA,CAAG;CAEL,OAAO;EACL,WAAW,KAAa,KAAA;EACxB,UAAU;GACR,GAAI,IAAY,EAAE,aAAU,IAAI,CAAC;GACjC,GAAI,EAAO,IAAI,GAAG,IAAI,EAAE,aAAa,EAAO,IAAI,GAAG,EAAE,IAAI,CAAC;EAC5D;CACF;AACF"}
|