@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.
Files changed (64) hide show
  1. package/dist/ggnomad/GGNomadRoot.js +4 -2
  2. package/dist/ggnomad/GGNomadRoot.js.map +1 -1
  3. package/dist/ggnomad/GGNomadRoutes.js.map +1 -1
  4. package/dist/ggnomad/components/AuditTrailDialog.js.map +1 -1
  5. package/dist/ggnomad/components/AvailabilityPanel.js.map +1 -1
  6. package/dist/ggnomad/components/BookingPanel.d.ts.map +1 -1
  7. package/dist/ggnomad/components/BookingPanel.js +53 -70
  8. package/dist/ggnomad/components/BookingPanel.js.map +1 -1
  9. package/dist/ggnomad/components/CreateProviderDialog.js.map +1 -1
  10. package/dist/ggnomad/components/CreateTravelPolicyDialog.js.map +1 -1
  11. package/dist/ggnomad/components/GroupTripPanel.js.map +1 -1
  12. package/dist/ggnomad/components/ListingEditDialog.js.map +1 -1
  13. package/dist/ggnomad/components/ListingFilters.js.map +1 -1
  14. package/dist/ggnomad/components/PhotoGallery.js.map +1 -1
  15. package/dist/ggnomad/components/ProviderCard.js.map +1 -1
  16. package/dist/ggnomad/components/ProviderDashboardCard.js.map +1 -1
  17. package/dist/ggnomad/components/RatingSummary.js.map +1 -1
  18. package/dist/ggnomad/components/RedeemTicketPanel.js.map +1 -1
  19. package/dist/ggnomad/components/ReviewsSection.js.map +1 -1
  20. package/dist/ggnomad/components/StatusBadge.js.map +1 -1
  21. package/dist/ggnomad/components/TicketDialog.js +1 -1
  22. package/dist/ggnomad/components/TicketDialog.js.map +1 -1
  23. package/dist/ggnomad/components/index.js +26 -25
  24. package/dist/ggnomad/index.js +3 -2
  25. package/dist/ggnomad/pages/ActivityDetailsPage.js.map +1 -1
  26. package/dist/ggnomad/pages/AnalyticsPage.js.map +1 -1
  27. package/dist/ggnomad/pages/BookingsPage.d.ts.map +1 -1
  28. package/dist/ggnomad/pages/BookingsPage.js +54 -52
  29. package/dist/ggnomad/pages/BookingsPage.js.map +1 -1
  30. package/dist/ggnomad/pages/DashboardPage.js.map +1 -1
  31. package/dist/ggnomad/pages/EntertainmentPage.js.map +1 -1
  32. package/dist/ggnomad/pages/EventDetailsPage.js.map +1 -1
  33. package/dist/ggnomad/pages/FavoritesPage.js.map +1 -1
  34. package/dist/ggnomad/pages/HospitalityPage.js.map +1 -1
  35. package/dist/ggnomad/pages/OperationsQueuePage.js.map +1 -1
  36. package/dist/ggnomad/pages/PageShell.js.map +1 -1
  37. package/dist/ggnomad/pages/PropertyDetailsPage.js.map +1 -1
  38. package/dist/ggnomad/pages/ProviderAnalyticsPage.js.map +1 -1
  39. package/dist/ggnomad/pages/SearchPage.js +1 -1
  40. package/dist/ggnomad/pages/SearchPage.js.map +1 -1
  41. package/dist/ggnomad/pages/StorefrontPage.js.map +1 -1
  42. package/dist/ggnomad/pages/StorefrontPublicPage.js.map +1 -1
  43. package/dist/ggnomad/pages/TourDetailsPage.js.map +1 -1
  44. package/dist/ggnomad/pages/TourismPage.js.map +1 -1
  45. package/dist/ggnomad/pages/TravelPage.js +1 -1
  46. package/dist/ggnomad/pages/TravelPage.js.map +1 -1
  47. package/dist/ggnomad/pages/TravelPolicyPage.js.map +1 -1
  48. package/dist/ggnomad/pages/TripPlanDetailPage.d.ts.map +1 -1
  49. package/dist/ggnomad/pages/TripPlanDetailPage.js +278 -273
  50. package/dist/ggnomad/pages/TripPlanDetailPage.js.map +1 -1
  51. package/dist/ggnomad/pages/TripsPage.js.map +1 -1
  52. package/dist/ggnomad/providers/GGNomadProvider.js.map +1 -1
  53. package/dist/ggnomad/utils/bookingDates.d.ts +21 -0
  54. package/dist/ggnomad/utils/bookingDates.d.ts.map +1 -0
  55. package/dist/ggnomad/utils/bookingDates.js +22 -0
  56. package/dist/ggnomad/utils/bookingDates.js.map +1 -0
  57. package/dist/ggnomad/utils/format.js.map +1 -1
  58. package/dist/ggnomad/utils/notify.js +1 -1
  59. package/dist/ggnomad/utils/providerListControls.js +1 -1
  60. package/dist/ggnomad/utils/providerListControls.js.map +1 -1
  61. package/dist/ggnomad/utils/providerTypeMeta.js.map +1 -1
  62. package/dist/utils/pageContext.js +1 -1
  63. package/dist/utils/pageContext.js.map +1 -1
  64. package/package.json +18 -17
