@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":"CreateTravelPolicyDialog.js","names":[],"sources":["../../../src/ggnomad/components/CreateTravelPolicyDialog.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 { ShieldCheck } from 'lucide-react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from '@burdenoff/fe-libs/ui';\n\nimport { CREATE_GGNOMAD_TRAVEL_POLICY_MUTATION } from '../graphql';\nimport { notify } from '../utils/notify';\n\ninterface CreateTravelPolicyDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onCreated?: () => void;\n}\n\ntype CreateGgnomadTravelPolicyInput = {\n name: string;\n costCenter?: string | null;\n currency?: string;\n maxBookingAmount?: number | null;\n maxMonthlyPerTraveler?: number | null;\n blockedDestinations?: string[];\n allowedTravelClasses?: string[];\n requireApprovalAlways?: boolean;\n};\n\nconst fieldClass =\n 'mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder-text-secondary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg disabled:opacity-50';\nconst labelClass = 'block text-sm font-medium text-text-primary';\n\n/**\n * Manager-only form (BOFF-4365) for defining a corporate travel policy:\n * budget caps, blocked destinations, allowed travel classes, and an\n * always-require-approval override. `costCenter` left blank applies the\n * policy workspace-wide; a value scopes it to just that department.\n */\nexport function CreateTravelPolicyDialog({\n open,\n onOpenChange,\n onCreated,\n}: CreateTravelPolicyDialogProps) {\n const { t } = useI18n();\n const [name, setName] = useState('');\n const [costCenter, setCostCenter] = useState('');\n const [currency, setCurrency] = useState('USD');\n const [maxBookingAmount, setMaxBookingAmount] = useState('');\n const [maxMonthlyPerTraveler, setMaxMonthlyPerTraveler] = useState('');\n const [blockedDestinationsText, setBlockedDestinationsText] = useState('');\n const [allowedTravelClassesText, setAllowedTravelClassesText] = useState('');\n const [requireApprovalAlways, setRequireApprovalAlways] = useState(false);\n\n const [createTravelPolicy, { loading }] = useMutation(\n CREATE_GGNOMAD_TRAVEL_POLICY_MUTATION as TypedDocumentNode<\n { createGgnomadTravelPolicy: { id: string; name: string } },\n { input: CreateGgnomadTravelPolicyInput }\n >\n );\n\n const resetForm = () => {\n setName('');\n setCostCenter('');\n setCurrency('USD');\n setMaxBookingAmount('');\n setMaxMonthlyPerTraveler('');\n setBlockedDestinationsText('');\n setAllowedTravelClassesText('');\n setRequireApprovalAlways(false);\n };\n\n const handleCreate = async () => {\n if (!name.trim()) {\n notify.warning(\n t('ggnomad.travelPolicy.create.nameRequiredTitle', 'Name is required'),\n t(\n 'ggnomad.travelPolicy.create.nameRequiredBody',\n 'Give this policy a short, recognizable name.'\n )\n );\n return;\n }\n\n const input: CreateGgnomadTravelPolicyInput = {\n name: name.trim(),\n costCenter: costCenter.trim() || null,\n currency: currency.trim() || 'USD',\n maxBookingAmount: maxBookingAmount.trim() ? Number(maxBookingAmount) : null,\n maxMonthlyPerTraveler: maxMonthlyPerTraveler.trim() ? Number(maxMonthlyPerTraveler) : null,\n blockedDestinations: blockedDestinationsText\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean),\n allowedTravelClasses: allowedTravelClassesText\n .split(',')\n .map((entry) => entry.trim().toUpperCase())\n .filter(Boolean),\n requireApprovalAlways,\n };\n\n try {\n const result = await createTravelPolicy({ variables: { input } });\n if (result.error) {\n notify.error(\n t('ggnomad.travelPolicy.create.errorTitle', 'Could not create policy'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.travelPolicy.create.successTitle', 'Travel policy created'),\n result.data?.createGgnomadTravelPolicy.name ?? name\n );\n resetForm();\n onOpenChange(false);\n onCreated?.();\n } catch (err) {\n notify.error(\n t('ggnomad.travelPolicy.create.errorTitle', 'Could not create policy'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n return (\n <AlertDialog\n open={open}\n onOpenChange={(next) => {\n if (!next) resetForm();\n onOpenChange(next);\n }}\n >\n <AlertDialogContent className=\"max-w-2xl\">\n <AlertDialogHeader>\n <AlertDialogTitle className=\"flex items-center gap-2\">\n <ShieldCheck className=\"size-4\" />\n {t('ggnomad.travelPolicy.create.title', 'New travel policy')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.travelPolicy.create.description',\n 'Bookings that violate an active policy are held for manager approval instead of confirming automatically.'\n )}\n </AlertDialogDescription>\n </AlertDialogHeader>\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"sm:col-span-2\">\n <label htmlFor=\"policy-name\" className={labelClass}>\n {t('ggnomad.travelPolicy.create.nameLabel', 'Policy name')}\n </label>\n <input\n id=\"policy-name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n disabled={loading}\n className={fieldClass}\n placeholder={t(\n 'ggnomad.travelPolicy.create.namePlaceholder',\n 'e.g. Sales Team Travel Policy'\n )}\n />\n </div>\n\n <div>\n <label htmlFor=\"policy-cost-center\" className={labelClass}>\n {t('ggnomad.travelPolicy.create.costCenterLabel', 'Cost center (optional)')}\n </label>\n <input\n id=\"policy-cost-center\"\n type=\"text\"\n value={costCenter}\n onChange={(e) => setCostCenter(e.target.value)}\n disabled={loading}\n className={fieldClass}\n placeholder={t(\n 'ggnomad.travelPolicy.create.costCenterPlaceholder',\n 'Blank = whole workspace'\n )}\n />\n </div>\n\n <div>\n <label htmlFor=\"policy-currency\" className={labelClass}>\n {t('ggnomad.travelPolicy.create.currencyLabel', 'Currency')}\n </label>\n <input\n id=\"policy-currency\"\n type=\"text\"\n value={currency}\n onChange={(e) => setCurrency(e.target.value.toUpperCase())}\n disabled={loading}\n maxLength={10}\n className={fieldClass}\n />\n </div>\n\n <div>\n <label htmlFor=\"policy-max-booking\" className={labelClass}>\n {t('ggnomad.travelPolicy.create.maxBookingLabel', 'Max per booking (optional)')}\n </label>\n <input\n id=\"policy-max-booking\"\n type=\"number\"\n step=\"0.01\"\n min=\"0\"\n value={maxBookingAmount}\n onChange={(e) => setMaxBookingAmount(e.target.value)}\n disabled={loading}\n className={fieldClass}\n placeholder=\"500.00\"\n />\n </div>\n\n <div>\n <label htmlFor=\"policy-max-monthly\" className={labelClass}>\n {t(\n 'ggnomad.travelPolicy.create.maxMonthlyLabel',\n 'Max per traveler / month (optional)'\n )}\n </label>\n <input\n id=\"policy-max-monthly\"\n type=\"number\"\n step=\"0.01\"\n min=\"0\"\n value={maxMonthlyPerTraveler}\n onChange={(e) => setMaxMonthlyPerTraveler(e.target.value)}\n disabled={loading}\n className={fieldClass}\n placeholder=\"2000.00\"\n />\n </div>\n\n <div className=\"sm:col-span-2\">\n <label htmlFor=\"policy-blocked-destinations\" className={labelClass}>\n {t(\n 'ggnomad.travelPolicy.create.blockedDestinationsLabel',\n 'Blocked destinations (comma-separated)'\n )}\n </label>\n <input\n id=\"policy-blocked-destinations\"\n type=\"text\"\n value={blockedDestinationsText}\n onChange={(e) => setBlockedDestinationsText(e.target.value)}\n disabled={loading}\n className={fieldClass}\n placeholder=\"Dubai, Moscow\"\n />\n </div>\n\n <div className=\"sm:col-span-2\">\n <label htmlFor=\"policy-allowed-classes\" className={labelClass}>\n {t(\n 'ggnomad.travelPolicy.create.allowedClassesLabel',\n 'Allowed travel classes (comma-separated, optional)'\n )}\n </label>\n <input\n id=\"policy-allowed-classes\"\n type=\"text\"\n value={allowedTravelClassesText}\n onChange={(e) => setAllowedTravelClassesText(e.target.value)}\n disabled={loading}\n className={fieldClass}\n placeholder=\"ECONOMY, PREMIUM_ECONOMY\"\n />\n </div>\n\n <label className=\"flex items-center gap-2 sm:col-span-2\">\n <input\n type=\"checkbox\"\n checked={requireApprovalAlways}\n onChange={(e) => setRequireApprovalAlways(e.target.checked)}\n disabled={loading}\n className=\"size-4 rounded border-border-seam\"\n />\n <span className=\"text-sm text-text-primary\">\n {t(\n 'ggnomad.travelPolicy.create.requireApprovalAlwaysLabel',\n 'Always require manager approval under this policy'\n )}\n </span>\n </label>\n </div>\n\n <AlertDialogFooter>\n <AlertDialogCancel disabled={loading}>\n {t('ggnomad.travelPolicy.create.cancel', 'Cancel')}\n </AlertDialogCancel>\n <AlertDialogAction\n onClick={(event) => {\n event.preventDefault();\n void handleCreate();\n }}\n disabled={loading}\n className=\"bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover\"\n >\n {loading\n ? t('ggnomad.travelPolicy.create.saving', 'Creating…')\n : t('ggnomad.travelPolicy.create.save', 'Create policy')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CreateTravelPolicyDialog.js","names":[],"sources":["../../../src/ggnomad/components/CreateTravelPolicyDialog.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 { ShieldCheck } from 'lucide-react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from '@burdenoff/fe-libs/ui';\n\nimport { CREATE_GGNOMAD_TRAVEL_POLICY_MUTATION } from '../graphql';\nimport { notify } from '../utils/notify';\n\ninterface CreateTravelPolicyDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onCreated?: () => void;\n}\n\ntype CreateGgnomadTravelPolicyInput = {\n name: string;\n costCenter?: string | null;\n currency?: string;\n maxBookingAmount?: number | null;\n maxMonthlyPerTraveler?: number | null;\n blockedDestinations?: string[];\n allowedTravelClasses?: string[];\n requireApprovalAlways?: boolean;\n};\n\nconst fieldClass =\n 'mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder-text-secondary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg disabled:opacity-50';\nconst labelClass = 'block text-sm font-medium text-text-primary';\n\n/**\n * Manager-only form (BOFF-4365) for defining a corporate travel policy:\n * budget caps, blocked destinations, allowed travel classes, and an\n * always-require-approval override. `costCenter` left blank applies the\n * policy workspace-wide; a value scopes it to just that department.\n */\nexport function CreateTravelPolicyDialog({\n open,\n onOpenChange,\n onCreated,\n}: CreateTravelPolicyDialogProps) {\n const { t } = useI18n();\n const [name, setName] = useState('');\n const [costCenter, setCostCenter] = useState('');\n const [currency, setCurrency] = useState('USD');\n const [maxBookingAmount, setMaxBookingAmount] = useState('');\n const [maxMonthlyPerTraveler, setMaxMonthlyPerTraveler] = useState('');\n const [blockedDestinationsText, setBlockedDestinationsText] = useState('');\n const [allowedTravelClassesText, setAllowedTravelClassesText] = useState('');\n const [requireApprovalAlways, setRequireApprovalAlways] = useState(false);\n\n const [createTravelPolicy, { loading }] = useMutation(\n CREATE_GGNOMAD_TRAVEL_POLICY_MUTATION as TypedDocumentNode<\n { createGgnomadTravelPolicy: { id: string; name: string } },\n { input: CreateGgnomadTravelPolicyInput }\n >\n );\n\n const resetForm = () => {\n setName('');\n setCostCenter('');\n setCurrency('USD');\n setMaxBookingAmount('');\n setMaxMonthlyPerTraveler('');\n setBlockedDestinationsText('');\n setAllowedTravelClassesText('');\n setRequireApprovalAlways(false);\n };\n\n const handleCreate = async () => {\n if (!name.trim()) {\n notify.warning(\n t('ggnomad.travelPolicy.create.nameRequiredTitle', 'Name is required'),\n t(\n 'ggnomad.travelPolicy.create.nameRequiredBody',\n 'Give this policy a short, recognizable name.'\n )\n );\n return;\n }\n\n const input: CreateGgnomadTravelPolicyInput = {\n name: name.trim(),\n costCenter: costCenter.trim() || null,\n currency: currency.trim() || 'USD',\n maxBookingAmount: maxBookingAmount.trim() ? Number(maxBookingAmount) : null,\n maxMonthlyPerTraveler: maxMonthlyPerTraveler.trim() ? Number(maxMonthlyPerTraveler) : null,\n blockedDestinations: blockedDestinationsText\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean),\n allowedTravelClasses: allowedTravelClassesText\n .split(',')\n .map((entry) => entry.trim().toUpperCase())\n .filter(Boolean),\n requireApprovalAlways,\n };\n\n try {\n const result = await createTravelPolicy({ variables: { input } });\n if (result.error) {\n notify.error(\n t('ggnomad.travelPolicy.create.errorTitle', 'Could not create policy'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.travelPolicy.create.successTitle', 'Travel policy created'),\n result.data?.createGgnomadTravelPolicy.name ?? name\n );\n resetForm();\n onOpenChange(false);\n onCreated?.();\n } catch (err) {\n notify.error(\n t('ggnomad.travelPolicy.create.errorTitle', 'Could not create policy'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n return (\n <AlertDialog\n open={open}\n onOpenChange={(next) => {\n if (!next) resetForm();\n onOpenChange(next);\n }}\n >\n <AlertDialogContent className=\"max-w-2xl\">\n <AlertDialogHeader>\n <AlertDialogTitle className=\"flex items-center gap-2\">\n <ShieldCheck className=\"size-4\" />\n {t('ggnomad.travelPolicy.create.title', 'New travel policy')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.travelPolicy.create.description',\n 'Bookings that violate an active policy are held for manager approval instead of confirming automatically.'\n )}\n </AlertDialogDescription>\n </AlertDialogHeader>\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"sm:col-span-2\">\n <label htmlFor=\"policy-name\" className={labelClass}>\n {t('ggnomad.travelPolicy.create.nameLabel', 'Policy name')}\n </label>\n <input\n id=\"policy-name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n disabled={loading}\n className={fieldClass}\n placeholder={t(\n 'ggnomad.travelPolicy.create.namePlaceholder',\n 'e.g. Sales Team Travel Policy'\n )}\n />\n </div>\n\n <div>\n <label htmlFor=\"policy-cost-center\" className={labelClass}>\n {t('ggnomad.travelPolicy.create.costCenterLabel', 'Cost center (optional)')}\n </label>\n <input\n id=\"policy-cost-center\"\n type=\"text\"\n value={costCenter}\n onChange={(e) => setCostCenter(e.target.value)}\n disabled={loading}\n className={fieldClass}\n placeholder={t(\n 'ggnomad.travelPolicy.create.costCenterPlaceholder',\n 'Blank = whole workspace'\n )}\n />\n </div>\n\n <div>\n <label htmlFor=\"policy-currency\" className={labelClass}>\n {t('ggnomad.travelPolicy.create.currencyLabel', 'Currency')}\n </label>\n <input\n id=\"policy-currency\"\n type=\"text\"\n value={currency}\n onChange={(e) => setCurrency(e.target.value.toUpperCase())}\n disabled={loading}\n maxLength={10}\n className={fieldClass}\n />\n </div>\n\n <div>\n <label htmlFor=\"policy-max-booking\" className={labelClass}>\n {t('ggnomad.travelPolicy.create.maxBookingLabel', 'Max per booking (optional)')}\n </label>\n <input\n id=\"policy-max-booking\"\n type=\"number\"\n step=\"0.01\"\n min=\"0\"\n value={maxBookingAmount}\n onChange={(e) => setMaxBookingAmount(e.target.value)}\n disabled={loading}\n className={fieldClass}\n placeholder=\"500.00\"\n />\n </div>\n\n <div>\n <label htmlFor=\"policy-max-monthly\" className={labelClass}>\n {t(\n 'ggnomad.travelPolicy.create.maxMonthlyLabel',\n 'Max per traveler / month (optional)'\n )}\n </label>\n <input\n id=\"policy-max-monthly\"\n type=\"number\"\n step=\"0.01\"\n min=\"0\"\n value={maxMonthlyPerTraveler}\n onChange={(e) => setMaxMonthlyPerTraveler(e.target.value)}\n disabled={loading}\n className={fieldClass}\n placeholder=\"2000.00\"\n />\n </div>\n\n <div className=\"sm:col-span-2\">\n <label htmlFor=\"policy-blocked-destinations\" className={labelClass}>\n {t(\n 'ggnomad.travelPolicy.create.blockedDestinationsLabel',\n 'Blocked destinations (comma-separated)'\n )}\n </label>\n <input\n id=\"policy-blocked-destinations\"\n type=\"text\"\n value={blockedDestinationsText}\n onChange={(e) => setBlockedDestinationsText(e.target.value)}\n disabled={loading}\n className={fieldClass}\n placeholder=\"Dubai, Moscow\"\n />\n </div>\n\n <div className=\"sm:col-span-2\">\n <label htmlFor=\"policy-allowed-classes\" className={labelClass}>\n {t(\n 'ggnomad.travelPolicy.create.allowedClassesLabel',\n 'Allowed travel classes (comma-separated, optional)'\n )}\n </label>\n <input\n id=\"policy-allowed-classes\"\n type=\"text\"\n value={allowedTravelClassesText}\n onChange={(e) => setAllowedTravelClassesText(e.target.value)}\n disabled={loading}\n className={fieldClass}\n placeholder=\"ECONOMY, PREMIUM_ECONOMY\"\n />\n </div>\n\n <label className=\"flex items-center gap-2 sm:col-span-2\">\n <input\n type=\"checkbox\"\n checked={requireApprovalAlways}\n onChange={(e) => setRequireApprovalAlways(e.target.checked)}\n disabled={loading}\n className=\"size-4 rounded border-border-seam\"\n />\n <span className=\"text-sm text-text-primary\">\n {t(\n 'ggnomad.travelPolicy.create.requireApprovalAlwaysLabel',\n 'Always require manager approval under this policy'\n )}\n </span>\n </label>\n </div>\n\n <AlertDialogFooter>\n <AlertDialogCancel disabled={loading}>\n {t('ggnomad.travelPolicy.create.cancel', 'Cancel')}\n </AlertDialogCancel>\n <AlertDialogAction\n onClick={(event) => {\n event.preventDefault();\n void handleCreate();\n }}\n disabled={loading}\n className=\"bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover\"\n >\n {loading\n ? t('ggnomad.travelPolicy.create.saving', 'Creating…')\n : t('ggnomad.travelPolicy.create.save', 'Create policy')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"GroupTripPanel.js","names":[],"sources":["../../../src/ggnomad/components/GroupTripPanel.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 { MinusCircle, ThumbsDown, ThumbsUp, UserPlus, Users, Wallet } from 'lucide-react';\nimport { Button, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\n\nimport {\n ADD_GGNOMAD_GROUP_TRIP_MEMBER_MUTATION,\n CREATE_GGNOMAD_GROUP_TRIP_MUTATION,\n GGNOMAD_GROUP_TRIP_COST_SPLIT_QUERY,\n GGNOMAD_GROUP_TRIP_VOTES_QUERY,\n GGNOMAD_MY_GROUP_TRIPS_QUERY,\n REMOVE_GGNOMAD_GROUP_TRIP_MEMBER_MUTATION,\n VOTE_GGNOMAD_TRIP_PLAN_ITEM_MUTATION,\n} from '../graphql';\nimport { formatMoney } from '../utils/format';\nimport { notify } from '../utils/notify';\nimport { SectionCard } from './SectionCard';\n\ntype SplitMethod = 'EVEN' | 'ITEMIZED';\ntype VoteChoice = 'YES' | 'NO' | 'ABSTAIN';\n\ninterface GroupTripMember {\n id: string;\n actorId: string;\n displayName: string;\n email?: string | null;\n createdAt: string;\n}\n\ninterface GroupTrip {\n id: string;\n name: string;\n tripPlanId: string;\n splitMethod: SplitMethod;\n currency: string;\n createdBy: string;\n createdAt: string;\n members: GroupTripMember[];\n}\n\ninterface CostSplit {\n currency: string;\n totalCost: number;\n splitMethod: SplitMethod;\n shares: Array<{\n memberId: string;\n actorId: string;\n displayName: string;\n amountOwed: number;\n assignedTotal: number;\n sharedTotal: number;\n }>;\n}\n\ninterface VoteTally {\n tripPlanItemId: string;\n yes: number;\n no: number;\n abstain: number;\n votes: Array<{\n id: string;\n actorId: string;\n choice: VoteChoice;\n comment?: string | null;\n createdAt: string;\n }>;\n}\n\nexport interface GroupTripItem {\n id: string;\n itemName: string;\n price: number;\n currency: string;\n}\n\ninterface GroupTripPanelProps {\n tripPlanId: string;\n tripPlanName: string;\n /** The trip plan's own items — the things members vote on. */\n items: GroupTripItem[];\n}\n\nconst INPUT_CLASSES =\n 'mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg';\n\nconst VOTE_CHOICES: VoteChoice[] = ['YES', 'NO', 'ABSTAIN'];\n\n/**\n * BOFF-4374 — turns a trip plan into a shared decision: who is travelling\n * (`GgnomadGroupTrip.members`), what the group has voted for (`ggnomadGroupTripVotes`)\n * and who owes what (`ggnomadGroupTripCostSplit`). Everything shown is a stored group\n * trip record; nothing about settlement or payment status is displayed because the\n * service tracks the split only, not who has actually paid.\n */\nexport function GroupTripPanel({ tripPlanId, tripPlanName, items }: GroupTripPanelProps) {\n const { t } = useI18n();\n const [splitMethod, setSplitMethod] = useState<SplitMethod>('EVEN');\n const [newMember, setNewMember] = useState({ actorId: '', displayName: '', email: '' });\n\n const groupTripsQuery = useQuery(\n GGNOMAD_MY_GROUP_TRIPS_QUERY as TypedDocumentNode<\n { myGgnomadGroupTrips: GroupTrip[] },\n Record<string, never>\n >\n );\n const groupTrip = (groupTripsQuery.data?.myGgnomadGroupTrips ?? []).find(\n (candidate) => candidate.tripPlanId === tripPlanId\n );\n\n const costSplitQuery = useQuery(\n GGNOMAD_GROUP_TRIP_COST_SPLIT_QUERY as TypedDocumentNode<\n { ggnomadGroupTripCostSplit: CostSplit },\n { groupTripId: string }\n >,\n { variables: { groupTripId: groupTrip?.id ?? '' }, skip: !groupTrip }\n );\n\n const votesQuery = useQuery(\n GGNOMAD_GROUP_TRIP_VOTES_QUERY as TypedDocumentNode<\n { ggnomadGroupTripVotes: VoteTally[] },\n { groupTripId: string }\n >,\n { variables: { groupTripId: groupTrip?.id ?? '' }, skip: !groupTrip }\n );\n\n const [createGroupTrip, { loading: creating }] = useMutation(\n CREATE_GGNOMAD_GROUP_TRIP_MUTATION as TypedDocumentNode<\n { createGgnomadGroupTrip: { id: string; name: string } },\n { input: { name: string; tripPlanId: string; splitMethod: SplitMethod } }\n >\n );\n const [addMember, { loading: addingMember }] = useMutation(\n ADD_GGNOMAD_GROUP_TRIP_MEMBER_MUTATION as TypedDocumentNode<\n { addGgnomadGroupTripMember: { id: string; displayName: string } },\n { groupTripId: string; input: { actorId: string; displayName: string; email?: string } }\n >\n );\n const [removeMember, { loading: removingMember }] = useMutation(\n REMOVE_GGNOMAD_GROUP_TRIP_MEMBER_MUTATION as TypedDocumentNode<\n { removeGgnomadGroupTripMember: boolean },\n { groupTripId: string; memberId: string }\n >\n );\n const [voteItem, { loading: voting }] = useMutation(\n VOTE_GGNOMAD_TRIP_PLAN_ITEM_MUTATION as TypedDocumentNode<\n { voteGgnomadTripPlanItem: { id: string; choice: VoteChoice } },\n { groupTripId: string; input: { tripPlanItemId: string; choice: VoteChoice } }\n >\n );\n\n const refreshAll = async () => {\n await Promise.all([\n groupTripsQuery.refetch(),\n groupTrip ? costSplitQuery.refetch() : Promise.resolve(),\n groupTrip ? votesQuery.refetch() : Promise.resolve(),\n ]);\n };\n\n const handleCreate = async () => {\n try {\n const result = await createGroupTrip({\n variables: { input: { name: tripPlanName, tripPlanId, splitMethod } },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.groupTrip.toast.createErrorTitle', 'Could not start the group trip'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.groupTrip.toast.createSuccessTitle', 'Group trip started'),\n tripPlanName\n );\n await groupTripsQuery.refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.groupTrip.toast.createErrorTitle', 'Could not start the group trip'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const handleAddMember = async () => {\n if (!groupTrip) return;\n if (!newMember.actorId.trim() || !newMember.displayName.trim()) {\n notify.warning(\n t('ggnomad.groupTrip.toast.memberMissingTitle', 'Actor ID and name required'),\n t(\n 'ggnomad.groupTrip.toast.memberMissingBody',\n 'A member is identified by their actor ID; the display name is how they appear in the split.'\n )\n );\n return;\n }\n try {\n const result = await addMember({\n variables: {\n groupTripId: groupTrip.id,\n input: {\n actorId: newMember.actorId.trim(),\n displayName: newMember.displayName.trim(),\n email: newMember.email.trim() || undefined,\n },\n },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.groupTrip.toast.memberErrorTitle', 'Could not add that member'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.groupTrip.toast.memberAdded', 'Member added'),\n newMember.displayName.trim()\n );\n setNewMember({ actorId: '', displayName: '', email: '' });\n await refreshAll();\n } catch (err) {\n notify.error(\n t('ggnomad.groupTrip.toast.memberErrorTitle', 'Could not add that member'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const handleRemoveMember = async (member: GroupTripMember) => {\n if (!groupTrip) return;\n try {\n const result = await removeMember({\n variables: { groupTripId: groupTrip.id, memberId: member.id },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.groupTrip.toast.removeErrorTitle', 'Could not remove that member'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.groupTrip.toast.memberRemoved', 'Member removed'),\n member.displayName\n );\n await refreshAll();\n } catch (err) {\n notify.error(\n t('ggnomad.groupTrip.toast.removeErrorTitle', 'Could not remove that member'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const handleVote = async (tripPlanItemId: string, choice: VoteChoice) => {\n if (!groupTrip) return;\n try {\n const result = await voteItem({\n variables: { groupTripId: groupTrip.id, input: { tripPlanItemId, choice } },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.groupTrip.toast.voteErrorTitle', 'Could not record your vote'),\n result.error.message\n );\n return;\n }\n await votesQuery.refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.groupTrip.toast.voteErrorTitle', 'Could not record your vote'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n if (!groupTrip) {\n return (\n <SectionCard title={t('ggnomad.groupTrip.title', 'Group trip')}>\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={t('ggnomad.groupTrip.emptyTitle', 'Travelling with other people?')}\n description={t(\n 'ggnomad.groupTrip.emptyDescription',\n 'Start a group trip on this plan to add travellers, collect votes on each itinerary item and split the cost.'\n )}\n />\n <div className=\"mt-4 grid gap-3 sm:grid-cols-2\">\n <div>\n <label\n htmlFor=\"ggnomad-group-split\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.groupTrip.splitMethod', 'Cost split method')}\n </label>\n <select\n id=\"ggnomad-group-split\"\n value={splitMethod}\n onChange={(event) => setSplitMethod(event.target.value as SplitMethod)}\n className={INPUT_CLASSES}\n >\n <option value=\"EVEN\">{t('ggnomad.groupTrip.split.even', 'Split evenly')}</option>\n <option value=\"ITEMIZED\">\n {t('ggnomad.groupTrip.split.itemized', 'Itemised — per assigned item')}\n </option>\n </select>\n </div>\n <div className=\"flex items-end\">\n <Button onClick={() => void handleCreate()} disabled={creating}>\n <Users className=\"size-4\" aria-hidden=\"true\" />\n {creating\n ? t('ggnomad.groupTrip.starting', 'Starting…')\n : t('ggnomad.groupTrip.start', 'Start group trip')}\n </Button>\n </div>\n </div>\n </SectionCard>\n );\n }\n\n const costSplit = costSplitQuery.data?.ggnomadGroupTripCostSplit;\n const tallies = votesQuery.data?.ggnomadGroupTripVotes ?? [];\n const talliesByItem = new Map(tallies.map((tally) => [tally.tripPlanItemId, tally]));\n\n return (\n <>\n <SectionCard\n title={t('ggnomad.groupTrip.travellers', 'Travellers')}\n action={\n <span className=\"text-sm tabular-nums text-text-secondary\">\n {t('ggnomad.groupTrip.memberCount', '{{n}} in this group').replace(\n '{{n}}',\n String(groupTrip.members.length)\n )}\n </span>\n }\n >\n <ul className=\"space-y-2\">\n {groupTrip.members.map((member) => (\n <li\n key={member.id}\n className=\"flex flex-wrap items-center justify-between gap-3 rounded-lg border border-border-seam bg-bg-surface/40 p-3\"\n >\n <div className=\"min-w-0\">\n <p className=\"truncate font-medium text-text-primary\">\n {member.displayName}\n {member.actorId === groupTrip.createdBy ? (\n <span className=\"ml-2 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary\">\n {t('ggnomad.groupTrip.organizer', 'Organiser')}\n </span>\n ) : null}\n </p>\n {member.email ? (\n <p className=\"truncate text-sm text-text-secondary\">{member.email}</p>\n ) : null}\n </div>\n {member.actorId === groupTrip.createdBy ? null : (\n <button\n type=\"button\"\n disabled={removingMember}\n onClick={() => void handleRemoveMember(member)}\n className=\"inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium text-text-secondary hover:bg-bg-sunken hover:text-status-error-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg disabled:opacity-60\"\n >\n <MinusCircle className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.groupTrip.remove', 'Remove')}\n </button>\n )}\n </li>\n ))}\n </ul>\n\n <div className=\"mt-4 grid gap-3 border-t border-border-seam pt-4 sm:grid-cols-4\">\n <div>\n <label\n htmlFor=\"ggnomad-group-member-name\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.groupTrip.memberName', 'Display name')}\n </label>\n <input\n id=\"ggnomad-group-member-name\"\n type=\"text\"\n value={newMember.displayName}\n onChange={(event) => setNewMember({ ...newMember, displayName: event.target.value })}\n className={INPUT_CLASSES}\n />\n </div>\n <div>\n <label\n htmlFor=\"ggnomad-group-member-actor\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.groupTrip.memberActorId', 'Actor ID')}\n </label>\n <input\n id=\"ggnomad-group-member-actor\"\n type=\"text\"\n value={newMember.actorId}\n onChange={(event) => setNewMember({ ...newMember, actorId: event.target.value })}\n className={INPUT_CLASSES}\n />\n </div>\n <div>\n <label\n htmlFor=\"ggnomad-group-member-email\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.groupTrip.memberEmail', 'Email (optional)')}\n </label>\n <input\n id=\"ggnomad-group-member-email\"\n type=\"email\"\n value={newMember.email}\n onChange={(event) => setNewMember({ ...newMember, email: event.target.value })}\n className={INPUT_CLASSES}\n />\n </div>\n <div className=\"flex items-end\">\n <Button\n variant=\"outline\"\n onClick={() => void handleAddMember()}\n disabled={addingMember}\n >\n <UserPlus className=\"size-4\" aria-hidden=\"true\" />\n {t('ggnomad.groupTrip.addMember', 'Add traveller')}\n </Button>\n </div>\n </div>\n <p className=\"mt-2 text-xs text-text-secondary\">\n {t(\n 'ggnomad.groupTrip.memberHint',\n 'A traveller joins by their platform actor ID — the same identity the gateway authenticates them with.'\n )}\n </p>\n </SectionCard>\n\n <SectionCard\n title={t('ggnomad.groupTrip.costSplit', 'Cost split')}\n action={\n <span className=\"text-sm text-text-secondary\">\n {costSplit?.splitMethod === 'ITEMIZED'\n ? t('ggnomad.groupTrip.split.itemizedShort', 'Itemised')\n : t('ggnomad.groupTrip.split.evenShort', 'Even')}\n </span>\n }\n >\n {costSplitQuery.error ? (\n <p className=\"text-sm text-status-error-text\">{costSplitQuery.error.message}</p>\n ) : !costSplit ? (\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.groupTrip.costSplitLoading', 'Working out the split…')}\n </p>\n ) : (\n <>\n <div className=\"mb-3 flex items-center justify-between gap-3\">\n <span className=\"inline-flex items-center gap-2 text-sm text-text-secondary\">\n <Wallet className=\"size-4\" aria-hidden=\"true\" />\n {t('ggnomad.groupTrip.tripTotal', 'Trip total')}\n </span>\n <span className=\"text-lg font-semibold tabular-nums text-text-primary\">\n {formatMoney(costSplit.totalCost, costSplit.currency)}\n </span>\n </div>\n {costSplit.shares.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.groupTrip.noShares', 'Add travellers to see how the cost divides.')}\n </p>\n ) : (\n <ul className=\"space-y-2\">\n {costSplit.shares.map((share) => (\n <li\n key={share.memberId}\n className=\"flex flex-wrap items-center justify-between gap-3 rounded-lg border border-border-seam bg-bg-surface/40 p-3\"\n >\n <div className=\"min-w-0\">\n <p className=\"truncate font-medium text-text-primary\">{share.displayName}</p>\n {costSplit.splitMethod === 'ITEMIZED' ? (\n <p className=\"text-xs tabular-nums text-text-secondary\">\n {t('ggnomad.groupTrip.shareBreakdown', 'Assigned {{a}} · shared {{s}}')\n .replace('{{a}}', formatMoney(share.assignedTotal, costSplit.currency))\n .replace('{{s}}', formatMoney(share.sharedTotal, costSplit.currency))}\n </p>\n ) : null}\n </div>\n <span className=\"font-medium tabular-nums text-text-primary\">\n {formatMoney(share.amountOwed, costSplit.currency)}\n </span>\n </li>\n ))}\n </ul>\n )}\n <p className=\"mt-3 text-xs text-text-secondary\">\n {t(\n 'ggnomad.groupTrip.costSplitNote',\n 'Each traveller’s share of the plan total. GGNomad tracks the split, not settlement — money movement happens outside the group trip.'\n )}\n </p>\n </>\n )}\n </SectionCard>\n\n <SectionCard title={t('ggnomad.groupTrip.decisions', 'Decisions & votes')}>\n {items.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {t(\n 'ggnomad.groupTrip.noItems',\n 'Add itinerary items first — the group votes on those.'\n )}\n </p>\n ) : (\n <ul className=\"space-y-3\">\n {items.map((item) => {\n const tally = talliesByItem.get(item.id);\n return (\n <li\n key={item.id}\n className=\"flex flex-wrap items-center justify-between gap-4 rounded-lg border border-border-seam bg-bg-surface/40 p-3\"\n >\n <div className=\"min-w-0\">\n <p className=\"truncate font-medium text-text-primary\">{item.itemName}</p>\n <p className=\"text-xs tabular-nums text-text-secondary\">\n {formatMoney(item.price, item.currency)}\n {tally\n ? ` · ${t(\n 'ggnomad.groupTrip.tally',\n '{{yes}} yes · {{no}} no · {{abstain}} abstain'\n )\n .replace('{{yes}}', String(tally.yes))\n .replace('{{no}}', String(tally.no))\n .replace('{{abstain}}', String(tally.abstain))}`\n : ` · ${t('ggnomad.groupTrip.noVotes', 'no votes yet')}`}\n </p>\n </div>\n <div className=\"flex items-center gap-1.5\">\n {VOTE_CHOICES.map((choice) => (\n <button\n key={choice}\n type=\"button\"\n disabled={voting}\n onClick={() => void handleVote(item.id, choice)}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-seam px-2.5 py-1 text-sm text-text-primary transition-colors hover:bg-bg-sunken focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg disabled:opacity-60\"\n >\n {choice === 'YES' ? (\n <ThumbsUp className=\"size-3.5\" aria-hidden=\"true\" />\n ) : choice === 'NO' ? (\n <ThumbsDown className=\"size-3.5\" aria-hidden=\"true\" />\n ) : null}\n {choice === 'YES'\n ? t('ggnomad.groupTrip.vote.yes', 'Yes')\n : choice === 'NO'\n ? t('ggnomad.groupTrip.vote.no', 'No')\n : t('ggnomad.groupTrip.vote.abstain', 'Abstain')}\n </button>\n ))}\n </div>\n </li>\n );\n })}\n </ul>\n )}\n <p className=\"mt-3 text-xs text-text-secondary\">\n {t(\n 'ggnomad.groupTrip.votesNote',\n 'Voting records your own choice; re-voting replaces it. Tallies count every traveller’s current vote.'\n )}\n </p>\n </SectionCard>\n </>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"GroupTripPanel.js","names":[],"sources":["../../../src/ggnomad/components/GroupTripPanel.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 { MinusCircle, ThumbsDown, ThumbsUp, UserPlus, Users, Wallet } from 'lucide-react';\nimport { Button, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\n\nimport {\n ADD_GGNOMAD_GROUP_TRIP_MEMBER_MUTATION,\n CREATE_GGNOMAD_GROUP_TRIP_MUTATION,\n GGNOMAD_GROUP_TRIP_COST_SPLIT_QUERY,\n GGNOMAD_GROUP_TRIP_VOTES_QUERY,\n GGNOMAD_MY_GROUP_TRIPS_QUERY,\n REMOVE_GGNOMAD_GROUP_TRIP_MEMBER_MUTATION,\n VOTE_GGNOMAD_TRIP_PLAN_ITEM_MUTATION,\n} from '../graphql';\nimport { formatMoney } from '../utils/format';\nimport { notify } from '../utils/notify';\nimport { SectionCard } from './SectionCard';\n\ntype SplitMethod = 'EVEN' | 'ITEMIZED';\ntype VoteChoice = 'YES' | 'NO' | 'ABSTAIN';\n\ninterface GroupTripMember {\n id: string;\n actorId: string;\n displayName: string;\n email?: string | null;\n createdAt: string;\n}\n\ninterface GroupTrip {\n id: string;\n name: string;\n tripPlanId: string;\n splitMethod: SplitMethod;\n currency: string;\n createdBy: string;\n createdAt: string;\n members: GroupTripMember[];\n}\n\ninterface CostSplit {\n currency: string;\n totalCost: number;\n splitMethod: SplitMethod;\n shares: Array<{\n memberId: string;\n actorId: string;\n displayName: string;\n amountOwed: number;\n assignedTotal: number;\n sharedTotal: number;\n }>;\n}\n\ninterface VoteTally {\n tripPlanItemId: string;\n yes: number;\n no: number;\n abstain: number;\n votes: Array<{\n id: string;\n actorId: string;\n choice: VoteChoice;\n comment?: string | null;\n createdAt: string;\n }>;\n}\n\nexport interface GroupTripItem {\n id: string;\n itemName: string;\n price: number;\n currency: string;\n}\n\ninterface GroupTripPanelProps {\n tripPlanId: string;\n tripPlanName: string;\n /** The trip plan's own items — the things members vote on. */\n items: GroupTripItem[];\n}\n\nconst INPUT_CLASSES =\n 'mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg';\n\nconst VOTE_CHOICES: VoteChoice[] = ['YES', 'NO', 'ABSTAIN'];\n\n/**\n * BOFF-4374 — turns a trip plan into a shared decision: who is travelling\n * (`GgnomadGroupTrip.members`), what the group has voted for (`ggnomadGroupTripVotes`)\n * and who owes what (`ggnomadGroupTripCostSplit`). Everything shown is a stored group\n * trip record; nothing about settlement or payment status is displayed because the\n * service tracks the split only, not who has actually paid.\n */\nexport function GroupTripPanel({ tripPlanId, tripPlanName, items }: GroupTripPanelProps) {\n const { t } = useI18n();\n const [splitMethod, setSplitMethod] = useState<SplitMethod>('EVEN');\n const [newMember, setNewMember] = useState({ actorId: '', displayName: '', email: '' });\n\n const groupTripsQuery = useQuery(\n GGNOMAD_MY_GROUP_TRIPS_QUERY as TypedDocumentNode<\n { myGgnomadGroupTrips: GroupTrip[] },\n Record<string, never>\n >\n );\n const groupTrip = (groupTripsQuery.data?.myGgnomadGroupTrips ?? []).find(\n (candidate) => candidate.tripPlanId === tripPlanId\n );\n\n const costSplitQuery = useQuery(\n GGNOMAD_GROUP_TRIP_COST_SPLIT_QUERY as TypedDocumentNode<\n { ggnomadGroupTripCostSplit: CostSplit },\n { groupTripId: string }\n >,\n { variables: { groupTripId: groupTrip?.id ?? '' }, skip: !groupTrip }\n );\n\n const votesQuery = useQuery(\n GGNOMAD_GROUP_TRIP_VOTES_QUERY as TypedDocumentNode<\n { ggnomadGroupTripVotes: VoteTally[] },\n { groupTripId: string }\n >,\n { variables: { groupTripId: groupTrip?.id ?? '' }, skip: !groupTrip }\n );\n\n const [createGroupTrip, { loading: creating }] = useMutation(\n CREATE_GGNOMAD_GROUP_TRIP_MUTATION as TypedDocumentNode<\n { createGgnomadGroupTrip: { id: string; name: string } },\n { input: { name: string; tripPlanId: string; splitMethod: SplitMethod } }\n >\n );\n const [addMember, { loading: addingMember }] = useMutation(\n ADD_GGNOMAD_GROUP_TRIP_MEMBER_MUTATION as TypedDocumentNode<\n { addGgnomadGroupTripMember: { id: string; displayName: string } },\n { groupTripId: string; input: { actorId: string; displayName: string; email?: string } }\n >\n );\n const [removeMember, { loading: removingMember }] = useMutation(\n REMOVE_GGNOMAD_GROUP_TRIP_MEMBER_MUTATION as TypedDocumentNode<\n { removeGgnomadGroupTripMember: boolean },\n { groupTripId: string; memberId: string }\n >\n );\n const [voteItem, { loading: voting }] = useMutation(\n VOTE_GGNOMAD_TRIP_PLAN_ITEM_MUTATION as TypedDocumentNode<\n { voteGgnomadTripPlanItem: { id: string; choice: VoteChoice } },\n { groupTripId: string; input: { tripPlanItemId: string; choice: VoteChoice } }\n >\n );\n\n const refreshAll = async () => {\n await Promise.all([\n groupTripsQuery.refetch(),\n groupTrip ? costSplitQuery.refetch() : Promise.resolve(),\n groupTrip ? votesQuery.refetch() : Promise.resolve(),\n ]);\n };\n\n const handleCreate = async () => {\n try {\n const result = await createGroupTrip({\n variables: { input: { name: tripPlanName, tripPlanId, splitMethod } },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.groupTrip.toast.createErrorTitle', 'Could not start the group trip'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.groupTrip.toast.createSuccessTitle', 'Group trip started'),\n tripPlanName\n );\n await groupTripsQuery.refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.groupTrip.toast.createErrorTitle', 'Could not start the group trip'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const handleAddMember = async () => {\n if (!groupTrip) return;\n if (!newMember.actorId.trim() || !newMember.displayName.trim()) {\n notify.warning(\n t('ggnomad.groupTrip.toast.memberMissingTitle', 'Actor ID and name required'),\n t(\n 'ggnomad.groupTrip.toast.memberMissingBody',\n 'A member is identified by their actor ID; the display name is how they appear in the split.'\n )\n );\n return;\n }\n try {\n const result = await addMember({\n variables: {\n groupTripId: groupTrip.id,\n input: {\n actorId: newMember.actorId.trim(),\n displayName: newMember.displayName.trim(),\n email: newMember.email.trim() || undefined,\n },\n },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.groupTrip.toast.memberErrorTitle', 'Could not add that member'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.groupTrip.toast.memberAdded', 'Member added'),\n newMember.displayName.trim()\n );\n setNewMember({ actorId: '', displayName: '', email: '' });\n await refreshAll();\n } catch (err) {\n notify.error(\n t('ggnomad.groupTrip.toast.memberErrorTitle', 'Could not add that member'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const handleRemoveMember = async (member: GroupTripMember) => {\n if (!groupTrip) return;\n try {\n const result = await removeMember({\n variables: { groupTripId: groupTrip.id, memberId: member.id },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.groupTrip.toast.removeErrorTitle', 'Could not remove that member'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.groupTrip.toast.memberRemoved', 'Member removed'),\n member.displayName\n );\n await refreshAll();\n } catch (err) {\n notify.error(\n t('ggnomad.groupTrip.toast.removeErrorTitle', 'Could not remove that member'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const handleVote = async (tripPlanItemId: string, choice: VoteChoice) => {\n if (!groupTrip) return;\n try {\n const result = await voteItem({\n variables: { groupTripId: groupTrip.id, input: { tripPlanItemId, choice } },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.groupTrip.toast.voteErrorTitle', 'Could not record your vote'),\n result.error.message\n );\n return;\n }\n await votesQuery.refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.groupTrip.toast.voteErrorTitle', 'Could not record your vote'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n if (!groupTrip) {\n return (\n <SectionCard title={t('ggnomad.groupTrip.title', 'Group trip')}>\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={t('ggnomad.groupTrip.emptyTitle', 'Travelling with other people?')}\n description={t(\n 'ggnomad.groupTrip.emptyDescription',\n 'Start a group trip on this plan to add travellers, collect votes on each itinerary item and split the cost.'\n )}\n />\n <div className=\"mt-4 grid gap-3 sm:grid-cols-2\">\n <div>\n <label\n htmlFor=\"ggnomad-group-split\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.groupTrip.splitMethod', 'Cost split method')}\n </label>\n <select\n id=\"ggnomad-group-split\"\n value={splitMethod}\n onChange={(event) => setSplitMethod(event.target.value as SplitMethod)}\n className={INPUT_CLASSES}\n >\n <option value=\"EVEN\">{t('ggnomad.groupTrip.split.even', 'Split evenly')}</option>\n <option value=\"ITEMIZED\">\n {t('ggnomad.groupTrip.split.itemized', 'Itemised — per assigned item')}\n </option>\n </select>\n </div>\n <div className=\"flex items-end\">\n <Button onClick={() => void handleCreate()} disabled={creating}>\n <Users className=\"size-4\" aria-hidden=\"true\" />\n {creating\n ? t('ggnomad.groupTrip.starting', 'Starting…')\n : t('ggnomad.groupTrip.start', 'Start group trip')}\n </Button>\n </div>\n </div>\n </SectionCard>\n );\n }\n\n const costSplit = costSplitQuery.data?.ggnomadGroupTripCostSplit;\n const tallies = votesQuery.data?.ggnomadGroupTripVotes ?? [];\n const talliesByItem = new Map(tallies.map((tally) => [tally.tripPlanItemId, tally]));\n\n return (\n <>\n <SectionCard\n title={t('ggnomad.groupTrip.travellers', 'Travellers')}\n action={\n <span className=\"text-sm tabular-nums text-text-secondary\">\n {t('ggnomad.groupTrip.memberCount', '{{n}} in this group').replace(\n '{{n}}',\n String(groupTrip.members.length)\n )}\n </span>\n }\n >\n <ul className=\"space-y-2\">\n {groupTrip.members.map((member) => (\n <li\n key={member.id}\n className=\"flex flex-wrap items-center justify-between gap-3 rounded-lg border border-border-seam bg-bg-surface/40 p-3\"\n >\n <div className=\"min-w-0\">\n <p className=\"truncate font-medium text-text-primary\">\n {member.displayName}\n {member.actorId === groupTrip.createdBy ? (\n <span className=\"ml-2 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary\">\n {t('ggnomad.groupTrip.organizer', 'Organiser')}\n </span>\n ) : null}\n </p>\n {member.email ? (\n <p className=\"truncate text-sm text-text-secondary\">{member.email}</p>\n ) : null}\n </div>\n {member.actorId === groupTrip.createdBy ? null : (\n <button\n type=\"button\"\n disabled={removingMember}\n onClick={() => void handleRemoveMember(member)}\n className=\"inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium text-text-secondary hover:bg-bg-sunken hover:text-status-error-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg disabled:opacity-60\"\n >\n <MinusCircle className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.groupTrip.remove', 'Remove')}\n </button>\n )}\n </li>\n ))}\n </ul>\n\n <div className=\"mt-4 grid gap-3 border-t border-border-seam pt-4 sm:grid-cols-4\">\n <div>\n <label\n htmlFor=\"ggnomad-group-member-name\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.groupTrip.memberName', 'Display name')}\n </label>\n <input\n id=\"ggnomad-group-member-name\"\n type=\"text\"\n value={newMember.displayName}\n onChange={(event) => setNewMember({ ...newMember, displayName: event.target.value })}\n className={INPUT_CLASSES}\n />\n </div>\n <div>\n <label\n htmlFor=\"ggnomad-group-member-actor\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.groupTrip.memberActorId', 'Actor ID')}\n </label>\n <input\n id=\"ggnomad-group-member-actor\"\n type=\"text\"\n value={newMember.actorId}\n onChange={(event) => setNewMember({ ...newMember, actorId: event.target.value })}\n className={INPUT_CLASSES}\n />\n </div>\n <div>\n <label\n htmlFor=\"ggnomad-group-member-email\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.groupTrip.memberEmail', 'Email (optional)')}\n </label>\n <input\n id=\"ggnomad-group-member-email\"\n type=\"email\"\n value={newMember.email}\n onChange={(event) => setNewMember({ ...newMember, email: event.target.value })}\n className={INPUT_CLASSES}\n />\n </div>\n <div className=\"flex items-end\">\n <Button\n variant=\"outline\"\n onClick={() => void handleAddMember()}\n disabled={addingMember}\n >\n <UserPlus className=\"size-4\" aria-hidden=\"true\" />\n {t('ggnomad.groupTrip.addMember', 'Add traveller')}\n </Button>\n </div>\n </div>\n <p className=\"mt-2 text-xs text-text-secondary\">\n {t(\n 'ggnomad.groupTrip.memberHint',\n 'A traveller joins by their platform actor ID — the same identity the gateway authenticates them with.'\n )}\n </p>\n </SectionCard>\n\n <SectionCard\n title={t('ggnomad.groupTrip.costSplit', 'Cost split')}\n action={\n <span className=\"text-sm text-text-secondary\">\n {costSplit?.splitMethod === 'ITEMIZED'\n ? t('ggnomad.groupTrip.split.itemizedShort', 'Itemised')\n : t('ggnomad.groupTrip.split.evenShort', 'Even')}\n </span>\n }\n >\n {costSplitQuery.error ? (\n <p className=\"text-sm text-status-error-text\">{costSplitQuery.error.message}</p>\n ) : !costSplit ? (\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.groupTrip.costSplitLoading', 'Working out the split…')}\n </p>\n ) : (\n <>\n <div className=\"mb-3 flex items-center justify-between gap-3\">\n <span className=\"inline-flex items-center gap-2 text-sm text-text-secondary\">\n <Wallet className=\"size-4\" aria-hidden=\"true\" />\n {t('ggnomad.groupTrip.tripTotal', 'Trip total')}\n </span>\n <span className=\"text-lg font-semibold tabular-nums text-text-primary\">\n {formatMoney(costSplit.totalCost, costSplit.currency)}\n </span>\n </div>\n {costSplit.shares.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.groupTrip.noShares', 'Add travellers to see how the cost divides.')}\n </p>\n ) : (\n <ul className=\"space-y-2\">\n {costSplit.shares.map((share) => (\n <li\n key={share.memberId}\n className=\"flex flex-wrap items-center justify-between gap-3 rounded-lg border border-border-seam bg-bg-surface/40 p-3\"\n >\n <div className=\"min-w-0\">\n <p className=\"truncate font-medium text-text-primary\">{share.displayName}</p>\n {costSplit.splitMethod === 'ITEMIZED' ? (\n <p className=\"text-xs tabular-nums text-text-secondary\">\n {t('ggnomad.groupTrip.shareBreakdown', 'Assigned {{a}} · shared {{s}}')\n .replace('{{a}}', formatMoney(share.assignedTotal, costSplit.currency))\n .replace('{{s}}', formatMoney(share.sharedTotal, costSplit.currency))}\n </p>\n ) : null}\n </div>\n <span className=\"font-medium tabular-nums text-text-primary\">\n {formatMoney(share.amountOwed, costSplit.currency)}\n </span>\n </li>\n ))}\n </ul>\n )}\n <p className=\"mt-3 text-xs text-text-secondary\">\n {t(\n 'ggnomad.groupTrip.costSplitNote',\n 'Each traveller’s share of the plan total. GGNomad tracks the split, not settlement — money movement happens outside the group trip.'\n )}\n </p>\n </>\n )}\n </SectionCard>\n\n <SectionCard title={t('ggnomad.groupTrip.decisions', 'Decisions & votes')}>\n {items.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {t(\n 'ggnomad.groupTrip.noItems',\n 'Add itinerary items first — the group votes on those.'\n )}\n </p>\n ) : (\n <ul className=\"space-y-3\">\n {items.map((item) => {\n const tally = talliesByItem.get(item.id);\n return (\n <li\n key={item.id}\n className=\"flex flex-wrap items-center justify-between gap-4 rounded-lg border border-border-seam bg-bg-surface/40 p-3\"\n >\n <div className=\"min-w-0\">\n <p className=\"truncate font-medium text-text-primary\">{item.itemName}</p>\n <p className=\"text-xs tabular-nums text-text-secondary\">\n {formatMoney(item.price, item.currency)}\n {tally\n ? ` · ${t(\n 'ggnomad.groupTrip.tally',\n '{{yes}} yes · {{no}} no · {{abstain}} abstain'\n )\n .replace('{{yes}}', String(tally.yes))\n .replace('{{no}}', String(tally.no))\n .replace('{{abstain}}', String(tally.abstain))}`\n : ` · ${t('ggnomad.groupTrip.noVotes', 'no votes yet')}`}\n </p>\n </div>\n <div className=\"flex items-center gap-1.5\">\n {VOTE_CHOICES.map((choice) => (\n <button\n key={choice}\n type=\"button\"\n disabled={voting}\n onClick={() => void handleVote(item.id, choice)}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-seam px-2.5 py-1 text-sm text-text-primary transition-colors hover:bg-bg-sunken focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg disabled:opacity-60\"\n >\n {choice === 'YES' ? (\n <ThumbsUp className=\"size-3.5\" aria-hidden=\"true\" />\n ) : choice === 'NO' ? (\n <ThumbsDown className=\"size-3.5\" aria-hidden=\"true\" />\n ) : null}\n {choice === 'YES'\n ? t('ggnomad.groupTrip.vote.yes', 'Yes')\n : choice === 'NO'\n ? t('ggnomad.groupTrip.vote.no', 'No')\n : t('ggnomad.groupTrip.vote.abstain', 'Abstain')}\n </button>\n ))}\n </div>\n </li>\n );\n })}\n </ul>\n )}\n <p className=\"mt-3 text-xs text-text-secondary\">\n {t(\n 'ggnomad.groupTrip.votesNote',\n 'Voting records your own choice; re-voting replaces it. Tallies count every traveller’s current vote.'\n )}\n </p>\n </SectionCard>\n </>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ListingEditDialog.js","names":[],"sources":["../../../src/ggnomad/components/ListingEditDialog.tsx"],"sourcesContent":["import { useState, useMemo } 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 { Pencil } from 'lucide-react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n UPDATE_GGNOMAD_PROPERTY_MUTATION,\n UPDATE_GGNOMAD_TOUR_MUTATION,\n UPDATE_GGNOMAD_EVENT_MUTATION,\n UPDATE_GGNOMAD_ACTIVITY_MUTATION,\n} from '../graphql';\nimport { notify } from '../utils/notify';\n\ntype TargetType = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ninterface ListingEditDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n targetType: TargetType;\n id: string;\n initial: {\n name?: string;\n description?: string;\n price?: number;\n pricePerNight?: number;\n status?: string;\n images?: string[];\n };\n onSaved?: () => void;\n}\n\n/** Edit dialog for listing details (property, tour, event, activity). */\nexport function ListingEditDialog({\n open,\n onOpenChange,\n targetType,\n id,\n initial,\n onSaved,\n}: ListingEditDialogProps) {\n const { t } = useI18n();\n const [name, setName] = useState(initial.name ?? '');\n const [description, setDescription] = useState(initial.description ?? '');\n const priceField = initial.pricePerNight ?? initial.price ?? 0;\n const [price, setPrice] = useState(String(priceField));\n const [status, setStatus] = useState(initial.status ?? 'ACTIVE');\n const [imagesText, setImagesText] = useState((initial.images ?? []).join('\\n'));\n\n const [updateProperty, { loading: updatingProperty }] = useMutation(\n UPDATE_GGNOMAD_PROPERTY_MUTATION as TypedDocumentNode<\n { updateGgnomadProperty: { id: string; name: string } },\n {\n id: string;\n input: {\n name?: string;\n description?: string;\n pricePerNight?: number;\n status?: string;\n images?: string[];\n };\n }\n >\n );\n\n const [updateTour, { loading: updatingTour }] = useMutation(\n UPDATE_GGNOMAD_TOUR_MUTATION as TypedDocumentNode<\n { updateGgnomadTour: { id: string; name: string } },\n {\n id: string;\n input: {\n name?: string;\n description?: string;\n price?: number;\n status?: string;\n images?: string[];\n };\n }\n >\n );\n\n const [updateEvent, { loading: updatingEvent }] = useMutation(\n UPDATE_GGNOMAD_EVENT_MUTATION as TypedDocumentNode<\n { updateGgnomadEvent: { id: string; name: string } },\n {\n id: string;\n input: {\n name?: string;\n description?: string;\n price?: number;\n status?: string;\n images?: string[];\n };\n }\n >\n );\n\n const [updateActivity, { loading: updatingActivity }] = useMutation(\n UPDATE_GGNOMAD_ACTIVITY_MUTATION as TypedDocumentNode<\n { updateGgnomadActivity: { id: string; name: string } },\n {\n id: string;\n input: {\n name?: string;\n description?: string;\n price?: number;\n status?: string;\n images?: string[];\n };\n }\n >\n );\n\n const loading = useMemo(\n () => updatingProperty || updatingTour || updatingEvent || updatingActivity,\n [updatingProperty, updatingTour, updatingEvent, updatingActivity]\n );\n\n const handleSave = async () => {\n try {\n // Build input with only changed fields\n const input: Record<string, unknown> = {};\n\n if (name !== (initial.name ?? '')) {\n input.name = name;\n }\n if (description !== (initial.description ?? '')) {\n input.description = description;\n }\n\n const priceValue = parseFloat(price);\n const initialPrice = initial.pricePerNight ?? initial.price ?? 0;\n if (priceValue !== initialPrice) {\n if (targetType === 'PROPERTY') {\n input.pricePerNight = priceValue;\n } else {\n input.price = priceValue;\n }\n }\n\n if (status !== (initial.status ?? 'ACTIVE')) {\n input.status = status;\n }\n\n const newImages = imagesText\n .split('\\n')\n .map((line) => line.trim())\n .filter(Boolean);\n const initialImages = initial.images ?? [];\n if (JSON.stringify(newImages) !== JSON.stringify(initialImages)) {\n input.images = newImages;\n }\n\n // If nothing changed, just close\n if (Object.keys(input).length === 0) {\n notify.info(\n t('ggnomad.listingEdit.noChangesTitle', 'No changes'),\n t('ggnomad.listingEdit.noChangesBody', 'Nothing was modified.')\n );\n onOpenChange(false);\n return;\n }\n\n let result;\n if (targetType === 'PROPERTY') {\n result = await updateProperty({\n variables: { id, input },\n });\n } else if (targetType === 'TOUR') {\n result = await updateTour({\n variables: { id, input },\n });\n } else if (targetType === 'EVENT') {\n result = await updateEvent({\n variables: { id, input },\n });\n } else {\n result = await updateActivity({\n variables: { id, input },\n });\n }\n\n if (result?.error) {\n notify.error(\n t('ggnomad.listingEdit.errorTitle', 'Could not update listing'),\n result.error.message\n );\n return;\n }\n\n notify.success(\n t('ggnomad.listingEdit.successTitle', 'Listing updated'),\n t('ggnomad.listingEdit.successBody', 'Your changes have been saved.')\n );\n\n onOpenChange(false);\n onSaved?.();\n } catch (err) {\n notify.error(\n t('ggnomad.listingEdit.errorTitle', 'Could not update listing'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const priceLabel = targetType === 'PROPERTY' ? 'Price per night' : 'Price';\n\n return (\n <AlertDialog open={open} onOpenChange={onOpenChange}>\n <AlertDialogContent className=\"max-w-2xl\">\n <AlertDialogHeader>\n <AlertDialogTitle className=\"flex items-center gap-2\">\n <Pencil className=\"size-4\" />\n {t('ggnomad.listingEdit.title', 'Edit listing')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t('ggnomad.listingEdit.description', 'Update the details for this listing.')}\n </AlertDialogDescription>\n </AlertDialogHeader>\n\n <div className=\"space-y-4\">\n {/* Name */}\n <div>\n <label\n htmlFor=\"listing-edit-name\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.listingEdit.nameLabel', 'Name')}\n </label>\n <input\n id=\"listing-edit-name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n disabled={loading}\n className=\"mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder-text-secondary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg disabled:opacity-50\"\n placeholder={t('ggnomad.listingEdit.namePlaceholder', 'Enter name')}\n />\n </div>\n\n {/* Description */}\n <div>\n <label\n htmlFor=\"listing-edit-description\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.listingEdit.descriptionLabel', 'Description')}\n </label>\n <textarea\n id=\"listing-edit-description\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n disabled={loading}\n rows={3}\n className=\"mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder-text-secondary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg disabled:opacity-50\"\n placeholder={t('ggnomad.listingEdit.descriptionPlaceholder', 'Enter description')}\n />\n </div>\n\n {/* Price */}\n <div>\n <label\n htmlFor=\"listing-edit-price\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t(`ggnomad.listingEdit.priceLabel`, priceLabel)}\n </label>\n <input\n id=\"listing-edit-price\"\n type=\"number\"\n step=\"0.01\"\n min=\"0\"\n value={price}\n onChange={(e) => setPrice(e.target.value)}\n disabled={loading}\n className=\"mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder-text-secondary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg disabled:opacity-50\"\n placeholder=\"0.00\"\n />\n </div>\n\n {/* Status */}\n <div>\n <label\n htmlFor=\"listing-edit-status\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.listingEdit.statusLabel', 'Status')}\n </label>\n <select\n id=\"listing-edit-status\"\n value={status}\n onChange={(e) => setStatus(e.target.value)}\n disabled={loading}\n className=\"mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg disabled:opacity-50\"\n >\n <option value=\"ACTIVE\">{t('ggnomad.listingEdit.statusActive', 'Active')}</option>\n <option value=\"PAUSED\">{t('ggnomad.listingEdit.statusPaused', 'Paused')}</option>\n <option value=\"DRAFT\">{t('ggnomad.listingEdit.statusDraft', 'Draft')}</option>\n </select>\n </div>\n\n {/* Images */}\n <div>\n <label\n htmlFor=\"listing-edit-images\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.listingEdit.imagesLabel', 'Images (one URL per line)')}\n </label>\n <textarea\n id=\"listing-edit-images\"\n value={imagesText}\n onChange={(e) => setImagesText(e.target.value)}\n disabled={loading}\n rows={3}\n className=\"mt-1 w-full 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 placeholder={t(\n 'ggnomad.listingEdit.imagesPlaceholder',\n 'https://example.com/image1.jpg\\nhttps://example.com/image2.jpg'\n )}\n />\n </div>\n </div>\n\n <AlertDialogFooter>\n <AlertDialogCancel disabled={loading}>\n {t('ggnomad.listingEdit.cancel', 'Cancel')}\n </AlertDialogCancel>\n <AlertDialogAction\n onClick={(event) => {\n event.preventDefault();\n void handleSave();\n }}\n disabled={loading}\n className=\"bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover\"\n >\n {loading\n ? t('ggnomad.listingEdit.saving', 'Saving…')\n : t('ggnomad.listingEdit.save', 'Save changes')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ListingEditDialog.js","names":[],"sources":["../../../src/ggnomad/components/ListingEditDialog.tsx"],"sourcesContent":["import { useState, useMemo } 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 { Pencil } from 'lucide-react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n UPDATE_GGNOMAD_PROPERTY_MUTATION,\n UPDATE_GGNOMAD_TOUR_MUTATION,\n UPDATE_GGNOMAD_EVENT_MUTATION,\n UPDATE_GGNOMAD_ACTIVITY_MUTATION,\n} from '../graphql';\nimport { notify } from '../utils/notify';\n\ntype TargetType = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ninterface ListingEditDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n targetType: TargetType;\n id: string;\n initial: {\n name?: string;\n description?: string;\n price?: number;\n pricePerNight?: number;\n status?: string;\n images?: string[];\n };\n onSaved?: () => void;\n}\n\n/** Edit dialog for listing details (property, tour, event, activity). */\nexport function ListingEditDialog({\n open,\n onOpenChange,\n targetType,\n id,\n initial,\n onSaved,\n}: ListingEditDialogProps) {\n const { t } = useI18n();\n const [name, setName] = useState(initial.name ?? '');\n const [description, setDescription] = useState(initial.description ?? '');\n const priceField = initial.pricePerNight ?? initial.price ?? 0;\n const [price, setPrice] = useState(String(priceField));\n const [status, setStatus] = useState(initial.status ?? 'ACTIVE');\n const [imagesText, setImagesText] = useState((initial.images ?? []).join('\\n'));\n\n const [updateProperty, { loading: updatingProperty }] = useMutation(\n UPDATE_GGNOMAD_PROPERTY_MUTATION as TypedDocumentNode<\n { updateGgnomadProperty: { id: string; name: string } },\n {\n id: string;\n input: {\n name?: string;\n description?: string;\n pricePerNight?: number;\n status?: string;\n images?: string[];\n };\n }\n >\n );\n\n const [updateTour, { loading: updatingTour }] = useMutation(\n UPDATE_GGNOMAD_TOUR_MUTATION as TypedDocumentNode<\n { updateGgnomadTour: { id: string; name: string } },\n {\n id: string;\n input: {\n name?: string;\n description?: string;\n price?: number;\n status?: string;\n images?: string[];\n };\n }\n >\n );\n\n const [updateEvent, { loading: updatingEvent }] = useMutation(\n UPDATE_GGNOMAD_EVENT_MUTATION as TypedDocumentNode<\n { updateGgnomadEvent: { id: string; name: string } },\n {\n id: string;\n input: {\n name?: string;\n description?: string;\n price?: number;\n status?: string;\n images?: string[];\n };\n }\n >\n );\n\n const [updateActivity, { loading: updatingActivity }] = useMutation(\n UPDATE_GGNOMAD_ACTIVITY_MUTATION as TypedDocumentNode<\n { updateGgnomadActivity: { id: string; name: string } },\n {\n id: string;\n input: {\n name?: string;\n description?: string;\n price?: number;\n status?: string;\n images?: string[];\n };\n }\n >\n );\n\n const loading = useMemo(\n () => updatingProperty || updatingTour || updatingEvent || updatingActivity,\n [updatingProperty, updatingTour, updatingEvent, updatingActivity]\n );\n\n const handleSave = async () => {\n try {\n // Build input with only changed fields\n const input: Record<string, unknown> = {};\n\n if (name !== (initial.name ?? '')) {\n input.name = name;\n }\n if (description !== (initial.description ?? '')) {\n input.description = description;\n }\n\n const priceValue = parseFloat(price);\n const initialPrice = initial.pricePerNight ?? initial.price ?? 0;\n if (priceValue !== initialPrice) {\n if (targetType === 'PROPERTY') {\n input.pricePerNight = priceValue;\n } else {\n input.price = priceValue;\n }\n }\n\n if (status !== (initial.status ?? 'ACTIVE')) {\n input.status = status;\n }\n\n const newImages = imagesText\n .split('\\n')\n .map((line) => line.trim())\n .filter(Boolean);\n const initialImages = initial.images ?? [];\n if (JSON.stringify(newImages) !== JSON.stringify(initialImages)) {\n input.images = newImages;\n }\n\n // If nothing changed, just close\n if (Object.keys(input).length === 0) {\n notify.info(\n t('ggnomad.listingEdit.noChangesTitle', 'No changes'),\n t('ggnomad.listingEdit.noChangesBody', 'Nothing was modified.')\n );\n onOpenChange(false);\n return;\n }\n\n let result;\n if (targetType === 'PROPERTY') {\n result = await updateProperty({\n variables: { id, input },\n });\n } else if (targetType === 'TOUR') {\n result = await updateTour({\n variables: { id, input },\n });\n } else if (targetType === 'EVENT') {\n result = await updateEvent({\n variables: { id, input },\n });\n } else {\n result = await updateActivity({\n variables: { id, input },\n });\n }\n\n if (result?.error) {\n notify.error(\n t('ggnomad.listingEdit.errorTitle', 'Could not update listing'),\n result.error.message\n );\n return;\n }\n\n notify.success(\n t('ggnomad.listingEdit.successTitle', 'Listing updated'),\n t('ggnomad.listingEdit.successBody', 'Your changes have been saved.')\n );\n\n onOpenChange(false);\n onSaved?.();\n } catch (err) {\n notify.error(\n t('ggnomad.listingEdit.errorTitle', 'Could not update listing'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const priceLabel = targetType === 'PROPERTY' ? 'Price per night' : 'Price';\n\n return (\n <AlertDialog open={open} onOpenChange={onOpenChange}>\n <AlertDialogContent className=\"max-w-2xl\">\n <AlertDialogHeader>\n <AlertDialogTitle className=\"flex items-center gap-2\">\n <Pencil className=\"size-4\" />\n {t('ggnomad.listingEdit.title', 'Edit listing')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t('ggnomad.listingEdit.description', 'Update the details for this listing.')}\n </AlertDialogDescription>\n </AlertDialogHeader>\n\n <div className=\"space-y-4\">\n {/* Name */}\n <div>\n <label\n htmlFor=\"listing-edit-name\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.listingEdit.nameLabel', 'Name')}\n </label>\n <input\n id=\"listing-edit-name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n disabled={loading}\n className=\"mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder-text-secondary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg disabled:opacity-50\"\n placeholder={t('ggnomad.listingEdit.namePlaceholder', 'Enter name')}\n />\n </div>\n\n {/* Description */}\n <div>\n <label\n htmlFor=\"listing-edit-description\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.listingEdit.descriptionLabel', 'Description')}\n </label>\n <textarea\n id=\"listing-edit-description\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n disabled={loading}\n rows={3}\n className=\"mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder-text-secondary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg disabled:opacity-50\"\n placeholder={t('ggnomad.listingEdit.descriptionPlaceholder', 'Enter description')}\n />\n </div>\n\n {/* Price */}\n <div>\n <label\n htmlFor=\"listing-edit-price\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t(`ggnomad.listingEdit.priceLabel`, priceLabel)}\n </label>\n <input\n id=\"listing-edit-price\"\n type=\"number\"\n step=\"0.01\"\n min=\"0\"\n value={price}\n onChange={(e) => setPrice(e.target.value)}\n disabled={loading}\n className=\"mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder-text-secondary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg disabled:opacity-50\"\n placeholder=\"0.00\"\n />\n </div>\n\n {/* Status */}\n <div>\n <label\n htmlFor=\"listing-edit-status\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.listingEdit.statusLabel', 'Status')}\n </label>\n <select\n id=\"listing-edit-status\"\n value={status}\n onChange={(e) => setStatus(e.target.value)}\n disabled={loading}\n className=\"mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg disabled:opacity-50\"\n >\n <option value=\"ACTIVE\">{t('ggnomad.listingEdit.statusActive', 'Active')}</option>\n <option value=\"PAUSED\">{t('ggnomad.listingEdit.statusPaused', 'Paused')}</option>\n <option value=\"DRAFT\">{t('ggnomad.listingEdit.statusDraft', 'Draft')}</option>\n </select>\n </div>\n\n {/* Images */}\n <div>\n <label\n htmlFor=\"listing-edit-images\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.listingEdit.imagesLabel', 'Images (one URL per line)')}\n </label>\n <textarea\n id=\"listing-edit-images\"\n value={imagesText}\n onChange={(e) => setImagesText(e.target.value)}\n disabled={loading}\n rows={3}\n className=\"mt-1 w-full 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 placeholder={t(\n 'ggnomad.listingEdit.imagesPlaceholder',\n 'https://example.com/image1.jpg\\nhttps://example.com/image2.jpg'\n )}\n />\n </div>\n </div>\n\n <AlertDialogFooter>\n <AlertDialogCancel disabled={loading}>\n {t('ggnomad.listingEdit.cancel', 'Cancel')}\n </AlertDialogCancel>\n <AlertDialogAction\n onClick={(event) => {\n event.preventDefault();\n void handleSave();\n }}\n disabled={loading}\n className=\"bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover\"\n >\n {loading\n ? t('ggnomad.listingEdit.saving', 'Saving…')\n : t('ggnomad.listingEdit.save', 'Save changes')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ListingFilters.js","names":[],"sources":["../../../src/ggnomad/components/ListingFilters.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Accessibility, ChevronDown, RotateCcw, SlidersHorizontal } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { humanizeEnum } from '../utils/format';\nimport { cn } from '../utils/cn';\n\nexport type ListingStatusFilter = 'ACTIVE' | 'PENDING_REVIEW' | 'DRAFT' | 'PAUSED' | 'ARCHIVED';\n\n/** Every value of `GgnomadAccessibilityFeature` on the service schema (BOFF-4379). */\nexport const ACCESSIBILITY_FEATURES = [\n 'WHEELCHAIR_ACCESSIBLE',\n 'STEP_FREE_ENTRANCE',\n 'ELEVATOR_OR_LIFT',\n 'ACCESSIBLE_BEDROOM',\n 'ACCESSIBLE_RESTROOM',\n 'ROLL_IN_SHOWER',\n 'ACCESSIBLE_PARKING',\n 'SERVICE_ANIMAL_FRIENDLY',\n 'HEARING_AIDS',\n 'VISUAL_AIDS',\n] as const;\n\nconst STATUSES: ListingStatusFilter[] = ['ACTIVE', 'PENDING_REVIEW', 'DRAFT', 'PAUSED', 'ARCHIVED'];\n\nconst SUSTAINABILITY_THRESHOLDS = [25, 50, 75];\n\nexport interface ListingFilterState {\n /** `status` argument — undefined means \"every status\". */\n status?: ListingStatusFilter;\n /** `minSustainabilityScore` argument (0-100). */\n minSustainabilityScore?: number;\n /** `hasAccessibilityFeatures` argument — a listing must declare ALL of them. */\n accessibilityFeatures: string[];\n}\n\nexport const EMPTY_LISTING_FILTERS: ListingFilterState = { accessibilityFeatures: [] };\n\nexport function hasActiveListingFilters(value: ListingFilterState): boolean {\n return (\n value.status !== undefined ||\n value.minSustainabilityScore !== undefined ||\n value.accessibilityFeatures.length > 0\n );\n}\n\nconst SELECT_CLASSES =\n 'mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg';\n\ninterface ListingFiltersProps {\n idPrefix: string;\n value: ListingFilterState;\n onChange: (next: ListingFilterState) => void;\n /** Number of rows currently rendered, shown next to the filter summary. */\n resultCount: number;\n}\n\n/**\n * Catalogue filter bar wired to the three filter arguments `ggnomadProperties`,\n * `ggnomadTours`, `ggnomadEvents` and `ggnomadActivities` actually accept —\n * `status`, `minSustainabilityScore` and `hasAccessibilityFeatures`. Every control\n * re-runs the query server-side; nothing here filters a page-local slice.\n */\nexport function ListingFilters({ idPrefix, value, onChange, resultCount }: ListingFiltersProps) {\n const { t } = useI18n();\n const [accessibilityOpen, setAccessibilityOpen] = useState(\n value.accessibilityFeatures.length > 0\n );\n\n const toggleFeature = (feature: string) => {\n const next = value.accessibilityFeatures.includes(feature)\n ? value.accessibilityFeatures.filter((item) => item !== feature)\n : [...value.accessibilityFeatures, feature];\n onChange({ ...value, accessibilityFeatures: next });\n };\n\n return (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-4\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <p className=\"inline-flex items-center gap-2 text-sm font-medium text-text-primary\">\n <SlidersHorizontal className=\"size-4 text-text-secondary\" aria-hidden=\"true\" />\n {t('ggnomad.filters.title', 'Filters')}\n <span className=\"font-normal tabular-nums text-text-secondary\">\n {t('ggnomad.filters.showing', '· {{n}} shown').replace(\n '{{n}}',\n resultCount.toLocaleString()\n )}\n </span>\n </p>\n {hasActiveListingFilters(value) ? (\n <button\n type=\"button\"\n onClick={() => onChange(EMPTY_LISTING_FILTERS)}\n className=\"inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium text-action-primary-bg hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg\"\n >\n <RotateCcw className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.filters.clearAll', 'Clear all')}\n </button>\n ) : null}\n </div>\n\n <div className=\"mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n <div>\n <label\n htmlFor={`${idPrefix}-status`}\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.filters.status', 'Listing status')}\n </label>\n <select\n id={`${idPrefix}-status`}\n value={value.status ?? ''}\n onChange={(event) =>\n onChange({\n ...value,\n status: event.target.value\n ? (event.target.value as ListingStatusFilter)\n : undefined,\n })\n }\n className={SELECT_CLASSES}\n >\n <option value=\"\">{t('ggnomad.filters.anyStatus', 'Any status')}</option>\n {STATUSES.map((status) => (\n <option key={status} value={status}>\n {humanizeEnum(status)}\n </option>\n ))}\n </select>\n </div>\n\n <div>\n <label\n htmlFor={`${idPrefix}-eco`}\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.filters.sustainability', 'Min. sustainability score')}\n </label>\n <select\n id={`${idPrefix}-eco`}\n value={value.minSustainabilityScore ?? ''}\n onChange={(event) =>\n onChange({\n ...value,\n minSustainabilityScore: event.target.value ? Number(event.target.value) : undefined,\n })\n }\n className={SELECT_CLASSES}\n >\n <option value=\"\">{t('ggnomad.filters.anyScore', 'Any score')}</option>\n {SUSTAINABILITY_THRESHOLDS.map((threshold) => (\n <option key={threshold} value={threshold}>\n {t('ggnomad.filters.scoreAtLeast', '{{n}}+').replace('{{n}}', String(threshold))}\n </option>\n ))}\n </select>\n </div>\n\n <div>\n <span className=\"block text-sm font-medium text-text-primary\">\n {t('ggnomad.filters.accessibility', 'Accessibility')}\n </span>\n <button\n type=\"button\"\n onClick={() => setAccessibilityOpen((open) => !open)}\n aria-expanded={accessibilityOpen}\n className={cn(\n SELECT_CLASSES,\n 'flex items-center justify-between gap-2 text-left hover:bg-bg-sunken'\n )}\n >\n <span className=\"inline-flex min-w-0 items-center gap-2\">\n <Accessibility\n className=\"size-4 flex-shrink-0 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <span className=\"truncate\">\n {value.accessibilityFeatures.length === 0\n ? t('ggnomad.filters.anyAccessibility', 'Any')\n : t('ggnomad.filters.accessibilitySelected', '{{n}} required').replace(\n '{{n}}',\n String(value.accessibilityFeatures.length)\n )}\n </span>\n </span>\n <ChevronDown\n className={cn(\n 'size-4 flex-shrink-0 transition-transform',\n accessibilityOpen && 'rotate-180'\n )}\n aria-hidden=\"true\"\n />\n </button>\n </div>\n </div>\n\n {accessibilityOpen ? (\n <div className=\"mt-3 border-t border-border-seam pt-3\">\n <p className=\"mb-2 text-xs text-text-secondary\">\n {t(\n 'ggnomad.filters.accessibilityHint',\n 'Only listings that declare every selected feature are returned. Features are self-reported by the provider.'\n )}\n </p>\n <ul className=\"flex flex-wrap gap-2\">\n {ACCESSIBILITY_FEATURES.map((feature) => {\n const selected = value.accessibilityFeatures.includes(feature);\n return (\n <li key={feature}>\n <button\n type=\"button\"\n aria-pressed={selected}\n onClick={() => toggleFeature(feature)}\n className={cn(\n 'rounded-full border px-3 py-1 text-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg',\n selected\n ? 'border-action-primary-bg bg-action-primary-bg text-action-primary-text'\n : 'border-border-seam bg-bg-sunken text-text-primary hover:border-border-default'\n )}\n >\n {humanizeEnum(feature)}\n </button>\n </li>\n );\n })}\n </ul>\n </div>\n ) : null}\n </GlassCard>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ListingFilters.js","names":[],"sources":["../../../src/ggnomad/components/ListingFilters.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Accessibility, ChevronDown, RotateCcw, SlidersHorizontal } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { humanizeEnum } from '../utils/format';\nimport { cn } from '../utils/cn';\n\nexport type ListingStatusFilter = 'ACTIVE' | 'PENDING_REVIEW' | 'DRAFT' | 'PAUSED' | 'ARCHIVED';\n\n/** Every value of `GgnomadAccessibilityFeature` on the service schema (BOFF-4379). */\nexport const ACCESSIBILITY_FEATURES = [\n 'WHEELCHAIR_ACCESSIBLE',\n 'STEP_FREE_ENTRANCE',\n 'ELEVATOR_OR_LIFT',\n 'ACCESSIBLE_BEDROOM',\n 'ACCESSIBLE_RESTROOM',\n 'ROLL_IN_SHOWER',\n 'ACCESSIBLE_PARKING',\n 'SERVICE_ANIMAL_FRIENDLY',\n 'HEARING_AIDS',\n 'VISUAL_AIDS',\n] as const;\n\nconst STATUSES: ListingStatusFilter[] = ['ACTIVE', 'PENDING_REVIEW', 'DRAFT', 'PAUSED', 'ARCHIVED'];\n\nconst SUSTAINABILITY_THRESHOLDS = [25, 50, 75];\n\nexport interface ListingFilterState {\n /** `status` argument — undefined means \"every status\". */\n status?: ListingStatusFilter;\n /** `minSustainabilityScore` argument (0-100). */\n minSustainabilityScore?: number;\n /** `hasAccessibilityFeatures` argument — a listing must declare ALL of them. */\n accessibilityFeatures: string[];\n}\n\nexport const EMPTY_LISTING_FILTERS: ListingFilterState = { accessibilityFeatures: [] };\n\nexport function hasActiveListingFilters(value: ListingFilterState): boolean {\n return (\n value.status !== undefined ||\n value.minSustainabilityScore !== undefined ||\n value.accessibilityFeatures.length > 0\n );\n}\n\nconst SELECT_CLASSES =\n 'mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg';\n\ninterface ListingFiltersProps {\n idPrefix: string;\n value: ListingFilterState;\n onChange: (next: ListingFilterState) => void;\n /** Number of rows currently rendered, shown next to the filter summary. */\n resultCount: number;\n}\n\n/**\n * Catalogue filter bar wired to the three filter arguments `ggnomadProperties`,\n * `ggnomadTours`, `ggnomadEvents` and `ggnomadActivities` actually accept —\n * `status`, `minSustainabilityScore` and `hasAccessibilityFeatures`. Every control\n * re-runs the query server-side; nothing here filters a page-local slice.\n */\nexport function ListingFilters({ idPrefix, value, onChange, resultCount }: ListingFiltersProps) {\n const { t } = useI18n();\n const [accessibilityOpen, setAccessibilityOpen] = useState(\n value.accessibilityFeatures.length > 0\n );\n\n const toggleFeature = (feature: string) => {\n const next = value.accessibilityFeatures.includes(feature)\n ? value.accessibilityFeatures.filter((item) => item !== feature)\n : [...value.accessibilityFeatures, feature];\n onChange({ ...value, accessibilityFeatures: next });\n };\n\n return (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-4\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <p className=\"inline-flex items-center gap-2 text-sm font-medium text-text-primary\">\n <SlidersHorizontal className=\"size-4 text-text-secondary\" aria-hidden=\"true\" />\n {t('ggnomad.filters.title', 'Filters')}\n <span className=\"font-normal tabular-nums text-text-secondary\">\n {t('ggnomad.filters.showing', '· {{n}} shown').replace(\n '{{n}}',\n resultCount.toLocaleString()\n )}\n </span>\n </p>\n {hasActiveListingFilters(value) ? (\n <button\n type=\"button\"\n onClick={() => onChange(EMPTY_LISTING_FILTERS)}\n className=\"inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium text-action-primary-bg hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg\"\n >\n <RotateCcw className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.filters.clearAll', 'Clear all')}\n </button>\n ) : null}\n </div>\n\n <div className=\"mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n <div>\n <label\n htmlFor={`${idPrefix}-status`}\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.filters.status', 'Listing status')}\n </label>\n <select\n id={`${idPrefix}-status`}\n value={value.status ?? ''}\n onChange={(event) =>\n onChange({\n ...value,\n status: event.target.value\n ? (event.target.value as ListingStatusFilter)\n : undefined,\n })\n }\n className={SELECT_CLASSES}\n >\n <option value=\"\">{t('ggnomad.filters.anyStatus', 'Any status')}</option>\n {STATUSES.map((status) => (\n <option key={status} value={status}>\n {humanizeEnum(status)}\n </option>\n ))}\n </select>\n </div>\n\n <div>\n <label\n htmlFor={`${idPrefix}-eco`}\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.filters.sustainability', 'Min. sustainability score')}\n </label>\n <select\n id={`${idPrefix}-eco`}\n value={value.minSustainabilityScore ?? ''}\n onChange={(event) =>\n onChange({\n ...value,\n minSustainabilityScore: event.target.value ? Number(event.target.value) : undefined,\n })\n }\n className={SELECT_CLASSES}\n >\n <option value=\"\">{t('ggnomad.filters.anyScore', 'Any score')}</option>\n {SUSTAINABILITY_THRESHOLDS.map((threshold) => (\n <option key={threshold} value={threshold}>\n {t('ggnomad.filters.scoreAtLeast', '{{n}}+').replace('{{n}}', String(threshold))}\n </option>\n ))}\n </select>\n </div>\n\n <div>\n <span className=\"block text-sm font-medium text-text-primary\">\n {t('ggnomad.filters.accessibility', 'Accessibility')}\n </span>\n <button\n type=\"button\"\n onClick={() => setAccessibilityOpen((open) => !open)}\n aria-expanded={accessibilityOpen}\n className={cn(\n SELECT_CLASSES,\n 'flex items-center justify-between gap-2 text-left hover:bg-bg-sunken'\n )}\n >\n <span className=\"inline-flex min-w-0 items-center gap-2\">\n <Accessibility\n className=\"size-4 flex-shrink-0 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <span className=\"truncate\">\n {value.accessibilityFeatures.length === 0\n ? t('ggnomad.filters.anyAccessibility', 'Any')\n : t('ggnomad.filters.accessibilitySelected', '{{n}} required').replace(\n '{{n}}',\n String(value.accessibilityFeatures.length)\n )}\n </span>\n </span>\n <ChevronDown\n className={cn(\n 'size-4 flex-shrink-0 transition-transform',\n accessibilityOpen && 'rotate-180'\n )}\n aria-hidden=\"true\"\n />\n </button>\n </div>\n </div>\n\n {accessibilityOpen ? (\n <div className=\"mt-3 border-t border-border-seam pt-3\">\n <p className=\"mb-2 text-xs text-text-secondary\">\n {t(\n 'ggnomad.filters.accessibilityHint',\n 'Only listings that declare every selected feature are returned. Features are self-reported by the provider.'\n )}\n </p>\n <ul className=\"flex flex-wrap gap-2\">\n {ACCESSIBILITY_FEATURES.map((feature) => {\n const selected = value.accessibilityFeatures.includes(feature);\n return (\n <li key={feature}>\n <button\n type=\"button\"\n aria-pressed={selected}\n onClick={() => toggleFeature(feature)}\n className={cn(\n 'rounded-full border px-3 py-1 text-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg',\n selected\n ? 'border-action-primary-bg bg-action-primary-bg text-action-primary-text'\n : 'border-border-seam bg-bg-sunken text-text-primary hover:border-border-default'\n )}\n >\n {humanizeEnum(feature)}\n </button>\n </li>\n );\n })}\n </ul>\n </div>\n ) : null}\n </GlassCard>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"PhotoGallery.js","names":[],"sources":["../../../src/ggnomad/components/PhotoGallery.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\nimport { cn } from '../utils/cn';\n\ninterface PhotoGalleryProps {\n /** Every photo the listing publishes (`images: [String!]!` on the service schema). */\n images: string[];\n alt: string;\n className?: string;\n}\n\n/**\n * Listing hero + thumbnail strip. Detail pages used to render `images[0]` only and\n * silently drop the rest of the array the query already returns; this shows the whole\n * set and lets the reader page through it. Renders nothing when the listing published\n * no photos — the caller falls back to `DetailHero`'s branded gradient.\n */\nexport function PhotoGallery({ images, alt, className }: PhotoGalleryProps) {\n const { t } = useI18n();\n const signature = images.join('|');\n const [selection, setSelection] = useState({ index: 0, signature });\n\n // Navigating between two listings reuses this component instance, so the\n // selection is reset during render when the photo set itself changes rather\n // than in an effect (which would paint the previous listing's photo first).\n if (selection.signature !== signature) {\n setSelection({ index: 0, signature });\n }\n\n if (images.length === 0) return null;\n\n const safeIndex = Math.min(selection.index, images.length - 1);\n\n return (\n <div className={cn('mb-5 space-y-2', className)}>\n <div className=\"relative aspect-[21/9] w-full overflow-hidden rounded-card border border-border-seam\">\n <img\n src={images[safeIndex]}\n alt={\n images.length > 1\n ? t('ggnomad.gallery.photoAlt', '{{name}} — photo {{n}} of {{total}}')\n .replace('{{name}}', alt)\n .replace('{{n}}', String(safeIndex + 1))\n .replace('{{total}}', String(images.length))\n : alt\n }\n loading=\"eager\"\n decoding=\"async\"\n className=\"size-full object-cover\"\n />\n <div className=\"pointer-events-none absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent\" />\n </div>\n\n {images.length > 1 ? (\n <div\n className=\"flex gap-2 overflow-x-auto pb-1\"\n role=\"group\"\n aria-label={t('ggnomad.gallery.thumbnails', 'Listing photos')}\n >\n {images.map((image, index) => (\n <button\n key={`${image}-${index}`}\n type=\"button\"\n onClick={() => setSelection({ index, signature })}\n aria-current={index === safeIndex}\n aria-label={t('ggnomad.gallery.showPhoto', 'Show photo {{n}}').replace(\n '{{n}}',\n String(index + 1)\n )}\n className={cn(\n 'h-16 w-24 flex-shrink-0 overflow-hidden rounded-md border transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg',\n index === safeIndex\n ? 'border-action-primary-bg'\n : 'border-border-seam hover:border-border-default'\n )}\n >\n <img\n src={image}\n alt=\"\"\n loading=\"lazy\"\n decoding=\"async\"\n className=\"size-full object-cover\"\n />\n </button>\n ))}\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;;AAkBA,SAAgB,EAAa,EAAE,WAAQ,QAAK,gBAAgC;CAC1E,IAAM,EAAE,SAAM,EAAQ,GAChB,IAAY,EAAO,KAAK,GAAG,GAC3B,CAAC,GAAW,KAAgB,EAAS;EAAE,OAAO;EAAG;CAAU,CAAC;CASlE,IAJI,EAAU,cAAc,KAC1B,EAAa;EAAE,OAAO;EAAG;CAAU,CAAC,GAGlC,EAAO,WAAW,GAAG,OAAO;CAEhC,IAAM,IAAY,KAAK,IAAI,EAAU,OAAO,EAAO,SAAS,CAAC;CAE7D,OACE,kBAAC,OAAD;EAAK,WAAW,EAAG,kBAAkB,CAAS;YAA9C,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IACE,KAAK,EAAO;IACZ,KACE,EAAO,SAAS,IACZ,EAAE,4BAA4B,qCAAqC,EAChE,QAAQ,YAAY,CAAG,EACvB,QAAQ,SAAS,OAAO,IAAY,CAAC,CAAC,EACtC,QAAQ,aAAa,OAAO,EAAO,MAAM,CAAC,IAC7C;IAEN,SAAQ;IACR,UAAS;IACT,WAAU;GACX,CAAA,GACD,kBAAC,OAAD,EAAK,WAAU,qGAAsG,CAAA,CAClH;MAEJ,EAAO,SAAS,IACf,kBAAC,OAAD;GACE,WAAU;GACV,MAAK;GACL,cAAY,EAAE,8BAA8B,gBAAgB;aAE3D,EAAO,KAAK,GAAO,MAClB,kBAAC,UAAD;IAEE,MAAK;IACL,eAAe,EAAa;KAAE;KAAO;IAAU,CAAC;IAChD,gBAAc,MAAU;IACxB,cAAY,EAAE,6BAA6B,kBAAkB,EAAE,QAC7D,SACA,OAAO,IAAQ,CAAC,CAClB;IACA,WAAW,EACT,oMACA,MAAU,IACN,6BACA,gDACN;cAEA,kBAAC,OAAD;KACE,KAAK;KACL,KAAI;KACJ,SAAQ;KACR,UAAS;KACT,WAAU;IACX,CAAA;GACK,GAtBD,GAAG,EAAM,GAAG,GAsBX,CACT;EACE,CAAA,IACH,IACD;;AAET"}
1
+ {"version":3,"file":"PhotoGallery.js","names":[],"sources":["../../../src/ggnomad/components/PhotoGallery.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\nimport { cn } from '../utils/cn';\n\ninterface PhotoGalleryProps {\n /** Every photo the listing publishes (`images: [String!]!` on the service schema). */\n images: string[];\n alt: string;\n className?: string;\n}\n\n/**\n * Listing hero + thumbnail strip. Detail pages used to render `images[0]` only and\n * silently drop the rest of the array the query already returns; this shows the whole\n * set and lets the reader page through it. Renders nothing when the listing published\n * no photos — the caller falls back to `DetailHero`'s branded gradient.\n */\nexport function PhotoGallery({ images, alt, className }: PhotoGalleryProps) {\n const { t } = useI18n();\n const signature = images.join('|');\n const [selection, setSelection] = useState({ index: 0, signature });\n\n // Navigating between two listings reuses this component instance, so the\n // selection is reset during render when the photo set itself changes rather\n // than in an effect (which would paint the previous listing's photo first).\n if (selection.signature !== signature) {\n setSelection({ index: 0, signature });\n }\n\n if (images.length === 0) return null;\n\n const safeIndex = Math.min(selection.index, images.length - 1);\n\n return (\n <div className={cn('mb-5 space-y-2', className)}>\n <div className=\"relative aspect-[21/9] w-full overflow-hidden rounded-card border border-border-seam\">\n <img\n src={images[safeIndex]}\n alt={\n images.length > 1\n ? t('ggnomad.gallery.photoAlt', '{{name}} — photo {{n}} of {{total}}')\n .replace('{{name}}', alt)\n .replace('{{n}}', String(safeIndex + 1))\n .replace('{{total}}', String(images.length))\n : alt\n }\n loading=\"eager\"\n decoding=\"async\"\n className=\"size-full object-cover\"\n />\n <div className=\"pointer-events-none absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent\" />\n </div>\n\n {images.length > 1 ? (\n <div\n className=\"flex gap-2 overflow-x-auto pb-1\"\n role=\"group\"\n aria-label={t('ggnomad.gallery.thumbnails', 'Listing photos')}\n >\n {images.map((image, index) => (\n <button\n key={`${image}-${index}`}\n type=\"button\"\n onClick={() => setSelection({ index, signature })}\n aria-current={index === safeIndex}\n aria-label={t('ggnomad.gallery.showPhoto', 'Show photo {{n}}').replace(\n '{{n}}',\n String(index + 1)\n )}\n className={cn(\n 'h-16 w-24 flex-shrink-0 overflow-hidden rounded-md border transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg',\n index === safeIndex\n ? 'border-action-primary-bg'\n : 'border-border-seam hover:border-border-default'\n )}\n >\n <img\n src={image}\n alt=\"\"\n loading=\"lazy\"\n decoding=\"async\"\n className=\"size-full object-cover\"\n />\n </button>\n ))}\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;;AAkBA,SAAgB,EAAa,EAAE,WAAQ,QAAK,gBAAgC;CAC1E,IAAM,EAAE,SAAM,EAAQ,GAChB,IAAY,EAAO,KAAK,GAAG,GAC3B,CAAC,GAAW,KAAgB,EAAS;EAAE,OAAO;EAAG;CAAU,CAAC;CASlE,IAJI,EAAU,cAAc,KAC1B,EAAa;EAAE,OAAO;EAAG;CAAU,CAAC,GAGlC,EAAO,WAAW,GAAG,OAAO;CAEhC,IAAM,IAAY,KAAK,IAAI,EAAU,OAAO,EAAO,SAAS,CAAC;CAE7D,OACE,kBAAC,OAAD;EAAK,WAAW,EAAG,kBAAkB,CAAS;YAA9C,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IACE,KAAK,EAAO;IACZ,KACE,EAAO,SAAS,IACZ,EAAE,4BAA4B,qCAAqC,CAAC,CACjE,QAAQ,YAAY,CAAG,CAAC,CACxB,QAAQ,SAAS,OAAO,IAAY,CAAC,CAAC,CAAC,CACvC,QAAQ,aAAa,OAAO,EAAO,MAAM,CAAC,IAC7C;IAEN,SAAQ;IACR,UAAS;IACT,WAAU;GACX,CAAA,GACD,kBAAC,OAAD,EAAK,WAAU,qGAAsG,CAAA,CAClH;MAEJ,EAAO,SAAS,IACf,kBAAC,OAAD;GACE,WAAU;GACV,MAAK;GACL,cAAY,EAAE,8BAA8B,gBAAgB;aAE3D,EAAO,KAAK,GAAO,MAClB,kBAAC,UAAD;IAEE,MAAK;IACL,eAAe,EAAa;KAAE;KAAO;IAAU,CAAC;IAChD,gBAAc,MAAU;IACxB,cAAY,EAAE,6BAA6B,kBAAkB,CAAC,CAAC,QAC7D,SACA,OAAO,IAAQ,CAAC,CAClB;IACA,WAAW,EACT,oMACA,MAAU,IACN,6BACA,gDACN;cAEA,kBAAC,OAAD;KACE,KAAK;KACL,KAAI;KACJ,SAAQ;KACR,UAAS;KACT,WAAU;IACX,CAAA;GACK,GAtBD,GAAG,EAAM,GAAG,GAsBX,CACT;EACE,CAAA,IACH,IACD;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"ProviderCard.js","names":[],"sources":["../../../src/ggnomad/components/ProviderCard.tsx"],"sourcesContent":["import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Building2, Globe, Mail } from 'lucide-react';\n\nimport { RatingSummary } from './RatingSummary';\nimport { SectionCard } from './SectionCard';\n\nexport interface ListingProvider {\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}\n\n/**\n * \"Hosted by\" panel for a listing detail page. Every field comes from the\n * `GgnomadProvider` the listing already resolves through its `provider` edge; each\n * line is omitted when the provider record does not carry it.\n */\nexport function ProviderCard({ provider }: { provider: ListingProvider }) {\n const { t } = useI18n();\n\n return (\n <SectionCard title={t('ggnomad.providerCard.title', 'Hosted by')}>\n <div className=\"flex items-start gap-3\">\n <span\n className=\"flex size-11 flex-shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-text-primary\"\n aria-hidden=\"true\"\n >\n <Building2 className=\"size-5\" />\n </span>\n <div className=\"min-w-0 space-y-1\">\n <p className=\"truncate font-medium text-text-primary\">{provider.name}</p>\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-text-secondary\">\n {provider.type ? <span>{provider.type}</span> : null}\n {provider.listingCount !== null && provider.listingCount !== undefined ? (\n <span className=\"tabular-nums\">\n {provider.listingCount === 1\n ? t('ggnomad.providerCard.oneListing', '1 listing')\n : t('ggnomad.providerCard.listings', '{{n}} listings').replace(\n '{{n}}',\n provider.listingCount.toLocaleString()\n )}\n </span>\n ) : null}\n <RatingSummary rating={provider.rating} reviewCount={provider.reviewCount} />\n </div>\n {provider.contactEmail || provider.website ? (\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1 pt-1 text-sm\">\n {provider.contactEmail ? (\n <a\n href={`mailto:${provider.contactEmail}`}\n className=\"inline-flex items-center gap-1.5 text-action-primary-bg hover:underline\"\n >\n <Mail className=\"size-3.5\" aria-hidden=\"true\" />\n {provider.contactEmail}\n </a>\n ) : null}\n {provider.website ? (\n <a\n href={provider.website}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className=\"inline-flex items-center gap-1.5 text-action-primary-bg hover:underline\"\n >\n <Globe className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.providerCard.website', 'Website')}\n </a>\n ) : null}\n </div>\n ) : null}\n </div>\n </div>\n </SectionCard>\n );\n}\n"],"mappings":";;;;;;AAsBA,SAAgB,EAAa,EAAE,eAA2C;CACxE,IAAM,EAAE,SAAM,EAAQ;CAEtB,OACE,kBAAC,GAAD;EAAa,OAAO,EAAE,8BAA8B,WAAW;YAC7D,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,QAAD;IACE,WAAU;IACV,eAAY;cAEZ,kBAAC,GAAD,EAAW,WAAU,SAAU,CAAA;GAC3B,CAAA,GACN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,KAAD;MAAG,WAAU;gBAA0C,EAAS;KAAQ,CAAA;KACxE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACG,EAAS,OAAO,kBAAC,QAAD,EAAA,UAAO,EAAS,KAAW,CAAA,IAAI;OAC/C,EAAS,iBAAiB,QAAQ,EAAS,iBAAiB,KAAA,IAC3D,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAS,iBAAiB,IACvB,EAAE,mCAAmC,WAAW,IAChD,EAAE,iCAAiC,gBAAgB,EAAE,QACnD,SACA,EAAS,aAAa,eAAe,CACvC;OACA,CAAA,IACJ;OACJ,kBAAC,GAAD;QAAe,QAAQ,EAAS;QAAQ,aAAa,EAAS;OAAc,CAAA;MACzE;;KACJ,EAAS,gBAAgB,EAAS,UACjC,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACG,EAAS,eACR,kBAAC,KAAD;OACE,MAAM,UAAU,EAAS;OACzB,WAAU;iBAFZ,CAIE,kBAAC,GAAD;QAAM,WAAU;QAAW,eAAY;OAAQ,CAAA,GAC9C,EAAS,YACT;WACD,MACH,EAAS,UACR,kBAAC,KAAD;OACE,MAAM,EAAS;OACf,QAAO;OACP,KAAI;OACJ,WAAU;iBAJZ,CAME,kBAAC,GAAD;QAAO,WAAU;QAAW,eAAY;OAAQ,CAAA,GAC/C,EAAE,gCAAgC,SAAS,CAC3C;WACD,IACD;UACH;IACD;KACF;;CACM,CAAA;AAEjB"}
1
+ {"version":3,"file":"ProviderCard.js","names":[],"sources":["../../../src/ggnomad/components/ProviderCard.tsx"],"sourcesContent":["import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Building2, Globe, Mail } from 'lucide-react';\n\nimport { RatingSummary } from './RatingSummary';\nimport { SectionCard } from './SectionCard';\n\nexport interface ListingProvider {\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}\n\n/**\n * \"Hosted by\" panel for a listing detail page. Every field comes from the\n * `GgnomadProvider` the listing already resolves through its `provider` edge; each\n * line is omitted when the provider record does not carry it.\n */\nexport function ProviderCard({ provider }: { provider: ListingProvider }) {\n const { t } = useI18n();\n\n return (\n <SectionCard title={t('ggnomad.providerCard.title', 'Hosted by')}>\n <div className=\"flex items-start gap-3\">\n <span\n className=\"flex size-11 flex-shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-text-primary\"\n aria-hidden=\"true\"\n >\n <Building2 className=\"size-5\" />\n </span>\n <div className=\"min-w-0 space-y-1\">\n <p className=\"truncate font-medium text-text-primary\">{provider.name}</p>\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-text-secondary\">\n {provider.type ? <span>{provider.type}</span> : null}\n {provider.listingCount !== null && provider.listingCount !== undefined ? (\n <span className=\"tabular-nums\">\n {provider.listingCount === 1\n ? t('ggnomad.providerCard.oneListing', '1 listing')\n : t('ggnomad.providerCard.listings', '{{n}} listings').replace(\n '{{n}}',\n provider.listingCount.toLocaleString()\n )}\n </span>\n ) : null}\n <RatingSummary rating={provider.rating} reviewCount={provider.reviewCount} />\n </div>\n {provider.contactEmail || provider.website ? (\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1 pt-1 text-sm\">\n {provider.contactEmail ? (\n <a\n href={`mailto:${provider.contactEmail}`}\n className=\"inline-flex items-center gap-1.5 text-action-primary-bg hover:underline\"\n >\n <Mail className=\"size-3.5\" aria-hidden=\"true\" />\n {provider.contactEmail}\n </a>\n ) : null}\n {provider.website ? (\n <a\n href={provider.website}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className=\"inline-flex items-center gap-1.5 text-action-primary-bg hover:underline\"\n >\n <Globe className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.providerCard.website', 'Website')}\n </a>\n ) : null}\n </div>\n ) : null}\n </div>\n </div>\n </SectionCard>\n );\n}\n"],"mappings":";;;;;;AAsBA,SAAgB,EAAa,EAAE,eAA2C;CACxE,IAAM,EAAE,SAAM,EAAQ;CAEtB,OACE,kBAAC,GAAD;EAAa,OAAO,EAAE,8BAA8B,WAAW;YAC7D,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,QAAD;IACE,WAAU;IACV,eAAY;cAEZ,kBAAC,GAAD,EAAW,WAAU,SAAU,CAAA;GAC3B,CAAA,GACN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,KAAD;MAAG,WAAU;gBAA0C,EAAS;KAAQ,CAAA;KACxE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACG,EAAS,OAAO,kBAAC,QAAD,EAAA,UAAO,EAAS,KAAW,CAAA,IAAI;OAC/C,EAAS,iBAAiB,QAAQ,EAAS,iBAAiB,KAAA,IAC3D,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAS,iBAAiB,IACvB,EAAE,mCAAmC,WAAW,IAChD,EAAE,iCAAiC,gBAAgB,CAAC,CAAC,QACnD,SACA,EAAS,aAAa,eAAe,CACvC;OACA,CAAA,IACJ;OACJ,kBAAC,GAAD;QAAe,QAAQ,EAAS;QAAQ,aAAa,EAAS;OAAc,CAAA;MACzE;;KACJ,EAAS,gBAAgB,EAAS,UACjC,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACG,EAAS,eACR,kBAAC,KAAD;OACE,MAAM,UAAU,EAAS;OACzB,WAAU;iBAFZ,CAIE,kBAAC,GAAD;QAAM,WAAU;QAAW,eAAY;OAAQ,CAAA,GAC9C,EAAS,YACT;WACD,MACH,EAAS,UACR,kBAAC,KAAD;OACE,MAAM,EAAS;OACf,QAAO;OACP,KAAI;OACJ,WAAU;iBAJZ,CAME,kBAAC,GAAD;QAAO,WAAU;QAAW,eAAY;OAAQ,CAAA,GAC/C,EAAE,gCAAgC,SAAS,CAC3C;WACD,IACD;UACH;IACD;KACF;;CACM,CAAA;AAEjB"}
@@ -1 +1 @@
1
- {"version":3,"file":"ProviderDashboardCard.js","names":[],"sources":["../../../src/ggnomad/components/ProviderDashboardCard.tsx"],"sourcesContent":["import { Link } from 'react-router';\nimport { CalendarPlus, CircleDot, Globe, Mail, Phone, Star, Store } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\nimport { formatDate, formatRevenue } from '../utils/format';\nimport {\n bannerTintFor,\n initialsFor,\n labelForProviderType,\n sentenceCase,\n} from '../utils/providerTypeMeta';\n\ninterface ProviderDashboardCardProps {\n id: string;\n name: string;\n type: string;\n status: string;\n description?: string | null;\n listingCount: number;\n totalRevenue: number;\n rating?: number | null;\n reviewCount: number;\n contactEmail?: string | null;\n contactPhone?: string | null;\n website?: string | null;\n createdAt: string;\n}\n\n/**\n * Provider Dashboard card. Distinct from the shared EntityCard on\n * purpose: EntityCard is built for listing inventory with a 16:10\n * cover image; providers use a flat banner + avatar treatment instead\n * so each operator's card stands apart visually.\n *\n * The banner color is keyed by provider type so the tint reads as a\n * type indicator. Green/red are intentionally not in the banner\n * palette because green means \"Active\" and red means \"error\".\n */\nexport function ProviderDashboardCard({\n id,\n name,\n type,\n status,\n description,\n listingCount,\n totalRevenue,\n rating,\n reviewCount,\n contactEmail,\n contactPhone,\n website,\n createdAt,\n}: ProviderDashboardCardProps) {\n const { t } = useI18n();\n const tint = bannerTintFor(type);\n const displayName = name;\n const joined = formatDate(createdAt);\n const initials = initialsFor(name);\n const websiteHref = safeWebsiteHref(website);\n\n const listingsDisplay =\n listingCount >= 10_000\n ? new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(listingCount)\n : listingCount.toLocaleString();\n const revenue = formatRevenue(totalRevenue);\n const ratingDisplay = rating && Number(rating) > 0 ? Number(rating).toFixed(1) : null;\n\n return (\n <GlassCard\n treatment=\"plain\"\n className=\"flex h-full flex-col overflow-hidden rounded-2xl border border-border-seam\"\n >\n {/* 1. BANNER — solid tinted band, status pill pinned top-right.\n * The aria-hidden wrapper carries only the decorative background\n * so the status badge (its sibling) stays in the accessibility\n * tree — without this scoping, the badge's status text would be\n * invisible to screen readers because aria-hidden cascades to\n * all descendants. */}\n <div className=\"relative flex h-[84px] items-start justify-end p-3\">\n <div\n className=\"absolute inset-0\"\n style={{ backgroundColor: tint.backgroundColor }}\n aria-hidden=\"true\"\n />\n <div className=\"relative z-10\">\n <ProviderDashboardStatusBadge status={status} t={t} />\n </div>\n </div>\n\n {/* Body. The negative top margin pulls the avatar up so it overlaps\n * the banner by ~half its diameter. min-h-[96px] on the avatar row\n * reserves the vertical space so the name/type rows below don't\n * shift on cards without descriptions or contact rows. */}\n <div className=\"relative flex flex-1 flex-col px-[22px] pb-[22px] pt-0\">\n {/* 3. AVATAR — 76px circle, 4px ring in the card surface color. */}\n <div className=\"-mt-[38px] mb-3 flex h-[76px] w-[76px] items-center justify-center overflow-hidden rounded-full border-[4px] border-bg-canvas bg-bg-canvas\">\n <span\n className=\"flex size-full items-center justify-center text-2xl font-medium\"\n style={tint}\n >\n {initials}\n </span>\n </div>\n\n {/* 4. IDENTITY BLOCK — name, type, description (single line, fixed\n * min-height so all cards align). */}\n <p className=\"truncate text-xl font-medium text-text-primary\" title={displayName}>\n {displayName}\n </p>\n <p className=\"mt-0.5 text-sm text-text-secondary\">{labelForProviderType(type, t)}</p>\n <p\n className={\n 'mt-1.5 min-h-[20px] truncate text-sm ' +\n (description ? 'text-text-secondary' : 'italic text-text-secondary/60')\n }\n title={description ?? ''}\n >\n {description || t('ggnomad.provider.noDescription', 'No description')}\n </p>\n\n {/* 5. STATS ROW — three columns, hairline borders above + below and\n * between, no individual tiles. Revenue column gets extra room.\n * Every value gets min-w-0 truncate as a safety net for extreme\n * strings. */}\n <dl\n className=\"mt-5 grid border-y border-border-seam\"\n style={{ gridTemplateColumns: '1fr 1.4fr 1fr' }}\n >\n <ProviderDashboardStat\n label={t('ggnomad.provider.listings', 'Listings')}\n value={\n <span title={String(listingCount)} className=\"min-w-0 truncate\">\n {listingsDisplay}\n </span>\n }\n />\n <ProviderDashboardStat\n label={t('ggnomad.provider.revenue', 'Revenue')}\n border\n value={\n <span title={revenue.exact} className=\"min-w-0 truncate\">\n {revenue.display}\n </span>\n }\n />\n <ProviderDashboardStat\n label={t('ggnomad.provider.rating', 'Rating')}\n border\n value={\n ratingDisplay !== null ? (\n <span\n className=\"inline-flex items-center gap-1\"\n title={\n ratingDisplay +\n (reviewCount > 0\n ? t('ggnomad.provider.reviewCount', ' ({{n}} reviews)', { n: reviewCount })\n : '')\n }\n >\n <Star\n className=\"size-4 fill-status-warning-text text-status-warning-text\"\n aria-hidden=\"true\"\n />\n <span className=\"min-w-0 truncate\">{ratingDisplay}</span>\n </span>\n ) : (\n <span\n className=\"min-w-0 truncate text-text-secondary/70\"\n title={t('ggnomad.provider.noRatings', 'No ratings yet')}\n >\n {t('ggnomad.provider.ratingNew', 'New')}\n </span>\n )\n }\n />\n </dl>\n\n {/* 6. CONTACT ROWS — ALWAYS exactly three, always rendered with the\n * same icon and 22px height so all cards in a row are the same\n * height. Missing values render as \"Not added\" in muted color, no\n * link, no hover, no pointer cursor. */}\n <div className=\"mt-4 space-y-2\">\n <ProviderDashboardContactRow\n icon={Mail}\n value={contactEmail ?? null}\n href={contactEmail ? `mailto:${contactEmail}` : null}\n label={t('ggnomad.provider.contactEmail', 'Email')}\n t={t}\n />\n <ProviderDashboardContactRow\n icon={Phone}\n value={contactPhone ?? null}\n href={contactPhone ? `tel:${contactPhone}` : null}\n label={t('ggnomad.provider.contactPhone', 'Phone')}\n t={t}\n />\n <ProviderDashboardContactRow\n icon={Globe}\n value={formatWebsiteDisplay(website)}\n href={websiteHref}\n label={t('ggnomad.provider.website', 'Website')}\n external\n t={t}\n />\n </div>\n\n {/* 7. FOOTER — pinned to the bottom of the card. Date on its own\n * line, then a full-width outlined secondary button. */}\n <footer className=\"mt-auto pt-5\">\n <p className=\"flex items-center gap-1.5 text-[13px] text-text-secondary\">\n <CalendarPlus className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n <span>\n {joined\n ? t('ggnomad.provider.joined', 'Joined {{date}}').replace('{{date}}', joined)\n : t('ggnomad.provider.joinedEmpty', 'Joined —')}\n </span>\n </p>\n <Link\n to={`${id}/storefront`}\n className=\"mt-2 flex h-[42px] w-full items-center justify-center gap-1.5 rounded-md border border-border-seam bg-bg-canvas text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2 focus-visible:ring-offset-bg-canvas\"\n >\n <Store className=\"size-4\" aria-hidden=\"true\" />\n {t('ggnomad.provider.manageStorefront', 'Manage storefront')}\n </Link>\n </footer>\n </div>\n </GlassCard>\n );\n}\n\n/**\n * Strip the protocol and trailing slash from a URL for display, e.g.\n * \"https://chrisleverseo.com/tools/random-url-generator/\" becomes\n * \"chrisleverseo.com/tools/random-url-generator\". Never used for the\n * actual href, only the visible label, so this can be aggressive.\n */\nfunction formatWebsiteDisplay(url: string | null | undefined): string | null {\n if (!url) return null;\n return url.replace(/^https?:\\/\\//i, '').replace(/\\/+$/, '');\n}\n\n/**\n * Only return the URL as a clickable href if it starts with http(s)://.\n * Providers created via CLI/MCP don't go through the create-dialog input\n * validator, so a malformed value like \"acme.com\" would otherwise be passed\n * to the anchor's `href` and break navigation. Returns null otherwise so\n * the row renders as plain text instead.\n */\nfunction safeWebsiteHref(url: string | null | undefined): string | null {\n if (!url) return null;\n return /^https?:\\/\\//i.test(url) ? url : null;\n}\n\ninterface StatProps {\n label: string;\n value: React.ReactNode;\n /** Render the left vertical divider for non-first columns. */\n border?: boolean;\n}\n\nfunction ProviderDashboardStat({ label, value, border }: StatProps) {\n // Spec: each column left-aligns label and value, gets 14px left padding\n // except the first, and a hairline vertical divider on cols 2 & 3. The\n // parent <dl> already supplies the top + bottom border; this cell only\n // owns its left divider + padding. Right-edge padding is added in the\n // Rating column (the last one) so the row is balanced.\n return (\n <div\n className={\n 'flex flex-col items-start py-2.5 pr-3 ' +\n (border ? 'border-l border-border-seam pl-3.5' : 'pl-0')\n }\n >\n <dt className=\"text-xs text-text-secondary\">{label}</dt>\n <dd className=\"mt-0.5 text-[22px] font-medium tabular-nums text-text-primary\">{value}</dd>\n </div>\n );\n}\n\ninterface ContactRowProps {\n icon: typeof Mail;\n value: string | null;\n href: string | null;\n label: string;\n external?: boolean;\n t: (key: string, fallback: string, vars?: Record<string, string | number>) => string;\n}\n\nfunction ProviderDashboardContactRow({\n icon: Icon,\n value,\n href,\n label,\n external,\n t,\n}: ContactRowProps) {\n const isLink = Boolean(href && value);\n const content = (\n <>\n <Icon className=\"size-4 shrink-0 text-text-secondary\" aria-hidden=\"true\" />\n <span\n className={\n 'min-w-0 truncate ' +\n (isLink ? 'text-action-primary-bg hover:underline' : 'text-text-secondary/60')\n }\n title={value ?? t('ggnomad.provider.notAddedFor', '{{label}} not added', { label })}\n >\n {value ?? t('ggnomad.provider.notAdded', 'Not added')}\n </span>\n </>\n );\n\n const rowStyle = 'flex h-[22px] items-center gap-2 text-sm';\n\n if (isLink && href) {\n return (\n <a\n href={href}\n target={external ? '_blank' : undefined}\n rel={external ? 'noreferrer noopener' : undefined}\n className={rowStyle}\n >\n {content}\n </a>\n );\n }\n // Missing value — render as plain text, no link, no hover, no pointer.\n return (\n <div className={rowStyle} style={{ cursor: 'default' }}>\n {content}\n </div>\n );\n}\n\ninterface StatusBadgeProps {\n status: string;\n t: (key: string, fallback: string) => string;\n}\n\n/**\n * Small status pill that lives inside the banner top-right. Inline here\n * (not the shared StatusBadge) because the shared component takes the\n * raw uppercase enum and renders the dot inline with the label; we\n * want sentence-case for the card.\n */\nfunction ProviderDashboardStatusBadge({ status, t }: StatusBadgeProps) {\n const { bg, fg, label } = statusTone(status, t);\n return (\n <span\n className={\n 'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium ' +\n bg +\n ' ' +\n fg\n }\n >\n <CircleDot className=\"size-3\" aria-hidden=\"true\" />\n {label}\n </span>\n );\n}\n\ninterface StatusTone {\n bg: string;\n fg: string;\n label: string;\n}\n\nfunction statusTone(status: string, t: (key: string, fallback: string) => string): StatusTone {\n switch (status) {\n case 'ACTIVE':\n return {\n bg: 'bg-status-success-bg-subtle border-status-success-text/40',\n fg: 'text-status-success-text',\n label: t('ggnomad.provider.statusActive', 'Active'),\n };\n case 'PENDING':\n return {\n bg: 'bg-status-warning-bg-subtle border-status-warning-text/40',\n fg: 'text-status-warning-text',\n label: t('ggnomad.provider.statusPending', 'Pending'),\n };\n case 'SUSPENDED':\n case 'ARCHIVED':\n return {\n bg: 'bg-status-error-bg-subtle border-status-error-text/40',\n fg: 'text-status-error-text',\n label: t('ggnomad.provider.status' + sentenceCase(status), sentenceCase(status)),\n };\n default:\n return {\n bg: 'bg-bg-sunken border-border-seam',\n fg: 'text-text-secondary',\n label: sentenceCase(status) || status,\n };\n }\n}\n"],"mappings":";;;;;;;;AAuCA,SAAgB,EAAsB,EACpC,OACA,SACA,SACA,WACA,gBACA,iBACA,iBACA,WACA,gBACA,iBACA,iBACA,YACA,gBAC6B;CAC7B,IAAM,EAAE,SAAM,EAAQ,GAChB,IAAO,EAAc,CAAI,GACzB,IAAc,GACd,IAAS,EAAW,CAAS,GAC7B,IAAW,EAAY,CAAI,GAC3B,IAAc,EAAgB,CAAO,GAErC,IACJ,KAAgB,MACZ,IAAI,KAAK,aAAa,SAAS;EAC7B,UAAU;EACV,uBAAuB;CACzB,CAAC,EAAE,OAAO,CAAY,IACtB,EAAa,eAAe,GAC5B,IAAU,EAAc,CAAY,GACpC,IAAgB,KAAU,OAAO,CAAM,IAAI,IAAI,OAAO,CAAM,EAAE,QAAQ,CAAC,IAAI;CAEjF,OACE,kBAAC,GAAD;EACE,WAAU;EACV,WAAU;YAFZ,CAUE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IACE,WAAU;IACV,OAAO,EAAE,iBAAiB,EAAK,gBAAgB;IAC/C,eAAY;GACb,CAAA,GACD,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAAsC;KAAW;IAAI,CAAA;GAClD,CAAA,CACF;MAML,kBAAC,OAAD;GAAK,WAAU;aAAf;IAEE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,QAAD;MACE,WAAU;MACV,OAAO;gBAEN;KACG,CAAA;IACH,CAAA;IAIL,kBAAC,KAAD;KAAG,WAAU;KAAiD,OAAO;eAClE;IACA,CAAA;IACH,kBAAC,KAAD;KAAG,WAAU;eAAsC,EAAqB,GAAM,CAAC;IAAK,CAAA;IACpF,kBAAC,KAAD;KACE,WACE,2CACC,IAAc,wBAAwB;KAEzC,OAAO,KAAe;eAErB,KAAe,EAAE,kCAAkC,gBAAgB;IACnE,CAAA;IAMH,kBAAC,MAAD;KACE,WAAU;KACV,OAAO,EAAE,qBAAqB,gBAAgB;eAFhD;MAIE,kBAAC,GAAD;OACE,OAAO,EAAE,6BAA6B,UAAU;OAChD,OACE,kBAAC,QAAD;QAAM,OAAO,OAAO,CAAY;QAAG,WAAU;kBAC1C;OACG,CAAA;MAET,CAAA;MACD,kBAAC,GAAD;OACE,OAAO,EAAE,4BAA4B,SAAS;OAC9C,QAAA;OACA,OACE,kBAAC,QAAD;QAAM,OAAO,EAAQ;QAAO,WAAU;kBACnC,EAAQ;OACL,CAAA;MAET,CAAA;MACD,kBAAC,GAAD;OACE,OAAO,EAAE,2BAA2B,QAAQ;OAC5C,QAAA;OACA,OACE,MAAkB,OAiBhB,kBAAC,QAAD;QACE,WAAU;QACV,OAAO,EAAE,8BAA8B,gBAAgB;kBAEtD,EAAE,8BAA8B,KAAK;OAClC,CAAA,IArBN,kBAAC,QAAD;QACE,WAAU;QACV,OACE,KACC,IAAc,IACX,EAAE,gCAAgC,oBAAoB,EAAE,GAAG,EAAY,CAAC,IACxE;kBANR,CASE,kBAAC,GAAD;SACE,WAAU;SACV,eAAY;QACb,CAAA,GACD,kBAAC,QAAD;SAAM,WAAU;mBAAoB;QAAoB,CAAA,CACpD;;MAUX,CAAA;KACC;;IAMJ,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD;OACE,MAAM;OACN,OAAO,KAAgB;OACvB,MAAM,IAAe,UAAU,MAAiB;OAChD,OAAO,EAAE,iCAAiC,OAAO;OAC9C;MACJ,CAAA;MACD,kBAAC,GAAD;OACE,MAAM;OACN,OAAO,KAAgB;OACvB,MAAM,IAAe,OAAO,MAAiB;OAC7C,OAAO,EAAE,iCAAiC,OAAO;OAC9C;MACJ,CAAA;MACD,kBAAC,GAAD;OACE,MAAM;OACN,OAAO,EAAqB,CAAO;OACnC,MAAM;OACN,OAAO,EAAE,4BAA4B,SAAS;OAC9C,UAAA;OACG;MACJ,CAAA;KACE;;IAIL,kBAAC,UAAD;KAAQ,WAAU;eAAlB,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAb,CACE,kBAAC,GAAD;OAAc,WAAU;OAAoB,eAAY;MAAQ,CAAA,GAChE,kBAAC,QAAD,EAAA,UACG,IACG,EAAE,2BAA2B,iBAAiB,EAAE,QAAQ,YAAY,CAAM,IAC1E,EAAE,gCAAgC,UAAU,EAC5C,CAAA,CACL;SACH,kBAAC,GAAD;MACE,IAAI,GAAG,EAAG;MACV,WAAU;gBAFZ,CAIE,kBAAC,GAAD;OAAO,WAAU;OAAS,eAAY;MAAQ,CAAA,GAC7C,EAAE,qCAAqC,mBAAmB,CACvD;OACA;;GACL;IACI;;AAEf;AAQA,SAAS,EAAqB,GAA+C;CAE3E,OADK,IACE,EAAI,QAAQ,iBAAiB,EAAE,EAAE,QAAQ,QAAQ,EAAE,IADzC;AAEnB;AASA,SAAS,EAAgB,GAA+C;CAEtE,OADK,KACE,gBAAgB,KAAK,CAAG,IAAI,IAAM;AAC3C;AASA,SAAS,EAAsB,EAAE,UAAO,UAAO,aAAqB;CAMlE,OACE,kBAAC,OAAD;EACE,WACE,4CACC,IAAS,uCAAuC;YAHrD,CAME,kBAAC,MAAD;GAAI,WAAU;aAA+B;EAAU,CAAA,GACvD,kBAAC,MAAD;GAAI,WAAU;aAAiE;EAAU,CAAA,CACtF;;AAET;AAWA,SAAS,EAA4B,EACnC,MAAM,GACN,UACA,SACA,UACA,aACA,QACkB;CAClB,IAAM,IAAS,GAAQ,KAAQ,IACzB,IACJ,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;EAAM,WAAU;EAAsC,eAAY;CAAQ,CAAA,GAC1E,kBAAC,QAAD;EACE,WACE,uBACC,IAAS,2CAA2C;EAEvD,OAAO,KAAS,EAAE,gCAAgC,uBAAuB,EAAE,SAAM,CAAC;YAEjF,KAAS,EAAE,6BAA6B,WAAW;CAChD,CAAA,CACN,EAAA,CAAA,GAGE,IAAW;CAejB,OAbI,KAAU,IAEV,kBAAC,KAAD;EACQ;EACN,QAAQ,IAAW,WAAW,KAAA;EAC9B,KAAK,IAAW,wBAAwB,KAAA;EACxC,WAAW;YAEV;CACA,CAAA,IAKL,kBAAC,OAAD;EAAK,WAAW;EAAU,OAAO,EAAE,QAAQ,UAAU;YAClD;CACE,CAAA;AAET;AAaA,SAAS,EAA6B,EAAE,WAAQ,KAAuB;CACrE,IAAM,EAAE,OAAI,OAAI,aAAU,EAAW,GAAQ,CAAC;CAC9C,OACE,kBAAC,QAAD;EACE,WACE,wFACA,IACA,MACA;YALJ,CAQE,kBAAC,GAAD;GAAW,WAAU;GAAS,eAAY;EAAQ,CAAA,GACjD,CACG;;AAEV;AAQA,SAAS,EAAW,GAAgB,GAA0D;CAC5F,QAAQ,GAAR;EACE,KAAK,UACH,OAAO;GACL,IAAI;GACJ,IAAI;GACJ,OAAO,EAAE,iCAAiC,QAAQ;EACpD;EACF,KAAK,WACH,OAAO;GACL,IAAI;GACJ,IAAI;GACJ,OAAO,EAAE,kCAAkC,SAAS;EACtD;EACF,KAAK;EACL,KAAK,YACH,OAAO;GACL,IAAI;GACJ,IAAI;GACJ,OAAO,EAAE,4BAA4B,EAAa,CAAM,GAAG,EAAa,CAAM,CAAC;EACjF;EACF,SACE,OAAO;GACL,IAAI;GACJ,IAAI;GACJ,OAAO,EAAa,CAAM,KAAK;EACjC;CACJ;AACF"}
1
+ {"version":3,"file":"ProviderDashboardCard.js","names":[],"sources":["../../../src/ggnomad/components/ProviderDashboardCard.tsx"],"sourcesContent":["import { Link } from 'react-router';\nimport { CalendarPlus, CircleDot, Globe, Mail, Phone, Star, Store } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\nimport { formatDate, formatRevenue } from '../utils/format';\nimport {\n bannerTintFor,\n initialsFor,\n labelForProviderType,\n sentenceCase,\n} from '../utils/providerTypeMeta';\n\ninterface ProviderDashboardCardProps {\n id: string;\n name: string;\n type: string;\n status: string;\n description?: string | null;\n listingCount: number;\n totalRevenue: number;\n rating?: number | null;\n reviewCount: number;\n contactEmail?: string | null;\n contactPhone?: string | null;\n website?: string | null;\n createdAt: string;\n}\n\n/**\n * Provider Dashboard card. Distinct from the shared EntityCard on\n * purpose: EntityCard is built for listing inventory with a 16:10\n * cover image; providers use a flat banner + avatar treatment instead\n * so each operator's card stands apart visually.\n *\n * The banner color is keyed by provider type so the tint reads as a\n * type indicator. Green/red are intentionally not in the banner\n * palette because green means \"Active\" and red means \"error\".\n */\nexport function ProviderDashboardCard({\n id,\n name,\n type,\n status,\n description,\n listingCount,\n totalRevenue,\n rating,\n reviewCount,\n contactEmail,\n contactPhone,\n website,\n createdAt,\n}: ProviderDashboardCardProps) {\n const { t } = useI18n();\n const tint = bannerTintFor(type);\n const displayName = name;\n const joined = formatDate(createdAt);\n const initials = initialsFor(name);\n const websiteHref = safeWebsiteHref(website);\n\n const listingsDisplay =\n listingCount >= 10_000\n ? new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(listingCount)\n : listingCount.toLocaleString();\n const revenue = formatRevenue(totalRevenue);\n const ratingDisplay = rating && Number(rating) > 0 ? Number(rating).toFixed(1) : null;\n\n return (\n <GlassCard\n treatment=\"plain\"\n className=\"flex h-full flex-col overflow-hidden rounded-2xl border border-border-seam\"\n >\n {/* 1. BANNER — solid tinted band, status pill pinned top-right.\n * The aria-hidden wrapper carries only the decorative background\n * so the status badge (its sibling) stays in the accessibility\n * tree — without this scoping, the badge's status text would be\n * invisible to screen readers because aria-hidden cascades to\n * all descendants. */}\n <div className=\"relative flex h-[84px] items-start justify-end p-3\">\n <div\n className=\"absolute inset-0\"\n style={{ backgroundColor: tint.backgroundColor }}\n aria-hidden=\"true\"\n />\n <div className=\"relative z-10\">\n <ProviderDashboardStatusBadge status={status} t={t} />\n </div>\n </div>\n\n {/* Body. The negative top margin pulls the avatar up so it overlaps\n * the banner by ~half its diameter. min-h-[96px] on the avatar row\n * reserves the vertical space so the name/type rows below don't\n * shift on cards without descriptions or contact rows. */}\n <div className=\"relative flex flex-1 flex-col px-[22px] pb-[22px] pt-0\">\n {/* 3. AVATAR — 76px circle, 4px ring in the card surface color. */}\n <div className=\"-mt-[38px] mb-3 flex h-[76px] w-[76px] items-center justify-center overflow-hidden rounded-full border-[4px] border-bg-canvas bg-bg-canvas\">\n <span\n className=\"flex size-full items-center justify-center text-2xl font-medium\"\n style={tint}\n >\n {initials}\n </span>\n </div>\n\n {/* 4. IDENTITY BLOCK — name, type, description (single line, fixed\n * min-height so all cards align). */}\n <p className=\"truncate text-xl font-medium text-text-primary\" title={displayName}>\n {displayName}\n </p>\n <p className=\"mt-0.5 text-sm text-text-secondary\">{labelForProviderType(type, t)}</p>\n <p\n className={\n 'mt-1.5 min-h-[20px] truncate text-sm ' +\n (description ? 'text-text-secondary' : 'italic text-text-secondary/60')\n }\n title={description ?? ''}\n >\n {description || t('ggnomad.provider.noDescription', 'No description')}\n </p>\n\n {/* 5. STATS ROW — three columns, hairline borders above + below and\n * between, no individual tiles. Revenue column gets extra room.\n * Every value gets min-w-0 truncate as a safety net for extreme\n * strings. */}\n <dl\n className=\"mt-5 grid border-y border-border-seam\"\n style={{ gridTemplateColumns: '1fr 1.4fr 1fr' }}\n >\n <ProviderDashboardStat\n label={t('ggnomad.provider.listings', 'Listings')}\n value={\n <span title={String(listingCount)} className=\"min-w-0 truncate\">\n {listingsDisplay}\n </span>\n }\n />\n <ProviderDashboardStat\n label={t('ggnomad.provider.revenue', 'Revenue')}\n border\n value={\n <span title={revenue.exact} className=\"min-w-0 truncate\">\n {revenue.display}\n </span>\n }\n />\n <ProviderDashboardStat\n label={t('ggnomad.provider.rating', 'Rating')}\n border\n value={\n ratingDisplay !== null ? (\n <span\n className=\"inline-flex items-center gap-1\"\n title={\n ratingDisplay +\n (reviewCount > 0\n ? t('ggnomad.provider.reviewCount', ' ({{n}} reviews)', { n: reviewCount })\n : '')\n }\n >\n <Star\n className=\"size-4 fill-status-warning-text text-status-warning-text\"\n aria-hidden=\"true\"\n />\n <span className=\"min-w-0 truncate\">{ratingDisplay}</span>\n </span>\n ) : (\n <span\n className=\"min-w-0 truncate text-text-secondary/70\"\n title={t('ggnomad.provider.noRatings', 'No ratings yet')}\n >\n {t('ggnomad.provider.ratingNew', 'New')}\n </span>\n )\n }\n />\n </dl>\n\n {/* 6. CONTACT ROWS — ALWAYS exactly three, always rendered with the\n * same icon and 22px height so all cards in a row are the same\n * height. Missing values render as \"Not added\" in muted color, no\n * link, no hover, no pointer cursor. */}\n <div className=\"mt-4 space-y-2\">\n <ProviderDashboardContactRow\n icon={Mail}\n value={contactEmail ?? null}\n href={contactEmail ? `mailto:${contactEmail}` : null}\n label={t('ggnomad.provider.contactEmail', 'Email')}\n t={t}\n />\n <ProviderDashboardContactRow\n icon={Phone}\n value={contactPhone ?? null}\n href={contactPhone ? `tel:${contactPhone}` : null}\n label={t('ggnomad.provider.contactPhone', 'Phone')}\n t={t}\n />\n <ProviderDashboardContactRow\n icon={Globe}\n value={formatWebsiteDisplay(website)}\n href={websiteHref}\n label={t('ggnomad.provider.website', 'Website')}\n external\n t={t}\n />\n </div>\n\n {/* 7. FOOTER — pinned to the bottom of the card. Date on its own\n * line, then a full-width outlined secondary button. */}\n <footer className=\"mt-auto pt-5\">\n <p className=\"flex items-center gap-1.5 text-[13px] text-text-secondary\">\n <CalendarPlus className=\"size-3.5 shrink-0\" aria-hidden=\"true\" />\n <span>\n {joined\n ? t('ggnomad.provider.joined', 'Joined {{date}}').replace('{{date}}', joined)\n : t('ggnomad.provider.joinedEmpty', 'Joined —')}\n </span>\n </p>\n <Link\n to={`${id}/storefront`}\n className=\"mt-2 flex h-[42px] w-full items-center justify-center gap-1.5 rounded-md border border-border-seam bg-bg-canvas text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2 focus-visible:ring-offset-bg-canvas\"\n >\n <Store className=\"size-4\" aria-hidden=\"true\" />\n {t('ggnomad.provider.manageStorefront', 'Manage storefront')}\n </Link>\n </footer>\n </div>\n </GlassCard>\n );\n}\n\n/**\n * Strip the protocol and trailing slash from a URL for display, e.g.\n * \"https://chrisleverseo.com/tools/random-url-generator/\" becomes\n * \"chrisleverseo.com/tools/random-url-generator\". Never used for the\n * actual href, only the visible label, so this can be aggressive.\n */\nfunction formatWebsiteDisplay(url: string | null | undefined): string | null {\n if (!url) return null;\n return url.replace(/^https?:\\/\\//i, '').replace(/\\/+$/, '');\n}\n\n/**\n * Only return the URL as a clickable href if it starts with http(s)://.\n * Providers created via CLI/MCP don't go through the create-dialog input\n * validator, so a malformed value like \"acme.com\" would otherwise be passed\n * to the anchor's `href` and break navigation. Returns null otherwise so\n * the row renders as plain text instead.\n */\nfunction safeWebsiteHref(url: string | null | undefined): string | null {\n if (!url) return null;\n return /^https?:\\/\\//i.test(url) ? url : null;\n}\n\ninterface StatProps {\n label: string;\n value: React.ReactNode;\n /** Render the left vertical divider for non-first columns. */\n border?: boolean;\n}\n\nfunction ProviderDashboardStat({ label, value, border }: StatProps) {\n // Spec: each column left-aligns label and value, gets 14px left padding\n // except the first, and a hairline vertical divider on cols 2 & 3. The\n // parent <dl> already supplies the top + bottom border; this cell only\n // owns its left divider + padding. Right-edge padding is added in the\n // Rating column (the last one) so the row is balanced.\n return (\n <div\n className={\n 'flex flex-col items-start py-2.5 pr-3 ' +\n (border ? 'border-l border-border-seam pl-3.5' : 'pl-0')\n }\n >\n <dt className=\"text-xs text-text-secondary\">{label}</dt>\n <dd className=\"mt-0.5 text-[22px] font-medium tabular-nums text-text-primary\">{value}</dd>\n </div>\n );\n}\n\ninterface ContactRowProps {\n icon: typeof Mail;\n value: string | null;\n href: string | null;\n label: string;\n external?: boolean;\n t: (key: string, fallback: string, vars?: Record<string, string | number>) => string;\n}\n\nfunction ProviderDashboardContactRow({\n icon: Icon,\n value,\n href,\n label,\n external,\n t,\n}: ContactRowProps) {\n const isLink = Boolean(href && value);\n const content = (\n <>\n <Icon className=\"size-4 shrink-0 text-text-secondary\" aria-hidden=\"true\" />\n <span\n className={\n 'min-w-0 truncate ' +\n (isLink ? 'text-action-primary-bg hover:underline' : 'text-text-secondary/60')\n }\n title={value ?? t('ggnomad.provider.notAddedFor', '{{label}} not added', { label })}\n >\n {value ?? t('ggnomad.provider.notAdded', 'Not added')}\n </span>\n </>\n );\n\n const rowStyle = 'flex h-[22px] items-center gap-2 text-sm';\n\n if (isLink && href) {\n return (\n <a\n href={href}\n target={external ? '_blank' : undefined}\n rel={external ? 'noreferrer noopener' : undefined}\n className={rowStyle}\n >\n {content}\n </a>\n );\n }\n // Missing value — render as plain text, no link, no hover, no pointer.\n return (\n <div className={rowStyle} style={{ cursor: 'default' }}>\n {content}\n </div>\n );\n}\n\ninterface StatusBadgeProps {\n status: string;\n t: (key: string, fallback: string) => string;\n}\n\n/**\n * Small status pill that lives inside the banner top-right. Inline here\n * (not the shared StatusBadge) because the shared component takes the\n * raw uppercase enum and renders the dot inline with the label; we\n * want sentence-case for the card.\n */\nfunction ProviderDashboardStatusBadge({ status, t }: StatusBadgeProps) {\n const { bg, fg, label } = statusTone(status, t);\n return (\n <span\n className={\n 'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium ' +\n bg +\n ' ' +\n fg\n }\n >\n <CircleDot className=\"size-3\" aria-hidden=\"true\" />\n {label}\n </span>\n );\n}\n\ninterface StatusTone {\n bg: string;\n fg: string;\n label: string;\n}\n\nfunction statusTone(status: string, t: (key: string, fallback: string) => string): StatusTone {\n switch (status) {\n case 'ACTIVE':\n return {\n bg: 'bg-status-success-bg-subtle border-status-success-text/40',\n fg: 'text-status-success-text',\n label: t('ggnomad.provider.statusActive', 'Active'),\n };\n case 'PENDING':\n return {\n bg: 'bg-status-warning-bg-subtle border-status-warning-text/40',\n fg: 'text-status-warning-text',\n label: t('ggnomad.provider.statusPending', 'Pending'),\n };\n case 'SUSPENDED':\n case 'ARCHIVED':\n return {\n bg: 'bg-status-error-bg-subtle border-status-error-text/40',\n fg: 'text-status-error-text',\n label: t('ggnomad.provider.status' + sentenceCase(status), sentenceCase(status)),\n };\n default:\n return {\n bg: 'bg-bg-sunken border-border-seam',\n fg: 'text-text-secondary',\n label: sentenceCase(status) || status,\n };\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"RatingSummary.js","names":[],"sources":["../../../src/ggnomad/components/RatingSummary.tsx"],"sourcesContent":["import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Star } from 'lucide-react';\n\nimport { cn } from '../utils/cn';\n\ninterface RatingSummaryProps {\n /** `rating` is nullable on every GGNomad listing type — null means \"not rated yet\". */\n rating?: number | null;\n reviewCount?: number | null;\n className?: string;\n}\n\n/**\n * Rating + review count, the pair every listing type carries (`rating: Float`,\n * `reviewCount: Int!`). Shows the unrated state explicitly instead of printing a\n * misleading zero.\n */\nexport function RatingSummary({ rating, reviewCount, className }: RatingSummaryProps) {\n const { t } = useI18n();\n const count = reviewCount ?? 0;\n\n if (rating === null || rating === undefined) {\n return (\n <span className={cn('text-sm text-text-secondary', className)}>\n {t('ggnomad.rating.none', 'Not rated yet')}\n </span>\n );\n }\n\n return (\n <span className={cn('inline-flex items-center gap-1.5 text-sm', className)}>\n <Star\n className=\"size-3.5 fill-status-warning-text text-status-warning-text\"\n aria-hidden=\"true\"\n />\n <span className=\"font-medium tabular-nums text-text-primary\">{rating.toFixed(1)}</span>\n <span className=\"tabular-nums text-text-secondary\">\n {count === 1\n ? t('ggnomad.rating.oneReview', '(1 review)')\n : t('ggnomad.rating.reviews', '({{n}} reviews)').replace('{{n}}', count.toLocaleString())}\n </span>\n </span>\n );\n}\n"],"mappings":";;;;;AAiBA,SAAgB,EAAc,EAAE,WAAQ,gBAAa,gBAAiC;CACpF,IAAM,EAAE,SAAM,EAAQ,GAChB,IAAQ,KAAe;CAU7B,OARI,KAAW,OAEX,kBAAC,QAAD;EAAM,WAAW,EAAG,+BAA+B,CAAS;YACzD,EAAE,uBAAuB,eAAe;CACrC,CAAA,IAKR,kBAAC,QAAD;EAAM,WAAW,EAAG,4CAA4C,CAAS;YAAzE;GACE,kBAAC,GAAD;IACE,WAAU;IACV,eAAY;GACb,CAAA;GACD,kBAAC,QAAD;IAAM,WAAU;cAA8C,EAAO,QAAQ,CAAC;GAAQ,CAAA;GACtF,kBAAC,QAAD;IAAM,WAAU;cACb,MAAU,IACP,EAAE,4BAA4B,YAAY,IAC1C,EAAE,0BAA0B,iBAAiB,EAAE,QAAQ,SAAS,EAAM,eAAe,CAAC;GACtF,CAAA;EACF;;AAEV"}
1
+ {"version":3,"file":"RatingSummary.js","names":[],"sources":["../../../src/ggnomad/components/RatingSummary.tsx"],"sourcesContent":["import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Star } from 'lucide-react';\n\nimport { cn } from '../utils/cn';\n\ninterface RatingSummaryProps {\n /** `rating` is nullable on every GGNomad listing type — null means \"not rated yet\". */\n rating?: number | null;\n reviewCount?: number | null;\n className?: string;\n}\n\n/**\n * Rating + review count, the pair every listing type carries (`rating: Float`,\n * `reviewCount: Int!`). Shows the unrated state explicitly instead of printing a\n * misleading zero.\n */\nexport function RatingSummary({ rating, reviewCount, className }: RatingSummaryProps) {\n const { t } = useI18n();\n const count = reviewCount ?? 0;\n\n if (rating === null || rating === undefined) {\n return (\n <span className={cn('text-sm text-text-secondary', className)}>\n {t('ggnomad.rating.none', 'Not rated yet')}\n </span>\n );\n }\n\n return (\n <span className={cn('inline-flex items-center gap-1.5 text-sm', className)}>\n <Star\n className=\"size-3.5 fill-status-warning-text text-status-warning-text\"\n aria-hidden=\"true\"\n />\n <span className=\"font-medium tabular-nums text-text-primary\">{rating.toFixed(1)}</span>\n <span className=\"tabular-nums text-text-secondary\">\n {count === 1\n ? t('ggnomad.rating.oneReview', '(1 review)')\n : t('ggnomad.rating.reviews', '({{n}} reviews)').replace('{{n}}', count.toLocaleString())}\n </span>\n </span>\n );\n}\n"],"mappings":";;;;;AAiBA,SAAgB,EAAc,EAAE,WAAQ,gBAAa,gBAAiC;CACpF,IAAM,EAAE,SAAM,EAAQ,GAChB,IAAQ,KAAe;CAU7B,OARI,KAAW,OAEX,kBAAC,QAAD;EAAM,WAAW,EAAG,+BAA+B,CAAS;YACzD,EAAE,uBAAuB,eAAe;CACrC,CAAA,IAKR,kBAAC,QAAD;EAAM,WAAW,EAAG,4CAA4C,CAAS;YAAzE;GACE,kBAAC,GAAD;IACE,WAAU;IACV,eAAY;GACb,CAAA;GACD,kBAAC,QAAD;IAAM,WAAU;cAA8C,EAAO,QAAQ,CAAC;GAAQ,CAAA;GACtF,kBAAC,QAAD;IAAM,WAAU;cACb,MAAU,IACP,EAAE,4BAA4B,YAAY,IAC1C,EAAE,0BAA0B,iBAAiB,CAAC,CAAC,QAAQ,SAAS,EAAM,eAAe,CAAC;GACtF,CAAA;EACF;;AAEV"}