@burdenoff/microfe-ecoimpacthub 2026.819.4 → 2026.830.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ecoimpacthub/hooks/index.d.ts +2 -0
- package/dist/ecoimpacthub/hooks/index.d.ts.map +1 -1
- package/dist/ecoimpacthub/hooks/index.js +2 -0
- package/dist/ecoimpacthub/hooks/useActiveEcoEntity.d.ts +11 -0
- package/dist/ecoimpacthub/hooks/useActiveEcoEntity.d.ts.map +1 -0
- package/dist/ecoimpacthub/hooks/useActiveEcoEntity.js +21 -0
- package/dist/ecoimpacthub/hooks/useActiveEcoEntity.js.map +1 -0
- package/dist/ecoimpacthub/hooks/useTr.d.ts +4 -0
- package/dist/ecoimpacthub/hooks/useTr.d.ts.map +1 -0
- package/dist/ecoimpacthub/hooks/useTr.js +20 -0
- package/dist/ecoimpacthub/hooks/useTr.js.map +1 -0
- package/dist/ecoimpacthub/pages/DashboardPage.d.ts.map +1 -1
- package/dist/ecoimpacthub/pages/DashboardPage.js +162 -138
- package/dist/ecoimpacthub/pages/DashboardPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/_shared/MethodPicker.d.ts +15 -0
- package/dist/ecoimpacthub/pages/_shared/MethodPicker.d.ts.map +1 -0
- package/dist/ecoimpacthub/pages/_shared/MethodPicker.js +36 -0
- package/dist/ecoimpacthub/pages/_shared/MethodPicker.js.map +1 -0
- package/dist/ecoimpacthub/pages/_shared/UseMyLocationButton.d.ts +15 -0
- package/dist/ecoimpacthub/pages/_shared/UseMyLocationButton.d.ts.map +1 -0
- package/dist/ecoimpacthub/pages/_shared/UseMyLocationButton.js +66 -0
- package/dist/ecoimpacthub/pages/_shared/UseMyLocationButton.js.map +1 -0
- package/dist/ecoimpacthub/pages/habitat/HabitatProjectDetailPage.d.ts.map +1 -1
- package/dist/ecoimpacthub/pages/habitat/HabitatProjectDetailPage.js +127 -124
- package/dist/ecoimpacthub/pages/habitat/HabitatProjectDetailPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/observations/ObservationsPage.d.ts.map +1 -1
- package/dist/ecoimpacthub/pages/observations/ObservationsPage.js +226 -224
- package/dist/ecoimpacthub/pages/observations/ObservationsPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/reports/ReportDetailPage.d.ts.map +1 -1
- package/dist/ecoimpacthub/pages/reports/ReportDetailPage.js +159 -156
- package/dist/ecoimpacthub/pages/reports/ReportDetailPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/sites/SiteDetailPage.d.ts.map +1 -1
- package/dist/ecoimpacthub/pages/sites/SiteDetailPage.js +40 -37
- package/dist/ecoimpacthub/pages/sites/SiteDetailPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/sites/SitesPage.d.ts.map +1 -1
- package/dist/ecoimpacthub/pages/sites/SitesPage.js +208 -171
- package/dist/ecoimpacthub/pages/sites/SitesPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/species/SpeciesDetailPage.d.ts.map +1 -1
- package/dist/ecoimpacthub/pages/species/SpeciesDetailPage.js +211 -214
- package/dist/ecoimpacthub/pages/species/SpeciesDetailPage.js.map +1 -1
- package/dist/ecoimpacthub/pages/species/SpeciesPage.d.ts.map +1 -1
- package/dist/ecoimpacthub/pages/species/SpeciesPage.js +174 -157
- package/dist/ecoimpacthub/pages/species/SpeciesPage.js.map +1 -1
- package/dist/ecoimpacthub/utils/activeEntity.d.ts +30 -0
- package/dist/ecoimpacthub/utils/activeEntity.d.ts.map +1 -0
- package/dist/ecoimpacthub/utils/activeEntity.js +12 -0
- package/dist/ecoimpacthub/utils/activeEntity.js.map +1 -0
- package/dist/ecoimpacthub/utils/pageContext.d.ts.map +1 -1
- package/dist/ecoimpacthub/utils/pageContext.js +30 -25
- package/dist/ecoimpacthub/utils/pageContext.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/ecoimpacthub/pages/DashboardPage.tsx"],"sourcesContent":["/**\n * Dashboard — EcoImpactHub environment, conservation & sustainability overview.\n * @module ecoimpacthub/pages\n *\n * KPIs are aggregated client-side from the real `eco*` list endpoints plus the\n * `ecoCarbonTotalKg` aggregate (by scope). Widgets show recent field\n * observations and open threshold breaches. No mock data.\n *\n * Customizable (BOFF-3943): the \"Customize dashboard\" CTA opens a widget\n * palette (show/hide/reorder + freeform notes), persisted per user/workspace\n * via {@link useDashboardLayout}. Uncustomized users see the exact same\n * layout as before this change — the widget order defaults to this file's\n * original hardcoded order, and the carbon-by-scope / recent-breaches pair\n * keeps its original side-by-side layout whenever they remain adjacent.\n */\n\nimport { useMemo, useState, type ReactNode } from 'react';\nimport {\n MapPin,\n PawPrint,\n Trees,\n Factory,\n TriangleAlert,\n Binoculars,\n ShieldCheck,\n ArrowRight,\n Radio,\n LayoutGrid,\n StickyNote,\n} from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n BarChart,\n Bar,\n XAxis,\n YAxis,\n CartesianGrid,\n ResponsiveContainer,\n Tooltip,\n Cell,\n LabelList,\n} from 'recharts';\nimport { PageLayout } from '../components/PageLayout';\nimport { MetricsCard } from '../components/MetricsCard';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { MiniBar } from '../components/MiniBar';\nimport { LoadingView, Section } from '../components/StateViews';\nimport { EmptyState } from '../components/EmptyState';\nimport { DashboardCustomizerDialog } from '../components/DashboardCustomizerDialog';\nimport { ScopeSplitBar, ScopeLegend, Meter, SCOPE_HUE } from './carbonVisuals';\nimport { useEcoImpactHub } from '../context';\nimport {\n useEcoSites,\n useEcoSpecies,\n useEcoHabitatProjects,\n useEcoCarbonTotals,\n useEcoMeasurements,\n useEcoObservations,\n useEcoSensors,\n useDashboardLayout,\n type DashboardWidgetId,\n} from '../hooks';\nimport {\n formatNumber,\n formatRelativeTime,\n formatCo2e,\n formatEnumLabel,\n formatPercent,\n} from '../utils/formatters';\nimport type { EcoConservationStatus, EcoEmissionScope } from '../types';\n\nconst ACTIVE_PROJECT_STATUSES = new Set(['PLANNED', 'IN_PROGRESS', 'MONITORING']);\n\n/** Conservation-status → meter tint (semantic, IUCN-severity ordered). */\nconst CONSERVATION_TONE: Record<EcoConservationStatus, string> = {\n LEAST_CONCERN: 'var(--color-status-success-text)',\n NEAR_THREATENED: 'var(--color-accent-teal)',\n VULNERABLE: 'var(--color-status-warning-text)',\n ENDANGERED: 'var(--color-status-error-text)',\n CRITICALLY_ENDANGERED: 'var(--color-status-error-text)',\n EXTINCT_IN_WILD: 'var(--color-text-tertiary)',\n DATA_DEFICIENT: 'var(--color-text-tertiary)',\n NOT_EVALUATED: 'var(--color-text-tertiary)',\n};\n\nexport function DashboardPage() {\n const { navigateTo, currentUser, workspaceId } = useEcoImpactHub();\n const [customizerOpen, setCustomizerOpen] = useState(false);\n const dashboardLayout = useDashboardLayout(workspaceId, currentUser?.id);\n\n const sites = useEcoSites({ pagination: { limit: 100, offset: 0 } });\n const species = useEcoSpecies({ pagination: { limit: 100, offset: 0 } });\n const projects = useEcoHabitatProjects();\n const carbon = useEcoCarbonTotals();\n const breaches = useEcoMeasurements({ breachesOnly: true, pagination: { limit: 6, offset: 0 } });\n const observations = useEcoObservations({ pagination: { limit: 6, offset: 0 } });\n const sensors = useEcoSensors();\n\n const siteRows = sites.data?.items ?? [];\n const speciesRows = species.data?.items ?? [];\n const projectRows = projects.data?.items ?? [];\n const sensorRows = sensors.data?.items ?? [];\n\n const activeSites = siteRows.filter((s) => s.status === 'ACTIVE').length;\n const priorityCount = speciesRows.filter((s) => s.isPriority).length;\n const threatened = speciesRows.filter((s) =>\n ['VULNERABLE', 'ENDANGERED', 'CRITICALLY_ENDANGERED'].includes(s.conservationStatus)\n ).length;\n const activeProjects = projectRows.filter((p) => ACTIVE_PROJECT_STATUSES.has(p.status)).length;\n const onlineSensors = sensorRows.filter((s) => s.status === 'ONLINE').length;\n const offlineSensors = sensorRows.filter((s) =>\n ['OFFLINE', 'DEGRADED', 'MAINTENANCE'].includes(s.status)\n ).length;\n\n const carbonData = useMemo<{ scope: EcoEmissionScope; label: string; kg: number }[]>(() => {\n const c = carbon.data;\n if (!c) return [];\n return [\n { scope: 'SCOPE_1', label: 'Scope 1', kg: c.scope1 },\n { scope: 'SCOPE_2', label: 'Scope 2', kg: c.scope2 },\n { scope: 'SCOPE_3', label: 'Scope 3', kg: c.scope3 },\n ];\n }, [carbon.data]);\n\n const isInitialLoading =\n sites.isLoading && species.isLoading && projects.isLoading && carbon.isLoading;\n\n const headerActions = (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => setCustomizerOpen(true)}>\n <LayoutGrid className=\"mr-1.5 h-3.5 w-3.5\" />\n Customize dashboard\n </Button>\n );\n\n if (isInitialLoading) {\n return (\n <PageLayout title=\"EcoImpactHub\" titleGradient aurora actions={headerActions}>\n <LoadingView label=\"Loading your impact overview…\" />\n </PageLayout>\n );\n }\n\n const greetingName = currentUser?.firstName || currentUser?.name || 'there';\n const breachRows = breaches.data?.items ?? [];\n const observationRows = observations.data?.items ?? [];\n\n const kpisWidget = (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6\">\n <MetricsCard\n icon={<MapPin className=\"h-5 w-5\" />}\n value={\n sites.data?.pageInfo?.hasMore ? `${siteRows.length}+` : formatNumber(siteRows.length)\n }\n label=\"Monitored sites\"\n footer={\n <div className=\"space-y-2\">\n <MiniBar\n value={siteRows.length ? (activeSites / siteRows.length) * 100 : 0}\n height={4}\n barClassName=\"bg-action-primary-bg\"\n />\n <p className=\"text-xs text-text-tertiary\">\n <span className=\"tabular-nums text-text-secondary\">{activeSites}</span> active\n </p>\n </div>\n }\n />\n <MetricsCard\n icon={<PawPrint className=\"h-5 w-5\" />}\n value={\n species.data?.pageInfo?.hasMore\n ? `${speciesRows.length}+`\n : formatNumber(speciesRows.length)\n }\n label=\"Species tracked\"\n accent={threatened > 0 ? 'warning' : undefined}\n footer={\n <div className=\"space-y-2\">\n <MiniBar\n value={speciesRows.length ? (threatened / speciesRows.length) * 100 : 0}\n height={4}\n barClassName=\"bg-status-warning-text\"\n />\n <p className=\"text-xs text-text-tertiary\">\n <span className=\"tabular-nums text-text-secondary\">{priorityCount}</span> priority ·{' '}\n <span className=\"tabular-nums text-text-secondary\">{threatened}</span> threatened\n </p>\n </div>\n }\n />\n <MetricsCard\n icon={<Trees className=\"h-5 w-5\" />}\n value={formatNumber(activeProjects)}\n label=\"Active habitat projects\"\n footer={\n <div className=\"space-y-2\">\n <MiniBar\n value={projectRows.length ? (activeProjects / projectRows.length) * 100 : 0}\n height={4}\n barClassName=\"bg-action-primary-bg\"\n />\n <p className=\"text-xs text-text-tertiary\">\n <span className=\"tabular-nums text-text-secondary\">\n {formatNumber(projectRows.length)}\n </span>{' '}\n total\n </p>\n </div>\n }\n />\n <MetricsCard\n icon={<Factory className=\"h-5 w-5\" />}\n value={carbon.data ? formatCo2e(carbon.data.total) : '—'}\n label=\"Total footprint\"\n featured\n footer={\n carbon.data && carbon.data.total > 0 ? (\n <div className=\"space-y-2\">\n <ScopeSplitBar\n scope1={carbon.data.scope1}\n scope2={carbon.data.scope2}\n scope3={carbon.data.scope3}\n height={6}\n />\n <p className=\"text-xs text-text-tertiary\">Scope 1 · 2 · 3 split</p>\n </div>\n ) : undefined\n }\n />\n <MetricsCard\n icon={<Radio className=\"h-5 w-5\" />}\n value={formatNumber(onlineSensors)}\n label=\"Sensors online\"\n accent={offlineSensors > 0 ? 'warning' : 'success'}\n footer={\n <div className=\"space-y-2\">\n <MiniBar\n value={sensorRows.length ? (onlineSensors / sensorRows.length) * 100 : 0}\n height={4}\n barClassName=\"bg-status-success-text\"\n />\n <p className=\"text-xs text-text-tertiary\">\n <span className=\"tabular-nums text-text-secondary\">{offlineSensors}</span> need\n attention\n </p>\n </div>\n }\n />\n <MetricsCard\n icon={<TriangleAlert className=\"h-5 w-5\" />}\n value={\n breaches.data?.pageInfo?.hasMore\n ? `${breachRows.length}+`\n : formatNumber(breachRows.length)\n }\n label=\"Open breaches\"\n accent={breachRows.length > 0 ? 'error' : 'success'}\n footer={\n <p\n className={\n breachRows.length > 0\n ? 'text-xs font-medium text-status-error-text'\n : 'text-xs text-text-tertiary'\n }\n >\n {breachRows.length > 0 ? 'Needs attention' : 'All parameters in range'}\n </p>\n }\n />\n </div>\n );\n\n const carbonByScopeWidget = (\n <GlassCard treatment=\"glass\" className=\"p-6 h-full\">\n <div className=\"mb-4 flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">Carbon footprint by scope</h2>\n <p className=\"mt-0.5 text-sm text-text-secondary\">\n GHG Protocol Scope 1 / 2 / 3 emissions (CO₂e).\n </p>\n </div>\n <div className=\"flex items-center gap-4\">\n {carbon.data && carbon.data.total > 0 && (\n <div className=\"text-right\">\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">\n {formatCo2e(carbon.data.total)}\n </p>\n <p className=\"text-xs text-text-tertiary\">total</p>\n </div>\n )}\n <button\n type=\"button\"\n onClick={() => navigateTo('carbon')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n Ledger <ArrowRight className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n {carbonData.every((d) => d.kg === 0) ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Factory className=\"h-10 w-10\" />}\n title=\"No carbon entries yet\"\n description=\"Record activity in the carbon ledger to see your Scope 1–3 footprint here.\"\n />\n ) : (\n <>\n <div className=\"h-56\">\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n <BarChart data={carbonData} margin={{ top: 20, right: 8, left: 8, bottom: 0 }}>\n <defs>\n {carbonData.map((d) => (\n <linearGradient\n key={d.scope}\n id={`ecoScopeGrad-${d.scope}`}\n x1=\"0\"\n y1=\"0\"\n x2=\"0\"\n y2=\"1\"\n >\n <stop offset=\"0%\" style={{ stopColor: SCOPE_HUE[d.scope], stopOpacity: 1 }} />\n <stop\n offset=\"100%\"\n style={{ stopColor: SCOPE_HUE[d.scope], stopOpacity: 0.55 }}\n />\n </linearGradient>\n ))}\n </defs>\n <CartesianGrid\n vertical={false}\n stroke=\"var(--color-border-seam)\"\n strokeOpacity={0.6}\n strokeDasharray=\"3 4\"\n />\n <XAxis\n dataKey=\"label\"\n tick={{ fill: 'var(--color-text-secondary)', fontSize: 12 }}\n axisLine={{ stroke: 'var(--color-border-seam)' }}\n tickLine={false}\n dy={4}\n />\n <YAxis\n tick={{ fill: 'var(--color-text-tertiary)', fontSize: 11 }}\n axisLine={false}\n tickLine={false}\n width={48}\n tickFormatter={(v: number) => (v >= 1000 ? `${Math.round(v / 1000)}t` : `${v}`)}\n />\n <Tooltip\n cursor={{ fill: 'var(--color-bg-sunken)', opacity: 0.4 }}\n contentStyle={{\n background: 'var(--color-bg-surface)',\n border: '1px solid var(--color-border-seam)',\n borderRadius: 12,\n color: 'var(--color-text-primary)',\n }}\n formatter={(value: number) => [formatCo2e(value), 'Emissions']}\n />\n <Bar dataKey=\"kg\" radius={[8, 8, 2, 2]} maxBarSize={72}>\n {carbonData.map((d) => (\n <Cell key={d.scope} fill={`url(#ecoScopeGrad-${d.scope})`} />\n ))}\n <LabelList\n dataKey=\"kg\"\n position=\"top\"\n formatter={(v: number) => (v > 0 ? formatCo2e(v) : '')}\n style={{ fill: 'var(--color-text-secondary)', fontSize: 11, fontWeight: 600 }}\n />\n </Bar>\n </BarChart>\n </ResponsiveContainer>\n </div>\n <ScopeLegend data={carbonData} className=\"mt-4 border-t border-border-seam pt-4\" />\n </>\n )}\n </GlassCard>\n );\n\n const recentBreachesWidget = (\n <GlassCard treatment=\"glass\" className=\"p-6 h-full\">\n <div className=\"mb-4 flex items-center justify-between gap-3\">\n <div className=\"flex items-center gap-2\">\n <h2 className=\"text-base font-semibold text-text-primary\">Recent breaches</h2>\n {breachRows.length > 0 && (\n <span className=\"rounded-full bg-status-error-bg-subtle px-2 py-0.5 text-xs font-medium tabular-nums text-status-error-text\">\n {breaches.data?.pageInfo?.hasMore ? `${breachRows.length}+` : breachRows.length}\n </span>\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => navigateTo('monitoring')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n Monitoring <ArrowRight className=\"h-4 w-4\" />\n </button>\n </div>\n {breachRows.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center rounded-xl border border-dashed border-border-seam py-10 text-center\">\n <span className=\"flex size-12 items-center justify-center rounded-full bg-status-success-bg-subtle\">\n <ShieldCheck className=\"h-6 w-6 text-status-success-text\" />\n </span>\n <p className=\"mt-3 text-sm font-medium text-text-primary\">All parameters in range</p>\n <p className=\"mt-1 text-xs text-text-tertiary\">No open threshold breaches.</p>\n </div>\n ) : (\n <ul className=\"space-y-2.5\">\n {breachRows.map((m) => (\n <li\n key={m.id}\n className=\"flex items-start justify-between gap-3 rounded-xl bg-bg-sunken/50 p-3\"\n >\n <div className=\"flex min-w-0 items-start gap-2.5\">\n <span\n aria-hidden=\"true\"\n className=\"mt-1.5 size-2 shrink-0 rounded-full bg-status-error-text motion-safe:animate-vc-glow-pulse\"\n />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{m.parameter}</p>\n <p className=\"text-xs text-text-tertiary\">\n <span className=\"tabular-nums\">\n {formatNumber(m.value)} {m.unit}\n </span>{' '}\n · {formatRelativeTime(m.recordedAt)}\n </p>\n </div>\n </div>\n <StatusBadge status=\"BREACH\" />\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n );\n\n const recentObservationsWidget = (\n <Section\n title=\"Recent field observations\"\n description=\"Latest wildlife & biodiversity sightings recorded across your sites.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('observations')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n All observations <ArrowRight className=\"h-4 w-4\" />\n </button>\n }\n >\n {observationRows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Binoculars className=\"h-10 w-10\" />}\n title=\"No observations yet\"\n description=\"Record your first field observation to build your biodiversity dataset.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {observationRows.map((o) => (\n <GlassCard\n key={o.id}\n treatment=\"plain\"\n className=\"p-4 transition-shadow hover:shadow-float\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex min-w-0 items-start gap-2.5\">\n <span className=\"mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-accent\">\n <Binoculars className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {o.species?.commonName ?? 'Unknown species'}\n </p>\n {o.species?.scientificName && (\n <p className=\"truncate text-xs italic text-text-tertiary\">\n {o.species.scientificName}\n </p>\n )}\n </div>\n </div>\n <span className=\"shrink-0 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium tabular-nums text-text-primary\">\n ×{formatNumber(o.count)}\n </span>\n </div>\n <div className=\"mt-3 flex items-center justify-between border-t border-border-seam/70 pt-2.5\">\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n {formatEnumLabel(o.method)}\n </span>\n <span className=\"text-xs text-text-tertiary\">\n {formatRelativeTime(o.observedAt)}\n </span>\n </div>\n </GlassCard>\n ))}\n </div>\n )}\n </Section>\n );\n\n const conservationStatusWidget =\n speciesRows.length > 0 ? (\n <Section\n title=\"Conservation status\"\n description=\"Distribution of tracked species by IUCN-style status.\"\n >\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"space-y-3.5\">\n {(() => {\n const counts = new Map<string, number>();\n for (const s of speciesRows) {\n counts.set(s.conservationStatus, (counts.get(s.conservationStatus) ?? 0) + 1);\n }\n const total = speciesRows.length;\n return [...counts.entries()]\n .sort((a, b) => b[1] - a[1])\n .map(([status, count]) => {\n const pct = (count / total) * 100;\n const tone =\n CONSERVATION_TONE[status as EcoConservationStatus] ??\n 'var(--color-text-tertiary)';\n return (\n <div key={status} className=\"flex items-center gap-3 sm:gap-4\">\n <div className=\"w-36 shrink-0 sm:w-48\">\n <StatusBadge status={status} />\n </div>\n <Meter value={pct} color={tone} className=\"flex-1\" />\n <span className=\"w-9 shrink-0 text-right text-sm font-medium tabular-nums text-text-primary\">\n {count}\n </span>\n <span className=\"hidden w-12 shrink-0 text-right text-xs tabular-nums text-text-tertiary sm:inline\">\n {formatPercent(pct)}\n </span>\n </div>\n );\n });\n })()}\n </div>\n </GlassCard>\n </Section>\n ) : null;\n\n const WIDGET_RENDER: Record<DashboardWidgetId, ReactNode> = {\n kpis: kpisWidget,\n carbonByScope: carbonByScopeWidget,\n recentBreaches: recentBreachesWidget,\n recentObservations: recentObservationsWidget,\n conservationStatus: conservationStatusWidget,\n };\n\n const order = dashboardLayout.layout.widgetOrder;\n const blocks: ReactNode[] = [];\n for (let i = 0; i < order.length; i++) {\n const id = order[i];\n if (id === 'kpis') {\n blocks.push(<div key={id}>{WIDGET_RENDER[id]}</div>);\n continue;\n }\n // Preserve the original side-by-side carbon-by-scope / recent-breaches pairing\n // whenever they remain adjacent in that order (the default, uncustomized case).\n if (id === 'carbonByScope' && order[i + 1] === 'recentBreaches') {\n blocks.push(\n <div key={`${id}-pair`} className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <div className=\"lg:col-span-2\">{carbonByScopeWidget}</div>\n <div>{recentBreachesWidget}</div>\n </div>\n );\n i += 1;\n continue;\n }\n if (id === 'recentBreaches' && order[i - 1] === 'carbonByScope') {\n // Already rendered as part of the pair above.\n continue;\n }\n const content = WIDGET_RENDER[id];\n if (!content) continue;\n blocks.push(<div key={id}>{content}</div>);\n }\n\n return (\n <PageLayout\n title=\"EcoImpactHub\"\n titleGradient\n aurora\n description={`Welcome back, ${greetingName} — your environment, conservation & sustainability control room.`}\n actions={headerActions}\n >\n <div className=\"space-y-6\">\n {blocks}\n\n {dashboardLayout.layout.notes.length > 0 && (\n <Section title=\"Custom notes\" description=\"Your personal notes, visible only to you.\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {dashboardLayout.layout.notes.map((note) => (\n <GlassCard key={note.id} treatment=\"plain\" className=\"p-4\">\n <div className=\"flex items-start gap-2.5\">\n <StickyNote className=\"mt-0.5 h-4 w-4 shrink-0 text-accent\" />\n <div className=\"min-w-0\">\n {note.title && (\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {note.title}\n </p>\n )}\n {note.body && (\n <p className=\"mt-0.5 text-sm text-text-secondary\">{note.body}</p>\n )}\n </div>\n </div>\n </GlassCard>\n ))}\n </div>\n </Section>\n )}\n </div>\n\n <DashboardCustomizerDialog\n open={customizerOpen}\n onOpenChange={setCustomizerOpen}\n layout={dashboardLayout}\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/ecoimpacthub/pages/DashboardPage.tsx"],"sourcesContent":["/**\n * Dashboard — EcoImpactHub environment, conservation & sustainability overview.\n * @module ecoimpacthub/pages\n *\n * KPIs are aggregated client-side from the real `eco*` list endpoints plus the\n * `ecoCarbonTotalKg` aggregate (by scope). Widgets show recent field\n * observations and open threshold breaches. No mock data.\n *\n * Customizable (BOFF-3943): the \"Customize dashboard\" CTA opens a widget\n * palette (show/hide/reorder + freeform notes), persisted per user/workspace\n * via {@link useDashboardLayout}. Uncustomized users see the exact same\n * layout as before this change — the widget order defaults to this file's\n * original hardcoded order, and the carbon-by-scope / recent-breaches pair\n * keeps its original side-by-side layout whenever they remain adjacent.\n */\n\nimport { useMemo, useState, type ReactNode } from 'react';\nimport {\n MapPin,\n PawPrint,\n Trees,\n Factory,\n TriangleAlert,\n Binoculars,\n ShieldCheck,\n ArrowRight,\n Radio,\n LayoutGrid,\n StickyNote,\n} from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n BarChart,\n Bar,\n XAxis,\n YAxis,\n CartesianGrid,\n ResponsiveContainer,\n Tooltip,\n Cell,\n LabelList,\n} from 'recharts';\nimport { PageLayout } from '../components/PageLayout';\nimport { MetricsCard } from '../components/MetricsCard';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { MiniBar } from '../components/MiniBar';\nimport { LoadingView, Section } from '../components/StateViews';\nimport { EmptyState } from '../components/EmptyState';\nimport { DashboardCustomizerDialog } from '../components/DashboardCustomizerDialog';\nimport { ScopeSplitBar, ScopeLegend, Meter, SCOPE_HUE } from './carbonVisuals';\nimport { useEcoImpactHub } from '../context';\nimport {\n useEcoSites,\n useEcoSpecies,\n useEcoHabitatProjects,\n useEcoCarbonTotals,\n useEcoMeasurements,\n useEcoObservations,\n useEcoSensors,\n useDashboardLayout,\n useTr,\n type DashboardWidgetId,\n} from '../hooks';\nimport {\n formatNumber,\n formatRelativeTime,\n formatCo2e,\n formatEnumLabel,\n formatPercent,\n} from '../utils/formatters';\nimport type { EcoConservationStatus, EcoEmissionScope } from '../types';\n\nconst ACTIVE_PROJECT_STATUSES = new Set(['PLANNED', 'IN_PROGRESS', 'MONITORING']);\n\n/** Conservation-status → meter tint (semantic, IUCN-severity ordered). */\nconst CONSERVATION_TONE: Record<EcoConservationStatus, string> = {\n LEAST_CONCERN: 'var(--color-status-success-text)',\n NEAR_THREATENED: 'var(--color-accent-teal)',\n VULNERABLE: 'var(--color-status-warning-text)',\n ENDANGERED: 'var(--color-status-error-text)',\n CRITICALLY_ENDANGERED: 'var(--color-status-error-text)',\n EXTINCT_IN_WILD: 'var(--color-text-tertiary)',\n DATA_DEFICIENT: 'var(--color-text-tertiary)',\n NOT_EVALUATED: 'var(--color-text-tertiary)',\n};\n\nexport function DashboardPage() {\n const tr = useTr();\n const { navigateTo, currentUser, workspaceId } = useEcoImpactHub();\n const [customizerOpen, setCustomizerOpen] = useState(false);\n const dashboardLayout = useDashboardLayout(workspaceId, currentUser?.id);\n\n const sites = useEcoSites({ pagination: { limit: 100, offset: 0 } });\n const species = useEcoSpecies({ pagination: { limit: 100, offset: 0 } });\n const projects = useEcoHabitatProjects();\n const carbon = useEcoCarbonTotals();\n const breaches = useEcoMeasurements({ breachesOnly: true, pagination: { limit: 6, offset: 0 } });\n const observations = useEcoObservations({ pagination: { limit: 6, offset: 0 } });\n const sensors = useEcoSensors();\n\n const siteRows = sites.data?.items ?? [];\n const speciesRows = species.data?.items ?? [];\n const projectRows = projects.data?.items ?? [];\n const sensorRows = sensors.data?.items ?? [];\n\n const activeSites = siteRows.filter((s) => s.status === 'ACTIVE').length;\n const priorityCount = speciesRows.filter((s) => s.isPriority).length;\n const threatened = speciesRows.filter((s) =>\n ['VULNERABLE', 'ENDANGERED', 'CRITICALLY_ENDANGERED'].includes(s.conservationStatus)\n ).length;\n const activeProjects = projectRows.filter((p) => ACTIVE_PROJECT_STATUSES.has(p.status)).length;\n const onlineSensors = sensorRows.filter((s) => s.status === 'ONLINE').length;\n const offlineSensors = sensorRows.filter((s) =>\n ['OFFLINE', 'DEGRADED', 'MAINTENANCE'].includes(s.status)\n ).length;\n\n const carbonData = useMemo<{ scope: EcoEmissionScope; label: string; kg: number }[]>(() => {\n const c = carbon.data;\n if (!c) return [];\n return [\n { scope: 'SCOPE_1', label: 'Scope 1', kg: c.scope1 },\n { scope: 'SCOPE_2', label: 'Scope 2', kg: c.scope2 },\n { scope: 'SCOPE_3', label: 'Scope 3', kg: c.scope3 },\n ];\n }, [carbon.data]);\n\n const isInitialLoading =\n sites.isLoading && species.isLoading && projects.isLoading && carbon.isLoading;\n\n const headerActions = (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => setCustomizerOpen(true)}>\n <LayoutGrid className=\"mr-1.5 h-3.5 w-3.5\" />\n {tr('ecoDashboard.customize', 'Customize dashboard')}\n </Button>\n );\n\n if (isInitialLoading) {\n return (\n <PageLayout title={tr('ecoDashboard.title', 'EcoImpactHub')} titleGradient aurora actions={headerActions}>\n <LoadingView label={tr('ecoDashboard.loading', 'Loading your impact overview…')} />\n </PageLayout>\n );\n }\n\n const greetingName = currentUser?.firstName || currentUser?.name || 'there';\n const breachRows = breaches.data?.items ?? [];\n const observationRows = observations.data?.items ?? [];\n\n const kpisWidget = (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6\">\n <MetricsCard\n icon={<MapPin className=\"h-5 w-5\" />}\n value={\n sites.data?.pageInfo?.hasMore ? `${siteRows.length}+` : formatNumber(siteRows.length)\n }\n label={tr('ecoDashboard.kpis.monitoredSites', 'Monitored sites')}\n footer={\n <div className=\"space-y-2\">\n <MiniBar\n value={siteRows.length ? (activeSites / siteRows.length) * 100 : 0}\n height={4}\n barClassName=\"bg-action-primary-bg\"\n />\n <p className=\"text-xs text-text-tertiary\">\n <span className=\"tabular-nums text-text-secondary\">{activeSites}</span>{' '}\n {tr('ecoDashboard.kpis.active', 'active')}\n </p>\n </div>\n }\n />\n <MetricsCard\n icon={<PawPrint className=\"h-5 w-5\" />}\n value={\n species.data?.pageInfo?.hasMore\n ? `${speciesRows.length}+`\n : formatNumber(speciesRows.length)\n }\n label={tr('ecoDashboard.kpis.speciesTracked', 'Species tracked')}\n accent={threatened > 0 ? 'warning' : undefined}\n footer={\n <div className=\"space-y-2\">\n <MiniBar\n value={speciesRows.length ? (threatened / speciesRows.length) * 100 : 0}\n height={4}\n barClassName=\"bg-status-warning-text\"\n />\n <p className=\"text-xs text-text-tertiary\">\n <span className=\"tabular-nums text-text-secondary\">{priorityCount}</span>{' '}\n {tr('ecoDashboard.kpis.priority', 'priority')} ·{' '}\n <span className=\"tabular-nums text-text-secondary\">{threatened}</span>{' '}\n {tr('ecoDashboard.kpis.threatened', 'threatened')}\n </p>\n </div>\n }\n />\n <MetricsCard\n icon={<Trees className=\"h-5 w-5\" />}\n value={formatNumber(activeProjects)}\n label={tr('ecoDashboard.kpis.activeHabitatProjects', 'Active habitat projects')}\n footer={\n <div className=\"space-y-2\">\n <MiniBar\n value={projectRows.length ? (activeProjects / projectRows.length) * 100 : 0}\n height={4}\n barClassName=\"bg-action-primary-bg\"\n />\n <p className=\"text-xs text-text-tertiary\">\n <span className=\"tabular-nums text-text-secondary\">\n {formatNumber(projectRows.length)}\n </span>{' '}\n {tr('ecoDashboard.kpis.total', 'total')}\n </p>\n </div>\n }\n />\n <MetricsCard\n icon={<Factory className=\"h-5 w-5\" />}\n value={carbon.data ? formatCo2e(carbon.data.total) : '—'}\n label={tr('ecoDashboard.kpis.totalFootprint', 'Total footprint')}\n featured\n footer={\n carbon.data && carbon.data.total > 0 ? (\n <div className=\"space-y-2\">\n <ScopeSplitBar\n scope1={carbon.data.scope1}\n scope2={carbon.data.scope2}\n scope3={carbon.data.scope3}\n height={6}\n />\n <p className=\"text-xs text-text-tertiary\">\n {tr('ecoDashboard.kpis.scopeSplit', 'Scope 1 · 2 · 3 split')}\n </p>\n </div>\n ) : undefined\n }\n />\n <MetricsCard\n icon={<Radio className=\"h-5 w-5\" />}\n value={formatNumber(onlineSensors)}\n label={tr('ecoDashboard.kpis.sensorsOnline', 'Sensors online')}\n accent={offlineSensors > 0 ? 'warning' : 'success'}\n footer={\n <div className=\"space-y-2\">\n <MiniBar\n value={sensorRows.length ? (onlineSensors / sensorRows.length) * 100 : 0}\n height={4}\n barClassName=\"bg-status-success-text\"\n />\n <p className=\"text-xs text-text-tertiary\">\n <span className=\"tabular-nums text-text-secondary\">{offlineSensors}</span>{' '}\n {tr('ecoDashboard.kpis.needAttention', 'need attention')}\n </p>\n </div>\n }\n />\n <MetricsCard\n icon={<TriangleAlert className=\"h-5 w-5\" />}\n value={\n breaches.data?.pageInfo?.hasMore\n ? `${breachRows.length}+`\n : formatNumber(breachRows.length)\n }\n label={tr('ecoDashboard.kpis.openBreaches', 'Open breaches')}\n accent={breachRows.length > 0 ? 'error' : 'success'}\n footer={\n <p\n className={\n breachRows.length > 0\n ? 'text-xs font-medium text-status-error-text'\n : 'text-xs text-text-tertiary'\n }\n >\n {breachRows.length > 0\n ? tr('ecoDashboard.kpis.needsAttention', 'Needs attention')\n : tr('ecoDashboard.kpis.allParametersInRange', 'All parameters in range')}\n </p>\n }\n />\n </div>\n );\n\n const carbonByScopeWidget = (\n <GlassCard treatment=\"glass\" className=\"p-6 h-full\">\n <div className=\"mb-4 flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">\n {tr('ecoDashboard.carbonByScope.title', 'Carbon footprint by scope')}\n </h2>\n <p className=\"mt-0.5 text-sm text-text-secondary\">\n {tr(\n 'ecoDashboard.carbonByScope.description',\n 'GHG Protocol Scope 1 / 2 / 3 emissions (CO₂e).',\n )}\n </p>\n </div>\n <div className=\"flex items-center gap-4\">\n {carbon.data && carbon.data.total > 0 && (\n <div className=\"text-right\">\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">\n {formatCo2e(carbon.data.total)}\n </p>\n <p className=\"text-xs text-text-tertiary\">\n {tr('ecoDashboard.carbonByScope.total', 'total')}\n </p>\n </div>\n )}\n <button\n type=\"button\"\n onClick={() => navigateTo('carbon')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n {tr('ecoDashboard.carbonByScope.ledger', 'Ledger')} <ArrowRight className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n {carbonData.every((d) => d.kg === 0) ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Factory className=\"h-10 w-10\" />}\n title={tr('ecoDashboard.carbonByScope.noDataTitle', 'No carbon entries yet')}\n description={tr(\n 'ecoDashboard.carbonByScope.noDataDescription',\n 'Record activity in the carbon ledger to see your Scope 1–3 footprint here.',\n )}\n />\n ) : (\n <>\n <div className=\"h-56\">\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n <BarChart data={carbonData} margin={{ top: 20, right: 8, left: 8, bottom: 0 }}>\n <defs>\n {carbonData.map((d) => (\n <linearGradient\n key={d.scope}\n id={`ecoScopeGrad-${d.scope}`}\n x1=\"0\"\n y1=\"0\"\n x2=\"0\"\n y2=\"1\"\n >\n <stop offset=\"0%\" style={{ stopColor: SCOPE_HUE[d.scope], stopOpacity: 1 }} />\n <stop\n offset=\"100%\"\n style={{ stopColor: SCOPE_HUE[d.scope], stopOpacity: 0.55 }}\n />\n </linearGradient>\n ))}\n </defs>\n <CartesianGrid\n vertical={false}\n stroke=\"var(--color-border-seam)\"\n strokeOpacity={0.6}\n strokeDasharray=\"3 4\"\n />\n <XAxis\n dataKey=\"label\"\n tick={{ fill: 'var(--color-text-secondary)', fontSize: 12 }}\n axisLine={{ stroke: 'var(--color-border-seam)' }}\n tickLine={false}\n dy={4}\n />\n <YAxis\n tick={{ fill: 'var(--color-text-tertiary)', fontSize: 11 }}\n axisLine={false}\n tickLine={false}\n width={48}\n tickFormatter={(v: number) => (v >= 1000 ? `${Math.round(v / 1000)}t` : `${v}`)}\n />\n <Tooltip\n cursor={{ fill: 'var(--color-bg-sunken)', opacity: 0.4 }}\n contentStyle={{\n background: 'var(--color-bg-surface)',\n border: '1px solid var(--color-border-seam)',\n borderRadius: 12,\n color: 'var(--color-text-primary)',\n }}\n formatter={(value: number) => [\n formatCo2e(value),\n tr('ecoDashboard.carbonByScope.emissions', 'Emissions'),\n ]}\n />\n <Bar dataKey=\"kg\" radius={[8, 8, 2, 2]} maxBarSize={72}>\n {carbonData.map((d) => (\n <Cell key={d.scope} fill={`url(#ecoScopeGrad-${d.scope})`} />\n ))}\n <LabelList\n dataKey=\"kg\"\n position=\"top\"\n formatter={(v: number) => (v > 0 ? formatCo2e(v) : '')}\n style={{ fill: 'var(--color-text-secondary)', fontSize: 11, fontWeight: 600 }}\n />\n </Bar>\n </BarChart>\n </ResponsiveContainer>\n </div>\n <ScopeLegend data={carbonData} className=\"mt-4 border-t border-border-seam pt-4\" />\n </>\n )}\n </GlassCard>\n );\n\n const recentBreachesWidget = (\n <GlassCard treatment=\"glass\" className=\"p-6 h-full\">\n <div className=\"mb-4 flex items-center justify-between gap-3\">\n <div className=\"flex items-center gap-2\">\n <h2 className=\"text-base font-semibold text-text-primary\">\n {tr('ecoDashboard.recentBreaches.title', 'Recent breaches')}\n </h2>\n {breachRows.length > 0 && (\n <span className=\"rounded-full bg-status-error-bg-subtle px-2 py-0.5 text-xs font-medium tabular-nums text-status-error-text\">\n {breaches.data?.pageInfo?.hasMore ? `${breachRows.length}+` : breachRows.length}\n </span>\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => navigateTo('monitoring')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n {tr('ecoDashboard.recentBreaches.monitoring', 'Monitoring')}{' '}\n <ArrowRight className=\"h-4 w-4\" />\n </button>\n </div>\n {breachRows.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center rounded-xl border border-dashed border-border-seam py-10 text-center\">\n <span className=\"flex size-12 items-center justify-center rounded-full bg-status-success-bg-subtle\">\n <ShieldCheck className=\"h-6 w-6 text-status-success-text\" />\n </span>\n <p className=\"mt-3 text-sm font-medium text-text-primary\">\n {tr('ecoDashboard.recentBreaches.noBreachesTitle', 'All parameters in range')}\n </p>\n <p className=\"mt-1 text-xs text-text-tertiary\">\n {tr(\n 'ecoDashboard.recentBreaches.noBreachesDescription',\n 'No open threshold breaches.',\n )}\n </p>\n </div>\n ) : (\n <ul className=\"space-y-2.5\">\n {breachRows.map((m) => (\n <li\n key={m.id}\n className=\"flex items-start justify-between gap-3 rounded-xl bg-bg-sunken/50 p-3\"\n >\n <div className=\"flex min-w-0 items-start gap-2.5\">\n <span\n aria-hidden=\"true\"\n className=\"mt-1.5 size-2 shrink-0 rounded-full bg-status-error-text motion-safe:animate-vc-glow-pulse\"\n />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{m.parameter}</p>\n <p className=\"text-xs text-text-tertiary\">\n <span className=\"tabular-nums\">\n {formatNumber(m.value)} {m.unit}\n </span>{' '}\n · {formatRelativeTime(m.recordedAt)}\n </p>\n </div>\n </div>\n <StatusBadge status=\"BREACH\" />\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n );\n\n const recentObservationsWidget = (\n <Section\n title={tr('ecoDashboard.recentObservations.title', 'Recent field observations')}\n description={tr(\n 'ecoDashboard.recentObservations.description',\n 'Latest wildlife & biodiversity sightings recorded across your sites.',\n )}\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('observations')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-accent hover:underline\"\n >\n {tr('ecoDashboard.recentObservations.all', 'All observations')}{' '}\n <ArrowRight className=\"h-4 w-4\" />\n </button>\n }\n >\n {observationRows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Binoculars className=\"h-10 w-10\" />}\n title={tr('ecoDashboard.recentObservations.noDataTitle', 'No observations yet')}\n description={tr(\n 'ecoDashboard.recentObservations.noDataDescription',\n 'Record your first field observation to build your biodiversity dataset.',\n )}\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {observationRows.map((o) => (\n <GlassCard\n key={o.id}\n treatment=\"plain\"\n className=\"p-4 transition-shadow hover:shadow-float\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex min-w-0 items-start gap-2.5\">\n <span className=\"mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-accent\">\n <Binoculars className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {o.species?.commonName ??\n tr('ecoDashboard.recentObservations.unknownSpecies', 'Unknown species')}\n </p>\n {o.species?.scientificName && (\n <p className=\"truncate text-xs italic text-text-tertiary\">\n {o.species.scientificName}\n </p>\n )}\n </div>\n </div>\n <span className=\"shrink-0 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium tabular-nums text-text-primary\">\n ×{formatNumber(o.count)}\n </span>\n </div>\n <div className=\"mt-3 flex items-center justify-between border-t border-border-seam/70 pt-2.5\">\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n {formatEnumLabel(o.method)}\n </span>\n <span className=\"text-xs text-text-tertiary\">\n {formatRelativeTime(o.observedAt)}\n </span>\n </div>\n </GlassCard>\n ))}\n </div>\n )}\n </Section>\n );\n\n const conservationStatusWidget =\n speciesRows.length > 0 ? (\n <Section\n title={tr('ecoDashboard.conservationStatus.title', 'Conservation status')}\n description={tr(\n 'ecoDashboard.conservationStatus.description',\n 'Distribution of tracked species by IUCN-style status.',\n )}\n >\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"space-y-3.5\">\n {(() => {\n const counts = new Map<string, number>();\n for (const s of speciesRows) {\n counts.set(s.conservationStatus, (counts.get(s.conservationStatus) ?? 0) + 1);\n }\n const total = speciesRows.length;\n return [...counts.entries()]\n .sort((a, b) => b[1] - a[1])\n .map(([status, count]) => {\n const pct = (count / total) * 100;\n const tone =\n CONSERVATION_TONE[status as EcoConservationStatus] ??\n 'var(--color-text-tertiary)';\n return (\n <div key={status} className=\"flex items-center gap-3 sm:gap-4\">\n <div className=\"w-36 shrink-0 sm:w-48\">\n <StatusBadge status={status} />\n </div>\n <Meter value={pct} color={tone} className=\"flex-1\" />\n <span className=\"w-9 shrink-0 text-right text-sm font-medium tabular-nums text-text-primary\">\n {count}\n </span>\n <span className=\"hidden w-12 shrink-0 text-right text-xs tabular-nums text-text-tertiary sm:inline\">\n {formatPercent(pct)}\n </span>\n </div>\n );\n });\n })()}\n </div>\n </GlassCard>\n </Section>\n ) : null;\n\n const WIDGET_RENDER: Record<DashboardWidgetId, ReactNode> = {\n kpis: kpisWidget,\n carbonByScope: carbonByScopeWidget,\n recentBreaches: recentBreachesWidget,\n recentObservations: recentObservationsWidget,\n conservationStatus: conservationStatusWidget,\n };\n\n const order = dashboardLayout.layout.widgetOrder;\n const blocks: ReactNode[] = [];\n for (let i = 0; i < order.length; i++) {\n const id = order[i];\n if (id === 'kpis') {\n blocks.push(<div key={id}>{WIDGET_RENDER[id]}</div>);\n continue;\n }\n // Preserve the original side-by-side carbon-by-scope / recent-breaches pairing\n // whenever they remain adjacent in that order (the default, uncustomized case).\n if (id === 'carbonByScope' && order[i + 1] === 'recentBreaches') {\n blocks.push(\n <div key={`${id}-pair`} className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <div className=\"lg:col-span-2\">{carbonByScopeWidget}</div>\n <div>{recentBreachesWidget}</div>\n </div>\n );\n i += 1;\n continue;\n }\n if (id === 'recentBreaches' && order[i - 1] === 'carbonByScope') {\n // Already rendered as part of the pair above.\n continue;\n }\n const content = WIDGET_RENDER[id];\n if (!content) continue;\n blocks.push(<div key={id}>{content}</div>);\n }\n\n return (\n <PageLayout\n title={tr('ecoDashboard.title', 'EcoImpactHub')}\n titleGradient\n aurora\n description={tr('ecoDashboard.description', 'Welcome back, {{name}} — your environment, conservation & sustainability control room.', { name: greetingName })}\n actions={headerActions}\n >\n <div className=\"space-y-6\">\n {blocks}\n\n {dashboardLayout.layout.notes.length > 0 && (\n <Section\n title={tr('ecoDashboard.customNotes.title', 'Custom notes')}\n description={tr(\n 'ecoDashboard.customNotes.description',\n 'Your personal notes, visible only to you.',\n )}\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {dashboardLayout.layout.notes.map((note) => (\n <GlassCard key={note.id} treatment=\"plain\" className=\"p-4\">\n <div className=\"flex items-start gap-2.5\">\n <StickyNote className=\"mt-0.5 h-4 w-4 shrink-0 text-accent\" />\n <div className=\"min-w-0\">\n {note.title && (\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {note.title}\n </p>\n )}\n {note.body && (\n <p className=\"mt-0.5 text-sm text-text-secondary\">{note.body}</p>\n )}\n </div>\n </div>\n </GlassCard>\n ))}\n </div>\n </Section>\n )}\n </div>\n\n <DashboardCustomizerDialog\n open={customizerOpen}\n onOpenChange={setCustomizerOpen}\n layout={dashboardLayout}\n />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
import { EcoObservationMethod } from '../../types';
|
|
3
|
+
|
|
4
|
+
interface MethodPickerProps {
|
|
5
|
+
/** Full option set, in display order. */
|
|
6
|
+
options: readonly EcoObservationMethod[];
|
|
7
|
+
value: EcoObservationMethod;
|
|
8
|
+
onChange: (method: EcoObservationMethod) => void;
|
|
9
|
+
/** Accessible name for the group (rendered by the caller's own label). */
|
|
10
|
+
label: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const MethodPicker: FC<MethodPickerProps>;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=MethodPicker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MethodPicker.d.ts","sourceRoot":"","sources":["../../../../src/ecoimpacthub/pages/_shared/MethodPicker.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAchC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAYxD,UAAU,iBAAiB;IACzB,yCAAyC;IACzC,OAAO,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACzC,KAAK,EAAE,oBAAoB,CAAC;IAC5B,QAAQ,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACjD,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,YAAY,EAAE,EAAE,CAAC,iBAAiB,CA+B9C,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { cn as e } from "../../utils/cn.js";
|
|
2
|
+
import { formatEnumLabel as t } from "../../utils/formatters.js";
|
|
3
|
+
import { AudioLines as n, Camera as r, Check as i, Dna as a, Eye as o, Footprints as s, SatelliteDish as c, Users as l } from "lucide-react";
|
|
4
|
+
import { jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
5
|
+
//#region src/ecoimpacthub/pages/_shared/MethodPicker.tsx
|
|
6
|
+
var f = {
|
|
7
|
+
DIRECT_SIGHTING: o,
|
|
8
|
+
CAMERA_TRAP: r,
|
|
9
|
+
ACOUSTIC: n,
|
|
10
|
+
TRACKS_SIGNS: s,
|
|
11
|
+
EDNA: a,
|
|
12
|
+
TELEMETRY: c,
|
|
13
|
+
COMMUNITY_REPORT: l
|
|
14
|
+
}, p = ({ options: n, value: r, onChange: a, label: s, className: c }) => /* @__PURE__ */ u("div", {
|
|
15
|
+
role: "radiogroup",
|
|
16
|
+
"aria-label": s,
|
|
17
|
+
className: e("flex flex-wrap gap-1.5", c),
|
|
18
|
+
children: n.map((n) => {
|
|
19
|
+
let s = n === r, c = s ? i : f[n.toUpperCase()] ?? o;
|
|
20
|
+
return /* @__PURE__ */ d("button", {
|
|
21
|
+
type: "button",
|
|
22
|
+
role: "radio",
|
|
23
|
+
"aria-checked": s,
|
|
24
|
+
onClick: () => a(n),
|
|
25
|
+
className: e("inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-sm font-medium transition-colors", s ? "border-transparent bg-action-primary-bg text-action-primary-text" : "border-border-seam bg-bg-surface text-text-secondary hover:bg-bg-sunken hover:text-text-primary"),
|
|
26
|
+
children: [/* @__PURE__ */ u(c, {
|
|
27
|
+
className: "h-3.5 w-3.5 shrink-0",
|
|
28
|
+
"aria-hidden": "true"
|
|
29
|
+
}), t(n)]
|
|
30
|
+
}, n);
|
|
31
|
+
})
|
|
32
|
+
});
|
|
33
|
+
//#endregion
|
|
34
|
+
export { p as MethodPicker };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=MethodPicker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MethodPicker.js","names":[],"sources":["../../../../src/ecoimpacthub/pages/_shared/MethodPicker.tsx"],"sourcesContent":["/**\n * MethodPicker — one-tap chip group for choosing an observation method.\n * @module ecoimpacthub/pages/_shared\n *\n * Field recording happens on a phone, one-handed, so the survey method is a\n * chip group rather than a dropdown: every option is visible and reachable in a\n * single tap. Presentational-only; semantic tokens exclusively. Shared by the\n * Observations feed and the Species detail record dialogs so the two\n * record-observation surfaces stay identical.\n */\n\nimport type { FC } from 'react';\nimport {\n AudioLines,\n Camera,\n Check,\n Dna,\n Eye,\n Footprints,\n SatelliteDish,\n Users,\n type LucideIcon,\n} from 'lucide-react';\nimport { cn } from '../../utils/cn';\nimport { formatEnumLabel } from '../../utils/formatters';\nimport type { EcoObservationMethod } from '../../types';\n\nconst METHOD_ICON: Record<string, LucideIcon> = {\n DIRECT_SIGHTING: Eye,\n CAMERA_TRAP: Camera,\n ACOUSTIC: AudioLines,\n TRACKS_SIGNS: Footprints,\n EDNA: Dna,\n TELEMETRY: SatelliteDish,\n COMMUNITY_REPORT: Users,\n};\n\ninterface MethodPickerProps {\n /** Full option set, in display order. */\n options: readonly EcoObservationMethod[];\n value: EcoObservationMethod;\n onChange: (method: EcoObservationMethod) => void;\n /** Accessible name for the group (rendered by the caller's own label). */\n label: string;\n className?: string;\n}\n\nexport const MethodPicker: FC<MethodPickerProps> = ({\n options,\n value,\n onChange,\n label,\n className,\n}) => (\n <div role=\"radiogroup\" aria-label={label} className={cn('flex flex-wrap gap-1.5', className)}>\n {options.map((method) => {\n const selected = method === value;\n const Icon = selected ? Check : (METHOD_ICON[method.toUpperCase()] ?? Eye);\n return (\n <button\n key={method}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n onClick={() => onChange(method)}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-sm font-medium transition-colors',\n selected\n ? 'border-transparent bg-action-primary-bg text-action-primary-text'\n : 'border-border-seam bg-bg-surface text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n )}\n >\n <Icon className=\"h-3.5 w-3.5 shrink-0\" aria-hidden=\"true\" />\n {formatEnumLabel(method)}\n </button>\n );\n })}\n </div>\n);\n"],"mappings":";;;;;AA2BA,IAAM,IAA0C;CAC9C,iBAAiB;CACjB,aAAa;CACb,UAAU;CACV,cAAc;CACd,MAAM;CACN,WAAW;CACX,kBAAkB;AACpB,GAYa,KAAuC,EAClD,YACA,UACA,aACA,UACA,mBAEA,kBAAC,OAAD;CAAK,MAAK;CAAa,cAAY;CAAO,WAAW,EAAG,0BAA0B,CAAS;WACxF,EAAQ,KAAK,MAAW;EACvB,IAAM,IAAW,MAAW,GACtB,IAAO,IAAW,IAAS,EAAY,EAAO,YAAY,MAAM;EACtE,OACE,kBAAC,UAAD;GAEE,MAAK;GACL,MAAK;GACL,gBAAc;GACd,eAAe,EAAS,CAAM;GAC9B,WAAW,EACT,0GACA,IACI,qEACA,iGACN;aAXF,CAaE,kBAAC,GAAD;IAAM,WAAU;IAAuB,eAAY;GAAQ,CAAA,GAC1D,EAAgB,CAAM,CACjB;KAdD,CAcC;CAEZ,CAAC;AACE,CAAA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface CapturedCoordinates {
|
|
4
|
+
latitude: number;
|
|
5
|
+
longitude: number;
|
|
6
|
+
/** Reported horizontal accuracy in metres, when the device supplies one. */
|
|
7
|
+
accuracyMeters: number | null;
|
|
8
|
+
}
|
|
9
|
+
interface UseMyLocationButtonProps {
|
|
10
|
+
onCapture: (coords: CapturedCoordinates) => void;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const UseMyLocationButton: FC<UseMyLocationButtonProps>;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=UseMyLocationButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UseMyLocationButton.d.ts","sourceRoot":"","sources":["../../../../src/ecoimpacthub/pages/_shared/UseMyLocationButton.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAY,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAK1C,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,UAAU,wBAAwB;IAChC,SAAS,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAiBD,eAAO,MAAM,mBAAmB,EAAE,EAAE,CAAC,wBAAwB,CAsD5D,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { cn as e } from "../../utils/cn.js";
|
|
2
|
+
import { useState as t } from "react";
|
|
3
|
+
import { Crosshair as n, LoaderCircle as r } from "lucide-react";
|
|
4
|
+
import { jsx as i, jsxs as a } from "react/jsx-runtime";
|
|
5
|
+
import { Button as o } from "@burdenoff/fe-libs/ui";
|
|
6
|
+
//#region src/ecoimpacthub/pages/_shared/UseMyLocationButton.tsx
|
|
7
|
+
var s = 15e3;
|
|
8
|
+
function c(e) {
|
|
9
|
+
switch (e.code) {
|
|
10
|
+
case e.PERMISSION_DENIED: return "Location permission denied — enter the coordinates manually.";
|
|
11
|
+
case e.POSITION_UNAVAILABLE: return "No position fix available right now — try again outdoors.";
|
|
12
|
+
case e.TIMEOUT: return "Timed out waiting for a GPS fix — try again.";
|
|
13
|
+
default: return "Could not read this device’s location.";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
var l = ({ onCapture: l, className: u }) => {
|
|
17
|
+
let d = typeof navigator < "u" && "geolocation" in navigator, [f, p] = t(!1), [m, h] = t(null), [g, _] = t(null);
|
|
18
|
+
return d ? /* @__PURE__ */ a("div", {
|
|
19
|
+
className: e("grid gap-1.5", u),
|
|
20
|
+
children: [/* @__PURE__ */ a(o, {
|
|
21
|
+
type: "button",
|
|
22
|
+
variant: "outline",
|
|
23
|
+
size: "sm",
|
|
24
|
+
onClick: () => {
|
|
25
|
+
p(!0), h(null), navigator.geolocation.getCurrentPosition((e) => {
|
|
26
|
+
let t = Number.isFinite(e.coords.accuracy) ? e.coords.accuracy : null;
|
|
27
|
+
_(t), p(!1), l({
|
|
28
|
+
latitude: e.coords.latitude,
|
|
29
|
+
longitude: e.coords.longitude,
|
|
30
|
+
accuracyMeters: t
|
|
31
|
+
});
|
|
32
|
+
}, (e) => {
|
|
33
|
+
_(null), p(!1), h(c(e));
|
|
34
|
+
}, {
|
|
35
|
+
enableHighAccuracy: !0,
|
|
36
|
+
timeout: s,
|
|
37
|
+
maximumAge: 0
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
disabled: f,
|
|
41
|
+
children: [f ? /* @__PURE__ */ i(r, {
|
|
42
|
+
className: "mr-1.5 h-4 w-4 animate-spin",
|
|
43
|
+
"aria-hidden": "true"
|
|
44
|
+
}) : /* @__PURE__ */ i(n, {
|
|
45
|
+
className: "mr-1.5 h-4 w-4",
|
|
46
|
+
"aria-hidden": "true"
|
|
47
|
+
}), f ? "Locating…" : "Use my location"]
|
|
48
|
+
}), m ? /* @__PURE__ */ i("p", {
|
|
49
|
+
className: "text-xs text-status-error-text",
|
|
50
|
+
role: "status",
|
|
51
|
+
children: m
|
|
52
|
+
}) : g == null ? null : /* @__PURE__ */ a("p", {
|
|
53
|
+
className: "text-xs text-text-tertiary",
|
|
54
|
+
role: "status",
|
|
55
|
+
children: [
|
|
56
|
+
"GPS fix accurate to ±",
|
|
57
|
+
Math.round(g),
|
|
58
|
+
" m."
|
|
59
|
+
]
|
|
60
|
+
})]
|
|
61
|
+
}) : null;
|
|
62
|
+
};
|
|
63
|
+
//#endregion
|
|
64
|
+
export { l as UseMyLocationButton };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=UseMyLocationButton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UseMyLocationButton.js","names":[],"sources":["../../../../src/ecoimpacthub/pages/_shared/UseMyLocationButton.tsx"],"sourcesContent":["/**\n * UseMyLocationButton — capture the device's GPS fix into a coordinate pair.\n * @module ecoimpacthub/pages/_shared\n *\n * Field observations are recorded where the animal is, on a phone; typing a\n * decimal latitude by hand is not a field workflow. This reads the browser\n * Geolocation API — real device data, never a synthesised coordinate — and\n * hands the caller the fix.\n *\n * Renders nothing when the browser exposes no Geolocation API, so the control\n * is never present unless it can actually do something. Permission denial and\n * timeouts surface as inline text rather than silently leaving the fields blank.\n */\n\nimport { useState, type FC } from 'react';\nimport { Crosshair, LoaderCircle } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../../utils/cn';\n\nexport interface CapturedCoordinates {\n latitude: number;\n longitude: number;\n /** Reported horizontal accuracy in metres, when the device supplies one. */\n accuracyMeters: number | null;\n}\n\ninterface UseMyLocationButtonProps {\n onCapture: (coords: CapturedCoordinates) => void;\n className?: string;\n}\n\nconst GEOLOCATION_TIMEOUT_MS = 15000;\n\nfunction describeGeolocationError(error: GeolocationPositionError): string {\n switch (error.code) {\n case error.PERMISSION_DENIED:\n return 'Location permission denied — enter the coordinates manually.';\n case error.POSITION_UNAVAILABLE:\n return 'No position fix available right now — try again outdoors.';\n case error.TIMEOUT:\n return 'Timed out waiting for a GPS fix — try again.';\n default:\n return 'Could not read this device’s location.';\n }\n}\n\nexport const UseMyLocationButton: FC<UseMyLocationButtonProps> = ({ onCapture, className }) => {\n const supported = typeof navigator !== 'undefined' && 'geolocation' in navigator;\n const [pending, setPending] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [accuracy, setAccuracy] = useState<number | null>(null);\n\n if (!supported) return null;\n\n const capture = () => {\n setPending(true);\n setError(null);\n navigator.geolocation.getCurrentPosition(\n (position) => {\n const accuracyMeters = Number.isFinite(position.coords.accuracy)\n ? position.coords.accuracy\n : null;\n setAccuracy(accuracyMeters);\n setPending(false);\n onCapture({\n latitude: position.coords.latitude,\n longitude: position.coords.longitude,\n accuracyMeters,\n });\n },\n (positionError) => {\n setAccuracy(null);\n setPending(false);\n setError(describeGeolocationError(positionError));\n },\n { enableHighAccuracy: true, timeout: GEOLOCATION_TIMEOUT_MS, maximumAge: 0 }\n );\n };\n\n return (\n <div className={cn('grid gap-1.5', className)}>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={capture} disabled={pending}>\n {pending ? (\n <LoaderCircle className=\"mr-1.5 h-4 w-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Crosshair className=\"mr-1.5 h-4 w-4\" aria-hidden=\"true\" />\n )}\n {pending ? 'Locating…' : 'Use my location'}\n </Button>\n {error ? (\n <p className=\"text-xs text-status-error-text\" role=\"status\">\n {error}\n </p>\n ) : accuracy != null ? (\n <p className=\"text-xs text-text-tertiary\" role=\"status\">\n GPS fix accurate to ±{Math.round(accuracy)} m.\n </p>\n ) : null}\n </div>\n );\n};\n"],"mappings":";;;;;;AA+BA,IAAM,IAAyB;AAE/B,SAAS,EAAyB,GAAyC;CACzE,QAAQ,EAAM,MAAd;EACE,KAAK,EAAM,mBACT,OAAO;EACT,KAAK,EAAM,sBACT,OAAO;EACT,KAAK,EAAM,SACT,OAAO;EACT,SACE,OAAO;CACX;AACF;AAEA,IAAa,KAAqD,EAAE,cAAW,mBAAgB;CAC7F,IAAM,IAAY,OAAO,YAAc,OAAe,iBAAiB,WACjE,CAAC,GAAS,KAAc,EAAS,EAAK,GACtC,CAAC,GAAO,KAAY,EAAwB,IAAI,GAChD,CAAC,GAAU,KAAe,EAAwB,IAAI;CA6B5D,OA3BK,IA4BH,kBAAC,OAAD;EAAK,WAAW,EAAG,gBAAgB,CAAS;YAA5C,CACE,kBAAC,GAAD;GAAQ,MAAK;GAAS,SAAQ;GAAU,MAAK;GAAK,eA3BhC;IAGpB,AAFA,EAAW,EAAI,GACf,EAAS,IAAI,GACb,UAAU,YAAY,oBACnB,MAAa;KACZ,IAAM,IAAiB,OAAO,SAAS,EAAS,OAAO,QAAQ,IAC3D,EAAS,OAAO,WAChB;KAGJ,AAFA,EAAY,CAAc,GAC1B,EAAW,EAAK,GAChB,EAAU;MACR,UAAU,EAAS,OAAO;MAC1B,WAAW,EAAS,OAAO;MAC3B;KACF,CAAC;IACH,IACC,MAAkB;KAGjB,AAFA,EAAY,IAAI,GAChB,EAAW,EAAK,GAChB,EAAS,EAAyB,CAAa,CAAC;IAClD,GACA;KAAE,oBAAoB;KAAM,SAAS;KAAwB,YAAY;IAAE,CAC7E;GACF;GAIwE,UAAU;aAA9E,CACG,IACC,kBAAC,GAAD;IAAc,WAAU;IAA8B,eAAY;GAAQ,CAAA,IAE1E,kBAAC,GAAD;IAAW,WAAU;IAAiB,eAAY;GAAQ,CAAA,GAE3D,IAAU,cAAc,iBACnB;MACP,IACC,kBAAC,KAAD;GAAG,WAAU;GAAiC,MAAK;aAChD;EACA,CAAA,IACD,KAAY,OAIZ,OAHF,kBAAC,KAAD;GAAG,WAAU;GAA6B,MAAK;aAA/C;IAAwD;IAChC,KAAK,MAAM,CAAQ;IAAE;GAC1C;IAEF;MA9CgB;AAgDzB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HabitatProjectDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/ecoimpacthub/pages/habitat/HabitatProjectDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;
|
|
1
|
+
{"version":3,"file":"HabitatProjectDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/ecoimpacthub/pages/habitat/HabitatProjectDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAoGH,wBAAgB,wBAAwB,4CAgWvC"}
|