@burdenoff/microfe-ggnomad 2026.1016.1 → 2026.1029.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ggnomad/GGNomadRoot.js +4 -2
- package/dist/ggnomad/GGNomadRoot.js.map +1 -1
- package/dist/ggnomad/GGNomadRoutes.js.map +1 -1
- package/dist/ggnomad/components/AuditTrailDialog.js.map +1 -1
- package/dist/ggnomad/components/AvailabilityPanel.js.map +1 -1
- package/dist/ggnomad/components/BookingPanel.d.ts.map +1 -1
- package/dist/ggnomad/components/BookingPanel.js +53 -70
- package/dist/ggnomad/components/BookingPanel.js.map +1 -1
- package/dist/ggnomad/components/CreateProviderDialog.js.map +1 -1
- package/dist/ggnomad/components/CreateTravelPolicyDialog.js.map +1 -1
- package/dist/ggnomad/components/GroupTripPanel.js.map +1 -1
- package/dist/ggnomad/components/ListingEditDialog.js.map +1 -1
- package/dist/ggnomad/components/ListingFilters.js.map +1 -1
- package/dist/ggnomad/components/PhotoGallery.js.map +1 -1
- package/dist/ggnomad/components/ProviderCard.js.map +1 -1
- package/dist/ggnomad/components/ProviderDashboardCard.js.map +1 -1
- package/dist/ggnomad/components/RatingSummary.js.map +1 -1
- package/dist/ggnomad/components/RedeemTicketPanel.js.map +1 -1
- package/dist/ggnomad/components/ReviewsSection.js.map +1 -1
- package/dist/ggnomad/components/StatusBadge.js.map +1 -1
- package/dist/ggnomad/components/TicketDialog.js +1 -1
- package/dist/ggnomad/components/TicketDialog.js.map +1 -1
- package/dist/ggnomad/components/index.js +26 -25
- package/dist/ggnomad/index.js +3 -2
- package/dist/ggnomad/pages/ActivityDetailsPage.js.map +1 -1
- package/dist/ggnomad/pages/AnalyticsPage.js.map +1 -1
- package/dist/ggnomad/pages/BookingsPage.d.ts.map +1 -1
- package/dist/ggnomad/pages/BookingsPage.js +54 -52
- package/dist/ggnomad/pages/BookingsPage.js.map +1 -1
- package/dist/ggnomad/pages/DashboardPage.js.map +1 -1
- package/dist/ggnomad/pages/EntertainmentPage.js.map +1 -1
- package/dist/ggnomad/pages/EventDetailsPage.js.map +1 -1
- package/dist/ggnomad/pages/FavoritesPage.js.map +1 -1
- package/dist/ggnomad/pages/HospitalityPage.js.map +1 -1
- package/dist/ggnomad/pages/OperationsQueuePage.js.map +1 -1
- package/dist/ggnomad/pages/PageShell.js.map +1 -1
- package/dist/ggnomad/pages/PropertyDetailsPage.js.map +1 -1
- package/dist/ggnomad/pages/ProviderAnalyticsPage.js.map +1 -1
- package/dist/ggnomad/pages/SearchPage.js +1 -1
- package/dist/ggnomad/pages/SearchPage.js.map +1 -1
- package/dist/ggnomad/pages/StorefrontPage.js.map +1 -1
- package/dist/ggnomad/pages/StorefrontPublicPage.js.map +1 -1
- package/dist/ggnomad/pages/TourDetailsPage.js.map +1 -1
- package/dist/ggnomad/pages/TourismPage.js.map +1 -1
- package/dist/ggnomad/pages/TravelPage.js +1 -1
- package/dist/ggnomad/pages/TravelPage.js.map +1 -1
- package/dist/ggnomad/pages/TravelPolicyPage.js.map +1 -1
- package/dist/ggnomad/pages/TripPlanDetailPage.d.ts.map +1 -1
- package/dist/ggnomad/pages/TripPlanDetailPage.js +278 -273
- package/dist/ggnomad/pages/TripPlanDetailPage.js.map +1 -1
- package/dist/ggnomad/pages/TripsPage.js.map +1 -1
- package/dist/ggnomad/providers/GGNomadProvider.js.map +1 -1
- package/dist/ggnomad/utils/bookingDates.d.ts +21 -0
- package/dist/ggnomad/utils/bookingDates.d.ts.map +1 -0
- package/dist/ggnomad/utils/bookingDates.js +22 -0
- package/dist/ggnomad/utils/bookingDates.js.map +1 -0
- package/dist/ggnomad/utils/format.js.map +1 -1
- package/dist/ggnomad/utils/notify.js +1 -1
- package/dist/ggnomad/utils/providerListControls.js +1 -1
- package/dist/ggnomad/utils/providerListControls.js.map +1 -1
- package/dist/ggnomad/utils/providerTypeMeta.js.map +1 -1
- package/dist/utils/pageContext.js +1 -1
- package/dist/utils/pageContext.js.map +1 -1
- package/package.json +18 -17
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProviderAnalyticsPage.js","names":[],"sources":["../../../src/ggnomad/pages/ProviderAnalyticsPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { BarChart3, CalendarCheck, DollarSign, Gauge, TrendingUp, XCircle } from 'lucide-react';\nimport { IllustratedEmptyState, PagePurpose, Spinner } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_MY_PROVIDERS_QUERY, GGNOMAD_MY_PROVIDER_ANALYTICS_QUERY } from '../graphql';\nimport { PageShell } from './PageShell';\nimport { SectionCard, StatCard } from '../components';\n\nconst CHART_COLORS = [\n 'var(--color-chart-1)',\n 'var(--color-chart-2)',\n 'var(--color-chart-3)',\n 'var(--color-chart-4)',\n 'var(--color-chart-5)',\n];\n\nconst 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\ntype MyProvider = { id: string; name: string; type: string; status: string };\n\ntype AnalyticsPoint = { period: string; bookings: number; revenue: number };\n\ntype ListingAnalytics = {\n listingId: string;\n listingType: string;\n listingName: string;\n bookings: number;\n revenue: number;\n cancellationRate: number;\n capacityUtilizationRate: number;\n};\n\ntype ProviderAnalytics = {\n providerId: string;\n providerName: string;\n totalBookings: number;\n totalRevenue: number;\n cancelledBookings: number;\n cancellationRate: number;\n capacityUtilizationRate: number;\n bookingsOverTime: AnalyticsPoint[];\n topListings: ListingAnalytics[];\n};\n\nfunction toISODate(date: Date): string {\n return date.toISOString().slice(0, 10);\n}\n\nfunction daysAgo(days: number): Date {\n return new Date(Date.now() - days * 24 * 60 * 60 * 1000);\n}\n\n/**\n * BOFF-4390 — provider-facing analytics dashboard: bookings-over-time,\n * revenue, cancellation rate and capacity utilization, plus a per-listing\n * drill-down table. Self-scoped: only providers the calling actor created\n * are listed, and the analytics call is owner-verified server-side — a\n * provider can never see another provider's numbers. Forecasting and\n * custom-report building are explicitly out of scope for this core; see the\n * `myGgnomadProviderAnalytics` schema doc comment.\n */\nexport function ProviderAnalyticsPage() {\n const { t } = useI18n();\n\n const { data: providersData, loading: providersLoading } = useQuery(\n GGNOMAD_MY_PROVIDERS_QUERY as TypedDocumentNode<\n { myGgnomadProviders: MyProvider[] },\n Record<string, never>\n >\n );\n const providers = providersData?.myGgnomadProviders ?? [];\n\n const [selectedProviderId, setSelectedProviderId] = useState('');\n // `dateFrom`/`dateTo` are the plain YYYY-MM-DD strings bound to the <input\n // type=\"date\"> controls. They are NOT sent to GraphQL as-is: a bare date\n // string parses to UTC midnight, so using \"today\" as dateTo would exclude\n // every booking made later that same day. The query variables below\n // convert dateFrom to the start of that UTC day and dateTo to the end of\n // it, so \"today\" always includes today's activity.\n const [dateFrom, setDateFrom] = useState(() => toISODate(daysAgo(30)));\n const [dateTo, setDateTo] = useState(() => toISODate(new Date()));\n const [granularity, setGranularity] = useState<'day' | 'week' | 'month'>('day');\n\n const activeProviderId = selectedProviderId || providers[0]?.id || '';\n\n const { data: analyticsData, loading: analyticsLoading } = useQuery(\n GGNOMAD_MY_PROVIDER_ANALYTICS_QUERY as TypedDocumentNode<\n { myGgnomadProviderAnalytics: ProviderAnalytics },\n { providerId: string; dateFrom?: string; dateTo?: string; granularity?: string }\n >,\n {\n variables: {\n providerId: activeProviderId,\n dateFrom: `${dateFrom}T00:00:00.000Z`,\n dateTo: `${dateTo}T23:59:59.999Z`,\n granularity,\n },\n skip: !activeProviderId,\n }\n );\n const analytics = analyticsData?.myGgnomadProviderAnalytics;\n\n const maxTrend = useMemo(() => {\n const points = analytics?.bookingsOverTime ?? [];\n return points.length > 0 ? Math.max(...points.map((p) => p.bookings)) : 0;\n }, [analytics?.bookingsOverTime]);\n\n return (\n <PageShell\n title={t('ggnomad.providerAnalytics.title', 'Provider Analytics')}\n titleAccent={t('ggnomad.providerAnalytics.title.accent', 'Analytics')}\n subtitle={t(\n 'ggnomad.providerAnalytics.subtitle',\n 'Your bookings, revenue and listing performance'\n )}\n >\n <PagePurpose icon={<TrendingUp className=\"size-4\" />}>\n {t(\n 'ggnomad.providerAnalytics.purpose',\n 'A self-service dashboard for providers: bookings and revenue over time, cancellation rate, capacity utilization, and a per-listing breakdown — scoped to only the listings you own. Pick a date range and a listing granularity to drill in.'\n )}\n </PagePurpose>\n\n {!providersLoading && providers.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={t('ggnomad.providerAnalytics.emptyTitle', 'No provider profile yet')}\n description={t(\n 'ggnomad.providerAnalytics.emptyDescription',\n 'Once you create a GGNomad provider profile, your bookings and revenue analytics will appear here.'\n )}\n />\n ) : (\n <>\n <SectionCard title={t('ggnomad.providerAnalytics.filters', 'Filters')}>\n <div className=\"grid gap-3 sm:grid-cols-4\">\n <div>\n <label\n htmlFor=\"provider-analytics-provider\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.providerAnalytics.provider', 'Provider')}\n </label>\n <select\n id=\"provider-analytics-provider\"\n value={activeProviderId}\n onChange={(e) => setSelectedProviderId(e.target.value)}\n className={INPUT_CLASSES}\n >\n {providers.map((provider) => (\n <option key={provider.id} value={provider.id}>\n {provider.name}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label\n htmlFor=\"provider-analytics-from\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.providerAnalytics.from', 'From')}\n </label>\n <input\n id=\"provider-analytics-from\"\n type=\"date\"\n value={dateFrom}\n max={dateTo}\n onChange={(e) => setDateFrom(e.target.value)}\n className={INPUT_CLASSES}\n />\n </div>\n <div>\n <label\n htmlFor=\"provider-analytics-to\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.providerAnalytics.to', 'To')}\n </label>\n <input\n id=\"provider-analytics-to\"\n type=\"date\"\n value={dateTo}\n min={dateFrom}\n max={toISODate(new Date())}\n onChange={(e) => setDateTo(e.target.value)}\n className={INPUT_CLASSES}\n />\n </div>\n <div>\n <label\n htmlFor=\"provider-analytics-granularity\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.providerAnalytics.granularity', 'Granularity')}\n </label>\n <select\n id=\"provider-analytics-granularity\"\n value={granularity}\n onChange={(e) => setGranularity(e.target.value as 'day' | 'week' | 'month')}\n className={INPUT_CLASSES}\n >\n <option value=\"day\">{t('ggnomad.providerAnalytics.day', 'Day')}</option>\n <option value=\"week\">{t('ggnomad.providerAnalytics.week', 'Week')}</option>\n <option value=\"month\">{t('ggnomad.providerAnalytics.month', 'Month')}</option>\n </select>\n </div>\n </div>\n </SectionCard>\n\n {analyticsLoading ? (\n <div className=\"flex justify-center py-12\">\n <Spinner size=\"lg\" />\n </div>\n ) : !analytics ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={t('ggnomad.providerAnalytics.noDataTitle', 'No bookings in this range')}\n description={t(\n 'ggnomad.providerAnalytics.noDataDescription',\n 'Try widening the date range — analytics appear here once bookings come in for your listings.'\n )}\n />\n ) : (\n <>\n <section className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n hero\n label={t('ggnomad.providerAnalytics.totalBookings', 'Bookings')}\n value={analytics.totalBookings.toLocaleString()}\n icon={<CalendarCheck />}\n />\n <StatCard\n label={t('ggnomad.providerAnalytics.totalRevenue', 'Revenue')}\n value={`$${Number(analytics.totalRevenue).toLocaleString()}`}\n icon={<DollarSign />}\n />\n <StatCard\n label={t('ggnomad.providerAnalytics.cancellationRate', 'Cancellation rate')}\n value={`${(analytics.cancellationRate * 100).toFixed(1)}%`}\n icon={<XCircle />}\n />\n <StatCard\n label={t('ggnomad.providerAnalytics.capacityUtilization', 'Capacity utilization')}\n value={`${(analytics.capacityUtilizationRate * 100).toFixed(1)}%`}\n icon={<Gauge />}\n />\n </section>\n\n <SectionCard\n title={t('ggnomad.providerAnalytics.bookingsOverTime', 'Bookings over time')}\n >\n {analytics.bookingsOverTime.length === 0 ? (\n <div className=\"flex flex-col items-center gap-2 py-8 text-center\">\n <span className=\"flex size-10 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-text-secondary\">\n <BarChart3 className=\"size-5\" />\n </span>\n <p className=\"text-sm font-medium text-text-primary\">\n {t('ggnomad.providerAnalytics.trendEmpty', 'No bookings in this window yet')}\n </p>\n </div>\n ) : (\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex items-end gap-2 overflow-x-auto pb-2\">\n {analytics.bookingsOverTime.map((point, index) => {\n const pct =\n maxTrend > 0 ? Math.max((point.bookings / maxTrend) * 100, 5) : 5;\n return (\n <div key={point.period} className=\"flex flex-col items-center gap-1\">\n <div className=\"flex h-24 items-end\">\n <div\n className=\"w-8 rounded-t transition-all duration-500 ease-out\"\n style={{\n height: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n title={`${point.period}: ${point.bookings} bookings, $${point.revenue.toLocaleString()} revenue`}\n />\n </div>\n <p className=\"truncate text-xs text-text-secondary\">{point.period}</p>\n </div>\n );\n })}\n </div>\n <p className=\"text-xs text-text-secondary\">\n {t(\n 'ggnomad.providerAnalytics.trendNote',\n 'Bar height is booking volume per period; hover a bar for its exact bookings and revenue.'\n )}\n </p>\n </div>\n )}\n </SectionCard>\n\n <SectionCard title={t('ggnomad.providerAnalytics.topListings', 'Top listings')}>\n {analytics.topListings.length === 0 ? (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n {t(\n 'ggnomad.providerAnalytics.topListingsEmpty',\n 'No listing bookings in this range yet.'\n )}\n </p>\n ) : (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[560px] text-sm\">\n <thead>\n <tr className=\"border-b border-border-seam text-left text-text-secondary\">\n <th className=\"py-2 pr-3 font-medium\">\n {t('ggnomad.providerAnalytics.listing', 'Listing')}\n </th>\n <th className=\"py-2 pr-3 font-medium\">\n {t('ggnomad.providerAnalytics.type', 'Type')}\n </th>\n <th className=\"py-2 pr-3 text-right font-medium\">\n {t('ggnomad.providerAnalytics.bookings', 'Bookings')}\n </th>\n <th className=\"py-2 pr-3 text-right font-medium\">\n {t('ggnomad.providerAnalytics.revenue', 'Revenue')}\n </th>\n <th className=\"py-2 pr-3 text-right font-medium\">\n {t('ggnomad.providerAnalytics.cancellation', 'Cancellation')}\n </th>\n <th className=\"py-2 text-right font-medium\">\n {t('ggnomad.providerAnalytics.utilization', 'Utilization')}\n </th>\n </tr>\n </thead>\n <tbody>\n {analytics.topListings.map((listing) => (\n <tr\n key={listing.listingId}\n className=\"border-b border-border-seam/50 last:border-0\"\n >\n <td className=\"py-2 pr-3 font-medium text-text-primary\">\n {listing.listingName}\n </td>\n <td className=\"py-2 pr-3 text-text-secondary\">{listing.listingType}</td>\n <td className=\"py-2 pr-3 text-right tabular-nums text-text-primary\">\n {listing.bookings}\n </td>\n <td className=\"py-2 pr-3 text-right tabular-nums text-text-primary\">\n ${listing.revenue.toLocaleString()}\n </td>\n <td className=\"py-2 pr-3 text-right tabular-nums text-text-primary\">\n {(listing.cancellationRate * 100).toFixed(0)}%\n </td>\n <td className=\"py-2 text-right tabular-nums text-text-primary\">\n {(listing.capacityUtilizationRate * 100).toFixed(0)}%\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </SectionCard>\n </>\n )}\n </>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProviderAnalyticsPage.js","names":[],"sources":["../../../src/ggnomad/pages/ProviderAnalyticsPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { BarChart3, CalendarCheck, DollarSign, Gauge, TrendingUp, XCircle } from 'lucide-react';\nimport { IllustratedEmptyState, PagePurpose, Spinner } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_MY_PROVIDERS_QUERY, GGNOMAD_MY_PROVIDER_ANALYTICS_QUERY } from '../graphql';\nimport { PageShell } from './PageShell';\nimport { SectionCard, StatCard } from '../components';\n\nconst CHART_COLORS = [\n 'var(--color-chart-1)',\n 'var(--color-chart-2)',\n 'var(--color-chart-3)',\n 'var(--color-chart-4)',\n 'var(--color-chart-5)',\n];\n\nconst 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\ntype MyProvider = { id: string; name: string; type: string; status: string };\n\ntype AnalyticsPoint = { period: string; bookings: number; revenue: number };\n\ntype ListingAnalytics = {\n listingId: string;\n listingType: string;\n listingName: string;\n bookings: number;\n revenue: number;\n cancellationRate: number;\n capacityUtilizationRate: number;\n};\n\ntype ProviderAnalytics = {\n providerId: string;\n providerName: string;\n totalBookings: number;\n totalRevenue: number;\n cancelledBookings: number;\n cancellationRate: number;\n capacityUtilizationRate: number;\n bookingsOverTime: AnalyticsPoint[];\n topListings: ListingAnalytics[];\n};\n\nfunction toISODate(date: Date): string {\n return date.toISOString().slice(0, 10);\n}\n\nfunction daysAgo(days: number): Date {\n return new Date(Date.now() - days * 24 * 60 * 60 * 1000);\n}\n\n/**\n * BOFF-4390 — provider-facing analytics dashboard: bookings-over-time,\n * revenue, cancellation rate and capacity utilization, plus a per-listing\n * drill-down table. Self-scoped: only providers the calling actor created\n * are listed, and the analytics call is owner-verified server-side — a\n * provider can never see another provider's numbers. Forecasting and\n * custom-report building are explicitly out of scope for this core; see the\n * `myGgnomadProviderAnalytics` schema doc comment.\n */\nexport function ProviderAnalyticsPage() {\n const { t } = useI18n();\n\n const { data: providersData, loading: providersLoading } = useQuery(\n GGNOMAD_MY_PROVIDERS_QUERY as TypedDocumentNode<\n { myGgnomadProviders: MyProvider[] },\n Record<string, never>\n >\n );\n const providers = providersData?.myGgnomadProviders ?? [];\n\n const [selectedProviderId, setSelectedProviderId] = useState('');\n // `dateFrom`/`dateTo` are the plain YYYY-MM-DD strings bound to the <input\n // type=\"date\"> controls. They are NOT sent to GraphQL as-is: a bare date\n // string parses to UTC midnight, so using \"today\" as dateTo would exclude\n // every booking made later that same day. The query variables below\n // convert dateFrom to the start of that UTC day and dateTo to the end of\n // it, so \"today\" always includes today's activity.\n const [dateFrom, setDateFrom] = useState(() => toISODate(daysAgo(30)));\n const [dateTo, setDateTo] = useState(() => toISODate(new Date()));\n const [granularity, setGranularity] = useState<'day' | 'week' | 'month'>('day');\n\n const activeProviderId = selectedProviderId || providers[0]?.id || '';\n\n const { data: analyticsData, loading: analyticsLoading } = useQuery(\n GGNOMAD_MY_PROVIDER_ANALYTICS_QUERY as TypedDocumentNode<\n { myGgnomadProviderAnalytics: ProviderAnalytics },\n { providerId: string; dateFrom?: string; dateTo?: string; granularity?: string }\n >,\n {\n variables: {\n providerId: activeProviderId,\n dateFrom: `${dateFrom}T00:00:00.000Z`,\n dateTo: `${dateTo}T23:59:59.999Z`,\n granularity,\n },\n skip: !activeProviderId,\n }\n );\n const analytics = analyticsData?.myGgnomadProviderAnalytics;\n\n const maxTrend = useMemo(() => {\n const points = analytics?.bookingsOverTime ?? [];\n return points.length > 0 ? Math.max(...points.map((p) => p.bookings)) : 0;\n }, [analytics?.bookingsOverTime]);\n\n return (\n <PageShell\n title={t('ggnomad.providerAnalytics.title', 'Provider Analytics')}\n titleAccent={t('ggnomad.providerAnalytics.title.accent', 'Analytics')}\n subtitle={t(\n 'ggnomad.providerAnalytics.subtitle',\n 'Your bookings, revenue and listing performance'\n )}\n >\n <PagePurpose icon={<TrendingUp className=\"size-4\" />}>\n {t(\n 'ggnomad.providerAnalytics.purpose',\n 'A self-service dashboard for providers: bookings and revenue over time, cancellation rate, capacity utilization, and a per-listing breakdown — scoped to only the listings you own. Pick a date range and a listing granularity to drill in.'\n )}\n </PagePurpose>\n\n {!providersLoading && providers.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={t('ggnomad.providerAnalytics.emptyTitle', 'No provider profile yet')}\n description={t(\n 'ggnomad.providerAnalytics.emptyDescription',\n 'Once you create a GGNomad provider profile, your bookings and revenue analytics will appear here.'\n )}\n />\n ) : (\n <>\n <SectionCard title={t('ggnomad.providerAnalytics.filters', 'Filters')}>\n <div className=\"grid gap-3 sm:grid-cols-4\">\n <div>\n <label\n htmlFor=\"provider-analytics-provider\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.providerAnalytics.provider', 'Provider')}\n </label>\n <select\n id=\"provider-analytics-provider\"\n value={activeProviderId}\n onChange={(e) => setSelectedProviderId(e.target.value)}\n className={INPUT_CLASSES}\n >\n {providers.map((provider) => (\n <option key={provider.id} value={provider.id}>\n {provider.name}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label\n htmlFor=\"provider-analytics-from\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.providerAnalytics.from', 'From')}\n </label>\n <input\n id=\"provider-analytics-from\"\n type=\"date\"\n value={dateFrom}\n max={dateTo}\n onChange={(e) => setDateFrom(e.target.value)}\n className={INPUT_CLASSES}\n />\n </div>\n <div>\n <label\n htmlFor=\"provider-analytics-to\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.providerAnalytics.to', 'To')}\n </label>\n <input\n id=\"provider-analytics-to\"\n type=\"date\"\n value={dateTo}\n min={dateFrom}\n max={toISODate(new Date())}\n onChange={(e) => setDateTo(e.target.value)}\n className={INPUT_CLASSES}\n />\n </div>\n <div>\n <label\n htmlFor=\"provider-analytics-granularity\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.providerAnalytics.granularity', 'Granularity')}\n </label>\n <select\n id=\"provider-analytics-granularity\"\n value={granularity}\n onChange={(e) => setGranularity(e.target.value as 'day' | 'week' | 'month')}\n className={INPUT_CLASSES}\n >\n <option value=\"day\">{t('ggnomad.providerAnalytics.day', 'Day')}</option>\n <option value=\"week\">{t('ggnomad.providerAnalytics.week', 'Week')}</option>\n <option value=\"month\">{t('ggnomad.providerAnalytics.month', 'Month')}</option>\n </select>\n </div>\n </div>\n </SectionCard>\n\n {analyticsLoading ? (\n <div className=\"flex justify-center py-12\">\n <Spinner size=\"lg\" />\n </div>\n ) : !analytics ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={t('ggnomad.providerAnalytics.noDataTitle', 'No bookings in this range')}\n description={t(\n 'ggnomad.providerAnalytics.noDataDescription',\n 'Try widening the date range — analytics appear here once bookings come in for your listings.'\n )}\n />\n ) : (\n <>\n <section className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n hero\n label={t('ggnomad.providerAnalytics.totalBookings', 'Bookings')}\n value={analytics.totalBookings.toLocaleString()}\n icon={<CalendarCheck />}\n />\n <StatCard\n label={t('ggnomad.providerAnalytics.totalRevenue', 'Revenue')}\n value={`$${Number(analytics.totalRevenue).toLocaleString()}`}\n icon={<DollarSign />}\n />\n <StatCard\n label={t('ggnomad.providerAnalytics.cancellationRate', 'Cancellation rate')}\n value={`${(analytics.cancellationRate * 100).toFixed(1)}%`}\n icon={<XCircle />}\n />\n <StatCard\n label={t('ggnomad.providerAnalytics.capacityUtilization', 'Capacity utilization')}\n value={`${(analytics.capacityUtilizationRate * 100).toFixed(1)}%`}\n icon={<Gauge />}\n />\n </section>\n\n <SectionCard\n title={t('ggnomad.providerAnalytics.bookingsOverTime', 'Bookings over time')}\n >\n {analytics.bookingsOverTime.length === 0 ? (\n <div className=\"flex flex-col items-center gap-2 py-8 text-center\">\n <span className=\"flex size-10 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-text-secondary\">\n <BarChart3 className=\"size-5\" />\n </span>\n <p className=\"text-sm font-medium text-text-primary\">\n {t('ggnomad.providerAnalytics.trendEmpty', 'No bookings in this window yet')}\n </p>\n </div>\n ) : (\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex items-end gap-2 overflow-x-auto pb-2\">\n {analytics.bookingsOverTime.map((point, index) => {\n const pct =\n maxTrend > 0 ? Math.max((point.bookings / maxTrend) * 100, 5) : 5;\n return (\n <div key={point.period} className=\"flex flex-col items-center gap-1\">\n <div className=\"flex h-24 items-end\">\n <div\n className=\"w-8 rounded-t transition-all duration-500 ease-out\"\n style={{\n height: `${pct}%`,\n backgroundColor: CHART_COLORS[index % CHART_COLORS.length],\n }}\n title={`${point.period}: ${point.bookings} bookings, $${point.revenue.toLocaleString()} revenue`}\n />\n </div>\n <p className=\"truncate text-xs text-text-secondary\">{point.period}</p>\n </div>\n );\n })}\n </div>\n <p className=\"text-xs text-text-secondary\">\n {t(\n 'ggnomad.providerAnalytics.trendNote',\n 'Bar height is booking volume per period; hover a bar for its exact bookings and revenue.'\n )}\n </p>\n </div>\n )}\n </SectionCard>\n\n <SectionCard title={t('ggnomad.providerAnalytics.topListings', 'Top listings')}>\n {analytics.topListings.length === 0 ? (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n {t(\n 'ggnomad.providerAnalytics.topListingsEmpty',\n 'No listing bookings in this range yet.'\n )}\n </p>\n ) : (\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[560px] text-sm\">\n <thead>\n <tr className=\"border-b border-border-seam text-left text-text-secondary\">\n <th className=\"py-2 pr-3 font-medium\">\n {t('ggnomad.providerAnalytics.listing', 'Listing')}\n </th>\n <th className=\"py-2 pr-3 font-medium\">\n {t('ggnomad.providerAnalytics.type', 'Type')}\n </th>\n <th className=\"py-2 pr-3 text-right font-medium\">\n {t('ggnomad.providerAnalytics.bookings', 'Bookings')}\n </th>\n <th className=\"py-2 pr-3 text-right font-medium\">\n {t('ggnomad.providerAnalytics.revenue', 'Revenue')}\n </th>\n <th className=\"py-2 pr-3 text-right font-medium\">\n {t('ggnomad.providerAnalytics.cancellation', 'Cancellation')}\n </th>\n <th className=\"py-2 text-right font-medium\">\n {t('ggnomad.providerAnalytics.utilization', 'Utilization')}\n </th>\n </tr>\n </thead>\n <tbody>\n {analytics.topListings.map((listing) => (\n <tr\n key={listing.listingId}\n className=\"border-b border-border-seam/50 last:border-0\"\n >\n <td className=\"py-2 pr-3 font-medium text-text-primary\">\n {listing.listingName}\n </td>\n <td className=\"py-2 pr-3 text-text-secondary\">{listing.listingType}</td>\n <td className=\"py-2 pr-3 text-right tabular-nums text-text-primary\">\n {listing.bookings}\n </td>\n <td className=\"py-2 pr-3 text-right tabular-nums text-text-primary\">\n ${listing.revenue.toLocaleString()}\n </td>\n <td className=\"py-2 pr-3 text-right tabular-nums text-text-primary\">\n {(listing.cancellationRate * 100).toFixed(0)}%\n </td>\n <td className=\"py-2 text-right tabular-nums text-text-primary\">\n {(listing.capacityUtilizationRate * 100).toFixed(0)}%\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </SectionCard>\n </>\n )}\n </>\n )}\n </PageShell>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAWA,IAAM,IAAe;CACnB;CACA;CACA;CACA;CACA;AACF,GAEM,IACJ;AA4BF,SAAS,EAAU,GAAoB;CACrC,OAAO,EAAK,YAAY,CAAC,CAAC,MAAM,GAAG,EAAE;AACvC;AAEA,SAAS,EAAQ,GAAoB;CACnC,uBAAO,IAAI,KAAK,KAAK,IAAI,IAAI,IAAO,KAAK,KAAK,KAAK,GAAI;AACzD;AAWA,SAAgB,IAAwB;CACtC,IAAM,EAAE,SAAM,EAAQ,GAEhB,EAAE,MAAM,GAAe,SAAS,MAAqB,EACzD,CAIF,GACM,IAAY,GAAe,sBAAsB,CAAC,GAElD,CAAC,GAAoB,KAAyB,EAAS,EAAE,GAOzD,CAAC,GAAU,KAAe,QAAe,EAAU,EAAQ,EAAE,CAAC,CAAC,GAC/D,CAAC,GAAQ,KAAa,QAAe,kBAAU,IAAI,KAAK,CAAC,CAAC,GAC1D,CAAC,GAAa,KAAkB,EAAmC,KAAK,GAExE,IAAmB,KAAsB,EAAU,EAAE,EAAE,MAAM,IAE7D,EAAE,MAAM,GAAe,SAAS,MAAqB,EACzD,GAIA;EACE,WAAW;GACT,YAAY;GACZ,UAAU,GAAG,EAAS;GACtB,QAAQ,GAAG,EAAO;GAClB;EACF;EACA,MAAM,CAAC;CACT,CACF,GACM,IAAY,GAAe,4BAE3B,IAAW,QAAc;EAC7B,IAAM,IAAS,GAAW,oBAAoB,CAAC;EAC/C,OAAO,EAAO,SAAS,IAAI,KAAK,IAAI,GAAG,EAAO,KAAK,MAAM,EAAE,QAAQ,CAAC,IAAI;CAC1E,GAAG,CAAC,GAAW,gBAAgB,CAAC;CAEhC,OACE,kBAAC,GAAD;EACE,OAAO,EAAE,mCAAmC,oBAAoB;EAChE,aAAa,EAAE,0CAA0C,WAAW;EACpE,UAAU,EACR,sCACA,gDACF;YANF,CAQE,kBAAC,GAAD;GAAa,MAAM,kBAAC,GAAD,EAAY,WAAU,SAAU,CAAA;aAChD,EACC,qCACA,8OACF;EACW,CAAA,GAEZ,CAAC,KAAoB,EAAU,WAAW,IACzC,kBAAC,GAAD;GACE,cAAa;GACb,OAAO,EAAE,wCAAwC,yBAAyB;GAC1E,aAAa,EACX,8CACA,mGACF;EACD,CAAA,IAED,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;GAAa,OAAO,EAAE,qCAAqC,SAAS;aAClE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MACE,SAAQ;MACR,WAAU;gBAET,EAAE,sCAAsC,UAAU;KAC9C,CAAA,GACP,kBAAC,UAAD;MACE,IAAG;MACH,OAAO;MACP,WAAW,MAAM,EAAsB,EAAE,OAAO,KAAK;MACrD,WAAW;gBAEV,EAAU,KAAK,MACd,kBAAC,UAAD;OAA0B,OAAO,EAAS;iBACvC,EAAS;MACJ,GAFK,EAAS,EAEd,CACT;KACK,CAAA,CACL,EAAA,CAAA;KACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MACE,SAAQ;MACR,WAAU;gBAET,EAAE,kCAAkC,MAAM;KACtC,CAAA,GACP,kBAAC,SAAD;MACE,IAAG;MACH,MAAK;MACL,OAAO;MACP,KAAK;MACL,WAAW,MAAM,EAAY,EAAE,OAAO,KAAK;MAC3C,WAAW;KACZ,CAAA,CACE,EAAA,CAAA;KACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MACE,SAAQ;MACR,WAAU;gBAET,EAAE,gCAAgC,IAAI;KAClC,CAAA,GACP,kBAAC,SAAD;MACE,IAAG;MACH,MAAK;MACL,OAAO;MACP,KAAK;MACL,KAAK,kBAAU,IAAI,KAAK,CAAC;MACzB,WAAW,MAAM,EAAU,EAAE,OAAO,KAAK;MACzC,WAAW;KACZ,CAAA,CACE,EAAA,CAAA;KACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MACE,SAAQ;MACR,WAAU;gBAET,EAAE,yCAAyC,aAAa;KACpD,CAAA,GACP,kBAAC,UAAD;MACE,IAAG;MACH,OAAO;MACP,WAAW,MAAM,EAAe,EAAE,OAAO,KAAiC;MAC1E,WAAW;gBAJb;OAME,kBAAC,UAAD;QAAQ,OAAM;kBAAO,EAAE,iCAAiC,KAAK;OAAU,CAAA;OACvE,kBAAC,UAAD;QAAQ,OAAM;kBAAQ,EAAE,kCAAkC,MAAM;OAAU,CAAA;OAC1E,kBAAC,UAAD;QAAQ,OAAM;kBAAS,EAAE,mCAAmC,OAAO;OAAU,CAAA;MACvE;OACL,EAAA,CAAA;IACF;;EACM,CAAA,GAEZ,IACC,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,GAAD,EAAS,MAAK,KAAM,CAAA;EACjB,CAAA,IACF,IAUH,kBAAA,GAAA,EAAA,UAAA;GACE,kBAAC,WAAD;IAAS,WAAU;cAAnB;KACE,kBAAC,GAAD;MACE,MAAA;MACA,OAAO,EAAE,2CAA2C,UAAU;MAC9D,OAAO,EAAU,cAAc,eAAe;MAC9C,MAAM,kBAAC,GAAD,CAAgB,CAAA;KACvB,CAAA;KACD,kBAAC,GAAD;MACE,OAAO,EAAE,0CAA0C,SAAS;MAC5D,OAAO,IAAI,OAAO,EAAU,YAAY,CAAC,CAAC,eAAe;MACzD,MAAM,kBAAC,GAAD,CAAa,CAAA;KACpB,CAAA;KACD,kBAAC,GAAD;MACE,OAAO,EAAE,8CAA8C,mBAAmB;MAC1E,OAAO,IAAI,EAAU,mBAAmB,IAAA,CAAK,QAAQ,CAAC,EAAE;MACxD,MAAM,kBAAC,GAAD,CAAU,CAAA;KACjB,CAAA;KACD,kBAAC,GAAD;MACE,OAAO,EAAE,iDAAiD,sBAAsB;MAChF,OAAO,IAAI,EAAU,0BAA0B,IAAA,CAAK,QAAQ,CAAC,EAAE;MAC/D,MAAM,kBAAC,GAAD,CAAQ,CAAA;KACf,CAAA;IACM;;GAET,kBAAC,GAAD;IACE,OAAO,EAAE,8CAA8C,oBAAoB;cAE1E,EAAU,iBAAiB,WAAW,IACrC,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MAAM,WAAU;gBACd,kBAAC,GAAD,EAAW,WAAU,SAAU,CAAA;KAC3B,CAAA,GACN,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAE,wCAAwC,gCAAgC;KAC1E,CAAA,CACA;SAEL,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAU,iBAAiB,KAAK,GAAO,MAIpC,kBAAC,OAAD;OAAwB,WAAU;iBAAlC,CACE,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,OAAD;SACE,WAAU;SACV,OAAO;UACL,QAAQ,GAPhB,IAAW,IAAI,KAAK,IAAK,EAAM,WAAW,IAAY,KAAK,CAAC,IAAI,EAOzC;UACf,iBAAiB,EAAa,IAAQ,EAAa;SACrD;SACA,OAAO,GAAG,EAAM,OAAO,IAAI,EAAM,SAAS,cAAc,EAAM,QAAQ,eAAe,EAAE;QACxF,CAAA;OACE,CAAA,GACL,kBAAC,KAAD;QAAG,WAAU;kBAAwC,EAAM;OAAU,CAAA,CAClE;SAZK,EAAM,MAYX,CAER;KACE,CAAA,GACL,kBAAC,KAAD;MAAG,WAAU;gBACV,EACC,uCACA,0FACF;KACC,CAAA,CACA;;GAEI,CAAA;GAEb,kBAAC,GAAD;IAAa,OAAO,EAAE,yCAAyC,cAAc;cAC1E,EAAU,YAAY,WAAW,IAChC,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,8CACA,wCACF;IACC,CAAA,IAEH,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,SAAD;MAAO,WAAU;gBAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;OAAI,WAAU;iBAAd;QACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,qCAAqC,SAAS;QAC/C,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,kCAAkC,MAAM;QACzC,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,sCAAsC,UAAU;QACjD,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,qCAAqC,SAAS;QAC/C,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,0CAA0C,cAAc;QACzD,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAE,yCAAyC,aAAa;QACvD,CAAA;OACF;SACC,CAAA,GACP,kBAAC,SAAD,EAAA,UACG,EAAU,YAAY,KAAK,MAC1B,kBAAC,MAAD;OAEE,WAAU;iBAFZ;QAIE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAQ;QACP,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBAAiC,EAAQ;QAAgB,CAAA;QACvE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAQ;QACP,CAAA;QACJ,kBAAC,MAAD;SAAI,WAAU;mBAAd,CAAoE,KAChE,EAAQ,QAAQ,eAAe,CAC/B;;QACJ,kBAAC,MAAD;SAAI,WAAU;mBAAd,EACI,EAAQ,mBAAmB,IAAA,CAAK,QAAQ,CAAC,GAAE,GAC3C;;QACJ,kBAAC,MAAD;SAAI,WAAU;mBAAd,EACI,EAAQ,0BAA0B,IAAA,CAAK,QAAQ,CAAC,GAAE,GAClD;;OACF;SAnBG,EAAQ,SAmBX,CACL,EACI,CAAA,CACF;;IACJ,CAAA;GAEI,CAAA;EACb,EAAA,CAAA,IA7IF,kBAAC,GAAD;GACE,cAAa;GACb,OAAO,EAAE,yCAAyC,2BAA2B;GAC7E,aAAa,EACX,+CACA,8FACF;EACD,CAAA,CAwIH,EAAA,CAAA,CAEK;;AAEf"}
|
|
@@ -72,7 +72,7 @@ function A() {
|
|
|
72
72
|
B.data?.ggnomadActivities
|
|
73
73
|
]), U = f(() => {
|
|
74
74
|
let e = A.trim().toLowerCase();
|
|
75
|
-
return H.filter((t) => M !== "ALL" && t.domain !== M ? !1 : e
|
|
75
|
+
return H.filter((t) => M !== "ALL" && t.domain !== M ? !1 : !e || t.haystack.includes(e));
|
|
76
76
|
}, [
|
|
77
77
|
H,
|
|
78
78
|
A,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SearchPage.js","names":[],"sources":["../../../src/ggnomad/pages/SearchPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Activity, Building, Compass, Search, Ticket, X } from 'lucide-react';\nimport { EmphasisPanel, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport {\n GGNOMAD_PROPERTIES_QUERY,\n GGNOMAD_TOURS_QUERY,\n GGNOMAD_EVENTS_QUERY,\n GGNOMAD_ACTIVITIES_QUERY,\n} from '../graphql';\nimport { humanizeEnum } from '../utils/format';\nimport { cn } from '../utils/cn';\nimport { PageShell } from './PageShell';\nimport { EntityCard, LoadMoreButton, RatingSummary, SectionCard, StatCard } from '../components';\n\ntype Domain = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ntype CatalogueRow = {\n id: string;\n name: string;\n description?: string | null;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n images?: string[];\n};\n\ntype PropertyRow = CatalogueRow & { type: string; pricePerNight: number };\ntype TourRow = CatalogueRow & { type: string; price: number; duration: string };\ntype EventRow = CatalogueRow & { category: string; price: number };\ntype ActivityRow = CatalogueRow & { category: string; price: number; duration: string };\n\ntype Result = {\n key: string;\n domain: Domain;\n id: string;\n name: string;\n /** The listing's own category / type label, used both for display and matching. */\n kind: string;\n price: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n image?: string;\n haystack: string;\n /** Route relative to the GGNomad base path. */\n path: string;\n};\n\nconst PAGE_SIZE = 24;\n\nconst DOMAIN_ROUTE: Record<Domain, string> = {\n PROPERTY: '../hospitality',\n TOUR: '../tourism/tours',\n EVENT: '../entertainment',\n ACTIVITY: '../tourism/activities',\n};\n\nfunction toResult(domain: Domain, row: CatalogueRow & { price: number; kind: string }): Result {\n return {\n key: `${domain}-${row.id}`,\n domain,\n id: row.id,\n name: row.name,\n kind: row.kind,\n price: row.price,\n currency: row.currency,\n rating: row.rating,\n reviewCount: row.reviewCount,\n image: row.images?.[0],\n haystack: [row.name, row.kind, row.description ?? ''].join(' ').toLowerCase(),\n path: `${DOMAIN_ROUTE[domain]}/${row.id}`,\n };\n}\n\nexport function SearchPage() {\n const { t } = useI18n();\n const [term, setTerm] = useState('');\n const [domain, setDomain] = useState<Domain | 'ALL'>('ALL');\n const [limit, setLimit] = useState(PAGE_SIZE);\n\n const variables = { limit, offset: 0 };\n const properties = useQuery(\n GGNOMAD_PROPERTIES_QUERY as TypedDocumentNode<\n { ggnomadProperties: PropertyRow[] },\n { limit: number; offset: number }\n >,\n { variables }\n );\n const tours = useQuery(\n GGNOMAD_TOURS_QUERY as TypedDocumentNode<\n { ggnomadTours: TourRow[] },\n { limit: number; offset: number }\n >,\n { variables }\n );\n const events = useQuery(\n GGNOMAD_EVENTS_QUERY as TypedDocumentNode<\n { ggnomadEvents: EventRow[] },\n { limit: number; offset: number }\n >,\n { variables }\n );\n const activities = useQuery(\n GGNOMAD_ACTIVITIES_QUERY as TypedDocumentNode<\n { ggnomadActivities: ActivityRow[] },\n { limit: number; offset: number }\n >,\n { variables }\n );\n\n const loading = properties.loading || tours.loading || events.loading || activities.loading;\n\n const allResults = useMemo<Result[]>(() => {\n const rows: Result[] = [];\n for (const property of properties.data?.ggnomadProperties ?? []) {\n rows.push(\n toResult('PROPERTY', { ...property, price: property.pricePerNight, kind: property.type })\n );\n }\n for (const tour of tours.data?.ggnomadTours ?? []) {\n rows.push(toResult('TOUR', { ...tour, kind: tour.type }));\n }\n for (const event of events.data?.ggnomadEvents ?? []) {\n rows.push(toResult('EVENT', { ...event, kind: event.category }));\n }\n for (const activity of activities.data?.ggnomadActivities ?? []) {\n rows.push(toResult('ACTIVITY', { ...activity, kind: activity.category }));\n }\n return rows;\n }, [\n properties.data?.ggnomadProperties,\n tours.data?.ggnomadTours,\n events.data?.ggnomadEvents,\n activities.data?.ggnomadActivities,\n ]);\n\n const matches = useMemo(() => {\n const needle = term.trim().toLowerCase();\n return allResults.filter((result) => {\n if (domain !== 'ALL' && result.domain !== domain) return false;\n if (!needle) return true;\n return result.haystack.includes(needle);\n });\n }, [allResults, term, domain]);\n\n const countFor = (target: Domain) =>\n allResults.filter(\n (result) =>\n result.domain === target &&\n (!term.trim() || result.haystack.includes(term.trim().toLowerCase()))\n ).length;\n\n const propertyCount = countFor('PROPERTY');\n const tourCount = countFor('TOUR');\n const eventCount = countFor('EVENT');\n const activityCount = countFor('ACTIVITY');\n const total = propertyCount + tourCount + eventCount + activityCount;\n\n // Every domain query asks for the same page size, so any list that came back\n // full may have more behind it.\n const mayHaveMore = [\n properties.data?.ggnomadProperties,\n tours.data?.ggnomadTours,\n events.data?.ggnomadEvents,\n activities.data?.ggnomadActivities,\n ].some((rows) => (rows?.length ?? 0) >= limit);\n\n const domainLabel = (value: Domain): string => {\n switch (value) {\n case 'PROPERTY':\n return t('ggnomad.search.domain.property', 'Property');\n case 'TOUR':\n return t('ggnomad.search.domain.tour', 'Tour');\n case 'EVENT':\n return t('ggnomad.search.domain.event', 'Event');\n case 'ACTIVITY':\n return t('ggnomad.search.domain.activity', 'Activity');\n }\n };\n\n const domainChips: Array<{ value: Domain | 'ALL'; label: string; count: number }> = [\n { value: 'ALL', label: t('ggnomad.search.domain.all', 'All'), count: total },\n {\n value: 'PROPERTY',\n label: t('ggnomad.search.results.properties', 'Properties'),\n count: propertyCount,\n },\n { value: 'TOUR', label: t('ggnomad.search.results.tours', 'Tours'), count: tourCount },\n { value: 'EVENT', label: t('ggnomad.search.results.events', 'Events'), count: eventCount },\n {\n value: 'ACTIVITY',\n label: t('ggnomad.search.results.activities', 'Activities'),\n count: activityCount,\n },\n ];\n\n return (\n <PageShell\n title={t('ggnomad.search.title', 'Global Search')}\n titleAccent={t('ggnomad.search.title.accent', 'Search')}\n subtitle={t('ggnomad.search.subtitle', 'Cross-domain search across all GGNomad inventory')}\n >\n <PagePurpose icon={<Search className=\"size-4\" />}>\n {t(\n 'ggnomad.search.purpose',\n 'One search across the entire GGNomad catalogue — properties, tours, events and activities at once — so you can see everything available for a destination in a single view instead of checking each domain separately.'\n )}\n </PagePurpose>\n\n <div>\n <label htmlFor=\"ggnomad-search-term\" className=\"sr-only\">\n {t('ggnomad.search.inputLabel', 'Search the catalogue')}\n </label>\n <div className=\"relative\">\n <Search\n className=\"pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n id=\"ggnomad-search-term\"\n type=\"search\"\n value={term}\n onChange={(event) => setTerm(event.target.value)}\n placeholder={t(\n 'ggnomad.search.placeholder',\n 'Search names, categories and descriptions…'\n )}\n className=\"w-full rounded-lg border border-border-seam bg-bg-canvas py-2.5 pl-9 pr-10 text-sm text-text-primary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg\"\n />\n {term ? (\n <button\n type=\"button\"\n onClick={() => setTerm('')}\n aria-label={t('ggnomad.search.clear', 'Clear search')}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n >\n <X className=\"size-4\" />\n </button>\n ) : null}\n </div>\n <p className=\"mt-2 text-xs text-text-secondary\">\n {t(\n 'ggnomad.search.scopeNote',\n 'Matches against the catalogue loaded on this page. Use “Load more” to pull further inventory into the search.'\n )}\n </p>\n </div>\n\n <ul className=\"flex flex-wrap gap-2\">\n {domainChips.map((chip) => (\n <li key={chip.value}>\n <button\n type=\"button\"\n aria-pressed={domain === chip.value}\n onClick={() => setDomain(chip.value)}\n className={cn(\n 'rounded-full border px-3 py-1 text-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg',\n domain === chip.value\n ? 'border-action-primary-bg bg-action-primary-bg text-action-primary-text'\n : 'border-border-seam bg-bg-sunken text-text-primary hover:border-border-default'\n )}\n >\n {chip.label}\n <span className=\"ml-1.5 tabular-nums opacity-80\">{chip.count}</span>\n </button>\n </li>\n ))}\n </ul>\n\n <EmphasisPanel>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.search.totalMatches', 'Total matches across all domains')}\n </p>\n <p className=\"mt-1 text-3xl font-semibold tabular-nums text-text-primary\">\n {total.toLocaleString()}\n </p>\n <p className=\"mt-2 text-sm text-text-secondary\">\n {t(\n 'ggnomad.search.totalMatchesNote',\n 'Combined inventory from properties, tours, events and activities — open a domain below to drill in.'\n )}\n </p>\n </EmphasisPanel>\n\n <section className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n label={t('ggnomad.search.results.propertiesFound', 'Properties found')}\n value={propertyCount}\n icon={<Building />}\n />\n <StatCard\n label={t('ggnomad.search.results.toursFound', 'Tours found')}\n value={tourCount}\n icon={<Compass />}\n />\n <StatCard\n label={t('ggnomad.search.results.eventsFound', 'Events found')}\n value={eventCount}\n icon={<Ticket />}\n />\n <StatCard\n label={t('ggnomad.search.results.activitiesFound', 'Activities found')}\n value={activityCount}\n icon={<Activity />}\n />\n </section>\n\n {matches.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title={\n loading\n ? t('ggnomad.search.loadingTitle', 'Searching the catalogue…')\n : t('ggnomad.search.emptyTitle', 'No matching inventory')\n }\n description={\n loading\n ? t(\n 'ggnomad.search.loadingDescription',\n 'Fetching properties, tours, events and activities.'\n )\n : term.trim()\n ? t(\n 'ggnomad.search.noTermMatch',\n 'Nothing matched “{{term}}”. Try a shorter term or another domain.'\n ).replace('{{term}}', term.trim())\n : t(\n 'ggnomad.search.emptyDescription',\n 'Nothing matched across properties, tours, events and activities yet.'\n )\n }\n />\n ) : (\n <SectionCard title={t('ggnomad.search.resultsTitle', 'Results')}>\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {matches.map((result) => (\n <EntityCard\n key={result.key}\n to={result.path}\n image={result.image}\n imageAlt={result.name}\n title={result.name}\n meta={humanizeEnum(result.kind)}\n badge={\n <span className=\"rounded-full bg-bg-surface/90 px-2.5 py-0.5 text-xs font-medium text-text-primary\">\n {domainLabel(result.domain)}\n </span>\n }\n price={`${result.currency} ${Number(result.price).toFixed(2)}`}\n footer={<RatingSummary rating={result.rating} reviewCount={result.reviewCount} />}\n />\n ))}\n </div>\n <LoadMoreButton\n visible={mayHaveMore}\n onLoadMore={() => setLimit((current) => current + PAGE_SIZE)}\n />\n </SectionCard>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SearchPage.js","names":[],"sources":["../../../src/ggnomad/pages/SearchPage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Activity, Building, Compass, Search, Ticket, X } from 'lucide-react';\nimport { EmphasisPanel, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport {\n GGNOMAD_PROPERTIES_QUERY,\n GGNOMAD_TOURS_QUERY,\n GGNOMAD_EVENTS_QUERY,\n GGNOMAD_ACTIVITIES_QUERY,\n} from '../graphql';\nimport { humanizeEnum } from '../utils/format';\nimport { cn } from '../utils/cn';\nimport { PageShell } from './PageShell';\nimport { EntityCard, LoadMoreButton, RatingSummary, SectionCard, StatCard } from '../components';\n\ntype Domain = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ntype CatalogueRow = {\n id: string;\n name: string;\n description?: string | null;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n images?: string[];\n};\n\ntype PropertyRow = CatalogueRow & { type: string; pricePerNight: number };\ntype TourRow = CatalogueRow & { type: string; price: number; duration: string };\ntype EventRow = CatalogueRow & { category: string; price: number };\ntype ActivityRow = CatalogueRow & { category: string; price: number; duration: string };\n\ntype Result = {\n key: string;\n domain: Domain;\n id: string;\n name: string;\n /** The listing's own category / type label, used both for display and matching. */\n kind: string;\n price: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n image?: string;\n haystack: string;\n /** Route relative to the GGNomad base path. */\n path: string;\n};\n\nconst PAGE_SIZE = 24;\n\nconst DOMAIN_ROUTE: Record<Domain, string> = {\n PROPERTY: '../hospitality',\n TOUR: '../tourism/tours',\n EVENT: '../entertainment',\n ACTIVITY: '../tourism/activities',\n};\n\nfunction toResult(domain: Domain, row: CatalogueRow & { price: number; kind: string }): Result {\n return {\n key: `${domain}-${row.id}`,\n domain,\n id: row.id,\n name: row.name,\n kind: row.kind,\n price: row.price,\n currency: row.currency,\n rating: row.rating,\n reviewCount: row.reviewCount,\n image: row.images?.[0],\n haystack: [row.name, row.kind, row.description ?? ''].join(' ').toLowerCase(),\n path: `${DOMAIN_ROUTE[domain]}/${row.id}`,\n };\n}\n\nexport function SearchPage() {\n const { t } = useI18n();\n const [term, setTerm] = useState('');\n const [domain, setDomain] = useState<Domain | 'ALL'>('ALL');\n const [limit, setLimit] = useState(PAGE_SIZE);\n\n const variables = { limit, offset: 0 };\n const properties = useQuery(\n GGNOMAD_PROPERTIES_QUERY as TypedDocumentNode<\n { ggnomadProperties: PropertyRow[] },\n { limit: number; offset: number }\n >,\n { variables }\n );\n const tours = useQuery(\n GGNOMAD_TOURS_QUERY as TypedDocumentNode<\n { ggnomadTours: TourRow[] },\n { limit: number; offset: number }\n >,\n { variables }\n );\n const events = useQuery(\n GGNOMAD_EVENTS_QUERY as TypedDocumentNode<\n { ggnomadEvents: EventRow[] },\n { limit: number; offset: number }\n >,\n { variables }\n );\n const activities = useQuery(\n GGNOMAD_ACTIVITIES_QUERY as TypedDocumentNode<\n { ggnomadActivities: ActivityRow[] },\n { limit: number; offset: number }\n >,\n { variables }\n );\n\n const loading = properties.loading || tours.loading || events.loading || activities.loading;\n\n const allResults = useMemo<Result[]>(() => {\n const rows: Result[] = [];\n for (const property of properties.data?.ggnomadProperties ?? []) {\n rows.push(\n toResult('PROPERTY', { ...property, price: property.pricePerNight, kind: property.type })\n );\n }\n for (const tour of tours.data?.ggnomadTours ?? []) {\n rows.push(toResult('TOUR', { ...tour, kind: tour.type }));\n }\n for (const event of events.data?.ggnomadEvents ?? []) {\n rows.push(toResult('EVENT', { ...event, kind: event.category }));\n }\n for (const activity of activities.data?.ggnomadActivities ?? []) {\n rows.push(toResult('ACTIVITY', { ...activity, kind: activity.category }));\n }\n return rows;\n }, [\n properties.data?.ggnomadProperties,\n tours.data?.ggnomadTours,\n events.data?.ggnomadEvents,\n activities.data?.ggnomadActivities,\n ]);\n\n const matches = useMemo(() => {\n const needle = term.trim().toLowerCase();\n return allResults.filter((result) => {\n if (domain !== 'ALL' && result.domain !== domain) return false;\n if (!needle) return true;\n return result.haystack.includes(needle);\n });\n }, [allResults, term, domain]);\n\n const countFor = (target: Domain) =>\n allResults.filter(\n (result) =>\n result.domain === target &&\n (!term.trim() || result.haystack.includes(term.trim().toLowerCase()))\n ).length;\n\n const propertyCount = countFor('PROPERTY');\n const tourCount = countFor('TOUR');\n const eventCount = countFor('EVENT');\n const activityCount = countFor('ACTIVITY');\n const total = propertyCount + tourCount + eventCount + activityCount;\n\n // Every domain query asks for the same page size, so any list that came back\n // full may have more behind it.\n const mayHaveMore = [\n properties.data?.ggnomadProperties,\n tours.data?.ggnomadTours,\n events.data?.ggnomadEvents,\n activities.data?.ggnomadActivities,\n ].some((rows) => (rows?.length ?? 0) >= limit);\n\n const domainLabel = (value: Domain): string => {\n switch (value) {\n case 'PROPERTY':\n return t('ggnomad.search.domain.property', 'Property');\n case 'TOUR':\n return t('ggnomad.search.domain.tour', 'Tour');\n case 'EVENT':\n return t('ggnomad.search.domain.event', 'Event');\n case 'ACTIVITY':\n return t('ggnomad.search.domain.activity', 'Activity');\n }\n };\n\n const domainChips: Array<{ value: Domain | 'ALL'; label: string; count: number }> = [\n { value: 'ALL', label: t('ggnomad.search.domain.all', 'All'), count: total },\n {\n value: 'PROPERTY',\n label: t('ggnomad.search.results.properties', 'Properties'),\n count: propertyCount,\n },\n { value: 'TOUR', label: t('ggnomad.search.results.tours', 'Tours'), count: tourCount },\n { value: 'EVENT', label: t('ggnomad.search.results.events', 'Events'), count: eventCount },\n {\n value: 'ACTIVITY',\n label: t('ggnomad.search.results.activities', 'Activities'),\n count: activityCount,\n },\n ];\n\n return (\n <PageShell\n title={t('ggnomad.search.title', 'Global Search')}\n titleAccent={t('ggnomad.search.title.accent', 'Search')}\n subtitle={t('ggnomad.search.subtitle', 'Cross-domain search across all GGNomad inventory')}\n >\n <PagePurpose icon={<Search className=\"size-4\" />}>\n {t(\n 'ggnomad.search.purpose',\n 'One search across the entire GGNomad catalogue — properties, tours, events and activities at once — so you can see everything available for a destination in a single view instead of checking each domain separately.'\n )}\n </PagePurpose>\n\n <div>\n <label htmlFor=\"ggnomad-search-term\" className=\"sr-only\">\n {t('ggnomad.search.inputLabel', 'Search the catalogue')}\n </label>\n <div className=\"relative\">\n <Search\n className=\"pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-text-secondary\"\n aria-hidden=\"true\"\n />\n <input\n id=\"ggnomad-search-term\"\n type=\"search\"\n value={term}\n onChange={(event) => setTerm(event.target.value)}\n placeholder={t(\n 'ggnomad.search.placeholder',\n 'Search names, categories and descriptions…'\n )}\n className=\"w-full rounded-lg border border-border-seam bg-bg-canvas py-2.5 pl-9 pr-10 text-sm text-text-primary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg\"\n />\n {term ? (\n <button\n type=\"button\"\n onClick={() => setTerm('')}\n aria-label={t('ggnomad.search.clear', 'Clear search')}\n className=\"absolute right-2 top-1/2 -translate-y-1/2 rounded-md p-1.5 text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n >\n <X className=\"size-4\" />\n </button>\n ) : null}\n </div>\n <p className=\"mt-2 text-xs text-text-secondary\">\n {t(\n 'ggnomad.search.scopeNote',\n 'Matches against the catalogue loaded on this page. Use “Load more” to pull further inventory into the search.'\n )}\n </p>\n </div>\n\n <ul className=\"flex flex-wrap gap-2\">\n {domainChips.map((chip) => (\n <li key={chip.value}>\n <button\n type=\"button\"\n aria-pressed={domain === chip.value}\n onClick={() => setDomain(chip.value)}\n className={cn(\n 'rounded-full border px-3 py-1 text-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-bg',\n domain === chip.value\n ? 'border-action-primary-bg bg-action-primary-bg text-action-primary-text'\n : 'border-border-seam bg-bg-sunken text-text-primary hover:border-border-default'\n )}\n >\n {chip.label}\n <span className=\"ml-1.5 tabular-nums opacity-80\">{chip.count}</span>\n </button>\n </li>\n ))}\n </ul>\n\n <EmphasisPanel>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.search.totalMatches', 'Total matches across all domains')}\n </p>\n <p className=\"mt-1 text-3xl font-semibold tabular-nums text-text-primary\">\n {total.toLocaleString()}\n </p>\n <p className=\"mt-2 text-sm text-text-secondary\">\n {t(\n 'ggnomad.search.totalMatchesNote',\n 'Combined inventory from properties, tours, events and activities — open a domain below to drill in.'\n )}\n </p>\n </EmphasisPanel>\n\n <section className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard\n label={t('ggnomad.search.results.propertiesFound', 'Properties found')}\n value={propertyCount}\n icon={<Building />}\n />\n <StatCard\n label={t('ggnomad.search.results.toursFound', 'Tours found')}\n value={tourCount}\n icon={<Compass />}\n />\n <StatCard\n label={t('ggnomad.search.results.eventsFound', 'Events found')}\n value={eventCount}\n icon={<Ticket />}\n />\n <StatCard\n label={t('ggnomad.search.results.activitiesFound', 'Activities found')}\n value={activityCount}\n icon={<Activity />}\n />\n </section>\n\n {matches.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title={\n loading\n ? t('ggnomad.search.loadingTitle', 'Searching the catalogue…')\n : t('ggnomad.search.emptyTitle', 'No matching inventory')\n }\n description={\n loading\n ? t(\n 'ggnomad.search.loadingDescription',\n 'Fetching properties, tours, events and activities.'\n )\n : term.trim()\n ? t(\n 'ggnomad.search.noTermMatch',\n 'Nothing matched “{{term}}”. Try a shorter term or another domain.'\n ).replace('{{term}}', term.trim())\n : t(\n 'ggnomad.search.emptyDescription',\n 'Nothing matched across properties, tours, events and activities yet.'\n )\n }\n />\n ) : (\n <SectionCard title={t('ggnomad.search.resultsTitle', 'Results')}>\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {matches.map((result) => (\n <EntityCard\n key={result.key}\n to={result.path}\n image={result.image}\n imageAlt={result.name}\n title={result.name}\n meta={humanizeEnum(result.kind)}\n badge={\n <span className=\"rounded-full bg-bg-surface/90 px-2.5 py-0.5 text-xs font-medium text-text-primary\">\n {domainLabel(result.domain)}\n </span>\n }\n price={`${result.currency} ${Number(result.price).toFixed(2)}`}\n footer={<RatingSummary rating={result.rating} reviewCount={result.reviewCount} />}\n />\n ))}\n </div>\n <LoadMoreButton\n visible={mayHaveMore}\n onLoadMore={() => setLimit((current) => current + PAGE_SIZE)}\n />\n </SectionCard>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StorefrontPage.js","names":[],"sources":["../../../src/ggnomad/pages/StorefrontPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useParams } from 'react-router';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Globe, Palette, Store } from 'lucide-react';\nimport { Button, GlassCard, Input, PagePurpose, Spinner, Textarea } from '@burdenoff/fe-libs/ui';\n\nimport {\n CREATE_GGNOMAD_OPERATOR_STOREFRONT_MUTATION,\n GGNOMAD_OPERATOR_STOREFRONT_QUERY,\n PUBLISH_GGNOMAD_OPERATOR_STOREFRONT_MUTATION,\n UNPUBLISH_GGNOMAD_OPERATOR_STOREFRONT_MUTATION,\n UPDATE_GGNOMAD_OPERATOR_STOREFRONT_MUTATION,\n} from '../graphql';\nimport { PageShell } from './PageShell';\nimport { notify } from '../utils/notify';\n\ntype GgnomadOperatorStorefront = {\n id: string;\n providerId: string;\n slug: string;\n displayName: string;\n logoUrl: string | null;\n primaryColor: string | null;\n aboutText: string | null;\n isPublished: boolean;\n publishedAt: string | null;\n};\n\nconst HEX_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;\nconst SLUG_PATTERN = /^[a-z0-9]([a-z0-9-]{1,78}[a-z0-9])?$/;\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 * BOFF-4363 — operator storefront settings: create/edit the branding\n * (logo/color/slug/about) for one provider and publish/unpublish it. The\n * right-hand panel renders a live, themeable preview of the branded page a\n * traveler would see at `ggnomadStorefrontBySlug` once published.\n *\n * Custom-domain DNS/SSL provisioning and a drag-and-drop page builder are\n * explicitly out of scope for this pass — see Linear BOFF-4363.\n */\nexport function StorefrontPage() {\n const { t } = useI18n();\n const { providerId = '' } = useParams();\n\n const { data, loading, refetch } = useQuery(\n GGNOMAD_OPERATOR_STOREFRONT_QUERY as TypedDocumentNode<\n { ggnomadOperatorStorefront: GgnomadOperatorStorefront | null },\n { providerId: string }\n >,\n { variables: { providerId }, skip: !providerId }\n );\n const storefront = data?.ggnomadOperatorStorefront ?? null;\n\n const [slug, setSlug] = useState('');\n const [displayName, setDisplayName] = useState('');\n const [logoUrl, setLogoUrl] = useState('');\n const [primaryColor, setPrimaryColor] = useState('#1E88E5');\n const [aboutText, setAboutText] = useState('');\n const [hydrated, setHydrated] = useState(false);\n\n if (storefront && !hydrated) {\n setSlug(storefront.slug);\n setDisplayName(storefront.displayName);\n setLogoUrl(storefront.logoUrl ?? '');\n setPrimaryColor(storefront.primaryColor ?? '#1E88E5');\n setAboutText(storefront.aboutText ?? '');\n setHydrated(true);\n }\n\n const [createStorefront, { loading: creating }] = useMutation(\n CREATE_GGNOMAD_OPERATOR_STOREFRONT_MUTATION as TypedDocumentNode<\n { createGgnomadOperatorStorefront: GgnomadOperatorStorefront },\n {\n input: {\n providerId: string;\n slug: string;\n displayName: string;\n logoUrl?: string | null;\n primaryColor?: string | null;\n aboutText?: string | null;\n };\n }\n >\n );\n const [updateStorefront, { loading: updating }] = useMutation(\n UPDATE_GGNOMAD_OPERATOR_STOREFRONT_MUTATION as TypedDocumentNode<\n { updateGgnomadOperatorStorefront: GgnomadOperatorStorefront },\n {\n id: string;\n input: {\n slug?: string;\n displayName?: string;\n logoUrl?: string | null;\n primaryColor?: string | null;\n aboutText?: string | null;\n };\n }\n >\n );\n const [publishStorefront, { loading: publishing }] = useMutation(\n PUBLISH_GGNOMAD_OPERATOR_STOREFRONT_MUTATION as TypedDocumentNode<\n { publishGgnomadOperatorStorefront: GgnomadOperatorStorefront },\n { id: string }\n >\n );\n const [unpublishStorefront, { loading: unpublishing }] = useMutation(\n UNPUBLISH_GGNOMAD_OPERATOR_STOREFRONT_MUTATION as TypedDocumentNode<\n { unpublishGgnomadOperatorStorefront: GgnomadOperatorStorefront },\n { id: string }\n >\n );\n\n const handleSave = async () => {\n if (!slug.trim() || !SLUG_PATTERN.test(slug.trim())) {\n notify.warning(\n t('ggnomad.storefront.invalidSlugTitle', 'Invalid slug'),\n t(\n 'ggnomad.storefront.invalidSlugBody',\n '3-80 lowercase letters, digits, or hyphens; no leading/trailing hyphen.'\n )\n );\n return;\n }\n if (!displayName.trim()) {\n notify.warning(\n t('ggnomad.storefront.nameRequiredTitle', 'Display name is required'),\n t('ggnomad.storefront.nameRequiredBody', 'Give your storefront a public-facing name.')\n );\n return;\n }\n if (primaryColor.trim() && !HEX_COLOR_PATTERN.test(primaryColor.trim())) {\n notify.warning(\n t('ggnomad.storefront.invalidColorTitle', 'Invalid color'),\n t('ggnomad.storefront.invalidColorBody', 'Use a 6-digit hex color, e.g. #1E88E5.')\n );\n return;\n }\n\n try {\n if (storefront) {\n const result = await updateStorefront({\n variables: {\n id: storefront.id,\n input: {\n slug: slug.trim().toLowerCase(),\n displayName: displayName.trim(),\n logoUrl: logoUrl.trim() || null,\n primaryColor: primaryColor.trim() || null,\n aboutText: aboutText.trim() || null,\n },\n },\n });\n if (result.error) throw result.error;\n notify.success(t('ggnomad.storefront.savedTitle', 'Storefront updated'), displayName);\n } else {\n const result = await createStorefront({\n variables: {\n input: {\n providerId,\n slug: slug.trim().toLowerCase(),\n displayName: displayName.trim(),\n logoUrl: logoUrl.trim() || null,\n primaryColor: primaryColor.trim() || null,\n aboutText: aboutText.trim() || null,\n },\n },\n });\n if (result.error) throw result.error;\n notify.success(t('ggnomad.storefront.createdTitle', 'Storefront created'), displayName);\n }\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.storefront.errorTitle', 'Could not save storefront'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const handleTogglePublish = async () => {\n if (!storefront) return;\n try {\n if (storefront.isPublished) {\n await unpublishStorefront({ variables: { id: storefront.id } });\n notify.success(\n t('ggnomad.storefront.unpublishedTitle', 'Storefront unpublished'),\n t('ggnomad.storefront.unpublishedBody', 'It is no longer visible to the public.')\n );\n } else {\n await publishStorefront({ variables: { id: storefront.id } });\n notify.success(\n t('ggnomad.storefront.publishedTitle', 'Storefront published'),\n t('ggnomad.storefront.publishedBody', 'Your branded page is now publicly reachable.')\n );\n }\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.storefront.errorTitle', 'Could not update storefront'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const saving = creating || updating;\n const previewColor = HEX_COLOR_PATTERN.test(primaryColor.trim())\n ? primaryColor.trim()\n : '#1E88E5';\n\n if (!providerId) {\n return (\n <PageShell\n title={t('ggnomad.storefront.title', 'Operator Storefront')}\n subtitle={t('ggnomad.storefront.missingProvider', 'No provider selected.')}\n >\n <PagePurpose icon={<Store className=\"size-4\" />}>\n {t(\n 'ggnomad.storefront.missingProviderHelp',\n 'Open a provider from the Provider Dashboard and choose “Manage storefront”.'\n )}\n </PagePurpose>\n </PageShell>\n );\n }\n\n return (\n <PageShell\n title={t('ggnomad.storefront.title', 'Operator Storefront')}\n titleAccent={t('ggnomad.storefront.title.accent', 'Storefront')}\n subtitle={t(\n 'ggnomad.storefront.subtitle',\n 'White-label branding for your public listing page — logo, color, slug, and about text.'\n )}\n >\n <PagePurpose icon={<Store className=\"size-4\" />}>\n {t(\n 'ggnomad.storefront.purpose',\n 'Configure how travelers see your business when they visit your branded storefront URL. Publish when ready — draft branding stays private until then. Custom domains and a drag-and-drop builder are on the roadmap, not part of this core.'\n )}\n </PagePurpose>\n\n {loading && !hydrated ? (\n <div className=\"flex justify-center py-10\">\n <Spinner />\n </div>\n ) : (\n <div className=\"grid gap-5 lg:grid-cols-2\">\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <h2 className=\"mb-4 flex items-center gap-2 text-lg font-medium tracking-tight text-text-primary\">\n <Palette className=\"size-4\" />\n {t('ggnomad.storefront.brandingHeading', 'Branding')}\n </h2>\n <div className=\"space-y-4\">\n <div>\n <label className={labelClass} htmlFor=\"storefront-slug\">\n {t('ggnomad.storefront.slugLabel', 'Public slug')}\n </label>\n <Input\n id=\"storefront-slug\"\n className={fieldClass}\n value={slug}\n onChange={(e) => setSlug(e.target.value.toLowerCase())}\n placeholder=\"my-eco-stays\"\n disabled={saving}\n />\n <p className=\"mt-1 text-xs text-text-secondary\">\n {t(\n 'ggnomad.storefront.slugHelp',\n 'Lowercase letters, digits, hyphens — 3-80 chars.'\n )}\n </p>\n </div>\n <div>\n <label className={labelClass} htmlFor=\"storefront-name\">\n {t('ggnomad.storefront.nameLabel', 'Display name')}\n </label>\n <Input\n id=\"storefront-name\"\n className={fieldClass}\n value={displayName}\n onChange={(e) => setDisplayName(e.target.value)}\n placeholder={t('ggnomad.storefront.namePlaceholder', 'Sunset Eco Stays')}\n disabled={saving}\n />\n </div>\n <div>\n <label className={labelClass} htmlFor=\"storefront-logo\">\n {t('ggnomad.storefront.logoLabel', 'Logo URL')}\n </label>\n <Input\n id=\"storefront-logo\"\n className={fieldClass}\n value={logoUrl}\n onChange={(e) => setLogoUrl(e.target.value)}\n placeholder=\"https://example.com/logo.png\"\n disabled={saving}\n />\n </div>\n <div>\n <label className={labelClass} htmlFor=\"storefront-color\">\n {t('ggnomad.storefront.colorLabel', 'Primary color')}\n </label>\n <div className=\"mt-1 flex items-center gap-2\">\n <input\n id=\"storefront-color\"\n type=\"color\"\n className=\"h-9 w-12 rounded border border-border-seam bg-bg-canvas\"\n value={HEX_COLOR_PATTERN.test(primaryColor) ? primaryColor : '#1E88E5'}\n onChange={(e) => setPrimaryColor(e.target.value)}\n disabled={saving}\n />\n <Input\n className={fieldClass}\n value={primaryColor}\n onChange={(e) => setPrimaryColor(e.target.value)}\n placeholder=\"#1E88E5\"\n disabled={saving}\n />\n </div>\n </div>\n <div>\n <label className={labelClass} htmlFor=\"storefront-about\">\n {t('ggnomad.storefront.aboutLabel', 'About text')}\n </label>\n <Textarea\n id=\"storefront-about\"\n className={fieldClass}\n rows={4}\n value={aboutText}\n onChange={(e) => setAboutText(e.target.value)}\n placeholder={t(\n 'ggnomad.storefront.aboutPlaceholder',\n 'Tell travelers what makes your business worth booking.'\n )}\n disabled={saving}\n />\n </div>\n\n <div className=\"flex flex-wrap items-center gap-3 pt-2\">\n <Button onClick={handleSave} disabled={saving}>\n {storefront\n ? t('ggnomad.storefront.saveButton', 'Save changes')\n : t('ggnomad.storefront.createButton', 'Create storefront')}\n </Button>\n {storefront ? (\n <Button\n variant=\"secondary\"\n onClick={handleTogglePublish}\n disabled={publishing || unpublishing}\n >\n {storefront.isPublished\n ? t('ggnomad.storefront.unpublishButton', 'Unpublish')\n : t('ggnomad.storefront.publishButton', 'Publish')}\n </Button>\n ) : null}\n {storefront?.isPublished ? (\n <span className=\"inline-flex items-center gap-1 text-xs text-text-secondary\">\n <Globe className=\"size-3.5\" />\n {t('ggnomad.storefront.livePathLabel', 'Live at /store/{slug}').replace(\n '{slug}',\n storefront.slug\n )}\n </span>\n ) : null}\n </div>\n </div>\n </GlassCard>\n\n {/* Live preview — the same visual shape ggnomadStorefrontBySlug renders publicly. */}\n <div\n className=\"overflow-hidden rounded-lg border border-border-seam\"\n style={{ borderTopColor: previewColor, borderTopWidth: 4 }}\n >\n <div className=\"p-5\" style={{ background: `${previewColor}14` }}>\n <div className=\"flex items-center gap-3\">\n {logoUrl ? (\n <img\n src={logoUrl}\n alt=\"\"\n className=\"size-12 rounded object-contain\"\n onError={(e) => {\n (e.currentTarget as HTMLImageElement).style.display = 'none';\n }}\n />\n ) : (\n <div\n className=\"flex size-12 items-center justify-center rounded text-lg font-semibold text-white\"\n style={{ background: previewColor }}\n >\n {(displayName || 'S').slice(0, 1).toUpperCase()}\n </div>\n )}\n <div>\n <h3 className=\"text-lg font-semibold text-text-primary\">\n {displayName ||\n t('ggnomad.storefront.previewPlaceholderName', 'Your Storefront')}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {slug\n ? `/store/${slug}`\n : t('ggnomad.storefront.previewSlugPlaceholder', '/store/…')}\n </p>\n </div>\n </div>\n {aboutText ? (\n <p className=\"mt-4 text-sm text-text-primary\">{aboutText}</p>\n ) : (\n <p className=\"mt-4 text-sm text-text-secondary\">\n {t(\n 'ggnomad.storefront.previewNoAbout',\n 'Add an about text to introduce your business.'\n )}\n </p>\n )}\n <div\n className=\"mt-4 inline-block rounded px-3 py-1.5 text-xs font-medium text-white\"\n style={{ background: previewColor }}\n >\n {t('ggnomad.storefront.previewCta', 'Book with us')}\n </div>\n </div>\n <div className=\"border-t border-border-seam bg-bg-surface p-4 text-xs text-text-secondary\">\n {t(\n 'ggnomad.storefront.previewFooter',\n 'ACTIVE properties, tours, events, and activities from this provider render below this banner on the public page.'\n )}\n </div>\n </div>\n </div>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"StorefrontPage.js","names":[],"sources":["../../../src/ggnomad/pages/StorefrontPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useParams } from 'react-router';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Globe, Palette, Store } from 'lucide-react';\nimport { Button, GlassCard, Input, PagePurpose, Spinner, Textarea } from '@burdenoff/fe-libs/ui';\n\nimport {\n CREATE_GGNOMAD_OPERATOR_STOREFRONT_MUTATION,\n GGNOMAD_OPERATOR_STOREFRONT_QUERY,\n PUBLISH_GGNOMAD_OPERATOR_STOREFRONT_MUTATION,\n UNPUBLISH_GGNOMAD_OPERATOR_STOREFRONT_MUTATION,\n UPDATE_GGNOMAD_OPERATOR_STOREFRONT_MUTATION,\n} from '../graphql';\nimport { PageShell } from './PageShell';\nimport { notify } from '../utils/notify';\n\ntype GgnomadOperatorStorefront = {\n id: string;\n providerId: string;\n slug: string;\n displayName: string;\n logoUrl: string | null;\n primaryColor: string | null;\n aboutText: string | null;\n isPublished: boolean;\n publishedAt: string | null;\n};\n\nconst HEX_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;\nconst SLUG_PATTERN = /^[a-z0-9]([a-z0-9-]{1,78}[a-z0-9])?$/;\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 * BOFF-4363 — operator storefront settings: create/edit the branding\n * (logo/color/slug/about) for one provider and publish/unpublish it. The\n * right-hand panel renders a live, themeable preview of the branded page a\n * traveler would see at `ggnomadStorefrontBySlug` once published.\n *\n * Custom-domain DNS/SSL provisioning and a drag-and-drop page builder are\n * explicitly out of scope for this pass — see Linear BOFF-4363.\n */\nexport function StorefrontPage() {\n const { t } = useI18n();\n const { providerId = '' } = useParams();\n\n const { data, loading, refetch } = useQuery(\n GGNOMAD_OPERATOR_STOREFRONT_QUERY as TypedDocumentNode<\n { ggnomadOperatorStorefront: GgnomadOperatorStorefront | null },\n { providerId: string }\n >,\n { variables: { providerId }, skip: !providerId }\n );\n const storefront = data?.ggnomadOperatorStorefront ?? null;\n\n const [slug, setSlug] = useState('');\n const [displayName, setDisplayName] = useState('');\n const [logoUrl, setLogoUrl] = useState('');\n const [primaryColor, setPrimaryColor] = useState('#1E88E5');\n const [aboutText, setAboutText] = useState('');\n const [hydrated, setHydrated] = useState(false);\n\n if (storefront && !hydrated) {\n setSlug(storefront.slug);\n setDisplayName(storefront.displayName);\n setLogoUrl(storefront.logoUrl ?? '');\n setPrimaryColor(storefront.primaryColor ?? '#1E88E5');\n setAboutText(storefront.aboutText ?? '');\n setHydrated(true);\n }\n\n const [createStorefront, { loading: creating }] = useMutation(\n CREATE_GGNOMAD_OPERATOR_STOREFRONT_MUTATION as TypedDocumentNode<\n { createGgnomadOperatorStorefront: GgnomadOperatorStorefront },\n {\n input: {\n providerId: string;\n slug: string;\n displayName: string;\n logoUrl?: string | null;\n primaryColor?: string | null;\n aboutText?: string | null;\n };\n }\n >\n );\n const [updateStorefront, { loading: updating }] = useMutation(\n UPDATE_GGNOMAD_OPERATOR_STOREFRONT_MUTATION as TypedDocumentNode<\n { updateGgnomadOperatorStorefront: GgnomadOperatorStorefront },\n {\n id: string;\n input: {\n slug?: string;\n displayName?: string;\n logoUrl?: string | null;\n primaryColor?: string | null;\n aboutText?: string | null;\n };\n }\n >\n );\n const [publishStorefront, { loading: publishing }] = useMutation(\n PUBLISH_GGNOMAD_OPERATOR_STOREFRONT_MUTATION as TypedDocumentNode<\n { publishGgnomadOperatorStorefront: GgnomadOperatorStorefront },\n { id: string }\n >\n );\n const [unpublishStorefront, { loading: unpublishing }] = useMutation(\n UNPUBLISH_GGNOMAD_OPERATOR_STOREFRONT_MUTATION as TypedDocumentNode<\n { unpublishGgnomadOperatorStorefront: GgnomadOperatorStorefront },\n { id: string }\n >\n );\n\n const handleSave = async () => {\n if (!slug.trim() || !SLUG_PATTERN.test(slug.trim())) {\n notify.warning(\n t('ggnomad.storefront.invalidSlugTitle', 'Invalid slug'),\n t(\n 'ggnomad.storefront.invalidSlugBody',\n '3-80 lowercase letters, digits, or hyphens; no leading/trailing hyphen.'\n )\n );\n return;\n }\n if (!displayName.trim()) {\n notify.warning(\n t('ggnomad.storefront.nameRequiredTitle', 'Display name is required'),\n t('ggnomad.storefront.nameRequiredBody', 'Give your storefront a public-facing name.')\n );\n return;\n }\n if (primaryColor.trim() && !HEX_COLOR_PATTERN.test(primaryColor.trim())) {\n notify.warning(\n t('ggnomad.storefront.invalidColorTitle', 'Invalid color'),\n t('ggnomad.storefront.invalidColorBody', 'Use a 6-digit hex color, e.g. #1E88E5.')\n );\n return;\n }\n\n try {\n if (storefront) {\n const result = await updateStorefront({\n variables: {\n id: storefront.id,\n input: {\n slug: slug.trim().toLowerCase(),\n displayName: displayName.trim(),\n logoUrl: logoUrl.trim() || null,\n primaryColor: primaryColor.trim() || null,\n aboutText: aboutText.trim() || null,\n },\n },\n });\n if (result.error) throw result.error;\n notify.success(t('ggnomad.storefront.savedTitle', 'Storefront updated'), displayName);\n } else {\n const result = await createStorefront({\n variables: {\n input: {\n providerId,\n slug: slug.trim().toLowerCase(),\n displayName: displayName.trim(),\n logoUrl: logoUrl.trim() || null,\n primaryColor: primaryColor.trim() || null,\n aboutText: aboutText.trim() || null,\n },\n },\n });\n if (result.error) throw result.error;\n notify.success(t('ggnomad.storefront.createdTitle', 'Storefront created'), displayName);\n }\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.storefront.errorTitle', 'Could not save storefront'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const handleTogglePublish = async () => {\n if (!storefront) return;\n try {\n if (storefront.isPublished) {\n await unpublishStorefront({ variables: { id: storefront.id } });\n notify.success(\n t('ggnomad.storefront.unpublishedTitle', 'Storefront unpublished'),\n t('ggnomad.storefront.unpublishedBody', 'It is no longer visible to the public.')\n );\n } else {\n await publishStorefront({ variables: { id: storefront.id } });\n notify.success(\n t('ggnomad.storefront.publishedTitle', 'Storefront published'),\n t('ggnomad.storefront.publishedBody', 'Your branded page is now publicly reachable.')\n );\n }\n await refetch();\n } catch (err) {\n notify.error(\n t('ggnomad.storefront.errorTitle', 'Could not update storefront'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const saving = creating || updating;\n const previewColor = HEX_COLOR_PATTERN.test(primaryColor.trim())\n ? primaryColor.trim()\n : '#1E88E5';\n\n if (!providerId) {\n return (\n <PageShell\n title={t('ggnomad.storefront.title', 'Operator Storefront')}\n subtitle={t('ggnomad.storefront.missingProvider', 'No provider selected.')}\n >\n <PagePurpose icon={<Store className=\"size-4\" />}>\n {t(\n 'ggnomad.storefront.missingProviderHelp',\n 'Open a provider from the Provider Dashboard and choose “Manage storefront”.'\n )}\n </PagePurpose>\n </PageShell>\n );\n }\n\n return (\n <PageShell\n title={t('ggnomad.storefront.title', 'Operator Storefront')}\n titleAccent={t('ggnomad.storefront.title.accent', 'Storefront')}\n subtitle={t(\n 'ggnomad.storefront.subtitle',\n 'White-label branding for your public listing page — logo, color, slug, and about text.'\n )}\n >\n <PagePurpose icon={<Store className=\"size-4\" />}>\n {t(\n 'ggnomad.storefront.purpose',\n 'Configure how travelers see your business when they visit your branded storefront URL. Publish when ready — draft branding stays private until then. Custom domains and a drag-and-drop builder are on the roadmap, not part of this core.'\n )}\n </PagePurpose>\n\n {loading && !hydrated ? (\n <div className=\"flex justify-center py-10\">\n <Spinner />\n </div>\n ) : (\n <div className=\"grid gap-5 lg:grid-cols-2\">\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <h2 className=\"mb-4 flex items-center gap-2 text-lg font-medium tracking-tight text-text-primary\">\n <Palette className=\"size-4\" />\n {t('ggnomad.storefront.brandingHeading', 'Branding')}\n </h2>\n <div className=\"space-y-4\">\n <div>\n <label className={labelClass} htmlFor=\"storefront-slug\">\n {t('ggnomad.storefront.slugLabel', 'Public slug')}\n </label>\n <Input\n id=\"storefront-slug\"\n className={fieldClass}\n value={slug}\n onChange={(e) => setSlug(e.target.value.toLowerCase())}\n placeholder=\"my-eco-stays\"\n disabled={saving}\n />\n <p className=\"mt-1 text-xs text-text-secondary\">\n {t(\n 'ggnomad.storefront.slugHelp',\n 'Lowercase letters, digits, hyphens — 3-80 chars.'\n )}\n </p>\n </div>\n <div>\n <label className={labelClass} htmlFor=\"storefront-name\">\n {t('ggnomad.storefront.nameLabel', 'Display name')}\n </label>\n <Input\n id=\"storefront-name\"\n className={fieldClass}\n value={displayName}\n onChange={(e) => setDisplayName(e.target.value)}\n placeholder={t('ggnomad.storefront.namePlaceholder', 'Sunset Eco Stays')}\n disabled={saving}\n />\n </div>\n <div>\n <label className={labelClass} htmlFor=\"storefront-logo\">\n {t('ggnomad.storefront.logoLabel', 'Logo URL')}\n </label>\n <Input\n id=\"storefront-logo\"\n className={fieldClass}\n value={logoUrl}\n onChange={(e) => setLogoUrl(e.target.value)}\n placeholder=\"https://example.com/logo.png\"\n disabled={saving}\n />\n </div>\n <div>\n <label className={labelClass} htmlFor=\"storefront-color\">\n {t('ggnomad.storefront.colorLabel', 'Primary color')}\n </label>\n <div className=\"mt-1 flex items-center gap-2\">\n <input\n id=\"storefront-color\"\n type=\"color\"\n className=\"h-9 w-12 rounded border border-border-seam bg-bg-canvas\"\n value={HEX_COLOR_PATTERN.test(primaryColor) ? primaryColor : '#1E88E5'}\n onChange={(e) => setPrimaryColor(e.target.value)}\n disabled={saving}\n />\n <Input\n className={fieldClass}\n value={primaryColor}\n onChange={(e) => setPrimaryColor(e.target.value)}\n placeholder=\"#1E88E5\"\n disabled={saving}\n />\n </div>\n </div>\n <div>\n <label className={labelClass} htmlFor=\"storefront-about\">\n {t('ggnomad.storefront.aboutLabel', 'About text')}\n </label>\n <Textarea\n id=\"storefront-about\"\n className={fieldClass}\n rows={4}\n value={aboutText}\n onChange={(e) => setAboutText(e.target.value)}\n placeholder={t(\n 'ggnomad.storefront.aboutPlaceholder',\n 'Tell travelers what makes your business worth booking.'\n )}\n disabled={saving}\n />\n </div>\n\n <div className=\"flex flex-wrap items-center gap-3 pt-2\">\n <Button onClick={handleSave} disabled={saving}>\n {storefront\n ? t('ggnomad.storefront.saveButton', 'Save changes')\n : t('ggnomad.storefront.createButton', 'Create storefront')}\n </Button>\n {storefront ? (\n <Button\n variant=\"secondary\"\n onClick={handleTogglePublish}\n disabled={publishing || unpublishing}\n >\n {storefront.isPublished\n ? t('ggnomad.storefront.unpublishButton', 'Unpublish')\n : t('ggnomad.storefront.publishButton', 'Publish')}\n </Button>\n ) : null}\n {storefront?.isPublished ? (\n <span className=\"inline-flex items-center gap-1 text-xs text-text-secondary\">\n <Globe className=\"size-3.5\" />\n {t('ggnomad.storefront.livePathLabel', 'Live at /store/{slug}').replace(\n '{slug}',\n storefront.slug\n )}\n </span>\n ) : null}\n </div>\n </div>\n </GlassCard>\n\n {/* Live preview — the same visual shape ggnomadStorefrontBySlug renders publicly. */}\n <div\n className=\"overflow-hidden rounded-lg border border-border-seam\"\n style={{ borderTopColor: previewColor, borderTopWidth: 4 }}\n >\n <div className=\"p-5\" style={{ background: `${previewColor}14` }}>\n <div className=\"flex items-center gap-3\">\n {logoUrl ? (\n <img\n src={logoUrl}\n alt=\"\"\n className=\"size-12 rounded object-contain\"\n onError={(e) => {\n (e.currentTarget as HTMLImageElement).style.display = 'none';\n }}\n />\n ) : (\n <div\n className=\"flex size-12 items-center justify-center rounded text-lg font-semibold text-white\"\n style={{ background: previewColor }}\n >\n {(displayName || 'S').slice(0, 1).toUpperCase()}\n </div>\n )}\n <div>\n <h3 className=\"text-lg font-semibold text-text-primary\">\n {displayName ||\n t('ggnomad.storefront.previewPlaceholderName', 'Your Storefront')}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {slug\n ? `/store/${slug}`\n : t('ggnomad.storefront.previewSlugPlaceholder', '/store/…')}\n </p>\n </div>\n </div>\n {aboutText ? (\n <p className=\"mt-4 text-sm text-text-primary\">{aboutText}</p>\n ) : (\n <p className=\"mt-4 text-sm text-text-secondary\">\n {t(\n 'ggnomad.storefront.previewNoAbout',\n 'Add an about text to introduce your business.'\n )}\n </p>\n )}\n <div\n className=\"mt-4 inline-block rounded px-3 py-1.5 text-xs font-medium text-white\"\n style={{ background: previewColor }}\n >\n {t('ggnomad.storefront.previewCta', 'Book with us')}\n </div>\n </div>\n <div className=\"border-t border-border-seam bg-bg-surface p-4 text-xs text-text-secondary\">\n {t(\n 'ggnomad.storefront.previewFooter',\n 'ACTIVE properties, tours, events, and activities from this provider render below this banner on the public page.'\n )}\n </div>\n </div>\n </div>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StorefrontPublicPage.js","names":[],"sources":["../../../src/ggnomad/pages/StorefrontPublicPage.tsx"],"sourcesContent":["import { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useParams } from 'react-router';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Leaf } from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, Spinner } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_STOREFRONT_BY_SLUG_QUERY } from '../graphql';\nimport { PageShell } from './PageShell';\nimport { useGGNomad } from '../providers/GGNomadProvider';\n\ntype StorefrontListing = {\n id: string;\n name: string;\n price?: number;\n pricePerNight?: number;\n currency: string;\n images: string[];\n rating?: number | null;\n sustainabilityScore: number;\n};\n\ntype StorefrontPage = {\n storefront: {\n id: string;\n displayName: string;\n logoUrl: string | null;\n primaryColor: string | null;\n aboutText: string | null;\n publishedAt: string | null;\n };\n properties: StorefrontListing[];\n tours: StorefrontListing[];\n events: StorefrontListing[];\n activities: StorefrontListing[];\n};\n\n/**\n * BOFF-4363 — the branded storefront page a traveler sees, backed by the\n * genuinely public `ggnomadStorefrontBySlug` query. Note: this route still\n * renders inside the authenticated app shell today — making it reachable\n * without any login at all is a routing/CDN concern for the app shell, not\n * this MFE (tracked alongside the deferred custom-domain work in BOFF-4363).\n */\nexport function StorefrontPublicPage() {\n const { t } = useI18n();\n const { slug = '' } = useParams();\n const { workspaceId = '' } = useGGNomad();\n\n const { data, loading } = useQuery(\n GGNOMAD_STOREFRONT_BY_SLUG_QUERY as TypedDocumentNode<\n { ggnomadStorefrontBySlug: StorefrontPage | null },\n { workspaceId: string; slug: string }\n >,\n { variables: { workspaceId, slug }, skip: !workspaceId || !slug }\n );\n\n const page = data?.ggnomadStorefrontBySlug ?? null;\n const brandColor = page?.storefront.primaryColor ?? '#1E88E5';\n const allListings = page\n ? [...page.properties, ...page.tours, ...page.events, ...page.activities]\n : [];\n\n if (loading) {\n return (\n <PageShell title={t('ggnomad.storefrontPublic.title', 'Storefront')} subtitle=\"\">\n <div className=\"flex justify-center py-10\">\n <Spinner />\n </div>\n </PageShell>\n );\n }\n\n if (!page) {\n return (\n <PageShell\n title={t('ggnomad.storefrontPublic.title', 'Storefront')}\n subtitle={t(\n 'ggnomad.storefrontPublic.notFoundSubtitle',\n 'This storefront is not available.'\n )}\n >\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title={t('ggnomad.storefrontPublic.notFoundTitle', 'Storefront not found')}\n description={t(\n 'ggnomad.storefrontPublic.notFoundDescription',\n 'This link may be mistyped, or the operator has not published a storefront yet.'\n )}\n />\n </PageShell>\n );\n }\n\n return (\n <div>\n <div\n className=\"p-6\"\n style={{ background: `${brandColor}14`, borderBottom: `4px solid ${brandColor}` }}\n >\n <div className=\"mx-auto flex max-w-4xl items-center gap-4\">\n {page.storefront.logoUrl ? (\n <img src={page.storefront.logoUrl} alt=\"\" className=\"size-16 rounded object-contain\" />\n ) : (\n <div\n className=\"flex size-16 items-center justify-center rounded text-2xl font-semibold text-white\"\n style={{ background: brandColor }}\n >\n {page.storefront.displayName.slice(0, 1).toUpperCase()}\n </div>\n )}\n <div>\n <h1 className=\"text-2xl font-semibold text-text-primary\">\n {page.storefront.displayName}\n </h1>\n {page.storefront.aboutText ? (\n <p className=\"mt-1 max-w-2xl text-sm text-text-secondary\">\n {page.storefront.aboutText}\n </p>\n ) : null}\n </div>\n </div>\n </div>\n\n <div className=\"mx-auto max-w-4xl p-6\">\n {allListings.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={t('ggnomad.storefrontPublic.emptyTitle', 'Nothing listed yet')}\n description={t(\n 'ggnomad.storefrontPublic.emptyDescription',\n 'This operator has not published any active listings.'\n )}\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {allListings.map((listing) => (\n <GlassCard key={listing.id} treatment=\"glass\" className=\"border-border-seam p-4\">\n <h3 className=\"font-medium text-text-primary\">{listing.name}</h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {(listing.price ?? listing.pricePerNight ?? 0).toLocaleString(undefined, {\n style: 'currency',\n currency: listing.currency,\n })}\n </p>\n {listing.sustainabilityScore > 0 ? (\n <p className=\"mt-2 inline-flex items-center gap-1 text-xs text-text-secondary\">\n <Leaf className=\"size-3.5 text-emerald-500\" />\n {t(\n 'ggnomad.storefrontPublic.sustainabilityScore',\n '{score}% self-reported sustainable'\n ).replace('{score}', String(listing.sustainabilityScore))}\n </p>\n ) : null}\n </GlassCard>\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;AA4CA,SAAgB,IAAuB;CACrC,IAAM,EAAE,SAAM,EAAQ,GAChB,EAAE,UAAO,OAAO,EAAU,GAC1B,EAAE,iBAAc,OAAO,EAAW,GAElC,EAAE,SAAM,eAAY,EACxB,GAIA;EAAE,WAAW;GAAE;GAAa;EAAK;EAAG,MAAM,CAAC,KAAe,CAAC;CAAK,CAClE,GAEM,IAAO,GAAM,2BAA2B,MACxC,IAAa,GAAM,WAAW,gBAAgB,WAC9C,IAAc,IAChB;EAAC,GAAG,EAAK;EAAY,GAAG,EAAK;EAAO,GAAG,EAAK;EAAQ,GAAG,EAAK;CAAU,IACtE,CAAC;CAiCL,OA/BI,IAEA,kBAAC,GAAD;EAAW,OAAO,EAAE,kCAAkC,YAAY;EAAG,UAAS;YAC5E,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,GAAD,CAAU,CAAA;EACP,CAAA;CACI,CAAA,IAIV,IAsBH,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;EACE,WAAU;EACV,OAAO;GAAE,YAAY,GAAG,EAAW;GAAK,cAAc,aAAa;EAAa;YAEhF,kBAAC,OAAD;GAAK,WAAU;aAAf,CACG,EAAK,WAAW,UACf,kBAAC,OAAD;IAAK,KAAK,EAAK,WAAW;IAAS,KAAI;IAAG,WAAU;GAAkC,CAAA,IAEtF,kBAAC,OAAD;IACE,WAAU;IACV,OAAO,EAAE,YAAY,EAAW;cAE/B,EAAK,WAAW,YAAY,MAAM,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"StorefrontPublicPage.js","names":[],"sources":["../../../src/ggnomad/pages/StorefrontPublicPage.tsx"],"sourcesContent":["import { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useParams } from 'react-router';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Leaf } from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, Spinner } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_STOREFRONT_BY_SLUG_QUERY } from '../graphql';\nimport { PageShell } from './PageShell';\nimport { useGGNomad } from '../providers/GGNomadProvider';\n\ntype StorefrontListing = {\n id: string;\n name: string;\n price?: number;\n pricePerNight?: number;\n currency: string;\n images: string[];\n rating?: number | null;\n sustainabilityScore: number;\n};\n\ntype StorefrontPage = {\n storefront: {\n id: string;\n displayName: string;\n logoUrl: string | null;\n primaryColor: string | null;\n aboutText: string | null;\n publishedAt: string | null;\n };\n properties: StorefrontListing[];\n tours: StorefrontListing[];\n events: StorefrontListing[];\n activities: StorefrontListing[];\n};\n\n/**\n * BOFF-4363 — the branded storefront page a traveler sees, backed by the\n * genuinely public `ggnomadStorefrontBySlug` query. Note: this route still\n * renders inside the authenticated app shell today — making it reachable\n * without any login at all is a routing/CDN concern for the app shell, not\n * this MFE (tracked alongside the deferred custom-domain work in BOFF-4363).\n */\nexport function StorefrontPublicPage() {\n const { t } = useI18n();\n const { slug = '' } = useParams();\n const { workspaceId = '' } = useGGNomad();\n\n const { data, loading } = useQuery(\n GGNOMAD_STOREFRONT_BY_SLUG_QUERY as TypedDocumentNode<\n { ggnomadStorefrontBySlug: StorefrontPage | null },\n { workspaceId: string; slug: string }\n >,\n { variables: { workspaceId, slug }, skip: !workspaceId || !slug }\n );\n\n const page = data?.ggnomadStorefrontBySlug ?? null;\n const brandColor = page?.storefront.primaryColor ?? '#1E88E5';\n const allListings = page\n ? [...page.properties, ...page.tours, ...page.events, ...page.activities]\n : [];\n\n if (loading) {\n return (\n <PageShell title={t('ggnomad.storefrontPublic.title', 'Storefront')} subtitle=\"\">\n <div className=\"flex justify-center py-10\">\n <Spinner />\n </div>\n </PageShell>\n );\n }\n\n if (!page) {\n return (\n <PageShell\n title={t('ggnomad.storefrontPublic.title', 'Storefront')}\n subtitle={t(\n 'ggnomad.storefrontPublic.notFoundSubtitle',\n 'This storefront is not available.'\n )}\n >\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title={t('ggnomad.storefrontPublic.notFoundTitle', 'Storefront not found')}\n description={t(\n 'ggnomad.storefrontPublic.notFoundDescription',\n 'This link may be mistyped, or the operator has not published a storefront yet.'\n )}\n />\n </PageShell>\n );\n }\n\n return (\n <div>\n <div\n className=\"p-6\"\n style={{ background: `${brandColor}14`, borderBottom: `4px solid ${brandColor}` }}\n >\n <div className=\"mx-auto flex max-w-4xl items-center gap-4\">\n {page.storefront.logoUrl ? (\n <img src={page.storefront.logoUrl} alt=\"\" className=\"size-16 rounded object-contain\" />\n ) : (\n <div\n className=\"flex size-16 items-center justify-center rounded text-2xl font-semibold text-white\"\n style={{ background: brandColor }}\n >\n {page.storefront.displayName.slice(0, 1).toUpperCase()}\n </div>\n )}\n <div>\n <h1 className=\"text-2xl font-semibold text-text-primary\">\n {page.storefront.displayName}\n </h1>\n {page.storefront.aboutText ? (\n <p className=\"mt-1 max-w-2xl text-sm text-text-secondary\">\n {page.storefront.aboutText}\n </p>\n ) : null}\n </div>\n </div>\n </div>\n\n <div className=\"mx-auto max-w-4xl p-6\">\n {allListings.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={t('ggnomad.storefrontPublic.emptyTitle', 'Nothing listed yet')}\n description={t(\n 'ggnomad.storefrontPublic.emptyDescription',\n 'This operator has not published any active listings.'\n )}\n />\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {allListings.map((listing) => (\n <GlassCard key={listing.id} treatment=\"glass\" className=\"border-border-seam p-4\">\n <h3 className=\"font-medium text-text-primary\">{listing.name}</h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {(listing.price ?? listing.pricePerNight ?? 0).toLocaleString(undefined, {\n style: 'currency',\n currency: listing.currency,\n })}\n </p>\n {listing.sustainabilityScore > 0 ? (\n <p className=\"mt-2 inline-flex items-center gap-1 text-xs text-text-secondary\">\n <Leaf className=\"size-3.5 text-emerald-500\" />\n {t(\n 'ggnomad.storefrontPublic.sustainabilityScore',\n '{score}% self-reported sustainable'\n ).replace('{score}', String(listing.sustainabilityScore))}\n </p>\n ) : null}\n </GlassCard>\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;AA4CA,SAAgB,IAAuB;CACrC,IAAM,EAAE,SAAM,EAAQ,GAChB,EAAE,UAAO,OAAO,EAAU,GAC1B,EAAE,iBAAc,OAAO,EAAW,GAElC,EAAE,SAAM,eAAY,EACxB,GAIA;EAAE,WAAW;GAAE;GAAa;EAAK;EAAG,MAAM,CAAC,KAAe,CAAC;CAAK,CAClE,GAEM,IAAO,GAAM,2BAA2B,MACxC,IAAa,GAAM,WAAW,gBAAgB,WAC9C,IAAc,IAChB;EAAC,GAAG,EAAK;EAAY,GAAG,EAAK;EAAO,GAAG,EAAK;EAAQ,GAAG,EAAK;CAAU,IACtE,CAAC;CAiCL,OA/BI,IAEA,kBAAC,GAAD;EAAW,OAAO,EAAE,kCAAkC,YAAY;EAAG,UAAS;YAC5E,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,GAAD,CAAU,CAAA;EACP,CAAA;CACI,CAAA,IAIV,IAsBH,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;EACE,WAAU;EACV,OAAO;GAAE,YAAY,GAAG,EAAW;GAAK,cAAc,aAAa;EAAa;YAEhF,kBAAC,OAAD;GAAK,WAAU;aAAf,CACG,EAAK,WAAW,UACf,kBAAC,OAAD;IAAK,KAAK,EAAK,WAAW;IAAS,KAAI;IAAG,WAAU;GAAkC,CAAA,IAEtF,kBAAC,OAAD;IACE,WAAU;IACV,OAAO,EAAE,YAAY,EAAW;cAE/B,EAAK,WAAW,YAAY,MAAM,GAAG,CAAC,CAAC,CAAC,YAAY;GAClD,CAAA,GAEP,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;IAAI,WAAU;cACX,EAAK,WAAW;GACf,CAAA,GACH,EAAK,WAAW,YACf,kBAAC,KAAD;IAAG,WAAU;cACV,EAAK,WAAW;GAChB,CAAA,IACD,IACD,EAAA,CAAA,CACF;;CACF,CAAA,GAEL,kBAAC,OAAD;EAAK,WAAU;YACZ,EAAY,WAAW,IACtB,kBAAC,GAAD;GACE,cAAa;GACb,OAAO,EAAE,uCAAuC,oBAAoB;GACpE,aAAa,EACX,6CACA,sDACF;EACD,CAAA,IAED,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAY,KAAK,MAChB,kBAAC,GAAD;IAA4B,WAAU;IAAQ,WAAU;cAAxD;KACE,kBAAC,MAAD;MAAI,WAAU;gBAAiC,EAAQ;KAAS,CAAA;KAChE,kBAAC,KAAD;MAAG,WAAU;iBACT,EAAQ,SAAS,EAAQ,iBAAiB,EAAA,CAAG,eAAe,KAAA,GAAW;OACvE,OAAO;OACP,UAAU,EAAQ;MACpB,CAAC;KACA,CAAA;KACF,EAAQ,sBAAsB,IAC7B,kBAAC,KAAD;MAAG,WAAU;gBAAb,CACE,kBAAC,GAAD,EAAM,WAAU,4BAA6B,CAAA,GAC5C,EACC,gDACA,oCACF,CAAC,CAAC,QAAQ,WAAW,OAAO,EAAQ,mBAAmB,CAAC,CACvD;UACD;IACK;MAjBK,EAAQ,EAiBb,CACZ;EACE,CAAA;CAEJ,CAAA,CACF,EAAA,CAAA,IApFH,kBAAC,GAAD;EACE,OAAO,EAAE,kCAAkC,YAAY;EACvD,UAAU,EACR,6CACA,mCACF;YAEA,kBAAC,GAAD;GACE,cAAa;GACb,OAAO,EAAE,0CAA0C,sBAAsB;GACzE,aAAa,EACX,gDACA,gFACF;EACD,CAAA;CACQ,CAAA;AAuEjB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TourDetailsPage.js","names":[],"sources":["../../../src/ggnomad/pages/TourDetailsPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useNavigate, useParams } from 'react-router';\nimport {\n CheckCircle2,\n Clock,\n Compass,\n Gauge,\n Languages as LanguagesIcon,\n MapPin,\n Pencil,\n UserRound,\n Users,\n Zap,\n} from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_TOUR_QUERY } from '../graphql';\nimport { useGGNomad } from '../providers/GGNomadProvider';\nimport { joinPath } from '../utils/navigation';\nimport { humanizeEnum, jsonLabel } from '../utils/format';\nimport { PageShell } from './PageShell';\nimport {\n AvailabilityPanel,\n BookingPanel,\n DetailHero,\n DetailRow,\n EcoAccessPanel,\n FactGrid,\n ListingEditDialog,\n PhotoGallery,\n ProviderCard,\n RatingSummary,\n ReviewsSection,\n SectionCard,\n StatusBadge,\n TagChips,\n} from '../components';\nimport type { Fact } from '../components';\n\ntype TourDetail = {\n id: string;\n name: string;\n description?: string | null;\n type: string;\n status?: string;\n duration: string;\n difficulty?: string | null;\n guideName?: string | null;\n currency: string;\n price: number;\n rating?: number | null;\n reviewCount: number;\n highlights: string[];\n images: string[];\n languages: string[];\n location?: unknown;\n maxGroupSize: number;\n instantBook: boolean;\n sustainabilityScore: number;\n sustainabilityPractices: string[];\n accessibilityFeatures: string[];\n provider?: {\n id: string;\n name: string;\n type?: string | null;\n rating?: number | null;\n reviewCount?: number | null;\n listingCount?: number | null;\n contactEmail?: string | null;\n website?: string | null;\n } | null;\n};\n\nexport function TourDetailsPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const { basePath = '/' } = useGGNomad();\n const { id = '' } = useParams();\n const [editDialogOpen, setEditDialogOpen] = useState(false);\n const { data, refetch } = useQuery(\n GGNOMAD_TOUR_QUERY as TypedDocumentNode<{ ggnomadTour: TourDetail | null }, { id: string }>,\n { variables: { id }, skip: !id }\n );\n const tour = data?.ggnomadTour;\n const locationLabel = jsonLabel(tour?.location);\n\n const facts: Fact[] = [];\n if (tour) {\n facts.push({\n id: 'duration',\n label: t('ggnomad.tour.duration', 'Duration'),\n value: tour.duration,\n icon: <Clock />,\n });\n if (tour.difficulty) {\n facts.push({\n id: 'difficulty',\n label: t('ggnomad.tour.difficulty', 'Difficulty'),\n value: humanizeEnum(tour.difficulty),\n icon: <Gauge />,\n });\n }\n facts.push({\n id: 'group',\n label: t('ggnomad.tour.maxGroupSize', 'Max group size'),\n value: String(tour.maxGroupSize),\n icon: <Users />,\n });\n if (tour.languages.length > 0) {\n facts.push({\n id: 'languages',\n label: t('ggnomad.tour.languages', 'Languages'),\n value: tour.languages.join(', '),\n icon: <LanguagesIcon />,\n });\n }\n if (locationLabel) {\n facts.push({\n id: 'location',\n label: t('ggnomad.tour.location', 'Location'),\n value: locationLabel,\n icon: <MapPin />,\n });\n }\n if (tour.guideName) {\n facts.push({\n id: 'guide',\n label: t('ggnomad.tour.guide', 'Guide'),\n value: tour.guideName,\n icon: <UserRound />,\n });\n }\n }\n\n return (\n <PageShell\n title={tour?.name ?? t('ggnomad.tour.fallbackTitle', 'Tour details')}\n subtitle={t('ggnomad.tour.subtitle', 'Tour package details and booking context')}\n >\n {!tour ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.tour.notFoundTitle', 'No tour found')}\n description={t(\n 'ggnomad.tour.notFoundDescription',\n 'This tour is unavailable or may have been removed.'\n )}\n />\n ) : (\n <>\n {tour.images.length > 0 ? (\n <PhotoGallery images={tour.images} alt={tour.name} />\n ) : (\n <DetailHero alt={tour.name} />\n )}\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <span className=\"rounded-full border border-border-seam bg-bg-sunken px-3 py-1 text-sm text-text-primary\">\n {humanizeEnum(tour.type)}\n </span>\n {tour.status ? <StatusBadge status={tour.status} /> : null}\n <RatingSummary rating={tour.rating} reviewCount={tour.reviewCount} />\n {tour.instantBook ? (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-status-success-bg-subtle px-3 py-1 text-sm font-medium text-status-success-text\">\n <Zap className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.listing.instantBook', 'Instant book')}\n </span>\n ) : null}\n </div>\n\n <PagePurpose icon={<Compass className=\"size-4\" />}>\n {t(\n 'ggnomad.tour.purpose',\n 'See exactly what this tour involves — duration, difficulty, your guide, price and the highlights you’ll experience — so you can confirm it fits your trip before booking.'\n )}\n </PagePurpose>\n\n <FactGrid facts={facts} />\n\n <NextSteps\n storageKey=\"ggnomad-tour-detail\"\n steps={[\n {\n id: 'browse',\n label: t('ggnomad.tour.next.browse', 'Explore more experiences'),\n description: t(\n 'ggnomad.tour.next.browseDesc',\n 'Return to Tourism to compare tours and activities.'\n ),\n icon: <Compass className=\"size-4\" />,\n onClick: () => navigate(joinPath(basePath, 'tourism')),\n },\n ]}\n />\n\n <div className=\"grid gap-4 lg:grid-cols-3\">\n <div className=\"lg:col-span-2 space-y-4\">\n {tour.description ? (\n <SectionCard title={t('ggnomad.tour.about', 'About this tour')}>\n <p className=\"text-text-primary\">{tour.description}</p>\n </SectionCard>\n ) : null}\n\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <div className=\"mb-4 flex items-center justify-between gap-3\">\n <h2 className=\"text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.tour.details', 'Details')}\n </h2>\n <button\n onClick={() => setEditDialogOpen(true)}\n className=\"rounded-md p-2 text-text-secondary hover:bg-action-primary-bg hover:text-action-primary-text\"\n aria-label={t('ggnomad.tour.editListing', 'Edit listing')}\n title={t('ggnomad.tour.editListing', 'Edit listing')}\n >\n <Pencil className=\"size-4\" />\n </button>\n </div>\n <dl className=\"space-y-2 text-sm\">\n <DetailRow label={t('ggnomad.tour.duration', 'Duration')} value={tour.duration} />\n <DetailRow\n label={t('ggnomad.tour.difficulty', 'Difficulty')}\n value={\n tour.difficulty ? humanizeEnum(tour.difficulty) : t('ggnomad.tour.na', 'N/A')\n }\n />\n <DetailRow\n label={t('ggnomad.tour.guide', 'Guide')}\n value={tour.guideName ?? t('ggnomad.tour.na', 'N/A')}\n />\n <DetailRow\n label={t('ggnomad.tour.price', 'Price per person')}\n value={`${tour.currency} ${Number(tour.price).toFixed(2)}`}\n />\n </dl>\n </GlassCard>\n\n {tour.highlights.length > 0 ? (\n <SectionCard title={t('ggnomad.tour.highlights', 'Highlights')}>\n <ul className=\"space-y-2 text-sm\">\n {tour.highlights.map((highlight) => (\n <li key={highlight} className=\"flex items-start gap-2 text-text-primary\">\n <CheckCircle2\n className=\"mt-0.5 size-4 flex-shrink-0 text-status-success-text\"\n aria-hidden=\"true\"\n />\n {highlight}\n </li>\n ))}\n </ul>\n </SectionCard>\n ) : null}\n\n {tour.languages.length > 0 ? (\n <SectionCard title={t('ggnomad.tour.languagesTitle', 'Guided in')}>\n <TagChips items={tour.languages} icon={<LanguagesIcon />} />\n </SectionCard>\n ) : null}\n\n <EcoAccessPanel\n sustainabilityScore={tour.sustainabilityScore}\n sustainabilityPractices={tour.sustainabilityPractices}\n accessibilityFeatures={tour.accessibilityFeatures}\n />\n\n <AvailabilityPanel listingId={tour.id ?? id} listingType=\"TOUR\" />\n\n {tour.provider ? <ProviderCard provider={tour.provider} /> : null}\n\n <ReviewsSection targetType=\"TOUR\" targetId={tour.id ?? id} />\n </div>\n <div>\n <BookingPanel\n bookingType=\"TOUR\"\n itemId={tour.id ?? id}\n itemName={tour.name}\n unitPrice={tour.price ?? 0}\n currency={tour.currency ?? 'USD'}\n />\n </div>\n </div>\n\n <ListingEditDialog\n open={editDialogOpen}\n onOpenChange={setEditDialogOpen}\n targetType=\"TOUR\"\n id={tour.id ?? id}\n initial={{\n name: tour.name,\n description: tour.description ?? undefined,\n price: tour.price,\n status: tour.status,\n images: tour.images,\n }}\n onSaved={() => refetch()}\n />\n </>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TourDetailsPage.js","names":[],"sources":["../../../src/ggnomad/pages/TourDetailsPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useNavigate, useParams } from 'react-router';\nimport {\n CheckCircle2,\n Clock,\n Compass,\n Gauge,\n Languages as LanguagesIcon,\n MapPin,\n Pencil,\n UserRound,\n Users,\n Zap,\n} from 'lucide-react';\nimport { GlassCard, IllustratedEmptyState, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_TOUR_QUERY } from '../graphql';\nimport { useGGNomad } from '../providers/GGNomadProvider';\nimport { joinPath } from '../utils/navigation';\nimport { humanizeEnum, jsonLabel } from '../utils/format';\nimport { PageShell } from './PageShell';\nimport {\n AvailabilityPanel,\n BookingPanel,\n DetailHero,\n DetailRow,\n EcoAccessPanel,\n FactGrid,\n ListingEditDialog,\n PhotoGallery,\n ProviderCard,\n RatingSummary,\n ReviewsSection,\n SectionCard,\n StatusBadge,\n TagChips,\n} from '../components';\nimport type { Fact } from '../components';\n\ntype TourDetail = {\n id: string;\n name: string;\n description?: string | null;\n type: string;\n status?: string;\n duration: string;\n difficulty?: string | null;\n guideName?: string | null;\n currency: string;\n price: number;\n rating?: number | null;\n reviewCount: number;\n highlights: string[];\n images: string[];\n languages: string[];\n location?: unknown;\n maxGroupSize: number;\n instantBook: boolean;\n sustainabilityScore: number;\n sustainabilityPractices: string[];\n accessibilityFeatures: string[];\n provider?: {\n id: string;\n name: string;\n type?: string | null;\n rating?: number | null;\n reviewCount?: number | null;\n listingCount?: number | null;\n contactEmail?: string | null;\n website?: string | null;\n } | null;\n};\n\nexport function TourDetailsPage() {\n const { t } = useI18n();\n const navigate = useNavigate();\n const { basePath = '/' } = useGGNomad();\n const { id = '' } = useParams();\n const [editDialogOpen, setEditDialogOpen] = useState(false);\n const { data, refetch } = useQuery(\n GGNOMAD_TOUR_QUERY as TypedDocumentNode<{ ggnomadTour: TourDetail | null }, { id: string }>,\n { variables: { id }, skip: !id }\n );\n const tour = data?.ggnomadTour;\n const locationLabel = jsonLabel(tour?.location);\n\n const facts: Fact[] = [];\n if (tour) {\n facts.push({\n id: 'duration',\n label: t('ggnomad.tour.duration', 'Duration'),\n value: tour.duration,\n icon: <Clock />,\n });\n if (tour.difficulty) {\n facts.push({\n id: 'difficulty',\n label: t('ggnomad.tour.difficulty', 'Difficulty'),\n value: humanizeEnum(tour.difficulty),\n icon: <Gauge />,\n });\n }\n facts.push({\n id: 'group',\n label: t('ggnomad.tour.maxGroupSize', 'Max group size'),\n value: String(tour.maxGroupSize),\n icon: <Users />,\n });\n if (tour.languages.length > 0) {\n facts.push({\n id: 'languages',\n label: t('ggnomad.tour.languages', 'Languages'),\n value: tour.languages.join(', '),\n icon: <LanguagesIcon />,\n });\n }\n if (locationLabel) {\n facts.push({\n id: 'location',\n label: t('ggnomad.tour.location', 'Location'),\n value: locationLabel,\n icon: <MapPin />,\n });\n }\n if (tour.guideName) {\n facts.push({\n id: 'guide',\n label: t('ggnomad.tour.guide', 'Guide'),\n value: tour.guideName,\n icon: <UserRound />,\n });\n }\n }\n\n return (\n <PageShell\n title={tour?.name ?? t('ggnomad.tour.fallbackTitle', 'Tour details')}\n subtitle={t('ggnomad.tour.subtitle', 'Tour package details and booking context')}\n >\n {!tour ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.tour.notFoundTitle', 'No tour found')}\n description={t(\n 'ggnomad.tour.notFoundDescription',\n 'This tour is unavailable or may have been removed.'\n )}\n />\n ) : (\n <>\n {tour.images.length > 0 ? (\n <PhotoGallery images={tour.images} alt={tour.name} />\n ) : (\n <DetailHero alt={tour.name} />\n )}\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <span className=\"rounded-full border border-border-seam bg-bg-sunken px-3 py-1 text-sm text-text-primary\">\n {humanizeEnum(tour.type)}\n </span>\n {tour.status ? <StatusBadge status={tour.status} /> : null}\n <RatingSummary rating={tour.rating} reviewCount={tour.reviewCount} />\n {tour.instantBook ? (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-status-success-bg-subtle px-3 py-1 text-sm font-medium text-status-success-text\">\n <Zap className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ggnomad.listing.instantBook', 'Instant book')}\n </span>\n ) : null}\n </div>\n\n <PagePurpose icon={<Compass className=\"size-4\" />}>\n {t(\n 'ggnomad.tour.purpose',\n 'See exactly what this tour involves — duration, difficulty, your guide, price and the highlights you’ll experience — so you can confirm it fits your trip before booking.'\n )}\n </PagePurpose>\n\n <FactGrid facts={facts} />\n\n <NextSteps\n storageKey=\"ggnomad-tour-detail\"\n steps={[\n {\n id: 'browse',\n label: t('ggnomad.tour.next.browse', 'Explore more experiences'),\n description: t(\n 'ggnomad.tour.next.browseDesc',\n 'Return to Tourism to compare tours and activities.'\n ),\n icon: <Compass className=\"size-4\" />,\n onClick: () => navigate(joinPath(basePath, 'tourism')),\n },\n ]}\n />\n\n <div className=\"grid gap-4 lg:grid-cols-3\">\n <div className=\"lg:col-span-2 space-y-4\">\n {tour.description ? (\n <SectionCard title={t('ggnomad.tour.about', 'About this tour')}>\n <p className=\"text-text-primary\">{tour.description}</p>\n </SectionCard>\n ) : null}\n\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam p-5\">\n <div className=\"mb-4 flex items-center justify-between gap-3\">\n <h2 className=\"text-lg font-medium tracking-tight text-text-primary\">\n {t('ggnomad.tour.details', 'Details')}\n </h2>\n <button\n onClick={() => setEditDialogOpen(true)}\n className=\"rounded-md p-2 text-text-secondary hover:bg-action-primary-bg hover:text-action-primary-text\"\n aria-label={t('ggnomad.tour.editListing', 'Edit listing')}\n title={t('ggnomad.tour.editListing', 'Edit listing')}\n >\n <Pencil className=\"size-4\" />\n </button>\n </div>\n <dl className=\"space-y-2 text-sm\">\n <DetailRow label={t('ggnomad.tour.duration', 'Duration')} value={tour.duration} />\n <DetailRow\n label={t('ggnomad.tour.difficulty', 'Difficulty')}\n value={\n tour.difficulty ? humanizeEnum(tour.difficulty) : t('ggnomad.tour.na', 'N/A')\n }\n />\n <DetailRow\n label={t('ggnomad.tour.guide', 'Guide')}\n value={tour.guideName ?? t('ggnomad.tour.na', 'N/A')}\n />\n <DetailRow\n label={t('ggnomad.tour.price', 'Price per person')}\n value={`${tour.currency} ${Number(tour.price).toFixed(2)}`}\n />\n </dl>\n </GlassCard>\n\n {tour.highlights.length > 0 ? (\n <SectionCard title={t('ggnomad.tour.highlights', 'Highlights')}>\n <ul className=\"space-y-2 text-sm\">\n {tour.highlights.map((highlight) => (\n <li key={highlight} className=\"flex items-start gap-2 text-text-primary\">\n <CheckCircle2\n className=\"mt-0.5 size-4 flex-shrink-0 text-status-success-text\"\n aria-hidden=\"true\"\n />\n {highlight}\n </li>\n ))}\n </ul>\n </SectionCard>\n ) : null}\n\n {tour.languages.length > 0 ? (\n <SectionCard title={t('ggnomad.tour.languagesTitle', 'Guided in')}>\n <TagChips items={tour.languages} icon={<LanguagesIcon />} />\n </SectionCard>\n ) : null}\n\n <EcoAccessPanel\n sustainabilityScore={tour.sustainabilityScore}\n sustainabilityPractices={tour.sustainabilityPractices}\n accessibilityFeatures={tour.accessibilityFeatures}\n />\n\n <AvailabilityPanel listingId={tour.id ?? id} listingType=\"TOUR\" />\n\n {tour.provider ? <ProviderCard provider={tour.provider} /> : null}\n\n <ReviewsSection targetType=\"TOUR\" targetId={tour.id ?? id} />\n </div>\n <div>\n <BookingPanel\n bookingType=\"TOUR\"\n itemId={tour.id ?? id}\n itemName={tour.name}\n unitPrice={tour.price ?? 0}\n currency={tour.currency ?? 'USD'}\n />\n </div>\n </div>\n\n <ListingEditDialog\n open={editDialogOpen}\n onOpenChange={setEditDialogOpen}\n targetType=\"TOUR\"\n id={tour.id ?? id}\n initial={{\n name: tour.name,\n description: tour.description ?? undefined,\n price: tour.price,\n status: tour.status,\n images: tour.images,\n }}\n onSaved={() => refetch()}\n />\n </>\n )}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TourismPage.js","names":[],"sources":["../../../src/ggnomad/pages/TourismPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Clock, Compass, Gauge, Leaf, Zap } from 'lucide-react';\nimport { EmphasisPanel, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_ACTIVITIES_QUERY, GGNOMAD_TOURS_QUERY } from '../graphql';\nimport { humanizeEnum } from '../utils/format';\nimport { PageShell } from './PageShell';\nimport {\n EntityCard,\n EMPTY_LISTING_FILTERS,\n ListingFilters,\n LoadMoreButton,\n RatingSummary,\n SectionCard,\n hasActiveListingFilters,\n} from '../components';\nimport type { ListingFilterState } from '../components';\n\ntype TourismItem = {\n id: string;\n name: string;\n /** Tours carry `type` (CULTURAL, ADVENTURE…), activities carry `category`. */\n type?: string;\n category?: string;\n duration: string;\n difficulty?: string | null;\n price: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n images?: string[];\n instantBook: boolean;\n sustainabilityScore: number;\n};\n\ntype ListingVariables = {\n limit: number;\n offset: number;\n status?: string;\n minSustainabilityScore?: number;\n hasAccessibilityFeatures?: string[];\n};\n\nexport function TourismPage() {\n const { t } = useI18n();\n const [tourLimit, setTourLimit] = useState(12);\n const [activityLimit, setActivityLimit] = useState(12);\n const [filters, setFilters] = useState<ListingFilterState>(EMPTY_LISTING_FILTERS);\n\n const filterVariables = {\n status: filters.status,\n minSustainabilityScore: filters.minSustainabilityScore,\n hasAccessibilityFeatures:\n filters.accessibilityFeatures.length > 0 ? filters.accessibilityFeatures : undefined,\n };\n\n const { data: toursData } = useQuery(\n GGNOMAD_TOURS_QUERY as TypedDocumentNode<{ ggnomadTours: TourismItem[] }, ListingVariables>,\n { variables: { limit: tourLimit, offset: 0, ...filterVariables } }\n );\n const { data: activitiesData } = useQuery(\n GGNOMAD_ACTIVITIES_QUERY as TypedDocumentNode<\n { ggnomadActivities: TourismItem[] },\n ListingVariables\n >,\n { variables: { limit: activityLimit, offset: 0, ...filterVariables } }\n );\n const tours = toursData?.ggnomadTours ?? [];\n const activities = activitiesData?.ggnomadActivities ?? [];\n const filtered = hasActiveListingFilters(filters);\n\n const renderList = (items: TourismItem[], basePath: string, emptyLabel: string) => {\n if (items.length === 0) {\n return (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={\n filtered\n ? t('ggnomad.tourism.noMatchTitle', 'Nothing matches these filters')\n : emptyLabel\n }\n description={\n filtered\n ? t(\n 'ggnomad.tourism.noMatchDescription',\n 'Try clearing a filter — status, sustainability score and accessibility all narrow the catalogue.'\n )\n : t(\n 'ggnomad.tourism.emptyDescription',\n 'Curated experiences will appear here once published.'\n )\n }\n />\n );\n }\n return (\n <div className=\"grid gap-3\">\n {items.map((item) => (\n <EntityCard\n key={item.id}\n to={`${basePath}/${item.id}`}\n image={item.images?.[0]}\n imageAlt={item.name}\n title={item.name}\n meta={humanizeEnum(item.type ?? item.category ?? '')}\n badge={\n item.instantBook ? (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-success-bg-subtle px-2.5 py-0.5 text-xs font-medium text-status-success-text\">\n <Zap className=\"size-3\" aria-hidden=\"true\" />\n {t('ggnomad.listing.instantBook', 'Instant book')}\n </span>\n ) : null\n }\n price={`${item.currency} ${Number(item.price).toFixed(2)}`}\n footer={\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1\">\n <span className=\"inline-flex items-center gap-1\">\n <Clock className=\"size-3.5\" aria-hidden=\"true\" />\n {item.duration}\n </span>\n {item.difficulty ? (\n <span className=\"inline-flex items-center gap-1\">\n <Gauge className=\"size-3.5\" aria-hidden=\"true\" />\n {humanizeEnum(item.difficulty)}\n </span>\n ) : null}\n <RatingSummary rating={item.rating} reviewCount={item.reviewCount} />\n {item.sustainabilityScore > 0 ? (\n <span className=\"inline-flex items-center gap-1 tabular-nums text-status-success-text\">\n <Leaf className=\"size-3.5\" aria-hidden=\"true\" />\n {item.sustainabilityScore}\n </span>\n ) : null}\n </div>\n }\n />\n ))}\n </div>\n );\n };\n\n return (\n <PageShell\n title={t('ggnomad.tourism.title', 'Tourism')}\n titleAccent={t('ggnomad.tourism.title.accent', 'Tourism')}\n subtitle={t('ggnomad.tourism.subtitle', 'Tours and curated activity experiences')}\n >\n <PagePurpose icon={<Compass className=\"size-4\" />}>\n {t(\n 'ggnomad.tourism.purpose',\n 'Plan what you’ll actually do at a destination — guided tours and bookable activities curated by local operators. Open any experience to review duration, price and what’s included, then add it to a booking.'\n )}\n </PagePurpose>\n\n <EmphasisPanel>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div>\n <p className=\"text-sm text-text-secondary\">{t('ggnomad.tourism.tours', 'Tours')}</p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums text-text-primary\">\n {tours.length}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.tourism.activities', 'Activities')}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums text-text-primary\">\n {activities.length}\n </p>\n </div>\n </div>\n <p className=\"mt-4 text-sm text-text-secondary\">\n {t(\n 'ggnomad.tourism.emphasisNote',\n 'Curated, bookable experiences across your destinations — pick a tour or activity below to see full details.'\n )}\n </p>\n </EmphasisPanel>\n\n <ListingFilters\n idPrefix=\"ggnomad-tourism\"\n value={filters}\n onChange={(next) => {\n setTourLimit(12);\n setActivityLimit(12);\n setFilters(next);\n }}\n resultCount={tours.length + activities.length}\n />\n\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <SectionCard title={t('ggnomad.tourism.tours', 'Tours')}>\n {renderList(tours, 'tours', t('ggnomad.tourism.toursEmpty', 'No tours yet'))}\n <LoadMoreButton\n visible={tours.length >= tourLimit}\n onLoadMore={() => setTourLimit((l) => l + 12)}\n />\n </SectionCard>\n <SectionCard title={t('ggnomad.tourism.activities', 'Activities')}>\n {renderList(\n activities,\n 'activities',\n t('ggnomad.tourism.activitiesEmpty', 'No activities yet')\n )}\n <LoadMoreButton\n visible={activities.length >= activityLimit}\n onLoadMore={() => setActivityLimit((l) => l + 12)}\n />\n </SectionCard>\n </div>\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TourismPage.js","names":[],"sources":["../../../src/ggnomad/pages/TourismPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Clock, Compass, Gauge, Leaf, Zap } from 'lucide-react';\nimport { EmphasisPanel, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\n\nimport { GGNOMAD_ACTIVITIES_QUERY, GGNOMAD_TOURS_QUERY } from '../graphql';\nimport { humanizeEnum } from '../utils/format';\nimport { PageShell } from './PageShell';\nimport {\n EntityCard,\n EMPTY_LISTING_FILTERS,\n ListingFilters,\n LoadMoreButton,\n RatingSummary,\n SectionCard,\n hasActiveListingFilters,\n} from '../components';\nimport type { ListingFilterState } from '../components';\n\ntype TourismItem = {\n id: string;\n name: string;\n /** Tours carry `type` (CULTURAL, ADVENTURE…), activities carry `category`. */\n type?: string;\n category?: string;\n duration: string;\n difficulty?: string | null;\n price: number;\n currency: string;\n rating?: number | null;\n reviewCount: number;\n images?: string[];\n instantBook: boolean;\n sustainabilityScore: number;\n};\n\ntype ListingVariables = {\n limit: number;\n offset: number;\n status?: string;\n minSustainabilityScore?: number;\n hasAccessibilityFeatures?: string[];\n};\n\nexport function TourismPage() {\n const { t } = useI18n();\n const [tourLimit, setTourLimit] = useState(12);\n const [activityLimit, setActivityLimit] = useState(12);\n const [filters, setFilters] = useState<ListingFilterState>(EMPTY_LISTING_FILTERS);\n\n const filterVariables = {\n status: filters.status,\n minSustainabilityScore: filters.minSustainabilityScore,\n hasAccessibilityFeatures:\n filters.accessibilityFeatures.length > 0 ? filters.accessibilityFeatures : undefined,\n };\n\n const { data: toursData } = useQuery(\n GGNOMAD_TOURS_QUERY as TypedDocumentNode<{ ggnomadTours: TourismItem[] }, ListingVariables>,\n { variables: { limit: tourLimit, offset: 0, ...filterVariables } }\n );\n const { data: activitiesData } = useQuery(\n GGNOMAD_ACTIVITIES_QUERY as TypedDocumentNode<\n { ggnomadActivities: TourismItem[] },\n ListingVariables\n >,\n { variables: { limit: activityLimit, offset: 0, ...filterVariables } }\n );\n const tours = toursData?.ggnomadTours ?? [];\n const activities = activitiesData?.ggnomadActivities ?? [];\n const filtered = hasActiveListingFilters(filters);\n\n const renderList = (items: TourismItem[], basePath: string, emptyLabel: string) => {\n if (items.length === 0) {\n return (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={\n filtered\n ? t('ggnomad.tourism.noMatchTitle', 'Nothing matches these filters')\n : emptyLabel\n }\n description={\n filtered\n ? t(\n 'ggnomad.tourism.noMatchDescription',\n 'Try clearing a filter — status, sustainability score and accessibility all narrow the catalogue.'\n )\n : t(\n 'ggnomad.tourism.emptyDescription',\n 'Curated experiences will appear here once published.'\n )\n }\n />\n );\n }\n return (\n <div className=\"grid gap-3\">\n {items.map((item) => (\n <EntityCard\n key={item.id}\n to={`${basePath}/${item.id}`}\n image={item.images?.[0]}\n imageAlt={item.name}\n title={item.name}\n meta={humanizeEnum(item.type ?? item.category ?? '')}\n badge={\n item.instantBook ? (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-success-bg-subtle px-2.5 py-0.5 text-xs font-medium text-status-success-text\">\n <Zap className=\"size-3\" aria-hidden=\"true\" />\n {t('ggnomad.listing.instantBook', 'Instant book')}\n </span>\n ) : null\n }\n price={`${item.currency} ${Number(item.price).toFixed(2)}`}\n footer={\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1\">\n <span className=\"inline-flex items-center gap-1\">\n <Clock className=\"size-3.5\" aria-hidden=\"true\" />\n {item.duration}\n </span>\n {item.difficulty ? (\n <span className=\"inline-flex items-center gap-1\">\n <Gauge className=\"size-3.5\" aria-hidden=\"true\" />\n {humanizeEnum(item.difficulty)}\n </span>\n ) : null}\n <RatingSummary rating={item.rating} reviewCount={item.reviewCount} />\n {item.sustainabilityScore > 0 ? (\n <span className=\"inline-flex items-center gap-1 tabular-nums text-status-success-text\">\n <Leaf className=\"size-3.5\" aria-hidden=\"true\" />\n {item.sustainabilityScore}\n </span>\n ) : null}\n </div>\n }\n />\n ))}\n </div>\n );\n };\n\n return (\n <PageShell\n title={t('ggnomad.tourism.title', 'Tourism')}\n titleAccent={t('ggnomad.tourism.title.accent', 'Tourism')}\n subtitle={t('ggnomad.tourism.subtitle', 'Tours and curated activity experiences')}\n >\n <PagePurpose icon={<Compass className=\"size-4\" />}>\n {t(\n 'ggnomad.tourism.purpose',\n 'Plan what you’ll actually do at a destination — guided tours and bookable activities curated by local operators. Open any experience to review duration, price and what’s included, then add it to a booking.'\n )}\n </PagePurpose>\n\n <EmphasisPanel>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div>\n <p className=\"text-sm text-text-secondary\">{t('ggnomad.tourism.tours', 'Tours')}</p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums text-text-primary\">\n {tours.length}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {t('ggnomad.tourism.activities', 'Activities')}\n </p>\n <p className=\"mt-1 text-2xl font-semibold tabular-nums text-text-primary\">\n {activities.length}\n </p>\n </div>\n </div>\n <p className=\"mt-4 text-sm text-text-secondary\">\n {t(\n 'ggnomad.tourism.emphasisNote',\n 'Curated, bookable experiences across your destinations — pick a tour or activity below to see full details.'\n )}\n </p>\n </EmphasisPanel>\n\n <ListingFilters\n idPrefix=\"ggnomad-tourism\"\n value={filters}\n onChange={(next) => {\n setTourLimit(12);\n setActivityLimit(12);\n setFilters(next);\n }}\n resultCount={tours.length + activities.length}\n />\n\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <SectionCard title={t('ggnomad.tourism.tours', 'Tours')}>\n {renderList(tours, 'tours', t('ggnomad.tourism.toursEmpty', 'No tours yet'))}\n <LoadMoreButton\n visible={tours.length >= tourLimit}\n onLoadMore={() => setTourLimit((l) => l + 12)}\n />\n </SectionCard>\n <SectionCard title={t('ggnomad.tourism.activities', 'Activities')}>\n {renderList(\n activities,\n 'activities',\n t('ggnomad.tourism.activitiesEmpty', 'No activities yet')\n )}\n <LoadMoreButton\n visible={activities.length >= activityLimit}\n onLoadMore={() => setActivityLimit((l) => l + 12)}\n />\n </SectionCard>\n </div>\n </PageShell>\n );\n}\n"],"mappings":"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"}
|