@@ -1 +1 @@
1
- {"version":3,"file":"RedeemTicketPanel.js","names":[],"sources":["../../../src/ggnomad/components/RedeemTicketPanel.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ScanLine } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { REDEEM_GGNOMAD_TICKET_MUTATION } from '../graphql';\nimport { notify } from '../utils/notify';\n\ninterface RedeemedTicket {\n id: string;\n bookingId: string;\n ticketCode: string;\n status: string;\n redeemedAt: string | null;\n redeemedBy: string | null;\n}\n\ninterface RedeemTicketPanelProps {\n onRedeemed?: () => void;\n}\n\n/**\n * Provider-facing check-in panel (BOFF-4362). A venue operator pastes/scans a\n * guest's QR token here to validate + redeem it — the same\n * `redeemGgnomadTicket` mutation a dedicated scanner device would call.\n * Redemption is server-side validated (signature + live ticket status), so\n * this panel never has to trust anything about the token itself.\n */\nexport function RedeemTicketPanel({ onRedeemed }: RedeemTicketPanelProps) {\n const { t } = useI18n();\n const [token, setToken] = useState('');\n\n const [redeemTicket, { loading }] = useMutation(\n REDEEM_GGNOMAD_TICKET_MUTATION as TypedDocumentNode<\n { redeemGgnomadTicket: RedeemedTicket },\n { token: string }\n >\n );\n\n const handleRedeem = async () => {\n const trimmed = token.trim();\n if (!trimmed) return;\n\n try {\n const result = await redeemTicket({ variables: { token: trimmed } });\n if (result.error) {\n notify.error(\n t('ggnomad.redeem.errorTitle', 'Could not redeem ticket'),\n result.error.message\n );\n return;\n }\n const redeemed = result.data?.redeemGgnomadTicket;\n notify.success(\n t('ggnomad.redeem.successTitle', 'Guest checked in'),\n redeemed\n ? t('ggnomad.redeem.successBody', 'Ticket {{code}} redeemed.').replace(\n '{{code}}',\n redeemed.ticketCode\n )\n : undefined\n );\n setToken('');\n onRedeemed?.();\n } catch (err) {\n notify.error(\n t('ggnomad.redeem.errorTitle', 'Could not redeem ticket'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n return (\n <GlassCard treatment=\"glass\" className=\"border-border-seam p-5\">\n <div className=\"mb-3 flex items-center gap-2\">\n <ScanLine className=\"size-4 text-text-secondary\" aria-hidden=\"true\" />\n <h2 className=\"text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.redeem.title', 'Check in a guest')}\n </h2>\n </div>\n <p className=\"mb-4 text-sm text-text-secondary\">\n {t(\n 'ggnomad.redeem.purpose',\n 'Paste the token from a scanned QR ticket to validate and check the guest in. Revoked or already-redeemed tickets are rejected automatically.'\n )}\n </p>\n <form\n className=\"flex flex-col gap-2 sm:flex-row\"\n onSubmit={(event) => {\n event.preventDefault();\n void handleRedeem();\n }}\n >\n <input\n type=\"text\"\n value={token}\n onChange={(e) => setToken(e.target.value)}\n disabled={loading}\n placeholder={t('ggnomad.redeem.placeholder', 'Scanned ticket token')}\n aria-label={t('ggnomad.redeem.inputLabel', 'Ticket token')}\n className=\"min-w-0 flex-1 rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm font-mono 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 />\n <button\n type=\"submit\"\n disabled={loading || !token.trim()}\n className=\"shrink-0 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bgHover disabled:opacity-50\"\n >\n {loading\n ? t('ggnomad.redeem.checking', 'Checking in…')\n : t('ggnomad.redeem.action', 'Check in')}\n </button>\n </form>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;;;;AA8BA,SAAgB,EAAkB,EAAE,iBAAsC;CACxE,IAAM,EAAE,SAAM,EAAQ,GAChB,CAAC,GAAO,KAAY,EAAS,EAAE,GAE/B,CAAC,GAAc,EAAE,gBAAa,EAClC,CAIF,GAEM,IAAe,YAAY;EAC/B,IAAM,IAAU,EAAM,KAAK;EACtB,OAEL,IAAI;GACF,IAAM,IAAS,MAAM,EAAa,EAAE,WAAW,EAAE,OAAO,EAAQ,EAAE,CAAC;GACnE,IAAI,EAAO,OAAO;IAChB,EAAO,MACL,EAAE,6BAA6B,yBAAyB,GACxD,EAAO,MAAM,OACf;IACA;GACF;GACA,IAAM,IAAW,EAAO,MAAM;GAW9B,AAVA,EAAO,QACL,EAAE,+BAA+B,kBAAkB,GACnD,IACI,EAAE,8BAA8B,2BAA2B,EAAE,QAC3D,YACA,EAAS,UACX,IACA,KAAA,CACN,GACA,EAAS,EAAE,GACX,IAAa;EACf,SAAS,GAAK;GACZ,EAAO,MACL,EAAE,6BAA6B,yBAAyB,GACxD,aAAe,QAAQ,EAAI,UAAU,OAAO,CAAG,CACjD;EACF;CACF;CAEA,OACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YAAvC;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAU,WAAU;KAA6B,eAAY;IAAQ,CAAA,GACrE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAE,wBAAwB,kBAAkB;IAC3C,CAAA,CACD;;GACL,kBAAC,KAAD;IAAG,WAAU;cACV,EACC,0BACA,8IACF;GACC,CAAA;GACH,kBAAC,QAAD;IACE,WAAU;IACV,WAAW,MAAU;KAEnB,AADA,EAAM,eAAe,GACrB,EAAkB;IACpB;cALF,CAOE,kBAAC,SAAD;KACE,MAAK;KACL,OAAO;KACP,WAAW,MAAM,EAAS,EAAE,OAAO,KAAK;KACxC,UAAU;KACV,aAAa,EAAE,8BAA8B,sBAAsB;KACnE,cAAY,EAAE,6BAA6B,cAAc;KACzD,WAAU;IACX,CAAA,GACD,kBAAC,UAAD;KACE,MAAK;KACL,UAAU,KAAW,CAAC,EAAM,KAAK;KACjC,WAAU;eAET,IACG,EAAE,2BAA2B,cAAc,IAC3C,EAAE,yBAAyB,UAAU;IACnC,CAAA,CACJ;;EACG;;AAEf"}
1
+ {"version":3,"file":"RedeemTicketPanel.js","names":[],"sources":["../../../src/ggnomad/components/RedeemTicketPanel.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ScanLine } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { REDEEM_GGNOMAD_TICKET_MUTATION } from '../graphql';\nimport { notify } from '../utils/notify';\n\ninterface RedeemedTicket {\n id: string;\n bookingId: string;\n ticketCode: string;\n status: string;\n redeemedAt: string | null;\n redeemedBy: string | null;\n}\n\ninterface RedeemTicketPanelProps {\n onRedeemed?: () => void;\n}\n\n/**\n * Provider-facing check-in panel (BOFF-4362). A venue operator pastes/scans a\n * guest's QR token here to validate + redeem it — the same\n * `redeemGgnomadTicket` mutation a dedicated scanner device would call.\n * Redemption is server-side validated (signature + live ticket status), so\n * this panel never has to trust anything about the token itself.\n */\nexport function RedeemTicketPanel({ onRedeemed }: RedeemTicketPanelProps) {\n const { t } = useI18n();\n const [token, setToken] = useState('');\n\n const [redeemTicket, { loading }] = useMutation(\n REDEEM_GGNOMAD_TICKET_MUTATION as TypedDocumentNode<\n { redeemGgnomadTicket: RedeemedTicket },\n { token: string }\n >\n );\n\n const handleRedeem = async () => {\n const trimmed = token.trim();\n if (!trimmed) return;\n\n try {\n const result = await redeemTicket({ variables: { token: trimmed } });\n if (result.error) {\n notify.error(\n t('ggnomad.redeem.errorTitle', 'Could not redeem ticket'),\n result.error.message\n );\n return;\n }\n const redeemed = result.data?.redeemGgnomadTicket;\n notify.success(\n t('ggnomad.redeem.successTitle', 'Guest checked in'),\n redeemed\n ? t('ggnomad.redeem.successBody', 'Ticket {{code}} redeemed.').replace(\n '{{code}}',\n redeemed.ticketCode\n )\n : undefined\n );\n setToken('');\n onRedeemed?.();\n } catch (err) {\n notify.error(\n t('ggnomad.redeem.errorTitle', 'Could not redeem ticket'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n return (\n <GlassCard treatment=\"glass\" className=\"border-border-seam p-5\">\n <div className=\"mb-3 flex items-center gap-2\">\n <ScanLine className=\"size-4 text-text-secondary\" aria-hidden=\"true\" />\n <h2 className=\"text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.redeem.title', 'Check in a guest')}\n </h2>\n </div>\n <p className=\"mb-4 text-sm text-text-secondary\">\n {t(\n 'ggnomad.redeem.purpose',\n 'Paste the token from a scanned QR ticket to validate and check the guest in. Revoked or already-redeemed tickets are rejected automatically.'\n )}\n </p>\n <form\n className=\"flex flex-col gap-2 sm:flex-row\"\n onSubmit={(event) => {\n event.preventDefault();\n void handleRedeem();\n }}\n >\n <input\n type=\"text\"\n value={token}\n onChange={(e) => setToken(e.target.value)}\n disabled={loading}\n placeholder={t('ggnomad.redeem.placeholder', 'Scanned ticket token')}\n aria-label={t('ggnomad.redeem.inputLabel', 'Ticket token')}\n className=\"min-w-0 flex-1 rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm font-mono 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 />\n <button\n type=\"submit\"\n disabled={loading || !token.trim()}\n className=\"shrink-0 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bgHover disabled:opacity-50\"\n >\n {loading\n ? t('ggnomad.redeem.checking', 'Checking in…')\n : t('ggnomad.redeem.action', 'Check in')}\n </button>\n </form>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;;;;AA8BA,SAAgB,EAAkB,EAAE,iBAAsC;CACxE,IAAM,EAAE,SAAM,EAAQ,GAChB,CAAC,GAAO,KAAY,EAAS,EAAE,GAE/B,CAAC,GAAc,EAAE,gBAAa,EAClC,CAIF,GAEM,IAAe,YAAY;EAC/B,IAAM,IAAU,EAAM,KAAK;EACtB,OAEL,IAAI;GACF,IAAM,IAAS,MAAM,EAAa,EAAE,WAAW,EAAE,OAAO,EAAQ,EAAE,CAAC;GACnE,IAAI,EAAO,OAAO;IAChB,EAAO,MACL,EAAE,6BAA6B,yBAAyB,GACxD,EAAO,MAAM,OACf;IACA;GACF;GACA,IAAM,IAAW,EAAO,MAAM;GAW9B,AAVA,EAAO,QACL,EAAE,+BAA+B,kBAAkB,GACnD,IACI,EAAE,8BAA8B,2BAA2B,CAAC,CAAC,QAC3D,YACA,EAAS,UACX,IACA,KAAA,CACN,GACA,EAAS,EAAE,GACX,IAAa;EACf,SAAS,GAAK;GACZ,EAAO,MACL,EAAE,6BAA6B,yBAAyB,GACxD,aAAe,QAAQ,EAAI,UAAU,OAAO,CAAG,CACjD;EACF;CACF;CAEA,OACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YAAvC;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAU,WAAU;KAA6B,eAAY;IAAQ,CAAA,GACrE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAE,wBAAwB,kBAAkB;IAC3C,CAAA,CACD;;GACL,kBAAC,KAAD;IAAG,WAAU;cACV,EACC,0BACA,8IACF;GACC,CAAA;GACH,kBAAC,QAAD;IACE,WAAU;IACV,WAAW,MAAU;KAEnB,AADA,EAAM,eAAe,GACrB,EAAkB;IACpB;cALF,CAOE,kBAAC,SAAD;KACE,MAAK;KACL,OAAO;KACP,WAAW,MAAM,EAAS,EAAE,OAAO,KAAK;KACxC,UAAU;KACV,aAAa,EAAE,8BAA8B,sBAAsB;KACnE,cAAY,EAAE,6BAA6B,cAAc;KACzD,WAAU;IACX,CAAA,GACD,kBAAC,UAAD;KACE,MAAK;KACL,UAAU,KAAW,CAAC,EAAM,KAAK;KACjC,WAAU;eAET,IACG,EAAE,2BAA2B,cAAc,IAC3C,EAAE,yBAAyB,UAAU;IACnC,CAAA,CACJ;;EACG;;AAEf"}
@@ -1 +1 @@
1
- {"version":3,"file":"ReviewsSection.js","names":[],"sources":["../../../src/ggnomad/components/ReviewsSection.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Star, MessageSquare, AlertCircle } from 'lucide-react';\nimport { Button, GlassCard, Input, Textarea } from '@burdenoff/fe-libs/ui';\n\nimport {\n GGNOMAD_REVIEW_SUMMARY_QUERY,\n GGNOMAD_REVIEWS_QUERY,\n CREATE_GGNOMAD_REVIEW_MUTATION,\n} from '../graphql';\nimport { notify } from '../utils/notify';\nimport { cn } from '../utils/cn';\n\ntype GgnomadBookingType = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ntype ReviewsSectionProps = {\n targetType: GgnomadBookingType;\n targetId: string;\n};\n\ntype ReviewSummaryData = {\n ggnomadReviewSummary: {\n average: number;\n count: number;\n distribution: number[];\n };\n};\n\ntype ReviewsData = {\n ggnomadReviews: Array<{\n id: string;\n rating: number;\n title?: string;\n content?: string;\n isVerified: boolean;\n helpfulCount: number;\n response?: string;\n respondedAt?: string;\n createdAt: string;\n }>;\n};\n\ntype CreateReviewInput = {\n targetType: GgnomadBookingType;\n targetId: string;\n rating: number;\n title?: string;\n content?: string;\n images?: string[];\n};\n\n/**\n * Star distribution bar chart component.\n * Displays a 5-row breakdown of rating distribution.\n */\nfunction StarDistributionChart({ distribution }: { distribution: number[] }) {\n const total = distribution.reduce((a, b) => a + b, 0);\n\n return (\n <div className=\"space-y-2\">\n {[5, 4, 3, 2, 1].map((stars, idx) => {\n const count = distribution[4 - idx] ?? 0;\n const percentage = total > 0 ? (count / total) * 100 : 0;\n\n return (\n <div key={stars} className=\"flex items-center gap-2\">\n <div className=\"flex items-center gap-1 whitespace-nowrap text-xs text-text-secondary\">\n <span>{stars}</span>\n <Star className=\"size-3 text-status-warning-text\" aria-hidden=\"true\" />\n </div>\n <div className=\"flex-1 h-2 rounded-full bg-bg-sunken overflow-hidden\">\n <div\n className=\"h-full bg-status-warning-bg transition-all\"\n style={{ width: `${percentage}%` }}\n role=\"progressbar\"\n aria-label={`${count} reviews with ${stars} stars`}\n aria-valuenow={percentage}\n aria-valuemin={0}\n aria-valuemax={100}\n />\n </div>\n <span className=\"text-xs text-text-secondary tabular-nums w-8 text-right\">{count}</span>\n </div>\n );\n })}\n </div>\n );\n}\n\n/**\n * Individual review card component.\n */\nfunction ReviewCard({ review }: { review: ReviewsData['ggnomadReviews'][0] }) {\n const { t } = useI18n();\n const createdDate = new Date(review.createdAt);\n const formattedDate = createdDate.toLocaleDateString(undefined, {\n month: 'short',\n day: 'numeric',\n year: createdDate.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined,\n });\n\n return (\n <div className=\"rounded-lg border border-border-seam bg-bg-surface/40 p-4 space-y-3\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex items-center gap-2\">\n <div className=\"flex gap-0.5\">\n {[...Array(5)].map((_, i) => (\n <Star\n key={i}\n className={cn(\n 'size-3.5',\n i < review.rating\n ? 'fill-status-warning-text text-status-warning-text'\n : 'text-border-seam'\n )}\n aria-hidden=\"true\"\n />\n ))}\n </div>\n <span className=\"text-xs text-text-secondary font-medium\">{review.rating}/5</span>\n </div>\n {review.isVerified && (\n <span className=\"inline-flex items-center gap-1 text-xs font-medium text-status-success-text bg-status-success-bg-subtle px-2 py-1 rounded-full\">\n <span className=\"size-1.5 bg-status-success-text rounded-full\" aria-hidden=\"true\" />\n {t('ggnomad.reviews.verified', 'Verified')}\n </span>\n )}\n </div>\n\n {review.title && <h4 className=\"font-medium text-text-primary text-sm\">{review.title}</h4>}\n\n {review.content && (\n <p className=\"text-sm text-text-secondary leading-relaxed\">{review.content}</p>\n )}\n\n <div className=\"flex items-center justify-between pt-2 border-t border-border-seam\">\n <span className=\"text-xs text-text-secondary\">{formattedDate}</span>\n {review.helpfulCount > 0 && (\n <span className=\"text-xs text-text-secondary flex items-center gap-1\">\n <MessageSquare className=\"size-3\" aria-hidden=\"true\" />\n {review.helpfulCount}\n </span>\n )}\n </div>\n\n {review.response && (\n <div className=\"mt-3 pt-3 border-t border-border-seam bg-bg-sunken/50 rounded p-3 text-sm\">\n <p className=\"font-medium text-text-primary mb-1\">\n {t('ggnomad.reviews.hostResponse', 'Host response')}\n </p>\n <p className=\"text-text-secondary\">{review.response}</p>\n {review.respondedAt && (\n <p className=\"text-xs text-text-secondary mt-2\">\n {new Date(review.respondedAt).toLocaleDateString()}\n </p>\n )}\n </div>\n )}\n </div>\n );\n}\n\n/**\n * Review composer component.\n */\nfunction ReviewComposer({\n targetType,\n targetId,\n onReviewCreated,\n}: {\n targetType: GgnomadBookingType;\n targetId: string;\n onReviewCreated: () => void;\n}) {\n const { t } = useI18n();\n const [rating, setRating] = useState(5);\n const [title, setTitle] = useState('');\n const [content, setContent] = useState('');\n\n const [createReview, { loading: creating }] = useMutation(\n CREATE_GGNOMAD_REVIEW_MUTATION as TypedDocumentNode<\n {\n createGgnomadReview: {\n id: string;\n rating: number;\n title?: string;\n content?: string;\n };\n },\n { input: CreateReviewInput }\n >\n );\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n if (rating === 0) {\n notify.warning(\n t('ggnomad.reviews.composer.ratingRequired', 'Rating required'),\n t('ggnomad.reviews.composer.selectRating', 'Please select a rating before submitting.')\n );\n return;\n }\n\n try {\n const result = await createReview({\n variables: {\n input: {\n targetType,\n targetId,\n rating,\n title: title.trim() || undefined,\n content: content.trim() || undefined,\n },\n },\n });\n\n if (result.error) {\n notify.error(\n t('ggnomad.reviews.composer.error', 'Could not post review'),\n result.error.message\n );\n return;\n }\n\n notify.success(\n t('ggnomad.reviews.composer.success', 'Review posted'),\n t('ggnomad.reviews.composer.successDesc', 'Thank you for sharing your experience!')\n );\n\n // Reset form\n setRating(5);\n setTitle('');\n setContent('');\n\n // Trigger refresh\n onReviewCreated();\n } catch (err) {\n notify.error(\n t('ggnomad.reviews.composer.error', 'Could not post review'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n return (\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <h3 className=\"mb-4 text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.reviews.composer.title', 'Share your experience')}\n </h3>\n\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {/* Rating selector */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-2\">\n {t('ggnomad.reviews.composer.rating', 'Your rating')}\n </label>\n <div className=\"flex gap-2\">\n {[1, 2, 3, 4, 5].map((star) => (\n <button\n key={star}\n type=\"button\"\n onClick={() => setRating(star)}\n className={cn(\n 'p-1.5 rounded-lg transition-all',\n rating >= star\n ? 'bg-status-warning-bg-subtle'\n : 'bg-bg-sunken hover:bg-bg-surface'\n )}\n aria-label={t(\n 'ggnomad.reviews.composer.ratingLabel',\n `Rate {{stars}} out of 5 stars`\n ).replace('{{stars}}', String(star))}\n aria-pressed={rating >= star}\n >\n <Star\n className={cn(\n 'size-5 transition-colors',\n rating >= star\n ? 'fill-status-warning-text text-status-warning-text'\n : 'text-border-seam'\n )}\n aria-hidden=\"true\"\n />\n </button>\n ))}\n </div>\n </div>\n\n {/* Title input */}\n <div>\n <label\n htmlFor=\"review-title\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {t('ggnomad.reviews.composer.titleLabel', 'Title (optional)')}\n </label>\n <Input\n id=\"review-title\"\n placeholder={t(\n 'ggnomad.reviews.composer.titlePlaceholder',\n 'e.g., \"Great location, friendly host\"'\n )}\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n maxLength={100}\n disabled={creating}\n className=\"bg-bg-surface border-border-seam\"\n />\n <p className=\"text-xs text-text-secondary mt-1\">{title.length}/100</p>\n </div>\n\n {/* Content textarea */}\n <div>\n <label\n htmlFor=\"review-content\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {t('ggnomad.reviews.composer.contentLabel', 'Your review (optional)')}\n </label>\n <Textarea\n id=\"review-content\"\n placeholder={t(\n 'ggnomad.reviews.composer.contentPlaceholder',\n 'Share the details of your experience...'\n )}\n value={content}\n onChange={(e) => setContent(e.target.value)}\n maxLength={1000}\n rows={4}\n disabled={creating}\n className=\"bg-bg-surface border-border-seam resize-none\"\n />\n <p className=\"text-xs text-text-secondary mt-1\">{content.length}/1000</p>\n </div>\n\n {/* Submit button */}\n <Button type=\"submit\" disabled={creating || rating === 0} className=\"w-full\">\n {creating\n ? t('ggnomad.reviews.composer.submitting', 'Posting…')\n : t('ggnomad.reviews.composer.submit', 'Post review')}\n </Button>\n </form>\n </GlassCard>\n );\n}\n\n/**\n * ReviewsSection component.\n * Displays review summary, list, and composition UI for a bookable item.\n */\nexport function ReviewsSection({ targetType, targetId }: ReviewsSectionProps) {\n const { t } = useI18n();\n const [showComposer, setShowComposer] = useState(false);\n\n // Fetch review summary\n const { data: summaryData, refetch: refetchSummary } = useQuery(\n GGNOMAD_REVIEW_SUMMARY_QUERY as TypedDocumentNode<\n ReviewSummaryData,\n { targetType: GgnomadBookingType; targetId: string }\n >,\n { variables: { targetType, targetId } }\n );\n\n // Fetch reviews list\n const { data: reviewsData, refetch: refetchReviews } = useQuery(\n GGNOMAD_REVIEWS_QUERY as TypedDocumentNode<\n ReviewsData,\n { targetType: GgnomadBookingType; targetId: string; limit?: number; offset?: number }\n >,\n {\n variables: { targetType, targetId, limit: 10, offset: 0 },\n }\n );\n\n const summary = summaryData?.ggnomadReviewSummary;\n const reviews = reviewsData?.ggnomadReviews ?? [];\n\n const handleReviewCreated = () => {\n setShowComposer(false);\n void refetchSummary();\n void refetchReviews();\n };\n\n return (\n <div className=\"space-y-6\">\n {/* Summary Section */}\n {summary && summary.count > 0 && (\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.reviews.summary', 'Ratings & reviews')}\n </h2>\n\n <div className=\"grid gap-6 sm:grid-cols-2\">\n {/* Average Rating */}\n <div className=\"flex flex-col items-center justify-center py-4\">\n <div className=\"text-5xl font-bold text-text-primary tabular-nums\">\n {summary.average.toFixed(1)}\n </div>\n <div className=\"flex gap-0.5 mt-2\">\n {[...Array(5)].map((_, i) => (\n <Star\n key={i}\n className={cn(\n 'size-4',\n i < Math.round(summary.average)\n ? 'fill-status-warning-text text-status-warning-text'\n : 'text-border-seam'\n )}\n aria-hidden=\"true\"\n />\n ))}\n </div>\n <p className=\"text-sm text-text-secondary mt-2\">\n {t('ggnomad.reviews.totalCount', '{{count}} {{plural}}')\n .replace('{{count}}', String(summary.count))\n .replace(\n '{{plural}}',\n summary.count === 1\n ? t('ggnomad.reviews.review', 'review')\n : t('ggnomad.reviews.reviews', 'reviews')\n )}\n </p>\n </div>\n\n {/* Distribution Chart */}\n <div>\n <StarDistributionChart distribution={summary.distribution} />\n </div>\n </div>\n </GlassCard>\n )}\n\n {/* Write Review Composer */}\n {!showComposer && (\n <Button onClick={() => setShowComposer(true)} variant=\"secondary\" className=\"w-full\">\n {t('ggnomad.reviews.writeReview', 'Write a review')}\n </Button>\n )}\n\n {showComposer && (\n <ReviewComposer\n targetType={targetType}\n targetId={targetId}\n onReviewCreated={handleReviewCreated}\n />\n )}\n\n {/* Reviews List */}\n {reviews.length > 0 ? (\n <div>\n <h3 className=\"mb-4 text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.reviews.allReviews', 'All reviews')}\n </h3>\n <div className=\"grid gap-3\">\n {reviews.map((review) => (\n <ReviewCard key={review.id} review={review} />\n ))}\n </div>\n </div>\n ) : summary && summary.count === 0 ? (\n <div className=\"text-center py-8 rounded-lg border border-border-seam bg-bg-surface/40\">\n <AlertCircle className=\"size-6 text-text-secondary mx-auto mb-2\" aria-hidden=\"true\" />\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.reviews.noReviewsYet', 'Be the first to review')}\n </p>\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ReviewsSection.js","names":[],"sources":["../../../src/ggnomad/components/ReviewsSection.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Star, MessageSquare, AlertCircle } from 'lucide-react';\nimport { Button, GlassCard, Input, Textarea } from '@burdenoff/fe-libs/ui';\n\nimport {\n GGNOMAD_REVIEW_SUMMARY_QUERY,\n GGNOMAD_REVIEWS_QUERY,\n CREATE_GGNOMAD_REVIEW_MUTATION,\n} from '../graphql';\nimport { notify } from '../utils/notify';\nimport { cn } from '../utils/cn';\n\ntype GgnomadBookingType = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ntype ReviewsSectionProps = {\n targetType: GgnomadBookingType;\n targetId: string;\n};\n\ntype ReviewSummaryData = {\n ggnomadReviewSummary: {\n average: number;\n count: number;\n distribution: number[];\n };\n};\n\ntype ReviewsData = {\n ggnomadReviews: Array<{\n id: string;\n rating: number;\n title?: string;\n content?: string;\n isVerified: boolean;\n helpfulCount: number;\n response?: string;\n respondedAt?: string;\n createdAt: string;\n }>;\n};\n\ntype CreateReviewInput = {\n targetType: GgnomadBookingType;\n targetId: string;\n rating: number;\n title?: string;\n content?: string;\n images?: string[];\n};\n\n/**\n * Star distribution bar chart component.\n * Displays a 5-row breakdown of rating distribution.\n */\nfunction StarDistributionChart({ distribution }: { distribution: number[] }) {\n const total = distribution.reduce((a, b) => a + b, 0);\n\n return (\n <div className=\"space-y-2\">\n {[5, 4, 3, 2, 1].map((stars, idx) => {\n const count = distribution[4 - idx] ?? 0;\n const percentage = total > 0 ? (count / total) * 100 : 0;\n\n return (\n <div key={stars} className=\"flex items-center gap-2\">\n <div className=\"flex items-center gap-1 whitespace-nowrap text-xs text-text-secondary\">\n <span>{stars}</span>\n <Star className=\"size-3 text-status-warning-text\" aria-hidden=\"true\" />\n </div>\n <div className=\"flex-1 h-2 rounded-full bg-bg-sunken overflow-hidden\">\n <div\n className=\"h-full bg-status-warning-bg transition-all\"\n style={{ width: `${percentage}%` }}\n role=\"progressbar\"\n aria-label={`${count} reviews with ${stars} stars`}\n aria-valuenow={percentage}\n aria-valuemin={0}\n aria-valuemax={100}\n />\n </div>\n <span className=\"text-xs text-text-secondary tabular-nums w-8 text-right\">{count}</span>\n </div>\n );\n })}\n </div>\n );\n}\n\n/**\n * Individual review card component.\n */\nfunction ReviewCard({ review }: { review: ReviewsData['ggnomadReviews'][0] }) {\n const { t } = useI18n();\n const createdDate = new Date(review.createdAt);\n const formattedDate = createdDate.toLocaleDateString(undefined, {\n month: 'short',\n day: 'numeric',\n year: createdDate.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined,\n });\n\n return (\n <div className=\"rounded-lg border border-border-seam bg-bg-surface/40 p-4 space-y-3\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex items-center gap-2\">\n <div className=\"flex gap-0.5\">\n {[...Array(5)].map((_, i) => (\n <Star\n key={i}\n className={cn(\n 'size-3.5',\n i < review.rating\n ? 'fill-status-warning-text text-status-warning-text'\n : 'text-border-seam'\n )}\n aria-hidden=\"true\"\n />\n ))}\n </div>\n <span className=\"text-xs text-text-secondary font-medium\">{review.rating}/5</span>\n </div>\n {review.isVerified && (\n <span className=\"inline-flex items-center gap-1 text-xs font-medium text-status-success-text bg-status-success-bg-subtle px-2 py-1 rounded-full\">\n <span className=\"size-1.5 bg-status-success-text rounded-full\" aria-hidden=\"true\" />\n {t('ggnomad.reviews.verified', 'Verified')}\n </span>\n )}\n </div>\n\n {review.title && <h4 className=\"font-medium text-text-primary text-sm\">{review.title}</h4>}\n\n {review.content && (\n <p className=\"text-sm text-text-secondary leading-relaxed\">{review.content}</p>\n )}\n\n <div className=\"flex items-center justify-between pt-2 border-t border-border-seam\">\n <span className=\"text-xs text-text-secondary\">{formattedDate}</span>\n {review.helpfulCount > 0 && (\n <span className=\"text-xs text-text-secondary flex items-center gap-1\">\n <MessageSquare className=\"size-3\" aria-hidden=\"true\" />\n {review.helpfulCount}\n </span>\n )}\n </div>\n\n {review.response && (\n <div className=\"mt-3 pt-3 border-t border-border-seam bg-bg-sunken/50 rounded p-3 text-sm\">\n <p className=\"font-medium text-text-primary mb-1\">\n {t('ggnomad.reviews.hostResponse', 'Host response')}\n </p>\n <p className=\"text-text-secondary\">{review.response}</p>\n {review.respondedAt && (\n <p className=\"text-xs text-text-secondary mt-2\">\n {new Date(review.respondedAt).toLocaleDateString()}\n </p>\n )}\n </div>\n )}\n </div>\n );\n}\n\n/**\n * Review composer component.\n */\nfunction ReviewComposer({\n targetType,\n targetId,\n onReviewCreated,\n}: {\n targetType: GgnomadBookingType;\n targetId: string;\n onReviewCreated: () => void;\n}) {\n const { t } = useI18n();\n const [rating, setRating] = useState(5);\n const [title, setTitle] = useState('');\n const [content, setContent] = useState('');\n\n const [createReview, { loading: creating }] = useMutation(\n CREATE_GGNOMAD_REVIEW_MUTATION as TypedDocumentNode<\n {\n createGgnomadReview: {\n id: string;\n rating: number;\n title?: string;\n content?: string;\n };\n },\n { input: CreateReviewInput }\n >\n );\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n\n if (rating === 0) {\n notify.warning(\n t('ggnomad.reviews.composer.ratingRequired', 'Rating required'),\n t('ggnomad.reviews.composer.selectRating', 'Please select a rating before submitting.')\n );\n return;\n }\n\n try {\n const result = await createReview({\n variables: {\n input: {\n targetType,\n targetId,\n rating,\n title: title.trim() || undefined,\n content: content.trim() || undefined,\n },\n },\n });\n\n if (result.error) {\n notify.error(\n t('ggnomad.reviews.composer.error', 'Could not post review'),\n result.error.message\n );\n return;\n }\n\n notify.success(\n t('ggnomad.reviews.composer.success', 'Review posted'),\n t('ggnomad.reviews.composer.successDesc', 'Thank you for sharing your experience!')\n );\n\n // Reset form\n setRating(5);\n setTitle('');\n setContent('');\n\n // Trigger refresh\n onReviewCreated();\n } catch (err) {\n notify.error(\n t('ggnomad.reviews.composer.error', 'Could not post review'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n return (\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <h3 className=\"mb-4 text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.reviews.composer.title', 'Share your experience')}\n </h3>\n\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {/* Rating selector */}\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-2\">\n {t('ggnomad.reviews.composer.rating', 'Your rating')}\n </label>\n <div className=\"flex gap-2\">\n {[1, 2, 3, 4, 5].map((star) => (\n <button\n key={star}\n type=\"button\"\n onClick={() => setRating(star)}\n className={cn(\n 'p-1.5 rounded-lg transition-all',\n rating >= star\n ? 'bg-status-warning-bg-subtle'\n : 'bg-bg-sunken hover:bg-bg-surface'\n )}\n aria-label={t(\n 'ggnomad.reviews.composer.ratingLabel',\n `Rate {{stars}} out of 5 stars`\n ).replace('{{stars}}', String(star))}\n aria-pressed={rating >= star}\n >\n <Star\n className={cn(\n 'size-5 transition-colors',\n rating >= star\n ? 'fill-status-warning-text text-status-warning-text'\n : 'text-border-seam'\n )}\n aria-hidden=\"true\"\n />\n </button>\n ))}\n </div>\n </div>\n\n {/* Title input */}\n <div>\n <label\n htmlFor=\"review-title\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {t('ggnomad.reviews.composer.titleLabel', 'Title (optional)')}\n </label>\n <Input\n id=\"review-title\"\n placeholder={t(\n 'ggnomad.reviews.composer.titlePlaceholder',\n 'e.g., \"Great location, friendly host\"'\n )}\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n maxLength={100}\n disabled={creating}\n className=\"bg-bg-surface border-border-seam\"\n />\n <p className=\"text-xs text-text-secondary mt-1\">{title.length}/100</p>\n </div>\n\n {/* Content textarea */}\n <div>\n <label\n htmlFor=\"review-content\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {t('ggnomad.reviews.composer.contentLabel', 'Your review (optional)')}\n </label>\n <Textarea\n id=\"review-content\"\n placeholder={t(\n 'ggnomad.reviews.composer.contentPlaceholder',\n 'Share the details of your experience...'\n )}\n value={content}\n onChange={(e) => setContent(e.target.value)}\n maxLength={1000}\n rows={4}\n disabled={creating}\n className=\"bg-bg-surface border-border-seam resize-none\"\n />\n <p className=\"text-xs text-text-secondary mt-1\">{content.length}/1000</p>\n </div>\n\n {/* Submit button */}\n <Button type=\"submit\" disabled={creating || rating === 0} className=\"w-full\">\n {creating\n ? t('ggnomad.reviews.composer.submitting', 'Posting…')\n : t('ggnomad.reviews.composer.submit', 'Post review')}\n </Button>\n </form>\n </GlassCard>\n );\n}\n\n/**\n * ReviewsSection component.\n * Displays review summary, list, and composition UI for a bookable item.\n */\nexport function ReviewsSection({ targetType, targetId }: ReviewsSectionProps) {\n const { t } = useI18n();\n const [showComposer, setShowComposer] = useState(false);\n\n // Fetch review summary\n const { data: summaryData, refetch: refetchSummary } = useQuery(\n GGNOMAD_REVIEW_SUMMARY_QUERY as TypedDocumentNode<\n ReviewSummaryData,\n { targetType: GgnomadBookingType; targetId: string }\n >,\n { variables: { targetType, targetId } }\n );\n\n // Fetch reviews list\n const { data: reviewsData, refetch: refetchReviews } = useQuery(\n GGNOMAD_REVIEWS_QUERY as TypedDocumentNode<\n ReviewsData,\n { targetType: GgnomadBookingType; targetId: string; limit?: number; offset?: number }\n >,\n {\n variables: { targetType, targetId, limit: 10, offset: 0 },\n }\n );\n\n const summary = summaryData?.ggnomadReviewSummary;\n const reviews = reviewsData?.ggnomadReviews ?? [];\n\n const handleReviewCreated = () => {\n setShowComposer(false);\n void refetchSummary();\n void refetchReviews();\n };\n\n return (\n <div className=\"space-y-6\">\n {/* Summary Section */}\n {summary && summary.count > 0 && (\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.reviews.summary', 'Ratings & reviews')}\n </h2>\n\n <div className=\"grid gap-6 sm:grid-cols-2\">\n {/* Average Rating */}\n <div className=\"flex flex-col items-center justify-center py-4\">\n <div className=\"text-5xl font-bold text-text-primary tabular-nums\">\n {summary.average.toFixed(1)}\n </div>\n <div className=\"flex gap-0.5 mt-2\">\n {[...Array(5)].map((_, i) => (\n <Star\n key={i}\n className={cn(\n 'size-4',\n i < Math.round(summary.average)\n ? 'fill-status-warning-text text-status-warning-text'\n : 'text-border-seam'\n )}\n aria-hidden=\"true\"\n />\n ))}\n </div>\n <p className=\"text-sm text-text-secondary mt-2\">\n {t('ggnomad.reviews.totalCount', '{{count}} {{plural}}')\n .replace('{{count}}', String(summary.count))\n .replace(\n '{{plural}}',\n summary.count === 1\n ? t('ggnomad.reviews.review', 'review')\n : t('ggnomad.reviews.reviews', 'reviews')\n )}\n </p>\n </div>\n\n {/* Distribution Chart */}\n <div>\n <StarDistributionChart distribution={summary.distribution} />\n </div>\n </div>\n </GlassCard>\n )}\n\n {/* Write Review Composer */}\n {!showComposer && (\n <Button onClick={() => setShowComposer(true)} variant=\"secondary\" className=\"w-full\">\n {t('ggnomad.reviews.writeReview', 'Write a review')}\n </Button>\n )}\n\n {showComposer && (\n <ReviewComposer\n targetType={targetType}\n targetId={targetId}\n onReviewCreated={handleReviewCreated}\n />\n )}\n\n {/* Reviews List */}\n {reviews.length > 0 ? (\n <div>\n <h3 className=\"mb-4 text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.reviews.allReviews', 'All reviews')}\n </h3>\n <div className=\"grid gap-3\">\n {reviews.map((review) => (\n <ReviewCard key={review.id} review={review} />\n ))}\n </div>\n </div>\n ) : summary && summary.count === 0 ? (\n <div className=\"text-center py-8 rounded-lg border border-border-seam bg-bg-surface/40\">\n <AlertCircle className=\"size-6 text-text-secondary mx-auto mb-2\" aria-hidden=\"true\" />\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.reviews.noReviewsYet', 'Be the first to review')}\n </p>\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/ggnomad/components/StatusBadge.tsx"],"sourcesContent":["import { cn } from '../utils/cn';\n\ntype Tone = 'success' | 'warning' | 'danger' | 'info' | 'neutral';\n\nconst TONE: Record<Tone, { dot: string; text: string; bg: string }> = {\n success: {\n dot: 'bg-status-success-text',\n text: 'text-status-success-text',\n bg: 'bg-status-success-bg-subtle',\n },\n warning: {\n dot: 'bg-status-warning-text',\n text: 'text-status-warning-text',\n bg: 'bg-status-warning-bg-subtle',\n },\n danger: {\n dot: 'bg-status-error-text',\n text: 'text-status-error-text',\n bg: 'bg-status-error-bg-subtle',\n },\n info: {\n dot: 'bg-status-info-text',\n text: 'text-status-info-text',\n bg: 'bg-status-info-bg-subtle',\n },\n neutral: {\n dot: 'bg-text-secondary',\n text: 'text-text-secondary',\n bg: 'bg-bg-sunken',\n },\n};\n\n/** Maps a free-form status string to a semantic tone. */\nexport function toneForStatus(status: string): Tone {\n const s = status.toUpperCase();\n if (['CONFIRMED', 'ACTIVE', 'COMPLETED', 'PAID', 'APPROVED'].includes(s)) {\n return 'success';\n }\n if (['PENDING', 'PROCESSING', 'AWAITING', 'IN_PROGRESS', 'ONBOARDING'].includes(s)) {\n return 'warning';\n }\n if (['CANCELLED', 'FAILED', 'REJECTED', 'SUSPENDED'].includes(s)) {\n return 'danger';\n }\n return 'info';\n}\n\ntype StatusBadgeProps = {\n status: string;\n /** Pulses the dot for live/active states (opacity only, reduced-motion-safe). */\n live?: boolean;\n className?: string;\n};\n\nexport function StatusBadge({ status, live = false, className }: StatusBadgeProps) {\n const tone = TONE[toneForStatus(status)];\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium',\n tone.bg,\n tone.text,\n className\n )}\n >\n <span\n className={cn(\n 'size-1.5 rounded-full',\n tone.dot,\n live ? 'motion-safe:animate-vc-glow-pulse' : ''\n )}\n aria-hidden=\"true\"\n />\n {status}\n </span>\n );\n}\n"],"mappings":";;;AAIA,IAAM,IAAgE;CACpE,SAAS;EACP,KAAK;EACL,MAAM;EACN,IAAI;CACN;CACA,SAAS;EACP,KAAK;EACL,MAAM;EACN,IAAI;CACN;CACA,QAAQ;EACN,KAAK;EACL,MAAM;EACN,IAAI;CACN;CACA,MAAM;EACJ,KAAK;EACL,MAAM;EACN,IAAI;CACN;CACA,SAAS;EACP,KAAK;EACL,MAAM;EACN,IAAI;CACN;AACF;AAGA,SAAgB,EAAc,GAAsB;CAClD,IAAM,IAAI,EAAO,YAAY;CAU7B,OATI;EAAC;EAAa;EAAU;EAAa;EAAQ;CAAU,EAAE,SAAS,CAAC,IAC9D,YAEL;EAAC;EAAW;EAAc;EAAY;EAAe;CAAY,EAAE,SAAS,CAAC,IACxE,YAEL;EAAC;EAAa;EAAU;EAAY;CAAW,EAAE,SAAS,CAAC,IACtD,WAEF;AACT;AASA,SAAgB,EAAY,EAAE,WAAQ,UAAO,IAAO,gBAA+B;CACjF,IAAM,IAAO,EAAK,EAAc,CAAM;CACtC,OACE,kBAAC,QAAD;EACE,WAAW,EACT,mFACA,EAAK,IACL,EAAK,MACL,CACF;YANF,CAQE,kBAAC,QAAD;GACE,WAAW,EACT,yBACA,EAAK,KACL,IAAO,sCAAsC,EAC/C;GACA,eAAY;EACb,CAAA,GACA,CACG;;AAEV"}
1
+ {"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/ggnomad/components/StatusBadge.tsx"],"sourcesContent":["import { cn } from '../utils/cn';\n\ntype Tone = 'success' | 'warning' | 'danger' | 'info' | 'neutral';\n\nconst TONE: Record<Tone, { dot: string; text: string; bg: string }> = {\n success: {\n dot: 'bg-status-success-text',\n text: 'text-status-success-text',\n bg: 'bg-status-success-bg-subtle',\n },\n warning: {\n dot: 'bg-status-warning-text',\n text: 'text-status-warning-text',\n bg: 'bg-status-warning-bg-subtle',\n },\n danger: {\n dot: 'bg-status-error-text',\n text: 'text-status-error-text',\n bg: 'bg-status-error-bg-subtle',\n },\n info: {\n dot: 'bg-status-info-text',\n text: 'text-status-info-text',\n bg: 'bg-status-info-bg-subtle',\n },\n neutral: {\n dot: 'bg-text-secondary',\n text: 'text-text-secondary',\n bg: 'bg-bg-sunken',\n },\n};\n\n/** Maps a free-form status string to a semantic tone. */\nexport function toneForStatus(status: string): Tone {\n const s = status.toUpperCase();\n if (['CONFIRMED', 'ACTIVE', 'COMPLETED', 'PAID', 'APPROVED'].includes(s)) {\n return 'success';\n }\n if (['PENDING', 'PROCESSING', 'AWAITING', 'IN_PROGRESS', 'ONBOARDING'].includes(s)) {\n return 'warning';\n }\n if (['CANCELLED', 'FAILED', 'REJECTED', 'SUSPENDED'].includes(s)) {\n return 'danger';\n }\n return 'info';\n}\n\ntype StatusBadgeProps = {\n status: string;\n /** Pulses the dot for live/active states (opacity only, reduced-motion-safe). */\n live?: boolean;\n className?: string;\n};\n\nexport function StatusBadge({ status, live = false, className }: StatusBadgeProps) {\n const tone = TONE[toneForStatus(status)];\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium',\n tone.bg,\n tone.text,\n className\n )}\n >\n <span\n className={cn(\n 'size-1.5 rounded-full',\n tone.dot,\n live ? 'motion-safe:animate-vc-glow-pulse' : ''\n )}\n aria-hidden=\"true\"\n />\n {status}\n </span>\n );\n}\n"],"mappings":";;;AAIA,IAAM,IAAgE;CACpE,SAAS;EACP,KAAK;EACL,MAAM;EACN,IAAI;CACN;CACA,SAAS;EACP,KAAK;EACL,MAAM;EACN,IAAI;CACN;CACA,QAAQ;EACN,KAAK;EACL,MAAM;EACN,IAAI;CACN;CACA,MAAM;EACJ,KAAK;EACL,MAAM;EACN,IAAI;CACN;CACA,SAAS;EACP,KAAK;EACL,MAAM;EACN,IAAI;CACN;AACF;AAGA,SAAgB,EAAc,GAAsB;CAClD,IAAM,IAAI,EAAO,YAAY;CAU7B,OATI;EAAC;EAAa;EAAU;EAAa;EAAQ;CAAU,CAAC,CAAC,SAAS,CAAC,IAC9D,YAEL;EAAC;EAAW;EAAc;EAAY;EAAe;CAAY,CAAC,CAAC,SAAS,CAAC,IACxE,YAEL;EAAC;EAAa;EAAU;EAAY;CAAW,CAAC,CAAC,SAAS,CAAC,IACtD,WAEF;AACT;AASA,SAAgB,EAAY,EAAE,WAAQ,UAAO,IAAO,gBAA+B;CACjF,IAAM,IAAO,EAAK,EAAc,CAAM;CACtC,OACE,kBAAC,QAAD;EACE,WAAW,EACT,mFACA,EAAK,IACL,EAAK,MACL,CACF;YANF,CAQE,kBAAC,QAAD;GACE,WAAW,EACT,yBACA,EAAK,KACL,IAAO,sCAAsC,EAC/C;GACA,eAAY;EACb,CAAA,GACA,CACG;;AAEV"}
@@ -7,7 +7,7 @@ import { useMutation as s, useQuery as c } from "@apollo/client/react";
7
7
  import { AlertDialog as l, AlertDialogCancel as u, AlertDialogContent as d, AlertDialogDescription as f, AlertDialogFooter as p, AlertDialogHeader as m, AlertDialogTitle as h } from "@burdenoff/fe-libs/ui";
8
8
  import { jsx as g, jsxs as _ } from "react/jsx-runtime";
9
9
  //#region src/ggnomad/components/TicketDialog.tsx
10
- var v = new Set([
10
+ var v = /* @__PURE__ */ new Set([
11
11
  "CONFIRMED",
12
12
  "CHECKED_IN",
13
13
  "COMPLETED"
@@ -1 +1 @@
1
- {"version":3,"file":"TicketDialog.js","names":[],"sources":["../../../src/ggnomad/components/TicketDialog.tsx"],"sourcesContent":["import { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { QrCode, Ticket as TicketIcon } from 'lucide-react';\nimport {\n AlertDialog,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_TICKETS_QUERY, ISSUE_GGNOMAD_TICKET_MUTATION } from '../graphql';\nimport { StatusBadge } from './StatusBadge';\nimport { notify } from '../utils/notify';\n\nexport interface GgnomadTicket {\n id: string;\n bookingId: string;\n ticketCode: string;\n qrCodeDataUrl: string;\n status: 'ISSUED' | 'REDEEMED' | 'REVOKED';\n issuedAt: string;\n redeemedAt: string | null;\n redeemedBy: string | null;\n revokedAt: string | null;\n revokedReason: string | null;\n}\n\n/** Bookings reach CONFIRMED (payment verified) before a ticket can ever exist. */\nconst TICKET_ELIGIBLE_STATUSES = new Set(['CONFIRMED', 'CHECKED_IN', 'COMPLETED']);\n\ninterface TicketDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n bookingId: string;\n referenceCode: string;\n bookingStatus: string;\n}\n\nfunction formatDateTime(value: string | null): string {\n if (!value) return '';\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return value;\n return date.toLocaleString();\n}\n\n/**\n * Traveler-facing mobile ticket view (BOFF-4362). Shows every QR ticket\n * issued for a booking — each ticket's QR code is rendered server-side from\n * its signed, revocable token (see wspace-ggnomad-svc `qrCodeDataUrl`), so\n * what's displayed here always reflects the ticket's live redemption state.\n */\nexport function TicketDialog({\n open,\n onOpenChange,\n bookingId,\n referenceCode,\n bookingStatus,\n}: TicketDialogProps) {\n const { t } = useI18n();\n\n const { data, loading, refetch } = useQuery(\n GGNOMAD_TICKETS_QUERY as TypedDocumentNode<\n { ggnomadTickets: GgnomadTicket[] },\n { bookingId: string }\n >,\n { variables: { bookingId }, skip: !open || !bookingId, fetchPolicy: 'network-only' }\n );\n\n const [issueTicket, { loading: issuing }] = useMutation(\n ISSUE_GGNOMAD_TICKET_MUTATION as TypedDocumentNode<\n { issueGgnomadTicket: GgnomadTicket },\n { bookingId: string }\n >\n );\n\n const tickets = data?.ggnomadTickets ?? [];\n const canIssue = TICKET_ELIGIBLE_STATUSES.has(bookingStatus.toUpperCase());\n\n const handleIssue = async () => {\n try {\n const result = await issueTicket({ variables: { bookingId } });\n if (result.error) {\n notify.error(\n t('ggnomad.ticket.issueErrorTitle', 'Could not issue ticket'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.ticket.issueSuccessTitle', 'Ticket issued'),\n t('ggnomad.ticket.issueSuccessBody', 'A new QR ticket is ready to show at check-in.')\n );\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.ticket.issueErrorTitle', 'Could not issue ticket'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n return (\n <AlertDialog open={open} onOpenChange={onOpenChange}>\n <AlertDialogContent className=\"max-w-md\">\n <AlertDialogHeader>\n <AlertDialogTitle className=\"flex items-center gap-2\">\n <TicketIcon className=\"size-4\" />\n {t('ggnomad.ticket.title', 'Mobile ticket')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t('ggnomad.ticket.description', 'Show this QR code at check-in for {{ref}}.').replace(\n '{{ref}}',\n referenceCode\n )}\n </AlertDialogDescription>\n </AlertDialogHeader>\n\n <div className=\"max-h-[65vh] space-y-4 overflow-y-auto\">\n {loading ? (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n {t('ggnomad.ticket.loading', 'Loading your ticket…')}\n </p>\n ) : tickets.length === 0 ? (\n <div className=\"flex flex-col items-center gap-3 rounded-lg border border-dashed border-border-seam py-8 text-center\">\n <QrCode className=\"size-8 text-text-secondary\" aria-hidden=\"true\" />\n <p className=\"text-sm text-text-secondary\">\n {canIssue\n ? t(\n 'ggnomad.ticket.noneYet',\n 'No QR ticket has been issued for this booking yet.'\n )\n : t(\n 'ggnomad.ticket.notConfirmed',\n 'A QR ticket will be issued automatically once this booking is confirmed.'\n )}\n </p>\n {canIssue ? (\n <button\n type=\"button\"\n onClick={() => void handleIssue()}\n disabled={issuing}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bgHover disabled:opacity-50\"\n >\n {issuing\n ? t('ggnomad.ticket.issuing', 'Issuing…')\n : t('ggnomad.ticket.issueAction', 'Issue QR ticket')}\n </button>\n ) : null}\n </div>\n ) : (\n tickets.map((ticket) => (\n <div\n key={ticket.id}\n className=\"flex flex-col items-center gap-3 rounded-lg border border-border-seam bg-bg-surface/40 p-4\"\n >\n <div className=\"flex w-full items-center justify-between\">\n <span className=\"font-mono text-xs text-text-secondary\">{ticket.ticketCode}</span>\n <StatusBadge status={ticket.status} live={ticket.status === 'ISSUED'} />\n </div>\n\n {ticket.status === 'ISSUED' ? (\n <img\n src={ticket.qrCodeDataUrl}\n alt={t('ggnomad.ticket.qrAlt', 'QR code for ticket {{code}}').replace(\n '{{code}}',\n ticket.ticketCode\n )}\n className=\"size-48 rounded-md border border-border-seam bg-white p-2\"\n width={192}\n height={192}\n />\n ) : (\n <div className=\"flex size-48 items-center justify-center rounded-md border border-dashed border-border-seam bg-bg-sunken text-center text-xs text-text-secondary\">\n {ticket.status === 'REDEEMED'\n ? t('ggnomad.ticket.alreadyRedeemed', 'Already redeemed — QR retired')\n : t('ggnomad.ticket.revokedNotice', 'Revoked — no longer valid')}\n </div>\n )}\n\n <div className=\"w-full space-y-1 text-xs text-text-secondary\">\n <p>\n {t('ggnomad.ticket.issuedAt', 'Issued')} {formatDateTime(ticket.issuedAt)}\n </p>\n {ticket.redeemedAt ? (\n <p>\n {t('ggnomad.ticket.redeemedAt', 'Redeemed')}{' '}\n {formatDateTime(ticket.redeemedAt)}\n </p>\n ) : null}\n {ticket.revokedAt ? (\n <p>\n {t('ggnomad.ticket.revokedAt', 'Revoked')} {formatDateTime(ticket.revokedAt)}\n {ticket.revokedReason ? ` — ${ticket.revokedReason}` : ''}\n </p>\n ) : null}\n </div>\n </div>\n ))\n )}\n </div>\n\n <AlertDialogFooter>\n <AlertDialogCancel>{t('ggnomad.ticket.close', 'Close')}</AlertDialogCancel>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TicketDialog.js","names":[],"sources":["../../../src/ggnomad/components/TicketDialog.tsx"],"sourcesContent":["import { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { QrCode, Ticket as TicketIcon } from 'lucide-react';\nimport {\n AlertDialog,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_TICKETS_QUERY, ISSUE_GGNOMAD_TICKET_MUTATION } from '../graphql';\nimport { StatusBadge } from './StatusBadge';\nimport { notify } from '../utils/notify';\n\nexport interface GgnomadTicket {\n id: string;\n bookingId: string;\n ticketCode: string;\n qrCodeDataUrl: string;\n status: 'ISSUED' | 'REDEEMED' | 'REVOKED';\n issuedAt: string;\n redeemedAt: string | null;\n redeemedBy: string | null;\n revokedAt: string | null;\n revokedReason: string | null;\n}\n\n/** Bookings reach CONFIRMED (payment verified) before a ticket can ever exist. */\nconst TICKET_ELIGIBLE_STATUSES = new Set(['CONFIRMED', 'CHECKED_IN', 'COMPLETED']);\n\ninterface TicketDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n bookingId: string;\n referenceCode: string;\n bookingStatus: string;\n}\n\nfunction formatDateTime(value: string | null): string {\n if (!value) return '';\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return value;\n return date.toLocaleString();\n}\n\n/**\n * Traveler-facing mobile ticket view (BOFF-4362). Shows every QR ticket\n * issued for a booking — each ticket's QR code is rendered server-side from\n * its signed, revocable token (see wspace-ggnomad-svc `qrCodeDataUrl`), so\n * what's displayed here always reflects the ticket's live redemption state.\n */\nexport function TicketDialog({\n open,\n onOpenChange,\n bookingId,\n referenceCode,\n bookingStatus,\n}: TicketDialogProps) {\n const { t } = useI18n();\n\n const { data, loading, refetch } = useQuery(\n GGNOMAD_TICKETS_QUERY as TypedDocumentNode<\n { ggnomadTickets: GgnomadTicket[] },\n { bookingId: string }\n >,\n { variables: { bookingId }, skip: !open || !bookingId, fetchPolicy: 'network-only' }\n );\n\n const [issueTicket, { loading: issuing }] = useMutation(\n ISSUE_GGNOMAD_TICKET_MUTATION as TypedDocumentNode<\n { issueGgnomadTicket: GgnomadTicket },\n { bookingId: string }\n >\n );\n\n const tickets = data?.ggnomadTickets ?? [];\n const canIssue = TICKET_ELIGIBLE_STATUSES.has(bookingStatus.toUpperCase());\n\n const handleIssue = async () => {\n try {\n const result = await issueTicket({ variables: { bookingId } });\n if (result.error) {\n notify.error(\n t('ggnomad.ticket.issueErrorTitle', 'Could not issue ticket'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.ticket.issueSuccessTitle', 'Ticket issued'),\n t('ggnomad.ticket.issueSuccessBody', 'A new QR ticket is ready to show at check-in.')\n );\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.ticket.issueErrorTitle', 'Could not issue ticket'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n return (\n <AlertDialog open={open} onOpenChange={onOpenChange}>\n <AlertDialogContent className=\"max-w-md\">\n <AlertDialogHeader>\n <AlertDialogTitle className=\"flex items-center gap-2\">\n <TicketIcon className=\"size-4\" />\n {t('ggnomad.ticket.title', 'Mobile ticket')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t('ggnomad.ticket.description', 'Show this QR code at check-in for {{ref}}.').replace(\n '{{ref}}',\n referenceCode\n )}\n </AlertDialogDescription>\n </AlertDialogHeader>\n\n <div className=\"max-h-[65vh] space-y-4 overflow-y-auto\">\n {loading ? (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n {t('ggnomad.ticket.loading', 'Loading your ticket…')}\n </p>\n ) : tickets.length === 0 ? (\n <div className=\"flex flex-col items-center gap-3 rounded-lg border border-dashed border-border-seam py-8 text-center\">\n <QrCode className=\"size-8 text-text-secondary\" aria-hidden=\"true\" />\n <p className=\"text-sm text-text-secondary\">\n {canIssue\n ? t(\n 'ggnomad.ticket.noneYet',\n 'No QR ticket has been issued for this booking yet.'\n )\n : t(\n 'ggnomad.ticket.notConfirmed',\n 'A QR ticket will be issued automatically once this booking is confirmed.'\n )}\n </p>\n {canIssue ? (\n <button\n type=\"button\"\n onClick={() => void handleIssue()}\n disabled={issuing}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bgHover disabled:opacity-50\"\n >\n {issuing\n ? t('ggnomad.ticket.issuing', 'Issuing…')\n : t('ggnomad.ticket.issueAction', 'Issue QR ticket')}\n </button>\n ) : null}\n </div>\n ) : (\n tickets.map((ticket) => (\n <div\n key={ticket.id}\n className=\"flex flex-col items-center gap-3 rounded-lg border border-border-seam bg-bg-surface/40 p-4\"\n >\n <div className=\"flex w-full items-center justify-between\">\n <span className=\"font-mono text-xs text-text-secondary\">{ticket.ticketCode}</span>\n <StatusBadge status={ticket.status} live={ticket.status === 'ISSUED'} />\n </div>\n\n {ticket.status === 'ISSUED' ? (\n <img\n src={ticket.qrCodeDataUrl}\n alt={t('ggnomad.ticket.qrAlt', 'QR code for ticket {{code}}').replace(\n '{{code}}',\n ticket.ticketCode\n )}\n className=\"size-48 rounded-md border border-border-seam bg-white p-2\"\n width={192}\n height={192}\n />\n ) : (\n <div className=\"flex size-48 items-center justify-center rounded-md border border-dashed border-border-seam bg-bg-sunken text-center text-xs text-text-secondary\">\n {ticket.status === 'REDEEMED'\n ? t('ggnomad.ticket.alreadyRedeemed', 'Already redeemed — QR retired')\n : t('ggnomad.ticket.revokedNotice', 'Revoked — no longer valid')}\n </div>\n )}\n\n <div className=\"w-full space-y-1 text-xs text-text-secondary\">\n <p>\n {t('ggnomad.ticket.issuedAt', 'Issued')} {formatDateTime(ticket.issuedAt)}\n </p>\n {ticket.redeemedAt ? (\n <p>\n {t('ggnomad.ticket.redeemedAt', 'Redeemed')}{' '}\n {formatDateTime(ticket.redeemedAt)}\n </p>\n ) : null}\n {ticket.revokedAt ? (\n <p>\n {t('ggnomad.ticket.revokedAt', 'Revoked')} {formatDateTime(ticket.revokedAt)}\n {ticket.revokedReason ? ` — ${ticket.revokedReason}` : ''}\n </p>\n ) : null}\n </div>\n </div>\n ))\n )}\n </div>\n\n <AlertDialogFooter>\n <AlertDialogCancel>{t('ggnomad.ticket.close', 'Close')}</AlertDialogCancel>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n"],"mappings":"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"}
@@ -1,25 +1,26 @@
1
- import "./StatCard.js";
2
- import "./StatusBadge.js";
3
- import "./SectionCard.js";
4
- import "./EntityCard.js";
5
- import "./DetailRow.js";
6
- import "./LoadMoreButton.js";
7
- import "./DetailHero.js";
8
- import "./BookingPanel.js";
9
- import "./ReviewsSection.js";
10
- import "./ListingEditDialog.js";
11
- import "./CreateTravelPolicyDialog.js";
12
- import "./CreateProviderDialog.js";
13
- import "./TicketDialog.js";
14
- import "./RedeemTicketPanel.js";
15
- import "./AuditTrailDialog.js";
16
- import "./PhotoGallery.js";
17
- import "./TagChips.js";
18
- import "./FactGrid.js";
19
- import "./RatingSummary.js";
20
- import "./EcoAccessPanel.js";
21
- import "./ProviderCard.js";
22
- import "./ProviderDashboardCard.js";
23
- import "./AvailabilityPanel.js";
24
- import "./ListingFilters.js";
25
- import "./GroupTripPanel.js";
1
+ import { StatCard as e } from "./StatCard.js";
2
+ import { StatusBadge as t } from "./StatusBadge.js";
3
+ import { SectionCard as n } from "./SectionCard.js";
4
+ import { EntityCard as r } from "./EntityCard.js";
5
+ import { DetailRow as i } from "./DetailRow.js";
6
+ import { LoadMoreButton as a } from "./LoadMoreButton.js";
7
+ import { DetailHero as o } from "./DetailHero.js";
8
+ import { BookingPanel as s } from "./BookingPanel.js";
9
+ import { ReviewsSection as c } from "./ReviewsSection.js";
10
+ import { ListingEditDialog as l } from "./ListingEditDialog.js";
11
+ import { CreateTravelPolicyDialog as u } from "./CreateTravelPolicyDialog.js";
12
+ import { CreateProviderDialog as d } from "./CreateProviderDialog.js";
13
+ import { TicketDialog as f } from "./TicketDialog.js";
14
+ import { RedeemTicketPanel as p } from "./RedeemTicketPanel.js";
15
+ import { AuditTrailDialog as m } from "./AuditTrailDialog.js";
16
+ import { PhotoGallery as h } from "./PhotoGallery.js";
17
+ import { TagChips as g } from "./TagChips.js";
18
+ import { FactGrid as _ } from "./FactGrid.js";
19
+ import { RatingSummary as v } from "./RatingSummary.js";
20
+ import { EcoAccessPanel as y } from "./EcoAccessPanel.js";
21
+ import { ProviderCard as b } from "./ProviderCard.js";
22
+ import { ProviderDashboardCard as x } from "./ProviderDashboardCard.js";
23
+ import { AvailabilityPanel as S } from "./AvailabilityPanel.js";
24
+ import { EMPTY_LISTING_FILTERS as C, ListingFilters as w, hasActiveListingFilters as T } from "./ListingFilters.js";
25
+ import { GroupTripPanel as E } from "./GroupTripPanel.js";
26
+ export { m as AuditTrailDialog, S as AvailabilityPanel, s as BookingPanel, d as CreateProviderDialog, u as CreateTravelPolicyDialog, o as DetailHero, i as DetailRow, C as EMPTY_LISTING_FILTERS, y as EcoAccessPanel, r as EntityCard, _ as FactGrid, E as GroupTripPanel, l as ListingEditDialog, w as ListingFilters, a as LoadMoreButton, h as PhotoGallery, b as ProviderCard, x as ProviderDashboardCard, v as RatingSummary, p as RedeemTicketPanel, c as ReviewsSection, n as SectionCard, e as StatCard, t as StatusBadge, g as TagChips, f as TicketDialog, T as hasActiveListingFilters };
@@ -1,2 +1,3 @@
1
- import "./providers/GGNomadProvider.js";
2
- import "./GGNomadRoot.js";
1
+ import { useGGNomad as e } from "./providers/GGNomadProvider.js";
2
+ import { GGNomadRoot as t } from "./GGNomadRoot.js";
3
+ export { t as GGNomadRoot, e as useGGNomad };
@@ -1 +1 @@
1
- {"version":3,"file":"ActivityDetailsPage.js","names":[],"sources":["../../../src/ggnomad/pages/ActivityDetailsPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useNavigate, useParams } from 'react-router';\nimport {\n Activity,\n CheckCircle2,\n Clock,\n Compass,\n Gauge,\n ListChecks,\n MapPin,\n Pencil,\n ShieldCheck,\n Users,\n Zap,\n} from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_ACTIVITY_QUERY } from '../graphql';\nimport { useGGNomad } from '../providers/GGNomadProvider';\nimport { joinPath } from '../utils/navigation';\nimport { humanizeEnum, 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 TagChips,\n} from '../components';\nimport type { Fact } from '../components';\n\ntype ActivityDetail = {\n id: string;\n name: string;\n description?: string | null;\n category: string;\n status?: string;\n location?: unknown;\n duration: string;\n difficulty?: string | null;\n maxParticipants: number;\n currency: string;\n price: number;\n rating?: number | null;\n reviewCount: number;\n includes: string[];\n requirements: string[];\n safetyInfo?: string | null;\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 ActivityDetailsPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const { basePath = '/' } = useGGNomad();\n const { id = '' } = useParams();\n const [editDialogOpen, setEditDialogOpen] = useState(false);\n const { data, refetch } = useQuery(\n GGNOMAD_ACTIVITY_QUERY as TypedDocumentNode<\n { ggnomadActivity: ActivityDetail | null },\n { id: string }\n >,\n { variables: { id }, skip: !id }\n );\n const activity = data?.ggnomadActivity;\n const locationLabel = jsonLabel(activity?.location);\n\n const facts: Fact[] = [];\n if (activity) {\n facts.push({\n id: 'duration',\n label: t('ggnomad.activity.duration', 'Duration'),\n value: activity.duration,\n icon: <Clock />,\n });\n if (activity.difficulty) {\n facts.push({\n id: 'difficulty',\n label: t('ggnomad.activity.difficulty', 'Difficulty'),\n value: humanizeEnum(activity.difficulty),\n icon: <Gauge />,\n });\n }\n facts.push({\n id: 'participants',\n label: t('ggnomad.activity.maxParticipants', 'Max participants'),\n value: String(activity.maxParticipants),\n icon: <Users />,\n });\n if (locationLabel) {\n facts.push({\n id: 'location',\n label: t('ggnomad.activity.location', 'Location'),\n value: locationLabel,\n icon: <MapPin />,\n });\n }\n }\n\n return (\n <PageShell\n title={activity?.name ?? t('ggnomad.activity.fallbackTitle', 'Activity details')}\n subtitle={t('ggnomad.activity.subtitle', 'Safety, inclusions and participation details')}\n >\n {!activity ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.activity.notFoundTitle', 'No activity found')}\n description={t(\n 'ggnomad.activity.notFoundDescription',\n 'This activity is unavailable or may have been removed.'\n )}\n />\n ) : (\n <>\n {activity.images.length > 0 ? (\n <PhotoGallery images={activity.images} alt={activity.name} />\n ) : (\n <DetailHero alt={activity.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(activity.category)}\n </span>\n {activity.status ? <StatusBadge status={activity.status} /> : null}\n <RatingSummary rating={activity.rating} reviewCount={activity.reviewCount} />\n {activity.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={<Activity className=\"size-4\" />}>\n {t(\n 'ggnomad.activity.purpose',\n 'Everything you need to know before joining this activity — its category, duration, group size, price and the safety policy that applies — so you can be confident it fits your group before you book.'\n )}\n </PagePurpose>\n\n <FactGrid facts={facts} />\n\n <NextSteps\n storageKey=\"ggnomad-activity-detail\"\n steps={[\n {\n id: 'browse',\n label: t('ggnomad.activity.next.browse', 'Explore more experiences'),\n description: t(\n 'ggnomad.activity.next.browseDesc',\n 'Return to Tourism to compare tours and activities.'\n ),\n icon: <Compass className=\"size-4\" />,\n onClick: () => navigate(joinPath(basePath, 'tourism')),\n },\n ]}\n />\n\n <div className=\"grid gap-4 lg:grid-cols-3\">\n <div className=\"lg:col-span-2 space-y-4\">\n {activity.description ? (\n <SectionCard title={t('ggnomad.activity.about', 'About this activity')}>\n <p className=\"text-text-primary\">{activity.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.activity.details', 'Details')}\n </h2>\n <button\n onClick={() => setEditDialogOpen(true)}\n className=\"rounded-md p-2 text-text-secondary hover:bg-action-primary-bg hover:text-action-primary-text\"\n aria-label={t('ggnomad.activity.editListing', 'Edit listing')}\n title={t('ggnomad.activity.editListing', 'Edit listing')}\n >\n <Pencil className=\"size-4\" />\n </button>\n </div>\n <dl className=\"space-y-2 text-sm\">\n <DetailRow\n label={t('ggnomad.activity.category', 'Category')}\n value={humanizeEnum(activity.category)}\n />\n <DetailRow\n label={t('ggnomad.activity.duration', 'Duration')}\n value={activity.duration}\n />\n <DetailRow\n label={t('ggnomad.activity.maxParticipants', 'Max participants')}\n value={String(activity.maxParticipants)}\n />\n <DetailRow\n label={t('ggnomad.activity.price', 'Price per participant')}\n value={`${activity.currency} ${Number(activity.price).toFixed(2)}`}\n />\n </dl>\n </GlassCard>\n\n {activity.includes.length > 0 ? (\n <SectionCard title={t('ggnomad.activity.includes', 'What’s included')}>\n <ul className=\"space-y-2 text-sm\">\n {activity.includes.map((item) => (\n <li key={item} className=\"flex items-start gap-2 text-text-primary\">\n <CheckCircle2\n className=\"mt-0.5 size-4 flex-shrink-0 text-status-success-text\"\n aria-hidden=\"true\"\n />\n {item}\n </li>\n ))}\n </ul>\n </SectionCard>\n ) : null}\n\n {activity.requirements.length > 0 ? (\n <SectionCard title={t('ggnomad.activity.requirements', 'What you need to bring')}>\n <TagChips items={activity.requirements} icon={<ListChecks />} />\n </SectionCard>\n ) : null}\n\n <SectionCard title={t('ggnomad.activity.safety', 'Safety information')}>\n <div className=\"flex items-start gap-2 text-sm text-text-primary\">\n <ShieldCheck\n className=\"mt-0.5 size-4 flex-shrink-0 text-status-success-text\"\n aria-hidden=\"true\"\n />\n <p>\n {activity.safetyInfo ??\n t(\n 'ggnomad.activity.defaultSafety',\n 'Standard provider safety policy applies.'\n )}\n </p>\n </div>\n </SectionCard>\n\n <EcoAccessPanel\n sustainabilityScore={activity.sustainabilityScore}\n sustainabilityPractices={activity.sustainabilityPractices}\n accessibilityFeatures={activity.accessibilityFeatures}\n />\n\n <AvailabilityPanel listingId={activity.id ?? id} listingType=\"ACTIVITY\" />\n\n {activity.provider ? <ProviderCard provider={activity.provider} /> : null}\n\n <ReviewsSection targetType=\"ACTIVITY\" targetId={activity.id ?? id} />\n </div>\n <div>\n <BookingPanel\n bookingType=\"ACTIVITY\"\n itemId={activity.id ?? id}\n itemName={activity.name}\n unitPrice={activity.price ?? 0}\n currency={activity.currency ?? 'USD'}\n />\n </div>\n </div>\n\n <ListingEditDialog\n open={editDialogOpen}\n onOpenChange={setEditDialogOpen}\n targetType=\"ACTIVITY\"\n id={activity.id ?? id}\n initial={{\n name: activity.name,\n description: activity.description ?? undefined,\n price: activity.price,\n status: activity.status,\n images: activity.images,\n }}\n onSaved={() => refetch()}\n />\n </>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ActivityDetailsPage.js","names":[],"sources":["../../../src/ggnomad/pages/ActivityDetailsPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useNavigate, useParams } from 'react-router';\nimport {\n Activity,\n CheckCircle2,\n Clock,\n Compass,\n Gauge,\n ListChecks,\n MapPin,\n Pencil,\n ShieldCheck,\n Users,\n Zap,\n} from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_ACTIVITY_QUERY } from '../graphql';\nimport { useGGNomad } from '../providers/GGNomadProvider';\nimport { joinPath } from '../utils/navigation';\nimport { humanizeEnum, 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 TagChips,\n} from '../components';\nimport type { Fact } from '../components';\n\ntype ActivityDetail = {\n id: string;\n name: string;\n description?: string | null;\n category: string;\n status?: string;\n location?: unknown;\n duration: string;\n difficulty?: string | null;\n maxParticipants: number;\n currency: string;\n price: number;\n rating?: number | null;\n reviewCount: number;\n includes: string[];\n requirements: string[];\n safetyInfo?: string | null;\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 ActivityDetailsPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const { basePath = '/' } = useGGNomad();\n const { id = '' } = useParams();\n const [editDialogOpen, setEditDialogOpen] = useState(false);\n const { data, refetch } = useQuery(\n GGNOMAD_ACTIVITY_QUERY as TypedDocumentNode<\n { ggnomadActivity: ActivityDetail | null },\n { id: string }\n >,\n { variables: { id }, skip: !id }\n );\n const activity = data?.ggnomadActivity;\n const locationLabel = jsonLabel(activity?.location);\n\n const facts: Fact[] = [];\n if (activity) {\n facts.push({\n id: 'duration',\n label: t('ggnomad.activity.duration', 'Duration'),\n value: activity.duration,\n icon: <Clock />,\n });\n if (activity.difficulty) {\n facts.push({\n id: 'difficulty',\n label: t('ggnomad.activity.difficulty', 'Difficulty'),\n value: humanizeEnum(activity.difficulty),\n icon: <Gauge />,\n });\n }\n facts.push({\n id: 'participants',\n label: t('ggnomad.activity.maxParticipants', 'Max participants'),\n value: String(activity.maxParticipants),\n icon: <Users />,\n });\n if (locationLabel) {\n facts.push({\n id: 'location',\n label: t('ggnomad.activity.location', 'Location'),\n value: locationLabel,\n icon: <MapPin />,\n });\n }\n }\n\n return (\n <PageShell\n title={activity?.name ?? t('ggnomad.activity.fallbackTitle', 'Activity details')}\n subtitle={t('ggnomad.activity.subtitle', 'Safety, inclusions and participation details')}\n >\n {!activity ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.activity.notFoundTitle', 'No activity found')}\n description={t(\n 'ggnomad.activity.notFoundDescription',\n 'This activity is unavailable or may have been removed.'\n )}\n />\n ) : (\n <>\n {activity.images.length > 0 ? (\n <PhotoGallery images={activity.images} alt={activity.name} />\n ) : (\n <DetailHero alt={activity.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(activity.category)}\n </span>\n {activity.status ? <StatusBadge status={activity.status} /> : null}\n <RatingSummary rating={activity.rating} reviewCount={activity.reviewCount} />\n {activity.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={<Activity className=\"size-4\" />}>\n {t(\n 'ggnomad.activity.purpose',\n 'Everything you need to know before joining this activity — its category, duration, group size, price and the safety policy that applies — so you can be confident it fits your group before you book.'\n )}\n </PagePurpose>\n\n <FactGrid facts={facts} />\n\n <NextSteps\n storageKey=\"ggnomad-activity-detail\"\n steps={[\n {\n id: 'browse',\n label: t('ggnomad.activity.next.browse', 'Explore more experiences'),\n description: t(\n 'ggnomad.activity.next.browseDesc',\n 'Return to Tourism to compare tours and activities.'\n ),\n icon: <Compass className=\"size-4\" />,\n onClick: () => navigate(joinPath(basePath, 'tourism')),\n },\n ]}\n />\n\n <div className=\"grid gap-4 lg:grid-cols-3\">\n <div className=\"lg:col-span-2 space-y-4\">\n {activity.description ? (\n <SectionCard title={t('ggnomad.activity.about', 'About this activity')}>\n <p className=\"text-text-primary\">{activity.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.activity.details', 'Details')}\n </h2>\n <button\n onClick={() => setEditDialogOpen(true)}\n className=\"rounded-md p-2 text-text-secondary hover:bg-action-primary-bg hover:text-action-primary-text\"\n aria-label={t('ggnomad.activity.editListing', 'Edit listing')}\n title={t('ggnomad.activity.editListing', 'Edit listing')}\n >\n <Pencil className=\"size-4\" />\n </button>\n </div>\n <dl className=\"space-y-2 text-sm\">\n <DetailRow\n label={t('ggnomad.activity.category', 'Category')}\n value={humanizeEnum(activity.category)}\n />\n <DetailRow\n label={t('ggnomad.activity.duration', 'Duration')}\n value={activity.duration}\n />\n <DetailRow\n label={t('ggnomad.activity.maxParticipants', 'Max participants')}\n value={String(activity.maxParticipants)}\n />\n <DetailRow\n label={t('ggnomad.activity.price', 'Price per participant')}\n value={`${activity.currency} ${Number(activity.price).toFixed(2)}`}\n />\n </dl>\n </GlassCard>\n\n {activity.includes.length > 0 ? (\n <SectionCard title={t('ggnomad.activity.includes', 'What’s included')}>\n <ul className=\"space-y-2 text-sm\">\n {activity.includes.map((item) => (\n <li key={item} className=\"flex items-start gap-2 text-text-primary\">\n <CheckCircle2\n className=\"mt-0.5 size-4 flex-shrink-0 text-status-success-text\"\n aria-hidden=\"true\"\n />\n {item}\n </li>\n ))}\n </ul>\n </SectionCard>\n ) : null}\n\n {activity.requirements.length > 0 ? (\n <SectionCard title={t('ggnomad.activity.requirements', 'What you need to bring')}>\n <TagChips items={activity.requirements} icon={<ListChecks />} />\n </SectionCard>\n ) : null}\n\n <SectionCard title={t('ggnomad.activity.safety', 'Safety information')}>\n <div className=\"flex items-start gap-2 text-sm text-text-primary\">\n <ShieldCheck\n className=\"mt-0.5 size-4 flex-shrink-0 text-status-success-text\"\n aria-hidden=\"true\"\n />\n <p>\n {activity.safetyInfo ??\n t(\n 'ggnomad.activity.defaultSafety',\n 'Standard provider safety policy applies.'\n )}\n </p>\n </div>\n </SectionCard>\n\n <EcoAccessPanel\n sustainabilityScore={activity.sustainabilityScore}\n sustainabilityPractices={activity.sustainabilityPractices}\n accessibilityFeatures={activity.accessibilityFeatures}\n />\n\n <AvailabilityPanel listingId={activity.id ?? id} listingType=\"ACTIVITY\" />\n\n {activity.provider ? <ProviderCard provider={activity.provider} /> : null}\n\n <ReviewsSection targetType=\"ACTIVITY\" targetId={activity.id ?? id} />\n </div>\n <div>\n <BookingPanel\n bookingType=\"ACTIVITY\"\n itemId={activity.id ?? id}\n itemName={activity.name}\n unitPrice={activity.price ?? 0}\n currency={activity.currency ?? 'USD'}\n />\n </div>\n </div>\n\n <ListingEditDialog\n open={editDialogOpen}\n onOpenChange={setEditDialogOpen}\n targetType=\"ACTIVITY\"\n id={activity.id ?? id}\n initial={{\n name: activity.name,\n description: activity.description ?? undefined,\n price: activity.price,\n status: activity.status,\n images: activity.images,\n }}\n onSaved={() => refetch()}\n />\n </>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../src/ggnomad/pages/AnalyticsPage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { BarChart3, CalendarCheck, DollarSign, MapPin, Percent, Users } from 'lucide-react';\nimport { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_OVERVIEW_QUERY } from '../graphql';\nimport { parseNamedCounts } from '../utils/format';\nimport { PageShell } from './PageShell';\nimport { SectionCard, StatCard } from '../components';\n\nconst CHART_COLORS = [\n 'var(--color-chart-1)',\n 'var(--color-chart-2)',\n 'var(--color-chart-3)',\n 'var(--color-chart-4)',\n 'var(--color-chart-5)',\n];\n\nconst humanizeCategory = (key: string): string =>\n key\n .replace(/[_-]+/g, ' ')\n .trim()\n .replace(/\\b\\w/g, (char) => char.toUpperCase()) || key;\n\nexport function AnalyticsPage() {\n const { t } = useI18n();\n const { data } = useQuery(\n GGNOMAD_OVERVIEW_QUERY as TypedDocumentNode<\n {\n ggnomadAnalytics: {\n totalRevenue: number;\n totalBookings: number;\n totalUsers: number;\n conversionRate: number;\n revenueByCategory: Record<string, unknown>;\n monthlyTrends?: Array<{ month: string; revenue: number }> | Record<string, number>;\n topDestinations?: unknown;\n };\n },\n Record<string, never>\n >\n );\n const analytics = data?.ggnomadAnalytics;\n\n const categories = useMemo(() => {\n const raw = analytics?.revenueByCategory ?? {};\n return Object.entries(raw)\n .map(([key, value]) => ({ label: humanizeCategory(key), value: Number(value) }))\n .filter((entry) => Number.isFinite(entry.value) && entry.value > 0)\n .sort((a, b) => b.value - a.value);\n }, [analytics?.revenueByCategory]);\n\n const maxCategory = categories.length > 0 ? categories[0].value : 0;\n\n const trends = useMemo(() => {\n const raw = analytics?.monthlyTrends;\n if (!raw) return [];\n if (Array.isArray(raw)) {\n return raw\n .map((entry) => ({\n label: entry.month || '',\n value: Number(entry.revenue || 0),\n }))\n .filter((entry) => Number.isFinite(entry.value) && entry.label);\n }\n return Object.entries(raw)\n .map(([key, value]) => ({\n label: key,\n value: Number(value),\n }))\n .filter((entry) => Number.isFinite(entry.value));\n }, [analytics?.monthlyTrends]);\n\n const maxTrend = trends.length > 0 ? Math.max(...trends.map((t) => t.value)) : 0;\n\n const destinations = useMemo(\n () =>\n parseNamedCounts(analytics?.topDestinations)\n .sort((a, b) => b.value - a.value)\n .slice(0, 10),\n [analytics?.topDestinations]\n );\n const maxDestination = destinations.length > 0 ? destinations[0].value : 0;\n\n return (\n <PageShell\n title={t('ggnomad.analytics.title', 'Analytics')}\n titleAccent={t('ggnomad.analytics.title.accent', 'Analytics')}\n subtitle={t('ggnomad.analytics.subtitle', 'GGNomad performance and growth analytics')}\n >\n <PagePurpose icon={<BarChart3 className=\"size-4\" />}>\n {t(\n 'ggnomad.analytics.purpose',\n 'Understand how the marketplace is performing — total revenue, bookings, active users and conversion rate at a glance, with a category breakdown to see which domains (stays, tours, events) are driving growth.'\n )}\n </PagePurpose>\n\n <EmphasisPanel>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.analytics.totalRevenue', 'Total revenue')}\n </p>\n <p className=\"mt-1 text-3xl font-semibold tabular-nums text-text-primary\">\n ${Number(analytics?.totalRevenue ?? 0).toLocaleString()}\n </p>\n </EmphasisPanel>\n\n <section className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n hero\n label={t('ggnomad.analytics.conversion', 'Conversion')}\n value={`${Number(analytics?.conversionRate ?? 0).toFixed(2)}%`}\n icon={<Percent />}\n />\n <StatCard\n label={t('ggnomad.analytics.revenue', 'Revenue')}\n value={`$${Number(analytics?.totalRevenue ?? 0).toLocaleString()}`}\n icon={<DollarSign />}\n />\n <StatCard\n label={t('ggnomad.analytics.bookings', 'Bookings')}\n value={Number(analytics?.totalBookings ?? 0).toLocaleString()}\n icon={<CalendarCheck />}\n />\n <StatCard\n label={t('ggnomad.analytics.users', 'Users')}\n value={Number(analytics?.totalUsers ?? 0).toLocaleString()}\n icon={<Users />}\n />\n </section>\n\n <SectionCard title={t('ggnomad.analytics.revenueByCategory', 'Revenue by category')}>\n {categories.length === 0 ? (\n <div className=\"flex flex-col items-center gap-2 py-8 text-center\">\n <span className=\"flex size-10 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-text-secondary\">\n <BarChart3 className=\"size-5\" />\n </span>\n <p className=\"text-sm font-medium text-text-primary\">\n {t('ggnomad.analytics.empty.title', 'No analytics yet')}\n </p>\n <p className=\"max-w-sm text-sm text-text-secondary\">\n {t(\n 'ggnomad.analytics.empty.description',\n 'Revenue by category appears here as bookings come in across stays, tours and events.'\n )}\n </p>\n </div>\n ) : (\n <ul className=\"flex flex-col gap-4\">\n {categories.map((category, index) => {\n const pct = maxCategory > 0 ? Math.max((category.value / maxCategory) * 100, 2) : 0;\n return (\n <li key={category.label}>\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"truncate text-sm font-medium text-text-primary\">\n {category.label}\n </span>\n <span className=\"flex-shrink-0 text-sm tabular-nums text-text-secondary\">\n ${category.value.toLocaleString()}\n </span>\n </div>\n <div className=\"mt-1.5 h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full transition-[width] duration-500 ease-out\"\n style={{\n width: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n />\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </SectionCard>\n\n {destinations.length > 0 && (\n <SectionCard title={t('ggnomad.analytics.topDestinations', 'Top destinations')}>\n <ul className=\"flex flex-col gap-4\">\n {destinations.map((destination, index) => {\n const pct =\n maxDestination > 0 ? Math.max((destination.value / maxDestination) * 100, 2) : 0;\n return (\n <li key={destination.label}>\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"inline-flex min-w-0 items-center gap-1.5 truncate text-sm font-medium text-text-primary\">\n <MapPin\n className=\"size-3.5 flex-shrink-0 text-text-secondary\"\n aria-hidden=\"true\"\n />\n {destination.label}\n </span>\n <span className=\"flex-shrink-0 text-sm tabular-nums text-text-secondary\">\n {destination.value.toLocaleString()}\n </span>\n </div>\n <div className=\"mt-1.5 h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full transition-[width] duration-500 ease-out\"\n style={{\n width: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n />\n </div>\n </li>\n );\n })}\n </ul>\n </SectionCard>\n )}\n\n {trends.length > 0 && (\n <SectionCard title={t('ggnomad.analytics.revenueTrend', 'Revenue trend')}>\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex items-end gap-2 overflow-x-auto pb-2\">\n {trends.map((trend, index) => {\n const pct = maxTrend > 0 ? Math.max((trend.value / maxTrend) * 100, 5) : 5;\n return (\n <div key={trend.label} className=\"flex flex-col items-center gap-1\">\n <div className=\"flex h-24 items-end\">\n <div\n className=\"w-8 rounded-t transition-all duration-500 ease-out\"\n style={{\n height: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n title={`${trend.label}: $${trend.value.toLocaleString()}`}\n />\n </div>\n <p className=\"truncate text-xs text-text-secondary\">{trend.label}</p>\n </div>\n );\n })}\n </div>\n <p className=\"text-xs text-text-secondary\">\n {t(\n 'ggnomad.analytics.trendNote',\n 'Revenue trends over time showing performance across all domains.'\n )}\n </p>\n </div>\n </SectionCard>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../src/ggnomad/pages/AnalyticsPage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { BarChart3, CalendarCheck, DollarSign, MapPin, Percent, Users } from 'lucide-react';\nimport { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_OVERVIEW_QUERY } from '../graphql';\nimport { parseNamedCounts } from '../utils/format';\nimport { PageShell } from './PageShell';\nimport { SectionCard, StatCard } from '../components';\n\nconst CHART_COLORS = [\n 'var(--color-chart-1)',\n 'var(--color-chart-2)',\n 'var(--color-chart-3)',\n 'var(--color-chart-4)',\n 'var(--color-chart-5)',\n];\n\nconst humanizeCategory = (key: string): string =>\n key\n .replace(/[_-]+/g, ' ')\n .trim()\n .replace(/\\b\\w/g, (char) => char.toUpperCase()) || key;\n\nexport function AnalyticsPage() {\n const { t } = useI18n();\n const { data } = useQuery(\n GGNOMAD_OVERVIEW_QUERY as TypedDocumentNode<\n {\n ggnomadAnalytics: {\n totalRevenue: number;\n totalBookings: number;\n totalUsers: number;\n conversionRate: number;\n revenueByCategory: Record<string, unknown>;\n monthlyTrends?: Array<{ month: string; revenue: number }> | Record<string, number>;\n topDestinations?: unknown;\n };\n },\n Record<string, never>\n >\n );\n const analytics = data?.ggnomadAnalytics;\n\n const categories = useMemo(() => {\n const raw = analytics?.revenueByCategory ?? {};\n return Object.entries(raw)\n .map(([key, value]) => ({ label: humanizeCategory(key), value: Number(value) }))\n .filter((entry) => Number.isFinite(entry.value) && entry.value > 0)\n .sort((a, b) => b.value - a.value);\n }, [analytics?.revenueByCategory]);\n\n const maxCategory = categories.length > 0 ? categories[0].value : 0;\n\n const trends = useMemo(() => {\n const raw = analytics?.monthlyTrends;\n if (!raw) return [];\n if (Array.isArray(raw)) {\n return raw\n .map((entry) => ({\n label: entry.month || '',\n value: Number(entry.revenue || 0),\n }))\n .filter((entry) => Number.isFinite(entry.value) && entry.label);\n }\n return Object.entries(raw)\n .map(([key, value]) => ({\n label: key,\n value: Number(value),\n }))\n .filter((entry) => Number.isFinite(entry.value));\n }, [analytics?.monthlyTrends]);\n\n const maxTrend = trends.length > 0 ? Math.max(...trends.map((t) => t.value)) : 0;\n\n const destinations = useMemo(\n () =>\n parseNamedCounts(analytics?.topDestinations)\n .sort((a, b) => b.value - a.value)\n .slice(0, 10),\n [analytics?.topDestinations]\n );\n const maxDestination = destinations.length > 0 ? destinations[0].value : 0;\n\n return (\n <PageShell\n title={t('ggnomad.analytics.title', 'Analytics')}\n titleAccent={t('ggnomad.analytics.title.accent', 'Analytics')}\n subtitle={t('ggnomad.analytics.subtitle', 'GGNomad performance and growth analytics')}\n >\n <PagePurpose icon={<BarChart3 className=\"size-4\" />}>\n {t(\n 'ggnomad.analytics.purpose',\n 'Understand how the marketplace is performing — total revenue, bookings, active users and conversion rate at a glance, with a category breakdown to see which domains (stays, tours, events) are driving growth.'\n )}\n </PagePurpose>\n\n <EmphasisPanel>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.analytics.totalRevenue', 'Total revenue')}\n </p>\n <p className=\"mt-1 text-3xl font-semibold tabular-nums text-text-primary\">\n ${Number(analytics?.totalRevenue ?? 0).toLocaleString()}\n </p>\n </EmphasisPanel>\n\n <section className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n hero\n label={t('ggnomad.analytics.conversion', 'Conversion')}\n value={`${Number(analytics?.conversionRate ?? 0).toFixed(2)}%`}\n icon={<Percent />}\n />\n <StatCard\n label={t('ggnomad.analytics.revenue', 'Revenue')}\n value={`$${Number(analytics?.totalRevenue ?? 0).toLocaleString()}`}\n icon={<DollarSign />}\n />\n <StatCard\n label={t('ggnomad.analytics.bookings', 'Bookings')}\n value={Number(analytics?.totalBookings ?? 0).toLocaleString()}\n icon={<CalendarCheck />}\n />\n <StatCard\n label={t('ggnomad.analytics.users', 'Users')}\n value={Number(analytics?.totalUsers ?? 0).toLocaleString()}\n icon={<Users />}\n />\n </section>\n\n <SectionCard title={t('ggnomad.analytics.revenueByCategory', 'Revenue by category')}>\n {categories.length === 0 ? (\n <div className=\"flex flex-col items-center gap-2 py-8 text-center\">\n <span className=\"flex size-10 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-text-secondary\">\n <BarChart3 className=\"size-5\" />\n </span>\n <p className=\"text-sm font-medium text-text-primary\">\n {t('ggnomad.analytics.empty.title', 'No analytics yet')}\n </p>\n <p className=\"max-w-sm text-sm text-text-secondary\">\n {t(\n 'ggnomad.analytics.empty.description',\n 'Revenue by category appears here as bookings come in across stays, tours and events.'\n )}\n </p>\n </div>\n ) : (\n <ul className=\"flex flex-col gap-4\">\n {categories.map((category, index) => {\n const pct = maxCategory > 0 ? Math.max((category.value / maxCategory) * 100, 2) : 0;\n return (\n <li key={category.label}>\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"truncate text-sm font-medium text-text-primary\">\n {category.label}\n </span>\n <span className=\"flex-shrink-0 text-sm tabular-nums text-text-secondary\">\n ${category.value.toLocaleString()}\n </span>\n </div>\n <div className=\"mt-1.5 h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full transition-[width] duration-500 ease-out\"\n style={{\n width: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n />\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </SectionCard>\n\n {destinations.length > 0 && (\n <SectionCard title={t('ggnomad.analytics.topDestinations', 'Top destinations')}>\n <ul className=\"flex flex-col gap-4\">\n {destinations.map((destination, index) => {\n const pct =\n maxDestination > 0 ? Math.max((destination.value / maxDestination) * 100, 2) : 0;\n return (\n <li key={destination.label}>\n <div className=\"flex items-baseline justify-between gap-3\">\n <span className=\"inline-flex min-w-0 items-center gap-1.5 truncate text-sm font-medium text-text-primary\">\n <MapPin\n className=\"size-3.5 flex-shrink-0 text-text-secondary\"\n aria-hidden=\"true\"\n />\n {destination.label}\n </span>\n <span className=\"flex-shrink-0 text-sm tabular-nums text-text-secondary\">\n {destination.value.toLocaleString()}\n </span>\n </div>\n <div className=\"mt-1.5 h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full transition-[width] duration-500 ease-out\"\n style={{\n width: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n />\n </div>\n </li>\n );\n })}\n </ul>\n </SectionCard>\n )}\n\n {trends.length > 0 && (\n <SectionCard title={t('ggnomad.analytics.revenueTrend', 'Revenue trend')}>\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex items-end gap-2 overflow-x-auto pb-2\">\n {trends.map((trend, index) => {\n const pct = maxTrend > 0 ? Math.max((trend.value / maxTrend) * 100, 5) : 5;\n return (\n <div key={trend.label} className=\"flex flex-col items-center gap-1\">\n <div className=\"flex h-24 items-end\">\n <div\n className=\"w-8 rounded-t transition-all duration-500 ease-out\"\n style={{\n height: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n title={`${trend.label}: $${trend.value.toLocaleString()}`}\n />\n </div>\n <p className=\"truncate text-xs text-text-secondary\">{trend.label}</p>\n </div>\n );\n })}\n </div>\n <p className=\"text-xs text-text-secondary\">\n {t(\n 'ggnomad.analytics.trendNote',\n 'Revenue trends over time showing performance across all domains.'\n )}\n </p>\n </div>\n </SectionCard>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"BookingsPage.d.ts","sourceRoot":"","sources":["../../../src/ggnomad/pages/BookingsPage.tsx"],"names":[],"mappings":"AA2EA,wBAAgB,YAAY,gCA4e3B"}
1
+ {"version":3,"file":"BookingsPage.d.ts","sourceRoot":"","sources":["../../../src/ggnomad/pages/BookingsPage.tsx"],"names":[],"mappings":"AA4EA,wBAAgB,YAAY,gCAif3B"}