@burdenoff/microfe-ggnomad 2026.1016.1 → 2026.1029.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ggnomad/GGNomadRoot.js +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.d.ts.map +1 -1
- package/dist/ggnomad/components/BookingPanel.js +53 -70
- 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.d.ts.map +1 -1
- package/dist/ggnomad/pages/BookingsPage.js +54 -52
- 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.d.ts.map +1 -1
- package/dist/ggnomad/pages/TripPlanDetailPage.js +278 -273
- 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/bookingDates.d.ts +21 -0
- package/dist/ggnomad/utils/bookingDates.d.ts.map +1 -0
- package/dist/ggnomad/utils/bookingDates.js +22 -0
- package/dist/ggnomad/utils/bookingDates.js.map +1 -0
- 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":"EventDetailsPage.js","names":[],"sources":["../../../src/ggnomad/pages/EventDetailsPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useNavigate, useParams } from 'react-router';\nimport { CalendarClock, CalendarX, MapPin, Mic2, Pencil, Ticket, Zap } from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_EVENT_QUERY } from '../graphql';\nimport { useGGNomad } from '../providers/GGNomadProvider';\nimport { joinPath } from '../utils/navigation';\nimport { formatDateTime, humanizeEnum, jsonFacts, jsonLabel } from '../utils/format';\nimport { PageShell } from './PageShell';\nimport {\n AvailabilityPanel,\n BookingPanel,\n DetailHero,\n DetailRow,\n EcoAccessPanel,\n FactGrid,\n ListingEditDialog,\n PhotoGallery,\n ProviderCard,\n RatingSummary,\n ReviewsSection,\n SectionCard,\n StatusBadge,\n} from '../components';\nimport type { Fact } from '../components';\n\ntype EventDetail = {\n id: string;\n name: string;\n description?: string | null;\n category: string;\n status?: string;\n startsAt: string;\n endsAt?: string | null;\n venue?: unknown;\n currency: string;\n price: number;\n rating?: number | null;\n reviewCount: number;\n performers: string[];\n images: string[];\n instantBook: boolean;\n sustainabilityScore: number;\n sustainabilityPractices: string[];\n accessibilityFeatures: string[];\n provider?: {\n id: string;\n name: string;\n type?: string | null;\n rating?: number | null;\n reviewCount?: number | null;\n listingCount?: number | null;\n contactEmail?: string | null;\n website?: string | null;\n } | null;\n};\n\nexport function EventDetailsPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const { basePath = '/' } = useGGNomad();\n const { id = '' } = useParams();\n const [editDialogOpen, setEditDialogOpen] = useState(false);\n const { data, refetch } = useQuery(\n GGNOMAD_EVENT_QUERY as TypedDocumentNode<{ ggnomadEvent: EventDetail | null }, { id: string }>,\n { variables: { id }, skip: !id }\n );\n const event = data?.ggnomadEvent;\n const venueLabel = jsonLabel(event?.venue);\n const venueFacts = jsonFacts(event?.venue);\n\n const facts: Fact[] = [];\n if (event) {\n const starts = formatDateTime(event.startsAt);\n if (starts) {\n facts.push({\n id: 'starts',\n label: t('ggnomad.event.starts', 'Starts'),\n value: starts,\n icon: <CalendarClock />,\n });\n }\n const ends = formatDateTime(event.endsAt);\n if (ends) {\n facts.push({\n id: 'ends',\n label: t('ggnomad.event.ends', 'Ends'),\n value: ends,\n icon: <CalendarX />,\n });\n }\n if (venueLabel) {\n facts.push({\n id: 'venue',\n label: t('ggnomad.event.venue', 'Venue'),\n value: venueLabel,\n icon: <MapPin />,\n });\n }\n if (event.performers.length > 0) {\n facts.push({\n id: 'lineup',\n label: t('ggnomad.event.lineupSize', 'Line-up'),\n value: t('ggnomad.event.performerCount', '{{n}} performing').replace(\n '{{n}}',\n String(event.performers.length)\n ),\n icon: <Mic2 />,\n });\n }\n }\n\n return (\n <PageShell\n title={event?.name ?? t('ggnomad.event.fallbackTitle', 'Event details')}\n subtitle={t('ggnomad.event.subtitle', 'Live event details, venue and lineup')}\n >\n {!event ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.event.notFoundTitle', 'No event found')}\n description={t(\n 'ggnomad.event.notFoundDescription',\n 'This event is unavailable or may have been removed.'\n )}\n />\n ) : (\n <>\n {event.images.length > 0 ? (\n <PhotoGallery images={event.images} alt={event.name} />\n ) : (\n <DetailHero alt={event.name} />\n )}\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <span className=\"rounded-full border border-border-seam bg-bg-sunken px-3 py-1 text-sm text-text-primary\">\n {humanizeEnum(event.category)}\n </span>\n {event.status ? <StatusBadge status={event.status} /> : null}\n <RatingSummary rating={event.rating} reviewCount={event.reviewCount} />\n {event.instantBook ? (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-status-success-bg-subtle px-3 py-1 text-sm font-medium text-status-success-text\">\n <Zap className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.listing.instantBook', 'Instant book')}\n </span>\n ) : null}\n </div>\n\n <PagePurpose icon={<Ticket className=\"size-4\" />}>\n {t(\n 'ggnomad.event.purpose',\n 'All the details for this live event in one place — category, start and end times, ticket price and the full lineup — so you can decide and book tickets before they sell out.'\n )}\n </PagePurpose>\n\n <FactGrid facts={facts} />\n\n <NextSteps\n storageKey=\"ggnomad-event-detail\"\n steps={[\n {\n id: 'browse',\n label: t('ggnomad.event.next.browse', 'See more events'),\n description: t(\n 'ggnomad.event.next.browseDesc',\n 'Return to Entertainment for other live experiences.'\n ),\n icon: <Ticket className=\"size-4\" />,\n onClick: () => navigate(joinPath(basePath, 'entertainment')),\n },\n ]}\n />\n\n <div className=\"grid gap-4 lg:grid-cols-3\">\n <div className=\"lg:col-span-2 space-y-4\">\n {event.description ? (\n <SectionCard title={t('ggnomad.event.about', 'About this event')}>\n <p className=\"text-text-primary\">{event.description}</p>\n </SectionCard>\n ) : null}\n\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <div className=\"mb-4 flex items-center justify-between gap-3\">\n <h2 className=\"text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.event.details', 'Details')}\n </h2>\n <button\n onClick={() => setEditDialogOpen(true)}\n className=\"rounded-md p-2 text-text-secondary hover:bg-action-primary-bg hover:text-action-primary-text\"\n aria-label={t('ggnomad.event.editListing', 'Edit listing')}\n title={t('ggnomad.event.editListing', 'Edit listing')}\n >\n <Pencil className=\"size-4\" />\n </button>\n </div>\n <dl className=\"space-y-2 text-sm\">\n <DetailRow\n label={t('ggnomad.event.category', 'Category')}\n value={humanizeEnum(event.category)}\n />\n <DetailRow\n label={t('ggnomad.event.starts', 'Starts')}\n value={formatDateTime(event.startsAt) ?? event.startsAt}\n />\n <DetailRow\n label={t('ggnomad.event.ends', 'Ends')}\n value={formatDateTime(event.endsAt) ?? t('ggnomad.event.na', 'N/A')}\n />\n <DetailRow\n label={t('ggnomad.event.price', 'Ticket price')}\n value={`${event.currency} ${Number(event.price).toFixed(2)}`}\n />\n </dl>\n </GlassCard>\n\n {venueFacts.length > 0 ? (\n <SectionCard title={t('ggnomad.event.venueTitle', 'Venue')}>\n <dl className=\"space-y-2 text-sm\">\n {venueFacts.map((fact) => (\n <DetailRow key={fact.key} label={fact.label} value={fact.value} />\n ))}\n </dl>\n </SectionCard>\n ) : null}\n\n {event.performers.length > 0 ? (\n <SectionCard title={t('ggnomad.event.performers', 'Performers')}>\n <ul className=\"space-y-2 text-sm\">\n {event.performers.map((performer) => (\n <li key={performer} className=\"flex items-start gap-2 text-text-primary\">\n <Mic2\n className=\"mt-0.5 size-4 flex-shrink-0 text-action-primary-bg\"\n aria-hidden=\"true\"\n />\n {performer}\n </li>\n ))}\n </ul>\n </SectionCard>\n ) : null}\n\n <EcoAccessPanel\n sustainabilityScore={event.sustainabilityScore}\n sustainabilityPractices={event.sustainabilityPractices}\n accessibilityFeatures={event.accessibilityFeatures}\n />\n\n <AvailabilityPanel listingId={event.id ?? id} listingType=\"EVENT\" />\n\n {event.provider ? <ProviderCard provider={event.provider} /> : null}\n\n <ReviewsSection targetType=\"EVENT\" targetId={event.id ?? id} />\n </div>\n <div>\n <BookingPanel\n bookingType=\"EVENT\"\n itemId={event.id ?? id}\n itemName={event.name}\n unitPrice={event.price ?? 0}\n currency={event.currency ?? 'USD'}\n eventDate={event.startsAt}\n />\n </div>\n </div>\n\n <ListingEditDialog\n open={editDialogOpen}\n onOpenChange={setEditDialogOpen}\n targetType=\"EVENT\"\n id={event.id ?? id}\n initial={{\n name: event.name,\n description: event.description ?? undefined,\n price: event.price,\n status: event.status,\n images: event.images,\n }}\n onSaved={() => refetch()}\n />\n </>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EventDetailsPage.js","names":[],"sources":["../../../src/ggnomad/pages/EventDetailsPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useNavigate, useParams } from 'react-router';\nimport { CalendarClock, CalendarX, MapPin, Mic2, Pencil, Ticket, Zap } from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_EVENT_QUERY } from '../graphql';\nimport { useGGNomad } from '../providers/GGNomadProvider';\nimport { joinPath } from '../utils/navigation';\nimport { formatDateTime, humanizeEnum, jsonFacts, jsonLabel } from '../utils/format';\nimport { PageShell } from './PageShell';\nimport {\n AvailabilityPanel,\n BookingPanel,\n DetailHero,\n DetailRow,\n EcoAccessPanel,\n FactGrid,\n ListingEditDialog,\n PhotoGallery,\n ProviderCard,\n RatingSummary,\n ReviewsSection,\n SectionCard,\n StatusBadge,\n} from '../components';\nimport type { Fact } from '../components';\n\ntype EventDetail = {\n id: string;\n name: string;\n description?: string | null;\n category: string;\n status?: string;\n startsAt: string;\n endsAt?: string | null;\n venue?: unknown;\n currency: string;\n price: number;\n rating?: number | null;\n reviewCount: number;\n performers: string[];\n images: string[];\n instantBook: boolean;\n sustainabilityScore: number;\n sustainabilityPractices: string[];\n accessibilityFeatures: string[];\n provider?: {\n id: string;\n name: string;\n type?: string | null;\n rating?: number | null;\n reviewCount?: number | null;\n listingCount?: number | null;\n contactEmail?: string | null;\n website?: string | null;\n } | null;\n};\n\nexport function EventDetailsPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const { basePath = '/' } = useGGNomad();\n const { id = '' } = useParams();\n const [editDialogOpen, setEditDialogOpen] = useState(false);\n const { data, refetch } = useQuery(\n GGNOMAD_EVENT_QUERY as TypedDocumentNode<{ ggnomadEvent: EventDetail | null }, { id: string }>,\n { variables: { id }, skip: !id }\n );\n const event = data?.ggnomadEvent;\n const venueLabel = jsonLabel(event?.venue);\n const venueFacts = jsonFacts(event?.venue);\n\n const facts: Fact[] = [];\n if (event) {\n const starts = formatDateTime(event.startsAt);\n if (starts) {\n facts.push({\n id: 'starts',\n label: t('ggnomad.event.starts', 'Starts'),\n value: starts,\n icon: <CalendarClock />,\n });\n }\n const ends = formatDateTime(event.endsAt);\n if (ends) {\n facts.push({\n id: 'ends',\n label: t('ggnomad.event.ends', 'Ends'),\n value: ends,\n icon: <CalendarX />,\n });\n }\n if (venueLabel) {\n facts.push({\n id: 'venue',\n label: t('ggnomad.event.venue', 'Venue'),\n value: venueLabel,\n icon: <MapPin />,\n });\n }\n if (event.performers.length > 0) {\n facts.push({\n id: 'lineup',\n label: t('ggnomad.event.lineupSize', 'Line-up'),\n value: t('ggnomad.event.performerCount', '{{n}} performing').replace(\n '{{n}}',\n String(event.performers.length)\n ),\n icon: <Mic2 />,\n });\n }\n }\n\n return (\n <PageShell\n title={event?.name ?? t('ggnomad.event.fallbackTitle', 'Event details')}\n subtitle={t('ggnomad.event.subtitle', 'Live event details, venue and lineup')}\n >\n {!event ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.event.notFoundTitle', 'No event found')}\n description={t(\n 'ggnomad.event.notFoundDescription',\n 'This event is unavailable or may have been removed.'\n )}\n />\n ) : (\n <>\n {event.images.length > 0 ? (\n <PhotoGallery images={event.images} alt={event.name} />\n ) : (\n <DetailHero alt={event.name} />\n )}\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <span className=\"rounded-full border border-border-seam bg-bg-sunken px-3 py-1 text-sm text-text-primary\">\n {humanizeEnum(event.category)}\n </span>\n {event.status ? <StatusBadge status={event.status} /> : null}\n <RatingSummary rating={event.rating} reviewCount={event.reviewCount} />\n {event.instantBook ? (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-status-success-bg-subtle px-3 py-1 text-sm font-medium text-status-success-text\">\n <Zap className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.listing.instantBook', 'Instant book')}\n </span>\n ) : null}\n </div>\n\n <PagePurpose icon={<Ticket className=\"size-4\" />}>\n {t(\n 'ggnomad.event.purpose',\n 'All the details for this live event in one place — category, start and end times, ticket price and the full lineup — so you can decide and book tickets before they sell out.'\n )}\n </PagePurpose>\n\n <FactGrid facts={facts} />\n\n <NextSteps\n storageKey=\"ggnomad-event-detail\"\n steps={[\n {\n id: 'browse',\n label: t('ggnomad.event.next.browse', 'See more events'),\n description: t(\n 'ggnomad.event.next.browseDesc',\n 'Return to Entertainment for other live experiences.'\n ),\n icon: <Ticket className=\"size-4\" />,\n onClick: () => navigate(joinPath(basePath, 'entertainment')),\n },\n ]}\n />\n\n <div className=\"grid gap-4 lg:grid-cols-3\">\n <div className=\"lg:col-span-2 space-y-4\">\n {event.description ? (\n <SectionCard title={t('ggnomad.event.about', 'About this event')}>\n <p className=\"text-text-primary\">{event.description}</p>\n </SectionCard>\n ) : null}\n\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <div className=\"mb-4 flex items-center justify-between gap-3\">\n <h2 className=\"text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.event.details', 'Details')}\n </h2>\n <button\n onClick={() => setEditDialogOpen(true)}\n className=\"rounded-md p-2 text-text-secondary hover:bg-action-primary-bg hover:text-action-primary-text\"\n aria-label={t('ggnomad.event.editListing', 'Edit listing')}\n title={t('ggnomad.event.editListing', 'Edit listing')}\n >\n <Pencil className=\"size-4\" />\n </button>\n </div>\n <dl className=\"space-y-2 text-sm\">\n <DetailRow\n label={t('ggnomad.event.category', 'Category')}\n value={humanizeEnum(event.category)}\n />\n <DetailRow\n label={t('ggnomad.event.starts', 'Starts')}\n value={formatDateTime(event.startsAt) ?? event.startsAt}\n />\n <DetailRow\n label={t('ggnomad.event.ends', 'Ends')}\n value={formatDateTime(event.endsAt) ?? t('ggnomad.event.na', 'N/A')}\n />\n <DetailRow\n label={t('ggnomad.event.price', 'Ticket price')}\n value={`${event.currency} ${Number(event.price).toFixed(2)}`}\n />\n </dl>\n </GlassCard>\n\n {venueFacts.length > 0 ? (\n <SectionCard title={t('ggnomad.event.venueTitle', 'Venue')}>\n <dl className=\"space-y-2 text-sm\">\n {venueFacts.map((fact) => (\n <DetailRow key={fact.key} label={fact.label} value={fact.value} />\n ))}\n </dl>\n </SectionCard>\n ) : null}\n\n {event.performers.length > 0 ? (\n <SectionCard title={t('ggnomad.event.performers', 'Performers')}>\n <ul className=\"space-y-2 text-sm\">\n {event.performers.map((performer) => (\n <li key={performer} className=\"flex items-start gap-2 text-text-primary\">\n <Mic2\n className=\"mt-0.5 size-4 flex-shrink-0 text-action-primary-bg\"\n aria-hidden=\"true\"\n />\n {performer}\n </li>\n ))}\n </ul>\n </SectionCard>\n ) : null}\n\n <EcoAccessPanel\n sustainabilityScore={event.sustainabilityScore}\n sustainabilityPractices={event.sustainabilityPractices}\n accessibilityFeatures={event.accessibilityFeatures}\n />\n\n <AvailabilityPanel listingId={event.id ?? id} listingType=\"EVENT\" />\n\n {event.provider ? <ProviderCard provider={event.provider} /> : null}\n\n <ReviewsSection targetType=\"EVENT\" targetId={event.id ?? id} />\n </div>\n <div>\n <BookingPanel\n bookingType=\"EVENT\"\n itemId={event.id ?? id}\n itemName={event.name}\n unitPrice={event.price ?? 0}\n currency={event.currency ?? 'USD'}\n eventDate={event.startsAt}\n />\n </div>\n </div>\n\n <ListingEditDialog\n open={editDialogOpen}\n onOpenChange={setEditDialogOpen}\n targetType=\"EVENT\"\n id={event.id ?? id}\n initial={{\n name: event.name,\n description: event.description ?? undefined,\n price: event.price,\n status: event.status,\n images: event.images,\n }}\n onSaved={() => refetch()}\n />\n </>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FavoritesPage.js","names":[],"sources":["../../../src/ggnomad/pages/FavoritesPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { Link } from 'react-router';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowUpRight, Heart, HeartOff } from 'lucide-react';\nimport {\n CTAOverflowMenu,\n GlassCard,\n IllustratedEmptyState,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n GGNOMAD_FAVORITES_QUERY,\n GGNOMAD_FAVORITE_TARGETS_QUERY,\n TOGGLE_GGNOMAD_FAVORITE_MUTATION,\n} from '../graphql';\nimport { humanizeEnum } from '../utils/format';\nimport { cn } from '../utils/cn';\nimport { PageShell } from './PageShell';\nimport { EntityCard, RatingSummary } from '../components';\nimport { notify } from '../utils/notify';\n\ntype FavoriteType = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY' | 'TRIP';\n\n/** A favouritable listing, normalised across the five target types. */\ntype Target = {\n id: string;\n name: string;\n kind?: string;\n price?: number;\n currency?: string;\n rating?: number | null;\n reviewCount?: number;\n images?: string[];\n /** Route to the listing's detail page, relative to this page. Trips have none. */\n path?: string;\n};\n\ntype Favorite = {\n id: string;\n targetType: string;\n targetId: string;\n createdAt?: string;\n};\n\nconst TOGGLEABLE_TYPES: FavoriteType[] = ['PROPERTY', 'TOUR', 'EVENT', 'ACTIVITY'];\n\nexport function FavoritesPage() {\n const { t } = useI18n();\n const [typeFilter, setTypeFilter] = useState<FavoriteType | 'ALL'>('ALL');\n\n const typeLabel = (type: string): string => {\n switch (type.toUpperCase()) {\n case 'PROPERTY':\n return t('ggnomad.favorites.type.property', 'Property');\n case 'TOUR':\n return t('ggnomad.favorites.type.tour', 'Tour');\n case 'EVENT':\n return t('ggnomad.favorites.type.event', 'Event');\n case 'ACTIVITY':\n return t('ggnomad.favorites.type.activity', 'Activity');\n case 'TRIP':\n return t('ggnomad.favorites.type.trip', 'Trip');\n default:\n return t('ggnomad.favorites.type.saved', 'Saved item');\n }\n };\n\n const { data, refetch } = useQuery(\n GGNOMAD_FAVORITES_QUERY as TypedDocumentNode<\n { ggnomadFavorites: Favorite[] },\n Record<string, never>\n >\n );\n\n // The backend stores a favorite as targetType + targetId only, so the listing\n // catalogue is joined client-side to show the real name, cover photo, price and\n // rating instead of a bare enum + UUID.\n const { data: targetsData } = useQuery(\n GGNOMAD_FAVORITE_TARGETS_QUERY as TypedDocumentNode<\n {\n ggnomadProperties: Array<{\n id: string;\n name: string;\n type: string;\n pricePerNight: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n images: string[];\n }>;\n ggnomadTours: Array<{\n id: string;\n name: string;\n type: string;\n price: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n images: string[];\n }>;\n ggnomadEvents: Array<{\n id: string;\n name: string;\n category: string;\n price: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n images: string[];\n }>;\n ggnomadActivities: Array<{\n id: string;\n name: string;\n category: string;\n price: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n images: string[];\n }>;\n ggnomadTrips: Array<{\n id: string;\n name: string;\n transportMode: string;\n price: number;\n currency: string;\n }>;\n },\n Record<string, never>\n >\n );\n\n const [toggleFavorite, { loading: toggling }] = useMutation(\n TOGGLE_GGNOMAD_FAVORITE_MUTATION as TypedDocumentNode<\n { toggleGgnomadFavorite: boolean },\n { input: { targetType: FavoriteType; targetId: string } }\n >\n );\n\n const targetsByType = useMemo(() => {\n const map = new Map<string, Target>();\n for (const property of targetsData?.ggnomadProperties ?? []) {\n map.set(`PROPERTY:${property.id}`, {\n id: property.id,\n name: property.name,\n kind: property.type,\n price: property.pricePerNight,\n currency: property.currency,\n rating: property.rating,\n reviewCount: property.reviewCount,\n images: property.images,\n path: `../hospitality/${property.id}`,\n });\n }\n for (const tour of targetsData?.ggnomadTours ?? []) {\n map.set(`TOUR:${tour.id}`, {\n id: tour.id,\n name: tour.name,\n kind: tour.type,\n price: tour.price,\n currency: tour.currency,\n rating: tour.rating,\n reviewCount: tour.reviewCount,\n images: tour.images,\n path: `../tourism/tours/${tour.id}`,\n });\n }\n for (const event of targetsData?.ggnomadEvents ?? []) {\n map.set(`EVENT:${event.id}`, {\n id: event.id,\n name: event.name,\n kind: event.category,\n price: event.price,\n currency: event.currency,\n rating: event.rating,\n reviewCount: event.reviewCount,\n images: event.images,\n path: `../entertainment/${event.id}`,\n });\n }\n for (const activity of targetsData?.ggnomadActivities ?? []) {\n map.set(`ACTIVITY:${activity.id}`, {\n id: activity.id,\n name: activity.name,\n kind: activity.category,\n price: activity.price,\n currency: activity.currency,\n rating: activity.rating,\n reviewCount: activity.reviewCount,\n images: activity.images,\n path: `../tourism/activities/${activity.id}`,\n });\n }\n for (const trip of targetsData?.ggnomadTrips ?? []) {\n map.set(`TRIP:${trip.id}`, {\n id: trip.id,\n name: trip.name,\n kind: trip.transportMode,\n price: trip.price,\n currency: trip.currency,\n });\n }\n return map;\n }, [targetsData]);\n\n const favorites = useMemo(() => data?.ggnomadFavorites ?? [], [data?.ggnomadFavorites]);\n\n const counts = useMemo(() => {\n const tally = new Map<string, number>();\n for (const favorite of favorites) {\n const key = favorite.targetType.toUpperCase();\n tally.set(key, (tally.get(key) ?? 0) + 1);\n }\n return tally;\n }, [favorites]);\n\n const visible = favorites.filter(\n (favorite) => typeFilter === 'ALL' || favorite.targetType.toUpperCase() === typeFilter\n );\n\n const chips: Array<{ value: FavoriteType | 'ALL'; label: string; count: number }> = [\n { value: 'ALL', label: t('ggnomad.favorites.filter.all', 'All'), count: favorites.length },\n ...(['PROPERTY', 'TOUR', 'EVENT', 'ACTIVITY', 'TRIP'] as FavoriteType[]).map((type) => ({\n value: type,\n label: typeLabel(type),\n count: counts.get(type) ?? 0,\n })),\n ];\n\n const runToggle = async (type: FavoriteType, targetId: string, label: string) => {\n try {\n const result = await toggleFavorite({\n variables: { input: { targetType: type, targetId } },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.favorites.toast.errorTitle', 'Could not update favorite'),\n result.error.message\n );\n return;\n }\n const added = result.data?.toggleGgnomadFavorite === true;\n notify.success(\n added\n ? t('ggnomad.favorites.toast.added', 'Added to favorites')\n : t('ggnomad.favorites.toast.removed', 'Removed from favorites'),\n label\n );\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.favorites.toast.errorTitle', 'Could not update favorite'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n /** First available real listing, in priority order, mapped to a favorite type. */\n const resolveDemoTarget = (): { type: FavoriteType; listing: Target } | null => {\n for (const type of TOGGLEABLE_TYPES) {\n const match = [...targetsByType.entries()].find(([key]) => key.startsWith(`${type}:`));\n if (match) return { type, listing: match[1] };\n }\n return null;\n };\n\n const handleToggleDemo = async () => {\n const target = resolveDemoTarget();\n if (!target) {\n notify.warning(\n t('ggnomad.favorites.toast.noListingsTitle', 'Nothing to favorite yet'),\n t(\n 'ggnomad.favorites.toast.noListingsBody',\n 'Add a property, tour, event or activity first, then save it to your favorites.'\n )\n );\n return;\n }\n await runToggle(target.type, target.listing.id, target.listing.name);\n };\n\n return (\n <PageShell\n title={t('ggnomad.favorites.title', 'Favorites')}\n titleAccent={t('ggnomad.favorites.title.accent', 'Favorites')}\n subtitle={t(\n 'ggnomad.favorites.subtitle',\n 'Social ecosystem: saved entities across GGNomad domains'\n )}\n actions={\n <CTAOverflowMenu\n primary={{\n label: t('ggnomad.favorites.actions.toggleDemo', 'Toggle demo favorite'),\n onSelect: () => void handleToggleDemo(),\n disabled: toggling,\n icon: <Heart />,\n }}\n actions={[]}\n />\n }\n >\n <PagePurpose icon={<Heart className=\"size-4\" />}>\n {t(\n 'ggnomad.favorites.purpose',\n 'A personal shortlist that spans every GGNomad domain — save properties, tours, events and trips you’re considering so you can compare and come back to them before you book.'\n )}\n </PagePurpose>\n\n {favorites.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-notifications\"\n title={t('ggnomad.favorites.emptyTitle', 'No favorites saved')}\n description={t(\n 'ggnomad.favorites.emptyDescription',\n 'Items you save across travel, hospitality and tourism will collect here.'\n )}\n />\n ) : (\n <>\n <ul className=\"flex flex-wrap gap-2\">\n {chips.map((chip) => (\n <li key={chip.value}>\n <button\n type=\"button\"\n aria-pressed={typeFilter === chip.value}\n onClick={() => setTypeFilter(chip.value)}\n className={cn(\n 'rounded-full border px-3 py-1 text-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg',\n typeFilter === chip.value\n ? 'border-action-primary-bg bg-action-primary-bg text-action-primary-text'\n : 'border-border-seam bg-bg-sunken text-text-primary hover:border-border-default'\n )}\n >\n {chip.label}\n <span className=\"ml-1.5 tabular-nums opacity-80\">{chip.count}</span>\n </button>\n </li>\n ))}\n </ul>\n\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <h2 className=\"mb-4 text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.favorites.savedItems', 'Saved items')}\n </h2>\n {visible.length === 0 ? (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n {t('ggnomad.favorites.noneOfType', 'Nothing saved in this category yet.')}\n </p>\n ) : (\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {visible.map((favorite) => {\n const type = favorite.targetType.toUpperCase();\n const target = targetsByType.get(`${type}:${favorite.targetId}`);\n const removable = (TOGGLEABLE_TYPES as string[]).includes(type);\n return (\n <EntityCard\n key={favorite.id}\n image={target?.images?.[0]}\n imageAlt={target?.name}\n title={target?.name ?? typeLabel(favorite.targetType)}\n meta={\n target?.kind\n ? `${typeLabel(favorite.targetType)} · ${humanizeEnum(target.kind)}`\n : typeLabel(favorite.targetType)\n }\n badge={\n <span className=\"rounded-full bg-bg-surface/90 px-2.5 py-0.5 text-xs font-medium text-text-primary\">\n {typeLabel(favorite.targetType)}\n </span>\n }\n price={\n target?.price !== undefined && target.currency\n ? `${target.currency} ${Number(target.price).toFixed(2)}`\n : undefined\n }\n footer={\n <div className=\"space-y-2\">\n {target ? (\n <RatingSummary\n rating={target.rating}\n reviewCount={target.reviewCount}\n />\n ) : (\n <span className=\"text-sm text-text-secondary\">\n {t('ggnomad.favorites.savedListing', 'Saved listing')}\n </span>\n )}\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n {target?.path ? (\n <Link\n to={target.path}\n className=\"inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium text-action-primary-bg hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg\"\n >\n <ArrowUpRight className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.favorites.view', 'View details')}\n </Link>\n ) : (\n <span />\n )}\n {removable ? (\n <button\n type=\"button\"\n disabled={toggling}\n onClick={() =>\n void runToggle(\n type as FavoriteType,\n favorite.targetId,\n target?.name ?? typeLabel(favorite.targetType)\n )\n }\n className=\"inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium text-text-secondary hover:bg-bg-sunken hover:text-status-error-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg disabled:opacity-60\"\n >\n <HeartOff className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.favorites.remove', 'Remove')}\n </button>\n ) : null}\n </div>\n </div>\n }\n />\n );\n })}\n </div>\n )}\n </GlassCard>\n </>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FavoritesPage.js","names":[],"sources":["../../../src/ggnomad/pages/FavoritesPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { Link } from 'react-router';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowUpRight, Heart, HeartOff } from 'lucide-react';\nimport {\n CTAOverflowMenu,\n GlassCard,\n IllustratedEmptyState,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n GGNOMAD_FAVORITES_QUERY,\n GGNOMAD_FAVORITE_TARGETS_QUERY,\n TOGGLE_GGNOMAD_FAVORITE_MUTATION,\n} from '../graphql';\nimport { humanizeEnum } from '../utils/format';\nimport { cn } from '../utils/cn';\nimport { PageShell } from './PageShell';\nimport { EntityCard, RatingSummary } from '../components';\nimport { notify } from '../utils/notify';\n\ntype FavoriteType = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY' | 'TRIP';\n\n/** A favouritable listing, normalised across the five target types. */\ntype Target = {\n id: string;\n name: string;\n kind?: string;\n price?: number;\n currency?: string;\n rating?: number | null;\n reviewCount?: number;\n images?: string[];\n /** Route to the listing's detail page, relative to this page. Trips have none. */\n path?: string;\n};\n\ntype Favorite = {\n id: string;\n targetType: string;\n targetId: string;\n createdAt?: string;\n};\n\nconst TOGGLEABLE_TYPES: FavoriteType[] = ['PROPERTY', 'TOUR', 'EVENT', 'ACTIVITY'];\n\nexport function FavoritesPage() {\n const { t } = useI18n();\n const [typeFilter, setTypeFilter] = useState<FavoriteType | 'ALL'>('ALL');\n\n const typeLabel = (type: string): string => {\n switch (type.toUpperCase()) {\n case 'PROPERTY':\n return t('ggnomad.favorites.type.property', 'Property');\n case 'TOUR':\n return t('ggnomad.favorites.type.tour', 'Tour');\n case 'EVENT':\n return t('ggnomad.favorites.type.event', 'Event');\n case 'ACTIVITY':\n return t('ggnomad.favorites.type.activity', 'Activity');\n case 'TRIP':\n return t('ggnomad.favorites.type.trip', 'Trip');\n default:\n return t('ggnomad.favorites.type.saved', 'Saved item');\n }\n };\n\n const { data, refetch } = useQuery(\n GGNOMAD_FAVORITES_QUERY as TypedDocumentNode<\n { ggnomadFavorites: Favorite[] },\n Record<string, never>\n >\n );\n\n // The backend stores a favorite as targetType + targetId only, so the listing\n // catalogue is joined client-side to show the real name, cover photo, price and\n // rating instead of a bare enum + UUID.\n const { data: targetsData } = useQuery(\n GGNOMAD_FAVORITE_TARGETS_QUERY as TypedDocumentNode<\n {\n ggnomadProperties: Array<{\n id: string;\n name: string;\n type: string;\n pricePerNight: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n images: string[];\n }>;\n ggnomadTours: Array<{\n id: string;\n name: string;\n type: string;\n price: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n images: string[];\n }>;\n ggnomadEvents: Array<{\n id: string;\n name: string;\n category: string;\n price: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n images: string[];\n }>;\n ggnomadActivities: Array<{\n id: string;\n name: string;\n category: string;\n price: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n images: string[];\n }>;\n ggnomadTrips: Array<{\n id: string;\n name: string;\n transportMode: string;\n price: number;\n currency: string;\n }>;\n },\n Record<string, never>\n >\n );\n\n const [toggleFavorite, { loading: toggling }] = useMutation(\n TOGGLE_GGNOMAD_FAVORITE_MUTATION as TypedDocumentNode<\n { toggleGgnomadFavorite: boolean },\n { input: { targetType: FavoriteType; targetId: string } }\n >\n );\n\n const targetsByType = useMemo(() => {\n const map = new Map<string, Target>();\n for (const property of targetsData?.ggnomadProperties ?? []) {\n map.set(`PROPERTY:${property.id}`, {\n id: property.id,\n name: property.name,\n kind: property.type,\n price: property.pricePerNight,\n currency: property.currency,\n rating: property.rating,\n reviewCount: property.reviewCount,\n images: property.images,\n path: `../hospitality/${property.id}`,\n });\n }\n for (const tour of targetsData?.ggnomadTours ?? []) {\n map.set(`TOUR:${tour.id}`, {\n id: tour.id,\n name: tour.name,\n kind: tour.type,\n price: tour.price,\n currency: tour.currency,\n rating: tour.rating,\n reviewCount: tour.reviewCount,\n images: tour.images,\n path: `../tourism/tours/${tour.id}`,\n });\n }\n for (const event of targetsData?.ggnomadEvents ?? []) {\n map.set(`EVENT:${event.id}`, {\n id: event.id,\n name: event.name,\n kind: event.category,\n price: event.price,\n currency: event.currency,\n rating: event.rating,\n reviewCount: event.reviewCount,\n images: event.images,\n path: `../entertainment/${event.id}`,\n });\n }\n for (const activity of targetsData?.ggnomadActivities ?? []) {\n map.set(`ACTIVITY:${activity.id}`, {\n id: activity.id,\n name: activity.name,\n kind: activity.category,\n price: activity.price,\n currency: activity.currency,\n rating: activity.rating,\n reviewCount: activity.reviewCount,\n images: activity.images,\n path: `../tourism/activities/${activity.id}`,\n });\n }\n for (const trip of targetsData?.ggnomadTrips ?? []) {\n map.set(`TRIP:${trip.id}`, {\n id: trip.id,\n name: trip.name,\n kind: trip.transportMode,\n price: trip.price,\n currency: trip.currency,\n });\n }\n return map;\n }, [targetsData]);\n\n const favorites = useMemo(() => data?.ggnomadFavorites ?? [], [data?.ggnomadFavorites]);\n\n const counts = useMemo(() => {\n const tally = new Map<string, number>();\n for (const favorite of favorites) {\n const key = favorite.targetType.toUpperCase();\n tally.set(key, (tally.get(key) ?? 0) + 1);\n }\n return tally;\n }, [favorites]);\n\n const visible = favorites.filter(\n (favorite) => typeFilter === 'ALL' || favorite.targetType.toUpperCase() === typeFilter\n );\n\n const chips: Array<{ value: FavoriteType | 'ALL'; label: string; count: number }> = [\n { value: 'ALL', label: t('ggnomad.favorites.filter.all', 'All'), count: favorites.length },\n ...(['PROPERTY', 'TOUR', 'EVENT', 'ACTIVITY', 'TRIP'] as FavoriteType[]).map((type) => ({\n value: type,\n label: typeLabel(type),\n count: counts.get(type) ?? 0,\n })),\n ];\n\n const runToggle = async (type: FavoriteType, targetId: string, label: string) => {\n try {\n const result = await toggleFavorite({\n variables: { input: { targetType: type, targetId } },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.favorites.toast.errorTitle', 'Could not update favorite'),\n result.error.message\n );\n return;\n }\n const added = result.data?.toggleGgnomadFavorite === true;\n notify.success(\n added\n ? t('ggnomad.favorites.toast.added', 'Added to favorites')\n : t('ggnomad.favorites.toast.removed', 'Removed from favorites'),\n label\n );\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.favorites.toast.errorTitle', 'Could not update favorite'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n /** First available real listing, in priority order, mapped to a favorite type. */\n const resolveDemoTarget = (): { type: FavoriteType; listing: Target } | null => {\n for (const type of TOGGLEABLE_TYPES) {\n const match = [...targetsByType.entries()].find(([key]) => key.startsWith(`${type}:`));\n if (match) return { type, listing: match[1] };\n }\n return null;\n };\n\n const handleToggleDemo = async () => {\n const target = resolveDemoTarget();\n if (!target) {\n notify.warning(\n t('ggnomad.favorites.toast.noListingsTitle', 'Nothing to favorite yet'),\n t(\n 'ggnomad.favorites.toast.noListingsBody',\n 'Add a property, tour, event or activity first, then save it to your favorites.'\n )\n );\n return;\n }\n await runToggle(target.type, target.listing.id, target.listing.name);\n };\n\n return (\n <PageShell\n title={t('ggnomad.favorites.title', 'Favorites')}\n titleAccent={t('ggnomad.favorites.title.accent', 'Favorites')}\n subtitle={t(\n 'ggnomad.favorites.subtitle',\n 'Social ecosystem: saved entities across GGNomad domains'\n )}\n actions={\n <CTAOverflowMenu\n primary={{\n label: t('ggnomad.favorites.actions.toggleDemo', 'Toggle demo favorite'),\n onSelect: () => void handleToggleDemo(),\n disabled: toggling,\n icon: <Heart />,\n }}\n actions={[]}\n />\n }\n >\n <PagePurpose icon={<Heart className=\"size-4\" />}>\n {t(\n 'ggnomad.favorites.purpose',\n 'A personal shortlist that spans every GGNomad domain — save properties, tours, events and trips you’re considering so you can compare and come back to them before you book.'\n )}\n </PagePurpose>\n\n {favorites.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-notifications\"\n title={t('ggnomad.favorites.emptyTitle', 'No favorites saved')}\n description={t(\n 'ggnomad.favorites.emptyDescription',\n 'Items you save across travel, hospitality and tourism will collect here.'\n )}\n />\n ) : (\n <>\n <ul className=\"flex flex-wrap gap-2\">\n {chips.map((chip) => (\n <li key={chip.value}>\n <button\n type=\"button\"\n aria-pressed={typeFilter === chip.value}\n onClick={() => setTypeFilter(chip.value)}\n className={cn(\n 'rounded-full border px-3 py-1 text-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg',\n typeFilter === chip.value\n ? 'border-action-primary-bg bg-action-primary-bg text-action-primary-text'\n : 'border-border-seam bg-bg-sunken text-text-primary hover:border-border-default'\n )}\n >\n {chip.label}\n <span className=\"ml-1.5 tabular-nums opacity-80\">{chip.count}</span>\n </button>\n </li>\n ))}\n </ul>\n\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <h2 className=\"mb-4 text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.favorites.savedItems', 'Saved items')}\n </h2>\n {visible.length === 0 ? (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n {t('ggnomad.favorites.noneOfType', 'Nothing saved in this category yet.')}\n </p>\n ) : (\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {visible.map((favorite) => {\n const type = favorite.targetType.toUpperCase();\n const target = targetsByType.get(`${type}:${favorite.targetId}`);\n const removable = (TOGGLEABLE_TYPES as string[]).includes(type);\n return (\n <EntityCard\n key={favorite.id}\n image={target?.images?.[0]}\n imageAlt={target?.name}\n title={target?.name ?? typeLabel(favorite.targetType)}\n meta={\n target?.kind\n ? `${typeLabel(favorite.targetType)} · ${humanizeEnum(target.kind)}`\n : typeLabel(favorite.targetType)\n }\n badge={\n <span className=\"rounded-full bg-bg-surface/90 px-2.5 py-0.5 text-xs font-medium text-text-primary\">\n {typeLabel(favorite.targetType)}\n </span>\n }\n price={\n target?.price !== undefined && target.currency\n ? `${target.currency} ${Number(target.price).toFixed(2)}`\n : undefined\n }\n footer={\n <div className=\"space-y-2\">\n {target ? (\n <RatingSummary\n rating={target.rating}\n reviewCount={target.reviewCount}\n />\n ) : (\n <span className=\"text-sm text-text-secondary\">\n {t('ggnomad.favorites.savedListing', 'Saved listing')}\n </span>\n )}\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n {target?.path ? (\n <Link\n to={target.path}\n className=\"inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium text-action-primary-bg hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg\"\n >\n <ArrowUpRight className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.favorites.view', 'View details')}\n </Link>\n ) : (\n <span />\n )}\n {removable ? (\n <button\n type=\"button\"\n disabled={toggling}\n onClick={() =>\n void runToggle(\n type as FavoriteType,\n favorite.targetId,\n target?.name ?? typeLabel(favorite.targetType)\n )\n }\n className=\"inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium text-text-secondary hover:bg-bg-sunken hover:text-status-error-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg disabled:opacity-60\"\n >\n <HeartOff className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.favorites.remove', 'Remove')}\n </button>\n ) : null}\n </div>\n </div>\n }\n />\n );\n })}\n </div>\n )}\n </GlassCard>\n </>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HospitalityPage.js","names":[],"sources":["../../../src/ggnomad/pages/HospitalityPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Hotel, Leaf, Users, Zap } from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_PROPERTIES_QUERY } from '../graphql';\nimport { PageShell } from './PageShell';\nimport {\n EntityCard,\n EMPTY_LISTING_FILTERS,\n ListingFilters,\n LoadMoreButton,\n RatingSummary,\n hasActiveListingFilters,\n} from '../components';\nimport type { ListingFilterState } from '../components';\n\ntype PropertyRow = {\n id: string;\n name: string;\n type: string;\n pricePerNight: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n maxGuests: number;\n images?: string[];\n instantBook: boolean;\n sustainabilityScore: number;\n};\n\nexport function HospitalityPage() {\n const { t } = useI18n();\n const [limit, setLimit] = useState(12);\n const [filters, setFilters] = useState<ListingFilterState>(EMPTY_LISTING_FILTERS);\n\n const { data, loading, error } = useQuery(\n GGNOMAD_PROPERTIES_QUERY as TypedDocumentNode<\n { ggnomadProperties: PropertyRow[] },\n {\n limit: number;\n offset: number;\n status?: string;\n minSustainabilityScore?: number;\n hasAccessibilityFeatures?: string[];\n }\n >,\n {\n variables: {\n limit,\n offset: 0,\n status: filters.status,\n minSustainabilityScore: filters.minSustainabilityScore,\n hasAccessibilityFeatures:\n filters.accessibilityFeatures.length > 0 ? filters.accessibilityFeatures : undefined,\n },\n }\n );\n const properties = data?.ggnomadProperties ?? [];\n const filtered = hasActiveListingFilters(filters);\n\n return (\n <PageShell\n title={t('ggnomad.hospitality.title', 'Hospitality')}\n titleAccent={t('ggnomad.hospitality.title.accent', 'Hospitality')}\n subtitle={t(\n 'ggnomad.hospitality.subtitle',\n 'Property discovery powered by live GraphQL data'\n )}\n >\n <PagePurpose icon={<Hotel className=\"size-4\" />}>\n {t(\n 'ggnomad.hospitality.purpose',\n 'Discover places to stay — hotels, homestays and short-stay rentals. Open any property to review its nightly price, guest capacity and rating, then create a booking for the dates you need.'\n )}\n </PagePurpose>\n\n <ListingFilters\n idPrefix=\"ggnomad-hospitality\"\n value={filters}\n onChange={(next) => {\n setLimit(12);\n setFilters(next);\n }}\n resultCount={properties.length}\n />\n\n {error ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.hospitality.errorTitle', 'Could not load properties')}\n description={error.message}\n />\n ) : properties.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={\n loading\n ? t('ggnomad.hospitality.loadingTitle', 'Loading properties…')\n : filtered\n ? t('ggnomad.hospitality.noMatchTitle', 'No properties match these filters')\n : t('ggnomad.hospitality.emptyTitle', 'No properties available')\n }\n description={\n loading\n ? t('ggnomad.hospitality.loadingDescription', 'Fetching the live property catalogue.')\n : filtered\n ? t(\n 'ggnomad.hospitality.noMatchDescription',\n 'Try clearing a filter — status, sustainability score and accessibility all narrow the catalogue.'\n )\n : t(\n 'ggnomad.hospitality.emptyDescription',\n 'Properties published by providers will be discoverable here.'\n )\n }\n />\n ) : (\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <h2 className=\"mb-4 text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.hospitality.featuredStays', 'Featured stays')}\n </h2>\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {properties.map((property) => (\n <EntityCard\n key={property.id}\n to={property.id}\n image={property.images?.[0]}\n imageAlt={property.name}\n title={property.name}\n meta={property.type}\n badge={\n property.instantBook ? (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-success-bg-subtle px-2.5 py-0.5 text-xs font-medium text-status-success-text\">\n <Zap className=\"size-3\" aria-hidden=\"true\" />\n {t('ggnomad.listing.instantBook', 'Instant book')}\n </span>\n ) : null\n }\n price={`${property.currency} ${Number(property.pricePerNight).toFixed(2)} ${t(\n 'ggnomad.hospitality.card.perNight',\n 'per night'\n )}`}\n footer={\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1\">\n <RatingSummary rating={property.rating} reviewCount={property.reviewCount} />\n <span className=\"inline-flex items-center gap-1 tabular-nums\">\n <Users className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.hospitality.card.guests', 'Up to {{n}}').replace(\n '{{n}}',\n String(property.maxGuests)\n )}\n </span>\n {property.sustainabilityScore > 0 ? (\n <span className=\"inline-flex items-center gap-1 tabular-nums text-status-success-text\">\n <Leaf className=\"size-3.5\" aria-hidden=\"true\" />\n {property.sustainabilityScore}\n </span>\n ) : null}\n </div>\n }\n />\n ))}\n </div>\n <LoadMoreButton\n visible={properties.length >= limit}\n onLoadMore={() => setLimit((l) => l + 12)}\n />\n </GlassCard>\n )}\n </PageShell>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAiCA,SAAgB,IAAkB;CAChC,IAAM,EAAE,SAAM,EAAQ,GAChB,CAAC,GAAO,KAAY,EAAS,EAAE,GAC/B,CAAC,GAAS,KAAc,EAA6B,CAAqB,GAE1E,EAAE,SAAM,YAAS,aAAU,EAC/B,GAUA,EACE,WAAW;EACT;EACA,QAAQ;EACR,QAAQ,EAAQ;EAChB,wBAAwB,EAAQ;EAChC,0BACE,EAAQ,sBAAsB,SAAS,IAAI,EAAQ,wBAAwB,KAAA;CAC/E,EACF,CACF,GACM,IAAa,GAAM,qBAAqB,CAAC,GACzC,IAAW,EAAwB,CAAO;CAEhD,OACE,kBAAC,GAAD;EACE,OAAO,EAAE,6BAA6B,aAAa;EACnD,aAAa,EAAE,oCAAoC,aAAa;EAChE,UAAU,EACR,gCACA,iDACF;YANF;GAQE,kBAAC,GAAD;IAAa,MAAM,kBAAC,GAAD,EAAO,WAAU,SAAU,CAAA;cAC3C,EACC,+BACA,6LACF;GACW,CAAA;GAEb,kBAAC,GAAD;IACE,UAAS;IACT,OAAO;IACP,WAAW,MAAS;KAElB,AADA,EAAS,EAAE,GACX,EAAW,CAAI;IACjB;IACA,aAAa,EAAW;GACzB,CAAA;GAEA,IACC,kBAAC,GAAD;IACE,cAAa;IACb,OAAO,EAAE,kCAAkC,2BAA2B;IACtE,aAAa,EAAM;GACpB,CAAA,IACC,EAAW,WAAW,IACxB,kBAAC,GAAD;IACE,cAAa;IACb,OACE,IACI,EAAE,oCAAoC,qBAAqB,IAC3D,IACE,EAAE,oCAAoC,mCAAmC,IACzE,EAAE,kCAAkC,yBAAyB;IAErE,aACE,IACI,EAAE,0CAA0C,uCAAuC,IACnF,IACE,EACE,0CACA,kGACF,IACA,EACE,wCACA,8DACF;GAET,CAAA,IAED,kBAAC,GAAD;IAAW,WAAU;IAAQ,MAAA;IAAK,WAAU;cAA5C;KACE,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAE,qCAAqC,gBAAgB;KACtD,CAAA;KACJ,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAW,KAAK,MACf,kBAAC,GAAD;OAEE,IAAI,EAAS;OACb,OAAO,EAAS,SAAS;OACzB,UAAU,EAAS;OACnB,OAAO,EAAS;OAChB,MAAM,EAAS;OACf,OACE,EAAS,cACP,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACE,kBAAC,GAAD;SAAK,WAAU;SAAS,eAAY;QAAQ,CAAA,GAC3C,EAAE,+BAA+B,cAAc,CAC5C;YACJ;OAEN,OAAO,GAAG,EAAS,SAAS,GAAG,OAAO,EAAS,aAAa,
|
|
1
|
+
{"version":3,"file":"HospitalityPage.js","names":[],"sources":["../../../src/ggnomad/pages/HospitalityPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Hotel, Leaf, Users, Zap } from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_PROPERTIES_QUERY } from '../graphql';\nimport { PageShell } from './PageShell';\nimport {\n EntityCard,\n EMPTY_LISTING_FILTERS,\n ListingFilters,\n LoadMoreButton,\n RatingSummary,\n hasActiveListingFilters,\n} from '../components';\nimport type { ListingFilterState } from '../components';\n\ntype PropertyRow = {\n id: string;\n name: string;\n type: string;\n pricePerNight: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n maxGuests: number;\n images?: string[];\n instantBook: boolean;\n sustainabilityScore: number;\n};\n\nexport function HospitalityPage() {\n const { t } = useI18n();\n const [limit, setLimit] = useState(12);\n const [filters, setFilters] = useState<ListingFilterState>(EMPTY_LISTING_FILTERS);\n\n const { data, loading, error } = useQuery(\n GGNOMAD_PROPERTIES_QUERY as TypedDocumentNode<\n { ggnomadProperties: PropertyRow[] },\n {\n limit: number;\n offset: number;\n status?: string;\n minSustainabilityScore?: number;\n hasAccessibilityFeatures?: string[];\n }\n >,\n {\n variables: {\n limit,\n offset: 0,\n status: filters.status,\n minSustainabilityScore: filters.minSustainabilityScore,\n hasAccessibilityFeatures:\n filters.accessibilityFeatures.length > 0 ? filters.accessibilityFeatures : undefined,\n },\n }\n );\n const properties = data?.ggnomadProperties ?? [];\n const filtered = hasActiveListingFilters(filters);\n\n return (\n <PageShell\n title={t('ggnomad.hospitality.title', 'Hospitality')}\n titleAccent={t('ggnomad.hospitality.title.accent', 'Hospitality')}\n subtitle={t(\n 'ggnomad.hospitality.subtitle',\n 'Property discovery powered by live GraphQL data'\n )}\n >\n <PagePurpose icon={<Hotel className=\"size-4\" />}>\n {t(\n 'ggnomad.hospitality.purpose',\n 'Discover places to stay — hotels, homestays and short-stay rentals. Open any property to review its nightly price, guest capacity and rating, then create a booking for the dates you need.'\n )}\n </PagePurpose>\n\n <ListingFilters\n idPrefix=\"ggnomad-hospitality\"\n value={filters}\n onChange={(next) => {\n setLimit(12);\n setFilters(next);\n }}\n resultCount={properties.length}\n />\n\n {error ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.hospitality.errorTitle', 'Could not load properties')}\n description={error.message}\n />\n ) : properties.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={\n loading\n ? t('ggnomad.hospitality.loadingTitle', 'Loading properties…')\n : filtered\n ? t('ggnomad.hospitality.noMatchTitle', 'No properties match these filters')\n : t('ggnomad.hospitality.emptyTitle', 'No properties available')\n }\n description={\n loading\n ? t('ggnomad.hospitality.loadingDescription', 'Fetching the live property catalogue.')\n : filtered\n ? t(\n 'ggnomad.hospitality.noMatchDescription',\n 'Try clearing a filter — status, sustainability score and accessibility all narrow the catalogue.'\n )\n : t(\n 'ggnomad.hospitality.emptyDescription',\n 'Properties published by providers will be discoverable here.'\n )\n }\n />\n ) : (\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <h2 className=\"mb-4 text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.hospitality.featuredStays', 'Featured stays')}\n </h2>\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {properties.map((property) => (\n <EntityCard\n key={property.id}\n to={property.id}\n image={property.images?.[0]}\n imageAlt={property.name}\n title={property.name}\n meta={property.type}\n badge={\n property.instantBook ? (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-success-bg-subtle px-2.5 py-0.5 text-xs font-medium text-status-success-text\">\n <Zap className=\"size-3\" aria-hidden=\"true\" />\n {t('ggnomad.listing.instantBook', 'Instant book')}\n </span>\n ) : null\n }\n price={`${property.currency} ${Number(property.pricePerNight).toFixed(2)} ${t(\n 'ggnomad.hospitality.card.perNight',\n 'per night'\n )}`}\n footer={\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1\">\n <RatingSummary rating={property.rating} reviewCount={property.reviewCount} />\n <span className=\"inline-flex items-center gap-1 tabular-nums\">\n <Users className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.hospitality.card.guests', 'Up to {{n}}').replace(\n '{{n}}',\n String(property.maxGuests)\n )}\n </span>\n {property.sustainabilityScore > 0 ? (\n <span className=\"inline-flex items-center gap-1 tabular-nums text-status-success-text\">\n <Leaf className=\"size-3.5\" aria-hidden=\"true\" />\n {property.sustainabilityScore}\n </span>\n ) : null}\n </div>\n }\n />\n ))}\n </div>\n <LoadMoreButton\n visible={properties.length >= limit}\n onLoadMore={() => setLimit((l) => l + 12)}\n />\n </GlassCard>\n )}\n </PageShell>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAiCA,SAAgB,IAAkB;CAChC,IAAM,EAAE,SAAM,EAAQ,GAChB,CAAC,GAAO,KAAY,EAAS,EAAE,GAC/B,CAAC,GAAS,KAAc,EAA6B,CAAqB,GAE1E,EAAE,SAAM,YAAS,aAAU,EAC/B,GAUA,EACE,WAAW;EACT;EACA,QAAQ;EACR,QAAQ,EAAQ;EAChB,wBAAwB,EAAQ;EAChC,0BACE,EAAQ,sBAAsB,SAAS,IAAI,EAAQ,wBAAwB,KAAA;CAC/E,EACF,CACF,GACM,IAAa,GAAM,qBAAqB,CAAC,GACzC,IAAW,EAAwB,CAAO;CAEhD,OACE,kBAAC,GAAD;EACE,OAAO,EAAE,6BAA6B,aAAa;EACnD,aAAa,EAAE,oCAAoC,aAAa;EAChE,UAAU,EACR,gCACA,iDACF;YANF;GAQE,kBAAC,GAAD;IAAa,MAAM,kBAAC,GAAD,EAAO,WAAU,SAAU,CAAA;cAC3C,EACC,+BACA,6LACF;GACW,CAAA;GAEb,kBAAC,GAAD;IACE,UAAS;IACT,OAAO;IACP,WAAW,MAAS;KAElB,AADA,EAAS,EAAE,GACX,EAAW,CAAI;IACjB;IACA,aAAa,EAAW;GACzB,CAAA;GAEA,IACC,kBAAC,GAAD;IACE,cAAa;IACb,OAAO,EAAE,kCAAkC,2BAA2B;IACtE,aAAa,EAAM;GACpB,CAAA,IACC,EAAW,WAAW,IACxB,kBAAC,GAAD;IACE,cAAa;IACb,OACE,IACI,EAAE,oCAAoC,qBAAqB,IAC3D,IACE,EAAE,oCAAoC,mCAAmC,IACzE,EAAE,kCAAkC,yBAAyB;IAErE,aACE,IACI,EAAE,0CAA0C,uCAAuC,IACnF,IACE,EACE,0CACA,kGACF,IACA,EACE,wCACA,8DACF;GAET,CAAA,IAED,kBAAC,GAAD;IAAW,WAAU;IAAQ,MAAA;IAAK,WAAU;cAA5C;KACE,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAE,qCAAqC,gBAAgB;KACtD,CAAA;KACJ,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAW,KAAK,MACf,kBAAC,GAAD;OAEE,IAAI,EAAS;OACb,OAAO,EAAS,SAAS;OACzB,UAAU,EAAS;OACnB,OAAO,EAAS;OAChB,MAAM,EAAS;OACf,OACE,EAAS,cACP,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACE,kBAAC,GAAD;SAAK,WAAU;SAAS,eAAY;QAAQ,CAAA,GAC3C,EAAE,+BAA+B,cAAc,CAC5C;YACJ;OAEN,OAAO,GAAG,EAAS,SAAS,GAAG,OAAO,EAAS,aAAa,CAAC,CAAC,QAAQ,CAAC,EAAE,GAAG,EAC1E,qCACA,WACF;OACA,QACE,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,GAAD;UAAe,QAAQ,EAAS;UAAQ,aAAa,EAAS;SAAc,CAAA;SAC5E,kBAAC,QAAD;UAAM,WAAU;oBAAhB,CACE,kBAAC,GAAD;WAAO,WAAU;WAAW,eAAY;UAAQ,CAAA,GAC/C,EAAE,mCAAmC,aAAa,CAAC,CAAC,QACnD,SACA,OAAO,EAAS,SAAS,CAC3B,CACI;;SACL,EAAS,sBAAsB,IAC9B,kBAAC,QAAD;UAAM,WAAU;oBAAhB,CACE,kBAAC,GAAD;WAAM,WAAU;WAAW,eAAY;UAAQ,CAAA,GAC9C,EAAS,mBACN;cACJ;QACD;;MAER,GApCM,EAAS,EAoCf,CACF;KACE,CAAA;KACL,kBAAC,GAAD;MACE,SAAS,EAAW,UAAU;MAC9B,kBAAkB,GAAU,MAAM,IAAI,EAAE;KACzC,CAAA;IACQ;;EAEJ;;AAEf"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OperationsQueuePage.js","names":[],"sources":["../../../src/ggnomad/pages/OperationsQueuePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n Check,\n ClipboardList,\n History,\n LogIn,\n LogOut,\n ShieldQuestion,\n UserX,\n X,\n XCircle,\n} from 'lucide-react';\nimport type { CTAAction } from '@burdenoff/fe-libs/ui';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n CTAOverflowMenu,\n IllustratedEmptyState,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n APPROVE_GGNOMAD_LISTING_MUTATION,\n CANCEL_GGNOMAD_BOOKING_MUTATION,\n CHECK_IN_GGNOMAD_BOOKING_MUTATION,\n COMPLETE_GGNOMAD_BOOKING_MUTATION,\n GGNOMAD_BOOKING_QUEUE_QUERY,\n GGNOMAD_LISTING_REVIEW_QUEUE_QUERY,\n MARK_GGNOMAD_BOOKING_NO_SHOW_MUTATION,\n REJECT_GGNOMAD_LISTING_MUTATION,\n} from '../graphql';\nimport { PageShell } from './PageShell';\nimport { AuditTrailDialog, EntityCard, SectionCard, StatusBadge } from '../components';\nimport { notify } from '../utils/notify';\n\ntype ListingType = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ninterface QueuedListing {\n __typename: string;\n id: string;\n name: string;\n status: string;\n price?: number;\n pricePerNight?: number;\n currency: string;\n providerId: string | null;\n}\n\ninterface QueuedBooking {\n id: string;\n referenceCode: string;\n bookingType: string;\n status: string;\n itemName: string;\n guestName: string;\n guests: number;\n checkIn: string | null;\n checkOut: string | null;\n eventDate: string | null;\n totalPrice: number;\n currency: string;\n}\n\nconst LISTING_TYPE_BY_TYPENAME: Record<string, ListingType> = {\n GgnomadProperty: 'PROPERTY',\n GgnomadTour: 'TOUR',\n GgnomadEvent: 'EVENT',\n GgnomadActivity: 'ACTIVITY',\n};\n\ntype HistoryTarget = { entityType: string; entityId: string; label: string };\ntype RejectTarget = { listingType: ListingType; id: string; name: string };\ntype NoShowTarget = { id: string; referenceCode: string };\ntype CancelTarget = { id: string; referenceCode: string };\n\n/**\n * BOFF-4388 — the operational side of the marketplace: a marketplace operator\n * (or host-operator for their own submissions) reviews PENDING_REVIEW\n * listings and works the incoming booking queue from one place. Every action\n * here calls a real, guarded mutation on wspace-ggnomad-svc — approve/reject\n * on listings are gated server-side by the `ggnomad:manage` RBAC action\n * (marketplace operator only); check-in/complete/no-show/cancel are gated by\n * `ggnomad:update` (host-operator and above).\n */\nexport function OperationsQueuePage() {\n const { t } = useI18n();\n const [rejectTarget, setRejectTarget] = useState<RejectTarget | null>(null);\n const [rejectReason, setRejectReason] = useState('');\n const [noShowTarget, setNoShowTarget] = useState<NoShowTarget | null>(null);\n const [cancelTarget, setCancelTarget] = useState<CancelTarget | null>(null);\n const [historyTarget, setHistoryTarget] = useState<HistoryTarget | null>(null);\n\n const {\n data: listingData,\n refetch: refetchListings,\n loading: listingsLoading,\n } = useQuery(\n GGNOMAD_LISTING_REVIEW_QUEUE_QUERY as TypedDocumentNode<\n { ggnomadListingReviewQueue: QueuedListing[] },\n { limit?: number; offset?: number }\n >,\n { variables: { limit: 20, offset: 0 } }\n );\n const listings = useMemo(\n () => listingData?.ggnomadListingReviewQueue ?? [],\n [listingData?.ggnomadListingReviewQueue]\n );\n\n const {\n data: bookingData,\n refetch: refetchBookings,\n loading: bookingsLoading,\n } = useQuery(\n GGNOMAD_BOOKING_QUEUE_QUERY as TypedDocumentNode<\n { ggnomadBookingQueue: QueuedBooking[] },\n { limit?: number; offset?: number }\n >,\n { variables: { limit: 20, offset: 0 } }\n );\n const bookings = useMemo(\n () => bookingData?.ggnomadBookingQueue ?? [],\n [bookingData?.ggnomadBookingQueue]\n );\n\n const [approveListing, { loading: approving }] = useMutation(\n APPROVE_GGNOMAD_LISTING_MUTATION as TypedDocumentNode<\n { approveGgnomadListing: { id: string; status: string } },\n { listingType: ListingType; id: string; note?: string }\n >\n );\n const [rejectListing, { loading: rejecting }] = useMutation(\n REJECT_GGNOMAD_LISTING_MUTATION as TypedDocumentNode<\n { rejectGgnomadListing: { id: string; status: string } },\n { listingType: ListingType; id: string; reason: string }\n >\n );\n const [checkInBooking, { loading: checkingIn }] = useMutation(\n CHECK_IN_GGNOMAD_BOOKING_MUTATION as TypedDocumentNode<\n { checkInGgnomadBooking: { id: string } },\n { id: string }\n >\n );\n const [completeBooking, { loading: completing }] = useMutation(\n COMPLETE_GGNOMAD_BOOKING_MUTATION as TypedDocumentNode<\n { completeGgnomadBooking: { id: string } },\n { id: string }\n >\n );\n const [markNoShow, { loading: markingNoShow }] = useMutation(\n MARK_GGNOMAD_BOOKING_NO_SHOW_MUTATION as TypedDocumentNode<\n { markGgnomadBookingNoShow: { id: string } },\n { id: string; reason?: string }\n >\n );\n const [cancelBooking, { loading: cancelling }] = useMutation(\n CANCEL_GGNOMAD_BOOKING_MUTATION as TypedDocumentNode<\n { cancelGgnomadBooking: { id: string } },\n { id: string; reason: string }\n >\n );\n\n const handleApprove = async (listing: QueuedListing) => {\n const listingType = LISTING_TYPE_BY_TYPENAME[listing.__typename];\n if (!listingType) return;\n try {\n const result = await approveListing({ variables: { listingType, id: listing.id } });\n if (result.error) {\n notify.error(\n t('ggnomad.opsQueue.approveErrorTitle', 'Could not approve listing'),\n result.error.message\n );\n return;\n }\n notify.success(t('ggnomad.opsQueue.approveSuccessTitle', 'Listing approved'), listing.name);\n await refetchListings();\n } catch (err) {\n notify.error(\n t('ggnomad.opsQueue.approveErrorTitle', 'Could not approve listing'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const confirmReject = async () => {\n if (!rejectTarget) return;\n if (!rejectReason.trim()) {\n notify.warning(\n t('ggnomad.opsQueue.reject.reasonRequiredTitle', 'A reason is required'),\n t('ggnomad.opsQueue.reject.reasonRequiredBody', 'Let the submitter know what needs fixing.')\n );\n return;\n }\n try {\n const result = await rejectListing({\n variables: {\n listingType: rejectTarget.listingType,\n id: rejectTarget.id,\n reason: rejectReason.trim(),\n },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.opsQueue.rejectErrorTitle', 'Could not reject listing'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.opsQueue.rejectSuccessTitle', 'Listing sent back to draft'),\n rejectTarget.name\n );\n await refetchListings();\n } catch (err) {\n notify.error(\n t('ggnomad.opsQueue.rejectErrorTitle', 'Could not reject listing'),\n err instanceof Error ? err.message : String(err)\n );\n } finally {\n setRejectTarget(null);\n setRejectReason('');\n }\n };\n\n const handleCheckIn = async (booking: QueuedBooking) => {\n try {\n const result = await checkInBooking({ variables: { id: booking.id } });\n if (result.error) {\n notify.error(\n t('ggnomad.opsQueue.checkInErrorTitle', 'Could not check in'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.opsQueue.checkInSuccessTitle', 'Checked in'),\n booking.referenceCode\n );\n await refetchBookings();\n } catch (err) {\n notify.error(\n t('ggnomad.opsQueue.checkInErrorTitle', 'Could not check in'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const handleComplete = async (booking: QueuedBooking) => {\n try {\n const result = await completeBooking({ variables: { id: booking.id } });\n if (result.error) {\n notify.error(\n t('ggnomad.opsQueue.completeErrorTitle', 'Could not complete booking'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.opsQueue.completeSuccessTitle', 'Booking completed'),\n booking.referenceCode\n );\n await refetchBookings();\n } catch (err) {\n notify.error(\n t('ggnomad.opsQueue.completeErrorTitle', 'Could not complete booking'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const confirmNoShow = async () => {\n if (!noShowTarget) return;\n try {\n const result = await markNoShow({\n variables: {\n id: noShowTarget.id,\n reason: t('ggnomad.opsQueue.noShowReason', 'Marked via operations queue'),\n },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.opsQueue.noShowErrorTitle', 'Could not mark as no-show'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.opsQueue.noShowSuccessTitle', 'Marked as no-show'),\n noShowTarget.referenceCode\n );\n await refetchBookings();\n } catch (err) {\n notify.error(\n t('ggnomad.opsQueue.noShowErrorTitle', 'Could not mark as no-show'),\n err instanceof Error ? err.message : String(err)\n );\n } finally {\n setNoShowTarget(null);\n }\n };\n\n const confirmCancel = async () => {\n if (!cancelTarget) return;\n try {\n const result = await cancelBooking({\n variables: {\n id: cancelTarget.id,\n reason: t('ggnomad.opsQueue.cancelReason', 'Cancelled via operations queue'),\n },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.opsQueue.cancelErrorTitle', 'Could not cancel booking'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.opsQueue.cancelSuccessTitle', 'Booking cancelled'),\n cancelTarget.referenceCode\n );\n await refetchBookings();\n } catch (err) {\n notify.error(\n t('ggnomad.opsQueue.cancelErrorTitle', 'Could not cancel booking'),\n err instanceof Error ? err.message : String(err)\n );\n } finally {\n setCancelTarget(null);\n }\n };\n\n return (\n <PageShell\n title={t('ggnomad.opsQueue.title', 'Operations Queue')}\n titleAccent={t('ggnomad.opsQueue.title.accent', 'Operations')}\n subtitle={t(\n 'ggnomad.opsQueue.subtitle',\n 'Verify listings before they go live and work the incoming booking queue'\n )}\n >\n <PagePurpose icon={<ClipboardList className=\"size-4\" />}>\n {t(\n 'ggnomad.opsQueue.purpose',\n 'Listings move DRAFT → PENDING_REVIEW → ACTIVE — approve or reject submissions here before they can be booked. Bookings move PENDING → CONFIRMED → CHECKED_IN → COMPLETED — check guests in, complete stays, or mark a no-show once the scheduled date has arrived. Every action is recorded in the transition history.'\n )}\n </PagePurpose>\n\n <SectionCard title={t('ggnomad.opsQueue.listingsTitle', 'Listings awaiting review')}>\n {!listingsLoading && listings.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.opsQueue.listingsEmptyTitle', 'Nothing waiting on you')}\n description={t(\n 'ggnomad.opsQueue.listingsEmptyDescription',\n 'Listings submitted for review will show up here in submission order.'\n )}\n />\n ) : (\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {listings.map((listing) => {\n const listingType = LISTING_TYPE_BY_TYPENAME[listing.__typename];\n const price = listing.pricePerNight ?? listing.price ?? 0;\n return (\n <EntityCard\n key={listing.id}\n title={listing.name}\n meta={listingType}\n price={`${listing.currency} ${Number(price).toFixed(2)}`}\n badge={<StatusBadge status={listing.status} live />}\n footer={\n <CTAOverflowMenu\n align=\"end\"\n actions={[\n {\n label: t('ggnomad.opsQueue.actions.approve', 'Approve'),\n onSelect: () => void handleApprove(listing),\n disabled: approving,\n icon: <Check />,\n },\n {\n label: t('ggnomad.opsQueue.actions.reject', 'Reject'),\n onSelect: () => {\n if (!listingType) return;\n setRejectTarget({ listingType, id: listing.id, name: listing.name });\n setRejectReason('');\n },\n intent: 'destructive',\n icon: <X />,\n },\n {\n label: t('ggnomad.opsQueue.actions.history', 'History'),\n onSelect: () =>\n setHistoryTarget({\n entityType: listingType ?? 'PROPERTY',\n entityId: listing.id,\n label: listing.name,\n }),\n icon: <History />,\n },\n ]}\n />\n }\n />\n );\n })}\n </div>\n )}\n </SectionCard>\n\n <SectionCard title={t('ggnomad.opsQueue.bookingsTitle', 'Booking queue')}>\n {!bookingsLoading && bookings.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.opsQueue.bookingsEmptyTitle', 'No bookings need attention')}\n description={t(\n 'ggnomad.opsQueue.bookingsEmptyDescription',\n 'Pending, confirmed, and checked-in bookings appear here in scheduled-start order.'\n )}\n />\n ) : (\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {bookings.map((booking) => {\n const actions: CTAAction[] = [];\n if (booking.status === 'CONFIRMED') {\n actions.push(\n {\n label: t('ggnomad.opsQueue.actions.checkIn', 'Check in'),\n onSelect: () => void handleCheckIn(booking),\n disabled: checkingIn,\n icon: <LogIn />,\n },\n {\n label: t('ggnomad.opsQueue.actions.noShow', 'Mark no-show'),\n onSelect: () =>\n setNoShowTarget({ id: booking.id, referenceCode: booking.referenceCode }),\n disabled: markingNoShow,\n icon: <ShieldQuestion />,\n }\n );\n }\n if (booking.status === 'CHECKED_IN') {\n actions.push({\n label: t('ggnomad.opsQueue.actions.complete', 'Complete'),\n onSelect: () => void handleComplete(booking),\n disabled: completing,\n icon: <LogOut />,\n });\n }\n actions.push(\n {\n label: t('ggnomad.opsQueue.actions.cancel', 'Cancel'),\n onSelect: () =>\n setCancelTarget({ id: booking.id, referenceCode: booking.referenceCode }),\n disabled: cancelling,\n icon: <XCircle />,\n intent: 'destructive',\n },\n {\n label: t('ggnomad.opsQueue.actions.history', 'History'),\n onSelect: () =>\n setHistoryTarget({\n entityType: 'BOOKING',\n entityId: booking.id,\n label: booking.referenceCode,\n }),\n icon: <History />,\n }\n );\n\n return (\n <EntityCard\n key={booking.id}\n title={booking.referenceCode}\n meta={`${booking.itemName} · ${booking.guestName}`}\n price={`${booking.currency} ${Number(booking.totalPrice).toFixed(2)}`}\n badge={<StatusBadge status={booking.status} live />}\n footer={<CTAOverflowMenu align=\"end\" actions={actions} />}\n />\n );\n })}\n </div>\n )}\n </SectionCard>\n\n <AlertDialog\n open={rejectTarget !== null}\n onOpenChange={(open) => {\n if (!open) {\n setRejectTarget(null);\n setRejectReason('');\n }\n }}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.opsQueue.rejectDialog.title', 'Reject this listing?')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.opsQueue.rejectDialog.body',\n 'This sends {{name}} back to DRAFT. The reason is recorded.'\n ).replace('{{name}}', rejectTarget?.name ?? '')}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <div>\n <label\n htmlFor=\"ops-reject-reason\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.opsQueue.rejectDialog.reasonLabel', 'Reason')}\n </label>\n <textarea\n id=\"ops-reject-reason\"\n value={rejectReason}\n onChange={(e) => setRejectReason(e.target.value)}\n disabled={rejecting}\n rows={3}\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-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg disabled:opacity-50\"\n placeholder={t(\n 'ggnomad.opsQueue.rejectDialog.reasonPlaceholder',\n 'e.g. Missing required photos'\n )}\n />\n </div>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={rejecting}>\n {t('ggnomad.opsQueue.rejectDialog.cancel', 'Keep pending')}\n </AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-action-danger-bg text-action-danger-text hover:bg-action-danger-bgHover\"\n disabled={rejecting}\n onClick={(event) => {\n event.preventDefault();\n void confirmReject();\n }}\n >\n {rejecting\n ? t('ggnomad.opsQueue.rejectDialog.rejecting', 'Rejecting…')\n : t('ggnomad.opsQueue.rejectDialog.confirm', 'Reject listing')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n\n <AlertDialog\n open={noShowTarget !== null}\n onOpenChange={(open) => !open && setNoShowTarget(null)}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.opsQueue.noShowDialog.title', 'Mark as no-show?')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.opsQueue.noShowDialog.body',\n '{{ref}} will be marked NO_SHOW. Only allowed once the scheduled start has arrived.'\n ).replace('{{ref}}', noShowTarget?.referenceCode ?? '')}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={markingNoShow}>\n {t('ggnomad.opsQueue.noShowDialog.cancel', 'Keep booking')}\n </AlertDialogCancel>\n <AlertDialogAction\n disabled={markingNoShow}\n onClick={(event) => {\n event.preventDefault();\n void confirmNoShow();\n }}\n >\n <UserX className=\"mr-1 size-4\" />\n {markingNoShow\n ? t('ggnomad.opsQueue.noShowDialog.marking', 'Marking…')\n : t('ggnomad.opsQueue.noShowDialog.confirm', 'Mark no-show')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n\n <AlertDialog\n open={cancelTarget !== null}\n onOpenChange={(open) => !open && setCancelTarget(null)}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.opsQueue.cancelDialog.title', 'Cancel this booking?')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.opsQueue.cancelDialog.body',\n 'This releases reservation {{ref}} and cannot be undone.'\n ).replace('{{ref}}', cancelTarget?.referenceCode ?? '')}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={cancelling}>\n {t('ggnomad.opsQueue.cancelDialog.keep', 'Keep booking')}\n </AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-action-danger-bg text-action-danger-text hover:bg-action-danger-bgHover\"\n disabled={cancelling}\n onClick={(event) => {\n event.preventDefault();\n void confirmCancel();\n }}\n >\n {cancelling\n ? t('ggnomad.opsQueue.cancelDialog.cancelling', 'Cancelling…')\n : t('ggnomad.opsQueue.cancelDialog.confirm', 'Cancel booking')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n\n {historyTarget ? (\n <AuditTrailDialog\n open={historyTarget !== null}\n onOpenChange={(open) => {\n if (!open) setHistoryTarget(null);\n }}\n entityType={historyTarget.entityType}\n entityId={historyTarget.entityId}\n label={historyTarget.label}\n />\n ) : null}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OperationsQueuePage.js","names":[],"sources":["../../../src/ggnomad/pages/OperationsQueuePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n Check,\n ClipboardList,\n History,\n LogIn,\n LogOut,\n ShieldQuestion,\n UserX,\n X,\n XCircle,\n} from 'lucide-react';\nimport type { CTAAction } from '@burdenoff/fe-libs/ui';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n CTAOverflowMenu,\n IllustratedEmptyState,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n APPROVE_GGNOMAD_LISTING_MUTATION,\n CANCEL_GGNOMAD_BOOKING_MUTATION,\n CHECK_IN_GGNOMAD_BOOKING_MUTATION,\n COMPLETE_GGNOMAD_BOOKING_MUTATION,\n GGNOMAD_BOOKING_QUEUE_QUERY,\n GGNOMAD_LISTING_REVIEW_QUEUE_QUERY,\n MARK_GGNOMAD_BOOKING_NO_SHOW_MUTATION,\n REJECT_GGNOMAD_LISTING_MUTATION,\n} from '../graphql';\nimport { PageShell } from './PageShell';\nimport { AuditTrailDialog, EntityCard, SectionCard, StatusBadge } from '../components';\nimport { notify } from '../utils/notify';\n\ntype ListingType = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ninterface QueuedListing {\n __typename: string;\n id: string;\n name: string;\n status: string;\n price?: number;\n pricePerNight?: number;\n currency: string;\n providerId: string | null;\n}\n\ninterface QueuedBooking {\n id: string;\n referenceCode: string;\n bookingType: string;\n status: string;\n itemName: string;\n guestName: string;\n guests: number;\n checkIn: string | null;\n checkOut: string | null;\n eventDate: string | null;\n totalPrice: number;\n currency: string;\n}\n\nconst LISTING_TYPE_BY_TYPENAME: Record<string, ListingType> = {\n GgnomadProperty: 'PROPERTY',\n GgnomadTour: 'TOUR',\n GgnomadEvent: 'EVENT',\n GgnomadActivity: 'ACTIVITY',\n};\n\ntype HistoryTarget = { entityType: string; entityId: string; label: string };\ntype RejectTarget = { listingType: ListingType; id: string; name: string };\ntype NoShowTarget = { id: string; referenceCode: string };\ntype CancelTarget = { id: string; referenceCode: string };\n\n/**\n * BOFF-4388 — the operational side of the marketplace: a marketplace operator\n * (or host-operator for their own submissions) reviews PENDING_REVIEW\n * listings and works the incoming booking queue from one place. Every action\n * here calls a real, guarded mutation on wspace-ggnomad-svc — approve/reject\n * on listings are gated server-side by the `ggnomad:manage` RBAC action\n * (marketplace operator only); check-in/complete/no-show/cancel are gated by\n * `ggnomad:update` (host-operator and above).\n */\nexport function OperationsQueuePage() {\n const { t } = useI18n();\n const [rejectTarget, setRejectTarget] = useState<RejectTarget | null>(null);\n const [rejectReason, setRejectReason] = useState('');\n const [noShowTarget, setNoShowTarget] = useState<NoShowTarget | null>(null);\n const [cancelTarget, setCancelTarget] = useState<CancelTarget | null>(null);\n const [historyTarget, setHistoryTarget] = useState<HistoryTarget | null>(null);\n\n const {\n data: listingData,\n refetch: refetchListings,\n loading: listingsLoading,\n } = useQuery(\n GGNOMAD_LISTING_REVIEW_QUEUE_QUERY as TypedDocumentNode<\n { ggnomadListingReviewQueue: QueuedListing[] },\n { limit?: number; offset?: number }\n >,\n { variables: { limit: 20, offset: 0 } }\n );\n const listings = useMemo(\n () => listingData?.ggnomadListingReviewQueue ?? [],\n [listingData?.ggnomadListingReviewQueue]\n );\n\n const {\n data: bookingData,\n refetch: refetchBookings,\n loading: bookingsLoading,\n } = useQuery(\n GGNOMAD_BOOKING_QUEUE_QUERY as TypedDocumentNode<\n { ggnomadBookingQueue: QueuedBooking[] },\n { limit?: number; offset?: number }\n >,\n { variables: { limit: 20, offset: 0 } }\n );\n const bookings = useMemo(\n () => bookingData?.ggnomadBookingQueue ?? [],\n [bookingData?.ggnomadBookingQueue]\n );\n\n const [approveListing, { loading: approving }] = useMutation(\n APPROVE_GGNOMAD_LISTING_MUTATION as TypedDocumentNode<\n { approveGgnomadListing: { id: string; status: string } },\n { listingType: ListingType; id: string; note?: string }\n >\n );\n const [rejectListing, { loading: rejecting }] = useMutation(\n REJECT_GGNOMAD_LISTING_MUTATION as TypedDocumentNode<\n { rejectGgnomadListing: { id: string; status: string } },\n { listingType: ListingType; id: string; reason: string }\n >\n );\n const [checkInBooking, { loading: checkingIn }] = useMutation(\n CHECK_IN_GGNOMAD_BOOKING_MUTATION as TypedDocumentNode<\n { checkInGgnomadBooking: { id: string } },\n { id: string }\n >\n );\n const [completeBooking, { loading: completing }] = useMutation(\n COMPLETE_GGNOMAD_BOOKING_MUTATION as TypedDocumentNode<\n { completeGgnomadBooking: { id: string } },\n { id: string }\n >\n );\n const [markNoShow, { loading: markingNoShow }] = useMutation(\n MARK_GGNOMAD_BOOKING_NO_SHOW_MUTATION as TypedDocumentNode<\n { markGgnomadBookingNoShow: { id: string } },\n { id: string; reason?: string }\n >\n );\n const [cancelBooking, { loading: cancelling }] = useMutation(\n CANCEL_GGNOMAD_BOOKING_MUTATION as TypedDocumentNode<\n { cancelGgnomadBooking: { id: string } },\n { id: string; reason: string }\n >\n );\n\n const handleApprove = async (listing: QueuedListing) => {\n const listingType = LISTING_TYPE_BY_TYPENAME[listing.__typename];\n if (!listingType) return;\n try {\n const result = await approveListing({ variables: { listingType, id: listing.id } });\n if (result.error) {\n notify.error(\n t('ggnomad.opsQueue.approveErrorTitle', 'Could not approve listing'),\n result.error.message\n );\n return;\n }\n notify.success(t('ggnomad.opsQueue.approveSuccessTitle', 'Listing approved'), listing.name);\n await refetchListings();\n } catch (err) {\n notify.error(\n t('ggnomad.opsQueue.approveErrorTitle', 'Could not approve listing'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const confirmReject = async () => {\n if (!rejectTarget) return;\n if (!rejectReason.trim()) {\n notify.warning(\n t('ggnomad.opsQueue.reject.reasonRequiredTitle', 'A reason is required'),\n t('ggnomad.opsQueue.reject.reasonRequiredBody', 'Let the submitter know what needs fixing.')\n );\n return;\n }\n try {\n const result = await rejectListing({\n variables: {\n listingType: rejectTarget.listingType,\n id: rejectTarget.id,\n reason: rejectReason.trim(),\n },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.opsQueue.rejectErrorTitle', 'Could not reject listing'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.opsQueue.rejectSuccessTitle', 'Listing sent back to draft'),\n rejectTarget.name\n );\n await refetchListings();\n } catch (err) {\n notify.error(\n t('ggnomad.opsQueue.rejectErrorTitle', 'Could not reject listing'),\n err instanceof Error ? err.message : String(err)\n );\n } finally {\n setRejectTarget(null);\n setRejectReason('');\n }\n };\n\n const handleCheckIn = async (booking: QueuedBooking) => {\n try {\n const result = await checkInBooking({ variables: { id: booking.id } });\n if (result.error) {\n notify.error(\n t('ggnomad.opsQueue.checkInErrorTitle', 'Could not check in'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.opsQueue.checkInSuccessTitle', 'Checked in'),\n booking.referenceCode\n );\n await refetchBookings();\n } catch (err) {\n notify.error(\n t('ggnomad.opsQueue.checkInErrorTitle', 'Could not check in'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const handleComplete = async (booking: QueuedBooking) => {\n try {\n const result = await completeBooking({ variables: { id: booking.id } });\n if (result.error) {\n notify.error(\n t('ggnomad.opsQueue.completeErrorTitle', 'Could not complete booking'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.opsQueue.completeSuccessTitle', 'Booking completed'),\n booking.referenceCode\n );\n await refetchBookings();\n } catch (err) {\n notify.error(\n t('ggnomad.opsQueue.completeErrorTitle', 'Could not complete booking'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const confirmNoShow = async () => {\n if (!noShowTarget) return;\n try {\n const result = await markNoShow({\n variables: {\n id: noShowTarget.id,\n reason: t('ggnomad.opsQueue.noShowReason', 'Marked via operations queue'),\n },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.opsQueue.noShowErrorTitle', 'Could not mark as no-show'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.opsQueue.noShowSuccessTitle', 'Marked as no-show'),\n noShowTarget.referenceCode\n );\n await refetchBookings();\n } catch (err) {\n notify.error(\n t('ggnomad.opsQueue.noShowErrorTitle', 'Could not mark as no-show'),\n err instanceof Error ? err.message : String(err)\n );\n } finally {\n setNoShowTarget(null);\n }\n };\n\n const confirmCancel = async () => {\n if (!cancelTarget) return;\n try {\n const result = await cancelBooking({\n variables: {\n id: cancelTarget.id,\n reason: t('ggnomad.opsQueue.cancelReason', 'Cancelled via operations queue'),\n },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.opsQueue.cancelErrorTitle', 'Could not cancel booking'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.opsQueue.cancelSuccessTitle', 'Booking cancelled'),\n cancelTarget.referenceCode\n );\n await refetchBookings();\n } catch (err) {\n notify.error(\n t('ggnomad.opsQueue.cancelErrorTitle', 'Could not cancel booking'),\n err instanceof Error ? err.message : String(err)\n );\n } finally {\n setCancelTarget(null);\n }\n };\n\n return (\n <PageShell\n title={t('ggnomad.opsQueue.title', 'Operations Queue')}\n titleAccent={t('ggnomad.opsQueue.title.accent', 'Operations')}\n subtitle={t(\n 'ggnomad.opsQueue.subtitle',\n 'Verify listings before they go live and work the incoming booking queue'\n )}\n >\n <PagePurpose icon={<ClipboardList className=\"size-4\" />}>\n {t(\n 'ggnomad.opsQueue.purpose',\n 'Listings move DRAFT → PENDING_REVIEW → ACTIVE — approve or reject submissions here before they can be booked. Bookings move PENDING → CONFIRMED → CHECKED_IN → COMPLETED — check guests in, complete stays, or mark a no-show once the scheduled date has arrived. Every action is recorded in the transition history.'\n )}\n </PagePurpose>\n\n <SectionCard title={t('ggnomad.opsQueue.listingsTitle', 'Listings awaiting review')}>\n {!listingsLoading && listings.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.opsQueue.listingsEmptyTitle', 'Nothing waiting on you')}\n description={t(\n 'ggnomad.opsQueue.listingsEmptyDescription',\n 'Listings submitted for review will show up here in submission order.'\n )}\n />\n ) : (\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {listings.map((listing) => {\n const listingType = LISTING_TYPE_BY_TYPENAME[listing.__typename];\n const price = listing.pricePerNight ?? listing.price ?? 0;\n return (\n <EntityCard\n key={listing.id}\n title={listing.name}\n meta={listingType}\n price={`${listing.currency} ${Number(price).toFixed(2)}`}\n badge={<StatusBadge status={listing.status} live />}\n footer={\n <CTAOverflowMenu\n align=\"end\"\n actions={[\n {\n label: t('ggnomad.opsQueue.actions.approve', 'Approve'),\n onSelect: () => void handleApprove(listing),\n disabled: approving,\n icon: <Check />,\n },\n {\n label: t('ggnomad.opsQueue.actions.reject', 'Reject'),\n onSelect: () => {\n if (!listingType) return;\n setRejectTarget({ listingType, id: listing.id, name: listing.name });\n setRejectReason('');\n },\n intent: 'destructive',\n icon: <X />,\n },\n {\n label: t('ggnomad.opsQueue.actions.history', 'History'),\n onSelect: () =>\n setHistoryTarget({\n entityType: listingType ?? 'PROPERTY',\n entityId: listing.id,\n label: listing.name,\n }),\n icon: <History />,\n },\n ]}\n />\n }\n />\n );\n })}\n </div>\n )}\n </SectionCard>\n\n <SectionCard title={t('ggnomad.opsQueue.bookingsTitle', 'Booking queue')}>\n {!bookingsLoading && bookings.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.opsQueue.bookingsEmptyTitle', 'No bookings need attention')}\n description={t(\n 'ggnomad.opsQueue.bookingsEmptyDescription',\n 'Pending, confirmed, and checked-in bookings appear here in scheduled-start order.'\n )}\n />\n ) : (\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {bookings.map((booking) => {\n const actions: CTAAction[] = [];\n if (booking.status === 'CONFIRMED') {\n actions.push(\n {\n label: t('ggnomad.opsQueue.actions.checkIn', 'Check in'),\n onSelect: () => void handleCheckIn(booking),\n disabled: checkingIn,\n icon: <LogIn />,\n },\n {\n label: t('ggnomad.opsQueue.actions.noShow', 'Mark no-show'),\n onSelect: () =>\n setNoShowTarget({ id: booking.id, referenceCode: booking.referenceCode }),\n disabled: markingNoShow,\n icon: <ShieldQuestion />,\n }\n );\n }\n if (booking.status === 'CHECKED_IN') {\n actions.push({\n label: t('ggnomad.opsQueue.actions.complete', 'Complete'),\n onSelect: () => void handleComplete(booking),\n disabled: completing,\n icon: <LogOut />,\n });\n }\n actions.push(\n {\n label: t('ggnomad.opsQueue.actions.cancel', 'Cancel'),\n onSelect: () =>\n setCancelTarget({ id: booking.id, referenceCode: booking.referenceCode }),\n disabled: cancelling,\n icon: <XCircle />,\n intent: 'destructive',\n },\n {\n label: t('ggnomad.opsQueue.actions.history', 'History'),\n onSelect: () =>\n setHistoryTarget({\n entityType: 'BOOKING',\n entityId: booking.id,\n label: booking.referenceCode,\n }),\n icon: <History />,\n }\n );\n\n return (\n <EntityCard\n key={booking.id}\n title={booking.referenceCode}\n meta={`${booking.itemName} · ${booking.guestName}`}\n price={`${booking.currency} ${Number(booking.totalPrice).toFixed(2)}`}\n badge={<StatusBadge status={booking.status} live />}\n footer={<CTAOverflowMenu align=\"end\" actions={actions} />}\n />\n );\n })}\n </div>\n )}\n </SectionCard>\n\n <AlertDialog\n open={rejectTarget !== null}\n onOpenChange={(open) => {\n if (!open) {\n setRejectTarget(null);\n setRejectReason('');\n }\n }}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.opsQueue.rejectDialog.title', 'Reject this listing?')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.opsQueue.rejectDialog.body',\n 'This sends {{name}} back to DRAFT. The reason is recorded.'\n ).replace('{{name}}', rejectTarget?.name ?? '')}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <div>\n <label\n htmlFor=\"ops-reject-reason\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.opsQueue.rejectDialog.reasonLabel', 'Reason')}\n </label>\n <textarea\n id=\"ops-reject-reason\"\n value={rejectReason}\n onChange={(e) => setRejectReason(e.target.value)}\n disabled={rejecting}\n rows={3}\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-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg disabled:opacity-50\"\n placeholder={t(\n 'ggnomad.opsQueue.rejectDialog.reasonPlaceholder',\n 'e.g. Missing required photos'\n )}\n />\n </div>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={rejecting}>\n {t('ggnomad.opsQueue.rejectDialog.cancel', 'Keep pending')}\n </AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-action-danger-bg text-action-danger-text hover:bg-action-danger-bgHover\"\n disabled={rejecting}\n onClick={(event) => {\n event.preventDefault();\n void confirmReject();\n }}\n >\n {rejecting\n ? t('ggnomad.opsQueue.rejectDialog.rejecting', 'Rejecting…')\n : t('ggnomad.opsQueue.rejectDialog.confirm', 'Reject listing')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n\n <AlertDialog\n open={noShowTarget !== null}\n onOpenChange={(open) => !open && setNoShowTarget(null)}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.opsQueue.noShowDialog.title', 'Mark as no-show?')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.opsQueue.noShowDialog.body',\n '{{ref}} will be marked NO_SHOW. Only allowed once the scheduled start has arrived.'\n ).replace('{{ref}}', noShowTarget?.referenceCode ?? '')}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={markingNoShow}>\n {t('ggnomad.opsQueue.noShowDialog.cancel', 'Keep booking')}\n </AlertDialogCancel>\n <AlertDialogAction\n disabled={markingNoShow}\n onClick={(event) => {\n event.preventDefault();\n void confirmNoShow();\n }}\n >\n <UserX className=\"mr-1 size-4\" />\n {markingNoShow\n ? t('ggnomad.opsQueue.noShowDialog.marking', 'Marking…')\n : t('ggnomad.opsQueue.noShowDialog.confirm', 'Mark no-show')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n\n <AlertDialog\n open={cancelTarget !== null}\n onOpenChange={(open) => !open && setCancelTarget(null)}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.opsQueue.cancelDialog.title', 'Cancel this booking?')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.opsQueue.cancelDialog.body',\n 'This releases reservation {{ref}} and cannot be undone.'\n ).replace('{{ref}}', cancelTarget?.referenceCode ?? '')}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={cancelling}>\n {t('ggnomad.opsQueue.cancelDialog.keep', 'Keep booking')}\n </AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-action-danger-bg text-action-danger-text hover:bg-action-danger-bgHover\"\n disabled={cancelling}\n onClick={(event) => {\n event.preventDefault();\n void confirmCancel();\n }}\n >\n {cancelling\n ? t('ggnomad.opsQueue.cancelDialog.cancelling', 'Cancelling…')\n : t('ggnomad.opsQueue.cancelDialog.confirm', 'Cancel booking')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n\n {historyTarget ? (\n <AuditTrailDialog\n open={historyTarget !== null}\n onOpenChange={(open) => {\n if (!open) setHistoryTarget(null);\n }}\n entityType={historyTarget.entityType}\n entityId={historyTarget.entityId}\n label={historyTarget.label}\n />\n ) : null}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PageShell.js","names":[],"sources":["../../../src/ggnomad/pages/PageShell.tsx"],"sourcesContent":["import type { PropsWithChildren, ReactNode } from 'react';\nimport { AuroraBackground, GradientText } from '@burdenoff/fe-libs/ui';\n\ntype PageShellProps = PropsWithChildren<{\n title: string;\n subtitle: string;\n /** Highlighted key noun rendered as a GradientText accent inside the H1. */\n titleAccent?: string;\n /** Single primary action / overflow menu cluster rendered at the header end. */\n actions?: ReactNode;\n /**\n * When true, the header becomes the MFE hero zone with a subtle aurora\n * backdrop. Use on exactly one page per MFE (the Dashboard).\n */\n hero?: boolean;\n}>;\n\n/**\n * Boff UI 2.0 page scaffold. The header is a seamless glass band; an optional\n * aurora backdrop turns it into the MFE hero. Content keeps its existing\n * vertical rhythm. The accent noun in the H1 is rendered via GradientText.\n */\nexport function PageShell({\n title,\n subtitle,\n titleAccent,\n actions,\n hero = false,\n children,\n}: PageShellProps) {\n const renderTitle = () => {\n if (!titleAccent || !title.includes(titleAccent)) {\n return title;\n }\n const [before, after] = title.split(titleAccent);\n return (\n <>\n {before}\n <GradientText>{titleAccent}</GradientText>\n {after}\n </>\n );\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <header\n className={\n hero\n ? 'relative overflow-hidden rounded-card border border-border-seam bg-gloss-card p-6 shadow-pop'\n : 'flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between'\n }\n >\n {hero ? <AuroraBackground intensity=\"subtle\" /> : null}\n <div className={hero ? 'relative' : 'min-w-0'}>\n <h1 className=\"text-3xl font-semibold tracking-tight text-text-primary\">\n {renderTitle()}\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">{subtitle}</p>\n </div>\n {actions ? (\n <div className={hero ? 'relative mt-4 sm:mt-0' : 'flex-shrink-0'}>{actions}</div>\n ) : null}\n </header>\n {children}\n </div>\n );\n}\n"],"mappings":";;;AAsBA,SAAgB,EAAU,EACxB,UACA,aACA,gBACA,YACA,UAAO,IACP,eACiB;CAejB,OACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,UAAD;GACE,WACE,IACI,iGACA;aAJR;IAOG,IAAO,kBAAC,GAAD,EAAkB,WAAU,SAAU,CAAA,IAAI;IAClD,kBAAC,OAAD;KAAK,WAAW,IAAO,aAAa;eAApC,CACE,kBAAC,MAAD;MAAI,WAAU;uBAzBI;OACxB,IAAI,CAAC,KAAe,CAAC,EAAM,SAAS,CAAW,GAC7C,OAAO;OAET,IAAM,CAAC,GAAQ,KAAS,EAAM,MAAM,CAAW;OAC/C,OACE,kBAAA,GAAA,EAAA,UAAA;QACG;QACD,kBAAC,GAAD,EAAA,UAAe,EAA0B,CAAA;QACxC;OACD,EAAA,CAAA;MAEN,
|
|
1
|
+
{"version":3,"file":"PageShell.js","names":[],"sources":["../../../src/ggnomad/pages/PageShell.tsx"],"sourcesContent":["import type { PropsWithChildren, ReactNode } from 'react';\nimport { AuroraBackground, GradientText } from '@burdenoff/fe-libs/ui';\n\ntype PageShellProps = PropsWithChildren<{\n title: string;\n subtitle: string;\n /** Highlighted key noun rendered as a GradientText accent inside the H1. */\n titleAccent?: string;\n /** Single primary action / overflow menu cluster rendered at the header end. */\n actions?: ReactNode;\n /**\n * When true, the header becomes the MFE hero zone with a subtle aurora\n * backdrop. Use on exactly one page per MFE (the Dashboard).\n */\n hero?: boolean;\n}>;\n\n/**\n * Boff UI 2.0 page scaffold. The header is a seamless glass band; an optional\n * aurora backdrop turns it into the MFE hero. Content keeps its existing\n * vertical rhythm. The accent noun in the H1 is rendered via GradientText.\n */\nexport function PageShell({\n title,\n subtitle,\n titleAccent,\n actions,\n hero = false,\n children,\n}: PageShellProps) {\n const renderTitle = () => {\n if (!titleAccent || !title.includes(titleAccent)) {\n return title;\n }\n const [before, after] = title.split(titleAccent);\n return (\n <>\n {before}\n <GradientText>{titleAccent}</GradientText>\n {after}\n </>\n );\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <header\n className={\n hero\n ? 'relative overflow-hidden rounded-card border border-border-seam bg-gloss-card p-6 shadow-pop'\n : 'flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between'\n }\n >\n {hero ? <AuroraBackground intensity=\"subtle\" /> : null}\n <div className={hero ? 'relative' : 'min-w-0'}>\n <h1 className=\"text-3xl font-semibold tracking-tight text-text-primary\">\n {renderTitle()}\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">{subtitle}</p>\n </div>\n {actions ? (\n <div className={hero ? 'relative mt-4 sm:mt-0' : 'flex-shrink-0'}>{actions}</div>\n ) : null}\n </header>\n {children}\n </div>\n );\n}\n"],"mappings":";;;AAsBA,SAAgB,EAAU,EACxB,UACA,aACA,gBACA,YACA,UAAO,IACP,eACiB;CAejB,OACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,UAAD;GACE,WACE,IACI,iGACA;aAJR;IAOG,IAAO,kBAAC,GAAD,EAAkB,WAAU,SAAU,CAAA,IAAI;IAClD,kBAAC,OAAD;KAAK,WAAW,IAAO,aAAa;eAApC,CACE,kBAAC,MAAD;MAAI,WAAU;uBAzBI;OACxB,IAAI,CAAC,KAAe,CAAC,EAAM,SAAS,CAAW,GAC7C,OAAO;OAET,IAAM,CAAC,GAAQ,KAAS,EAAM,MAAM,CAAW;OAC/C,OACE,kBAAA,GAAA,EAAA,UAAA;QACG;QACD,kBAAC,GAAD,EAAA,UAAe,EAA0B,CAAA;QACxC;OACD,EAAA,CAAA;MAEN,EAcW,CAAY;KACX,CAAA,GACJ,kBAAC,KAAD;MAAG,WAAU;gBAAoC;KAAY,CAAA,CAC1D;;IACJ,IACC,kBAAC,OAAD;KAAK,WAAW,IAAO,0BAA0B;eAAkB;IAAa,CAAA,IAC9E;GACE;MACP,CACE;;AAET"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PropertyDetailsPage.js","names":[],"sources":["../../../src/ggnomad/pages/PropertyDetailsPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useNavigate, useParams } from 'react-router';\nimport { Bath, BedDouble, Hotel, LogIn, LogOut, Pencil, Users, Zap } from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_PROPERTY_QUERY } from '../graphql';\nimport { useGGNomad } from '../providers/GGNomadProvider';\nimport { joinPath } from '../utils/navigation';\nimport { jsonFacts } from '../utils/format';\nimport { PageShell } from './PageShell';\nimport {\n AvailabilityPanel,\n BookingPanel,\n DetailHero,\n DetailRow,\n EcoAccessPanel,\n FactGrid,\n ListingEditDialog,\n PhotoGallery,\n ProviderCard,\n RatingSummary,\n ReviewsSection,\n SectionCard,\n StatusBadge,\n TagChips,\n} from '../components';\nimport type { Fact } from '../components';\n\ntype PropertyDetail = {\n id: string;\n name: string;\n type: string;\n status?: string;\n description?: string | null;\n address?: unknown;\n currency: string;\n pricePerNight: number;\n rating?: number | null;\n reviewCount: number;\n bedrooms?: number | null;\n bathrooms?: number | null;\n maxGuests: number;\n checkInTime?: string | null;\n checkOutTime?: string | null;\n amenities: string[];\n images: string[];\n instantBook: boolean;\n sustainabilityScore: number;\n sustainabilityPractices: string[];\n accessibilityFeatures: string[];\n provider?: {\n id: string;\n name: string;\n type?: string | null;\n rating?: number | null;\n reviewCount?: number | null;\n listingCount?: number | null;\n contactEmail?: string | null;\n website?: string | null;\n } | null;\n};\n\nexport function PropertyDetailsPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const { basePath = '/' } = useGGNomad();\n const { id = '' } = useParams();\n const [editDialogOpen, setEditDialogOpen] = useState(false);\n const { data, refetch } = useQuery(\n GGNOMAD_PROPERTY_QUERY as TypedDocumentNode<\n { ggnomadProperty: PropertyDetail | null },\n { id: string }\n >,\n { variables: { id }, skip: !id }\n );\n const property = data?.ggnomadProperty;\n\n const facts: Fact[] = [];\n if (property) {\n facts.push({\n id: 'guests',\n label: t('ggnomad.property.guests', 'Guests'),\n value: String(property.maxGuests),\n icon: <Users />,\n });\n if (property.bedrooms !== null && property.bedrooms !== undefined) {\n facts.push({\n id: 'bedrooms',\n label: t('ggnomad.property.bedrooms', 'Bedrooms'),\n value: String(property.bedrooms),\n icon: <BedDouble />,\n });\n }\n if (property.bathrooms !== null && property.bathrooms !== undefined) {\n facts.push({\n id: 'bathrooms',\n label: t('ggnomad.property.bathrooms', 'Bathrooms'),\n value: String(property.bathrooms),\n icon: <Bath />,\n });\n }\n if (property.checkInTime) {\n facts.push({\n id: 'check-in',\n label: t('ggnomad.property.checkInTime', 'Check-in'),\n value: property.checkInTime,\n icon: <LogIn />,\n });\n }\n if (property.checkOutTime) {\n facts.push({\n id: 'check-out',\n label: t('ggnomad.property.checkOutTime', 'Check-out'),\n value: property.checkOutTime,\n icon: <LogOut />,\n });\n }\n }\n\n const addressFacts = jsonFacts(property?.address);\n\n return (\n <PageShell\n title={property?.name ?? t('ggnomad.property.fallbackTitle', 'Property details')}\n subtitle={t('ggnomad.property.subtitle', 'Detailed hospitality listing view')}\n >\n {!property ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.property.notFoundTitle', 'No property found')}\n description={t(\n 'ggnomad.property.notFoundDescription',\n 'This property is unavailable or may have been removed.'\n )}\n />\n ) : (\n <>\n {property.images.length > 0 ? (\n <PhotoGallery images={property.images} alt={property.name} />\n ) : (\n <DetailHero alt={property.name} />\n )}\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <span className=\"rounded-full border border-border-seam bg-bg-sunken px-3 py-1 text-sm text-text-primary\">\n {property.type}\n </span>\n {property.status ? <StatusBadge status={property.status} /> : null}\n <RatingSummary rating={property.rating} reviewCount={property.reviewCount} />\n {property.instantBook ? (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-status-success-bg-subtle px-3 py-1 text-sm font-medium text-status-success-text\">\n <Zap className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.listing.instantBook', 'Instant book')}\n </span>\n ) : null}\n </div>\n\n <PagePurpose icon={<Hotel className=\"size-4\" />}>\n {t(\n 'ggnomad.property.purpose',\n 'Everything you need to decide on this stay — its type, description, nightly price, guest capacity, rating and the provider behind it — before you reserve it.'\n )}\n </PagePurpose>\n\n <FactGrid facts={facts} />\n\n <NextSteps\n storageKey=\"ggnomad-property-detail\"\n steps={[\n {\n id: 'browse',\n label: t('ggnomad.property.next.browse', 'Compare other stays'),\n description: t(\n 'ggnomad.property.next.browseDesc',\n 'Go back to hospitality listings to compare options.'\n ),\n icon: <Hotel className=\"size-4\" />,\n onClick: () => navigate(joinPath(basePath, 'hospitality')),\n },\n ]}\n />\n\n <div className=\"grid gap-4 lg:grid-cols-3\">\n <div className=\"lg:col-span-2 space-y-4\">\n <SectionCard title={t('ggnomad.property.about', 'About')}>\n <p className=\"text-sm text-text-secondary\">{t('ggnomad.property.type', 'Type')}</p>\n <p className=\"text-text-primary\">{property.type}</p>\n <p className=\"mt-3 text-sm text-text-secondary\">\n {t('ggnomad.property.description', 'Description')}\n </p>\n <p className=\"text-text-primary\">\n {property.description ?? t('ggnomad.property.noDescription', 'No description')}\n </p>\n {addressFacts.length > 0 ? (\n <>\n <p className=\"mt-3 text-sm text-text-secondary\">\n {t('ggnomad.property.address', 'Address')}\n </p>\n <dl className=\"mt-1 space-y-1 text-sm\">\n {addressFacts.map((fact) => (\n <DetailRow key={fact.key} label={fact.label} value={fact.value} />\n ))}\n </dl>\n </>\n ) : null}\n </SectionCard>\n\n {property.amenities.length > 0 ? (\n <SectionCard title={t('ggnomad.property.amenities', 'Amenities')}>\n <TagChips items={property.amenities} />\n </SectionCard>\n ) : null}\n\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <div className=\"mb-4 flex items-center justify-between gap-3\">\n <h2 className=\"text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.property.details', 'Details')}\n </h2>\n <button\n onClick={() => setEditDialogOpen(true)}\n className=\"rounded-md p-2 text-text-secondary hover:bg-action-primary-bg hover:text-action-primary-text\"\n aria-label={t('ggnomad.property.editListing', 'Edit listing')}\n title={t('ggnomad.property.editListing', 'Edit listing')}\n >\n <Pencil className=\"size-4\" />\n </button>\n </div>\n <dl className=\"space-y-2 text-sm\">\n <DetailRow\n label={t('ggnomad.property.price', 'Price per night')}\n value={`${property.currency} ${Number(property.pricePerNight).toFixed(2)}`}\n />\n <DetailRow\n label={t('ggnomad.property.guests', 'Guests')}\n value={String(property.maxGuests)}\n />\n <DetailRow\n label={t('ggnomad.property.rating', 'Rating')}\n value={\n <RatingSummary rating={property.rating} reviewCount={property.reviewCount} />\n }\n />\n <DetailRow\n label={t('ggnomad.property.provider', 'Provider')}\n value={\n property.provider?.name ?? t('ggnomad.property.unassigned', 'Unassigned')\n }\n />\n </dl>\n </GlassCard>\n\n <EcoAccessPanel\n sustainabilityScore={property.sustainabilityScore}\n sustainabilityPractices={property.sustainabilityPractices}\n accessibilityFeatures={property.accessibilityFeatures}\n />\n\n <AvailabilityPanel listingId={property.id ?? id} listingType=\"PROPERTY\" />\n\n {property.provider ? <ProviderCard provider={property.provider} /> : null}\n\n <ReviewsSection targetType=\"PROPERTY\" targetId={property.id ?? id} />\n </div>\n <div>\n <BookingPanel\n bookingType=\"PROPERTY\"\n itemId={property.id ?? id}\n itemName={property.name}\n unitPrice={property.pricePerNight ?? 0}\n currency={property.currency ?? 'USD'}\n />\n </div>\n </div>\n\n <ListingEditDialog\n open={editDialogOpen}\n onOpenChange={setEditDialogOpen}\n targetType=\"PROPERTY\"\n id={property.id ?? id}\n initial={{\n name: property.name,\n description: property.description ?? undefined,\n pricePerNight: property.pricePerNight,\n status: property.status,\n images: property.images,\n }}\n onSaved={() => refetch()}\n />\n </>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PropertyDetailsPage.js","names":[],"sources":["../../../src/ggnomad/pages/PropertyDetailsPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useNavigate, useParams } from 'react-router';\nimport { Bath, BedDouble, Hotel, LogIn, LogOut, Pencil, Users, Zap } from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_PROPERTY_QUERY } from '../graphql';\nimport { useGGNomad } from '../providers/GGNomadProvider';\nimport { joinPath } from '../utils/navigation';\nimport { jsonFacts } from '../utils/format';\nimport { PageShell } from './PageShell';\nimport {\n AvailabilityPanel,\n BookingPanel,\n DetailHero,\n DetailRow,\n EcoAccessPanel,\n FactGrid,\n ListingEditDialog,\n PhotoGallery,\n ProviderCard,\n RatingSummary,\n ReviewsSection,\n SectionCard,\n StatusBadge,\n TagChips,\n} from '../components';\nimport type { Fact } from '../components';\n\ntype PropertyDetail = {\n id: string;\n name: string;\n type: string;\n status?: string;\n description?: string | null;\n address?: unknown;\n currency: string;\n pricePerNight: number;\n rating?: number | null;\n reviewCount: number;\n bedrooms?: number | null;\n bathrooms?: number | null;\n maxGuests: number;\n checkInTime?: string | null;\n checkOutTime?: string | null;\n amenities: string[];\n images: string[];\n instantBook: boolean;\n sustainabilityScore: number;\n sustainabilityPractices: string[];\n accessibilityFeatures: string[];\n provider?: {\n id: string;\n name: string;\n type?: string | null;\n rating?: number | null;\n reviewCount?: number | null;\n listingCount?: number | null;\n contactEmail?: string | null;\n website?: string | null;\n } | null;\n};\n\nexport function PropertyDetailsPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const { basePath = '/' } = useGGNomad();\n const { id = '' } = useParams();\n const [editDialogOpen, setEditDialogOpen] = useState(false);\n const { data, refetch } = useQuery(\n GGNOMAD_PROPERTY_QUERY as TypedDocumentNode<\n { ggnomadProperty: PropertyDetail | null },\n { id: string }\n >,\n { variables: { id }, skip: !id }\n );\n const property = data?.ggnomadProperty;\n\n const facts: Fact[] = [];\n if (property) {\n facts.push({\n id: 'guests',\n label: t('ggnomad.property.guests', 'Guests'),\n value: String(property.maxGuests),\n icon: <Users />,\n });\n if (property.bedrooms !== null && property.bedrooms !== undefined) {\n facts.push({\n id: 'bedrooms',\n label: t('ggnomad.property.bedrooms', 'Bedrooms'),\n value: String(property.bedrooms),\n icon: <BedDouble />,\n });\n }\n if (property.bathrooms !== null && property.bathrooms !== undefined) {\n facts.push({\n id: 'bathrooms',\n label: t('ggnomad.property.bathrooms', 'Bathrooms'),\n value: String(property.bathrooms),\n icon: <Bath />,\n });\n }\n if (property.checkInTime) {\n facts.push({\n id: 'check-in',\n label: t('ggnomad.property.checkInTime', 'Check-in'),\n value: property.checkInTime,\n icon: <LogIn />,\n });\n }\n if (property.checkOutTime) {\n facts.push({\n id: 'check-out',\n label: t('ggnomad.property.checkOutTime', 'Check-out'),\n value: property.checkOutTime,\n icon: <LogOut />,\n });\n }\n }\n\n const addressFacts = jsonFacts(property?.address);\n\n return (\n <PageShell\n title={property?.name ?? t('ggnomad.property.fallbackTitle', 'Property details')}\n subtitle={t('ggnomad.property.subtitle', 'Detailed hospitality listing view')}\n >\n {!property ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.property.notFoundTitle', 'No property found')}\n description={t(\n 'ggnomad.property.notFoundDescription',\n 'This property is unavailable or may have been removed.'\n )}\n />\n ) : (\n <>\n {property.images.length > 0 ? (\n <PhotoGallery images={property.images} alt={property.name} />\n ) : (\n <DetailHero alt={property.name} />\n )}\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <span className=\"rounded-full border border-border-seam bg-bg-sunken px-3 py-1 text-sm text-text-primary\">\n {property.type}\n </span>\n {property.status ? <StatusBadge status={property.status} /> : null}\n <RatingSummary rating={property.rating} reviewCount={property.reviewCount} />\n {property.instantBook ? (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-status-success-bg-subtle px-3 py-1 text-sm font-medium text-status-success-text\">\n <Zap className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.listing.instantBook', 'Instant book')}\n </span>\n ) : null}\n </div>\n\n <PagePurpose icon={<Hotel className=\"size-4\" />}>\n {t(\n 'ggnomad.property.purpose',\n 'Everything you need to decide on this stay — its type, description, nightly price, guest capacity, rating and the provider behind it — before you reserve it.'\n )}\n </PagePurpose>\n\n <FactGrid facts={facts} />\n\n <NextSteps\n storageKey=\"ggnomad-property-detail\"\n steps={[\n {\n id: 'browse',\n label: t('ggnomad.property.next.browse', 'Compare other stays'),\n description: t(\n 'ggnomad.property.next.browseDesc',\n 'Go back to hospitality listings to compare options.'\n ),\n icon: <Hotel className=\"size-4\" />,\n onClick: () => navigate(joinPath(basePath, 'hospitality')),\n },\n ]}\n />\n\n <div className=\"grid gap-4 lg:grid-cols-3\">\n <div className=\"lg:col-span-2 space-y-4\">\n <SectionCard title={t('ggnomad.property.about', 'About')}>\n <p className=\"text-sm text-text-secondary\">{t('ggnomad.property.type', 'Type')}</p>\n <p className=\"text-text-primary\">{property.type}</p>\n <p className=\"mt-3 text-sm text-text-secondary\">\n {t('ggnomad.property.description', 'Description')}\n </p>\n <p className=\"text-text-primary\">\n {property.description ?? t('ggnomad.property.noDescription', 'No description')}\n </p>\n {addressFacts.length > 0 ? (\n <>\n <p className=\"mt-3 text-sm text-text-secondary\">\n {t('ggnomad.property.address', 'Address')}\n </p>\n <dl className=\"mt-1 space-y-1 text-sm\">\n {addressFacts.map((fact) => (\n <DetailRow key={fact.key} label={fact.label} value={fact.value} />\n ))}\n </dl>\n </>\n ) : null}\n </SectionCard>\n\n {property.amenities.length > 0 ? (\n <SectionCard title={t('ggnomad.property.amenities', 'Amenities')}>\n <TagChips items={property.amenities} />\n </SectionCard>\n ) : null}\n\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <div className=\"mb-4 flex items-center justify-between gap-3\">\n <h2 className=\"text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.property.details', 'Details')}\n </h2>\n <button\n onClick={() => setEditDialogOpen(true)}\n className=\"rounded-md p-2 text-text-secondary hover:bg-action-primary-bg hover:text-action-primary-text\"\n aria-label={t('ggnomad.property.editListing', 'Edit listing')}\n title={t('ggnomad.property.editListing', 'Edit listing')}\n >\n <Pencil className=\"size-4\" />\n </button>\n </div>\n <dl className=\"space-y-2 text-sm\">\n <DetailRow\n label={t('ggnomad.property.price', 'Price per night')}\n value={`${property.currency} ${Number(property.pricePerNight).toFixed(2)}`}\n />\n <DetailRow\n label={t('ggnomad.property.guests', 'Guests')}\n value={String(property.maxGuests)}\n />\n <DetailRow\n label={t('ggnomad.property.rating', 'Rating')}\n value={\n <RatingSummary rating={property.rating} reviewCount={property.reviewCount} />\n }\n />\n <DetailRow\n label={t('ggnomad.property.provider', 'Provider')}\n value={\n property.provider?.name ?? t('ggnomad.property.unassigned', 'Unassigned')\n }\n />\n </dl>\n </GlassCard>\n\n <EcoAccessPanel\n sustainabilityScore={property.sustainabilityScore}\n sustainabilityPractices={property.sustainabilityPractices}\n accessibilityFeatures={property.accessibilityFeatures}\n />\n\n <AvailabilityPanel listingId={property.id ?? id} listingType=\"PROPERTY\" />\n\n {property.provider ? <ProviderCard provider={property.provider} /> : null}\n\n <ReviewsSection targetType=\"PROPERTY\" targetId={property.id ?? id} />\n </div>\n <div>\n <BookingPanel\n bookingType=\"PROPERTY\"\n itemId={property.id ?? id}\n itemName={property.name}\n unitPrice={property.pricePerNight ?? 0}\n currency={property.currency ?? 'USD'}\n />\n </div>\n </div>\n\n <ListingEditDialog\n open={editDialogOpen}\n onOpenChange={setEditDialogOpen}\n targetType=\"PROPERTY\"\n id={property.id ?? id}\n initial={{\n name: property.name,\n description: property.description ?? undefined,\n pricePerNight: property.pricePerNight,\n status: property.status,\n images: property.images,\n }}\n onSaved={() => refetch()}\n />\n </>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|