@burdenoff/microfe-tours 2026.608.1 → 2026.624.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/tours/ToursUserRoutes.js +3 -3
- package/dist/tours/ToursUserRoutes.js.map +1 -1
- package/dist/tours/components/RouteHelpers.js +6 -6
- package/dist/tours/components/RouteHelpers.js.map +1 -1
- package/dist/tours/pages/AnalyticsDashboardPage.js +21 -21
- package/dist/tours/pages/AnalyticsDashboardPage.js.map +1 -1
- package/dist/tours/pages/MediaLibraryPage.js +18 -18
- package/dist/tours/pages/MediaLibraryPage.js.map +1 -1
- package/dist/tours/pages/MyToursPage.js +10 -10
- package/dist/tours/pages/MyToursPage.js.map +1 -1
- package/dist/tours/pages/MyToursProgressPage.js +11 -11
- package/dist/tours/pages/MyToursProgressPage.js.map +1 -1
- package/dist/tours/pages/SchedulerPage.js +15 -15
- package/dist/tours/pages/SchedulerPage.js.map +1 -1
- package/dist/tours/pages/StepEditorPage.js +47 -47
- package/dist/tours/pages/StepEditorPage.js.map +1 -1
- package/dist/tours/pages/SystemAnalyticsPage.js +17 -17
- package/dist/tours/pages/SystemAnalyticsPage.js.map +1 -1
- package/dist/tours/pages/TemplateLibraryPage.js +19 -19
- package/dist/tours/pages/TemplateLibraryPage.js.map +1 -1
- package/dist/tours/pages/TourBuilderPage.js +39 -39
- package/dist/tours/pages/TourBuilderPage.js.map +1 -1
- package/dist/tours/pages/TourPreviewPage.js +26 -26
- package/dist/tours/pages/TourPreviewPage.js.map +1 -1
- package/dist/tours/pages/ToursListPage.js +24 -24
- package/dist/tours/pages/ToursListPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -17,10 +17,10 @@ function g() {
|
|
|
17
17
|
}, [e, r]), /* @__PURE__ */ d("div", {
|
|
18
18
|
className: "flex flex-col items-center justify-center min-h-[300px] text-center p-6 gap-2",
|
|
19
19
|
children: [/* @__PURE__ */ u("h2", {
|
|
20
|
-
className: "text-lg font-semibold text-
|
|
20
|
+
className: "text-lg font-semibold text-text-primary",
|
|
21
21
|
children: n("myTours.notFoundTitle")
|
|
22
22
|
}), /* @__PURE__ */ u("p", {
|
|
23
|
-
className: "text-sm text-
|
|
23
|
+
className: "text-sm text-text-secondary max-w-md",
|
|
24
24
|
children: n("myTours.notFoundBody")
|
|
25
25
|
})]
|
|
26
26
|
});
|
|
@@ -53,7 +53,7 @@ function v() {
|
|
|
53
53
|
return /* @__PURE__ */ u("output", {
|
|
54
54
|
className: "flex min-h-[300px] items-center justify-center p-6 text-center",
|
|
55
55
|
children: /* @__PURE__ */ u("span", {
|
|
56
|
-
className: "max-w-md text-sm text-
|
|
56
|
+
className: "max-w-md text-sm text-text-secondary",
|
|
57
57
|
children: "Tours are not available for this workspace right now."
|
|
58
58
|
})
|
|
59
59
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ToursUserRoutes.js","names":[],"sources":["../../src/tours/ToursUserRoutes.tsx"],"sourcesContent":["import { lazy, Suspense, useEffect } from 'react';\nimport { Route, Routes, useNavigate } from 'react-router-dom';\nimport { FeatureGate } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { TOURS_FEATURES } from '../constants/featureFlags';\nimport { LazyErrorBoundary, PageLoader } from './components/RouteHelpers';\nimport { useToursContext } from './providers/ToursProvider';\nimport { useToursI18n } from './i18n';\n\n// Eager — primary landing\nimport { MyToursPage } from './pages/MyToursPage';\n\n// Lazy — secondary surface\nconst MyToursProgressPage = lazy(() =>\n import('./pages/MyToursProgressPage').then((m) => ({ default: m.MyToursProgressPage }))\n);\n\n/**\n * 404-style landing shown briefly before redirecting any unknown\n * `/tours/*` path back to the user landing. Gives bookmarkers a signal\n * that the page they tried to load doesn't exist under the user surface,\n * rather than silently snapping them back to `/tours`.\n *\n * Note: react-router's relative `navigate('')` / `navigate('.')` resolves\n * to the current pathname, so for a `*` wildcard matched at\n * `/tours/new` those calls would keep the browser on `/tours/new`. We\n * redirect absolutely via `useToursContext().basePath` so the redirect\n * lands on the landing regardless of how deep the unknown URL was.\n */\nfunction UserRouteNotFound() {\n const navigate = useNavigate();\n const { tr } = useToursI18n();\n const { basePath } = useToursContext();\n\n useEffect(() => {\n const target = basePath && basePath.length > 0 ? basePath : '/';\n const id = window.setTimeout(() => navigate(target, { replace: true }), 1500);\n return () => window.clearTimeout(id);\n }, [navigate, basePath]);\n\n return (\n <div className=\"flex flex-col items-center justify-center min-h-[300px] text-center p-6 gap-2\">\n <h2 className=\"text-lg font-semibold text-
|
|
1
|
+
{"version":3,"file":"ToursUserRoutes.js","names":[],"sources":["../../src/tours/ToursUserRoutes.tsx"],"sourcesContent":["import { lazy, Suspense, useEffect } from 'react';\nimport { Route, Routes, useNavigate } from 'react-router-dom';\nimport { FeatureGate } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { TOURS_FEATURES } from '../constants/featureFlags';\nimport { LazyErrorBoundary, PageLoader } from './components/RouteHelpers';\nimport { useToursContext } from './providers/ToursProvider';\nimport { useToursI18n } from './i18n';\n\n// Eager — primary landing\nimport { MyToursPage } from './pages/MyToursPage';\n\n// Lazy — secondary surface\nconst MyToursProgressPage = lazy(() =>\n import('./pages/MyToursProgressPage').then((m) => ({ default: m.MyToursProgressPage }))\n);\n\n/**\n * 404-style landing shown briefly before redirecting any unknown\n * `/tours/*` path back to the user landing. Gives bookmarkers a signal\n * that the page they tried to load doesn't exist under the user surface,\n * rather than silently snapping them back to `/tours`.\n *\n * Note: react-router's relative `navigate('')` / `navigate('.')` resolves\n * to the current pathname, so for a `*` wildcard matched at\n * `/tours/new` those calls would keep the browser on `/tours/new`. We\n * redirect absolutely via `useToursContext().basePath` so the redirect\n * lands on the landing regardless of how deep the unknown URL was.\n */\nfunction UserRouteNotFound() {\n const navigate = useNavigate();\n const { tr } = useToursI18n();\n const { basePath } = useToursContext();\n\n useEffect(() => {\n const target = basePath && basePath.length > 0 ? basePath : '/';\n const id = window.setTimeout(() => navigate(target, { replace: true }), 1500);\n return () => window.clearTimeout(id);\n }, [navigate, basePath]);\n\n return (\n <div className=\"flex flex-col items-center justify-center min-h-[300px] text-center p-6 gap-2\">\n <h2 className=\"text-lg font-semibold text-text-primary\">{tr('myTours.notFoundTitle')}</h2>\n <p className=\"text-sm text-text-secondary max-w-md\">{tr('myTours.notFoundBody')}</p>\n </div>\n );\n}\n\n/**\n * User-facing routes for the Tours microfrontend.\n *\n * Exposes a tight surface — browse available tours and review own progress.\n * Tour playback itself is handled by the SDK overlay, not by routed pages.\n *\n * Routes (relative to basePath, e.g. `/tours`):\n * - `/` → MyToursPage (browse + status)\n * - `/progress` → MyToursProgressPage (own progress, grouped)\n * - `*` → UserRouteNotFound, then absolute navigate to basePath\n * (collapses any admin-style URL like `/new`, `/media`,\n * `/templates` after briefly signalling unavailability)\n */\nexport function ToursUserRoutes() {\n return (\n <FeatureGate flag={TOURS_FEATURES.USER} defaultValue={true} fallback={<UserToursUnavailable />}>\n <LazyErrorBoundary>\n <Suspense fallback={<PageLoader />}>\n <Routes>\n <Route index element={<MyToursPage />} />\n <Route path=\"progress\" element={<MyToursProgressPage />} />\n <Route path=\"*\" element={<UserRouteNotFound />} />\n </Routes>\n </Suspense>\n </LazyErrorBoundary>\n </FeatureGate>\n );\n}\n\nfunction UserToursUnavailable() {\n return (\n <output className=\"flex min-h-[300px] items-center justify-center p-6 text-center\">\n <span className=\"max-w-md text-sm text-text-secondary\">\n Tours are not available for this workspace right now.\n </span>\n </output>\n );\n}\n"],"mappings":";;;;;;;;;;AAYA,IAAM,IAAsB,QAC1B,OAAO,kCAA+B,MAAM,OAAO,EAAE,SAAS,EAAE,qBAAqB,EAAE,CACxF;AAcD,SAAS,IAAoB;CAC3B,IAAM,IAAW,GAAa,EACxB,EAAE,UAAO,GAAc,EACvB,EAAE,gBAAa,GAAiB;AAQtC,QANA,QAAgB;EACd,IAAM,IAAS,KAAY,EAAS,SAAS,IAAI,IAAW,KACtD,IAAK,OAAO,iBAAiB,EAAS,GAAQ,EAAE,SAAS,IAAM,CAAC,EAAE,KAAK;AAC7E,eAAa,OAAO,aAAa,EAAG;IACnC,CAAC,GAAU,EAAS,CAAC,EAGtB,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,MAAD;GAAI,WAAU;aAA2C,EAAG,wBAAwB;GAAM,CAAA,EAC1F,kBAAC,KAAD;GAAG,WAAU;aAAwC,EAAG,uBAAuB;GAAK,CAAA,CAChF;;;AAiBV,SAAgB,IAAkB;AAChC,QACE,kBAAC,GAAD;EAAa,MAAM,EAAe;EAAM,cAAc;EAAM,UAAU,kBAAC,GAAD,EAAwB,CAAA;YAC5F,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;GAAU,UAAU,kBAAC,GAAD,EAAc,CAAA;aAChC,kBAAC,GAAD,EAAA,UAAA;IACE,kBAAC,GAAD;KAAO,OAAA;KAAM,SAAS,kBAAC,GAAD,EAAe,CAAA;KAAI,CAAA;IACzC,kBAAC,GAAD;KAAO,MAAK;KAAW,SAAS,kBAAC,GAAD,EAAuB,CAAA;KAAI,CAAA;IAC3D,kBAAC,GAAD;KAAO,MAAK;KAAI,SAAS,kBAAC,GAAD,EAAqB,CAAA;KAAI,CAAA;IAC3C,EAAA,CAAA;GACA,CAAA,EACO,CAAA;EACR,CAAA;;AAIlB,SAAS,IAAuB;AAC9B,QACE,kBAAC,UAAD;EAAQ,WAAU;YAChB,kBAAC,QAAD;GAAM,WAAU;aAAuC;GAEhD,CAAA;EACA,CAAA"}
|
|
@@ -39,28 +39,28 @@ var c = 3, l = class extends e {
|
|
|
39
39
|
className: "flex flex-col items-center justify-center min-h-[400px] gap-4 text-center p-6",
|
|
40
40
|
children: [
|
|
41
41
|
/* @__PURE__ */ a("div", {
|
|
42
|
-
className: "p-3 rounded-full bg-
|
|
43
|
-
children: /* @__PURE__ */ a(n, { className: "size-8 text-
|
|
42
|
+
className: "p-3 rounded-full bg-status-error-bg/10",
|
|
43
|
+
children: /* @__PURE__ */ a(n, { className: "size-8 text-status-error-text" })
|
|
44
44
|
}),
|
|
45
45
|
/* @__PURE__ */ o("div", {
|
|
46
46
|
className: "space-y-2",
|
|
47
47
|
children: [/* @__PURE__ */ a("h3", {
|
|
48
|
-
className: "text-lg font-semibold text-
|
|
48
|
+
className: "text-lg font-semibold text-text-primary",
|
|
49
49
|
children: "Failed to load page"
|
|
50
50
|
}), /* @__PURE__ */ a("p", {
|
|
51
|
-
className: "text-sm text-
|
|
51
|
+
className: "text-sm text-text-secondary max-w-md",
|
|
52
52
|
children: e ? "The page chunk could not be loaded. This may be due to a network issue." : "The page chunk could not be loaded after multiple attempts. Please refresh the page."
|
|
53
53
|
})]
|
|
54
54
|
}),
|
|
55
55
|
e ? /* @__PURE__ */ o("button", {
|
|
56
56
|
type: "button",
|
|
57
57
|
onClick: this.handleRetry,
|
|
58
|
-
className: "inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md bg-primary text-primary-
|
|
58
|
+
className: "inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90 transition-colors",
|
|
59
59
|
children: [/* @__PURE__ */ a(i, { className: "size-4" }), "Try Again"]
|
|
60
60
|
}) : /* @__PURE__ */ o("button", {
|
|
61
61
|
type: "button",
|
|
62
62
|
onClick: () => window.location.reload(),
|
|
63
|
-
className: "inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md bg-primary text-primary-
|
|
63
|
+
className: "inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90 transition-colors",
|
|
64
64
|
children: [/* @__PURE__ */ a(i, { className: "size-4" }), "Refresh Page"]
|
|
65
65
|
})
|
|
66
66
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RouteHelpers.js","names":[],"sources":["../../../src/tours/components/RouteHelpers.tsx"],"sourcesContent":["import { Component, Fragment, type ReactNode, type ErrorInfo } from 'react';\nimport { Loader2, AlertTriangle, RefreshCw } from 'lucide-react';\n\n/**\n * Loading fallback for lazy-loaded pages.\n * Shared between ToursAdminRoutes and ToursUserRoutes (Section D/E of BOFF-2391).\n */\nexport function PageLoader() {\n return (\n <div className=\"flex items-center justify-center min-h-[200px]\">\n <Loader2 className=\"size-8 animate-spin text-primary\" />\n </div>\n );\n}\n\n/** Max consecutive retries before the error screen suggests a full reload. */\nconst MAX_RETRY_COUNT = 3;\n\n/**\n * Error boundary for lazy-loaded page chunks.\n *\n * React.lazy() caches rejected promises, so resetting error state alone would\n * re-throw the same cached rejection. The retryCount key forces a full remount\n * of the subtree on retry, creating fresh lazy-component instances.\n */\nexport class LazyErrorBoundary extends Component<\n { children: ReactNode },\n { hasError: boolean; error: Error | null; retryCount: number }\n> {\n constructor(props: { children: ReactNode }) {\n super(props);\n this.state = { hasError: false, error: null, retryCount: 0 };\n }\n\n static getDerivedStateFromError(error: Error) {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n // TODO: forward to error monitoring service (Sentry, etc.)\n // Log the full error server-side / in the console; do NOT surface the raw\n // message to users since chunk-load errors can include CDN paths, build\n // hashes, or deployment details that shouldn't appear in the UI.\n console.error('[Tours] Lazy page load failed:', error, errorInfo);\n }\n\n handleRetry = () => {\n this.setState((prev) => ({\n hasError: false,\n error: null,\n retryCount: prev.retryCount + 1,\n }));\n };\n\n render() {\n if (this.state.hasError) {\n const canRetry = this.state.retryCount < MAX_RETRY_COUNT;\n return (\n <div className=\"flex flex-col items-center justify-center min-h-[400px] gap-4 text-center p-6\">\n <div className=\"p-3 rounded-full bg-
|
|
1
|
+
{"version":3,"file":"RouteHelpers.js","names":[],"sources":["../../../src/tours/components/RouteHelpers.tsx"],"sourcesContent":["import { Component, Fragment, type ReactNode, type ErrorInfo } from 'react';\nimport { Loader2, AlertTriangle, RefreshCw } from 'lucide-react';\n\n/**\n * Loading fallback for lazy-loaded pages.\n * Shared between ToursAdminRoutes and ToursUserRoutes (Section D/E of BOFF-2391).\n */\nexport function PageLoader() {\n return (\n <div className=\"flex items-center justify-center min-h-[200px]\">\n <Loader2 className=\"size-8 animate-spin text-primary\" />\n </div>\n );\n}\n\n/** Max consecutive retries before the error screen suggests a full reload. */\nconst MAX_RETRY_COUNT = 3;\n\n/**\n * Error boundary for lazy-loaded page chunks.\n *\n * React.lazy() caches rejected promises, so resetting error state alone would\n * re-throw the same cached rejection. The retryCount key forces a full remount\n * of the subtree on retry, creating fresh lazy-component instances.\n */\nexport class LazyErrorBoundary extends Component<\n { children: ReactNode },\n { hasError: boolean; error: Error | null; retryCount: number }\n> {\n constructor(props: { children: ReactNode }) {\n super(props);\n this.state = { hasError: false, error: null, retryCount: 0 };\n }\n\n static getDerivedStateFromError(error: Error) {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n // TODO: forward to error monitoring service (Sentry, etc.)\n // Log the full error server-side / in the console; do NOT surface the raw\n // message to users since chunk-load errors can include CDN paths, build\n // hashes, or deployment details that shouldn't appear in the UI.\n console.error('[Tours] Lazy page load failed:', error, errorInfo);\n }\n\n handleRetry = () => {\n this.setState((prev) => ({\n hasError: false,\n error: null,\n retryCount: prev.retryCount + 1,\n }));\n };\n\n render() {\n if (this.state.hasError) {\n const canRetry = this.state.retryCount < MAX_RETRY_COUNT;\n return (\n <div className=\"flex flex-col items-center justify-center min-h-[400px] gap-4 text-center p-6\">\n <div className=\"p-3 rounded-full bg-status-error-bg/10\">\n <AlertTriangle className=\"size-8 text-status-error-text\" />\n </div>\n <div className=\"space-y-2\">\n <h3 className=\"text-lg font-semibold text-text-primary\">Failed to load page</h3>\n <p className=\"text-sm text-text-secondary max-w-md\">\n {canRetry\n ? 'The page chunk could not be loaded. This may be due to a network issue.'\n : 'The page chunk could not be loaded after multiple attempts. Please refresh the page.'}\n </p>\n </div>\n {canRetry ? (\n <button\n type=\"button\"\n onClick={this.handleRetry}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90 transition-colors\"\n >\n <RefreshCw className=\"size-4\" />\n Try Again\n </button>\n ) : (\n <button\n type=\"button\"\n onClick={() => window.location.reload()}\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90 transition-colors\"\n >\n <RefreshCw className=\"size-4\" />\n Refresh Page\n </button>\n )}\n </div>\n );\n }\n\n return <Fragment key={this.state.retryCount}>{this.props.children}</Fragment>;\n }\n}\n"],"mappings":";;;;AAOA,SAAgB,IAAa;AAC3B,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD,EAAS,WAAU,oCAAqC,CAAA;EACpD,CAAA;;AAKV,IAAM,IAAkB,GASX,IAAb,cAAuC,EAGrC;CACA,YAAY,GAAgC;AAE1C,EADA,MAAM,EAAM,EACZ,KAAK,QAAQ;GAAE,UAAU;GAAO,OAAO;GAAM,YAAY;GAAG;;CAG9D,OAAO,yBAAyB,GAAc;AAC5C,SAAO;GAAE,UAAU;GAAM;GAAO;;CAGlC,kBAAkB,GAAc,GAAsB;AAKpD,UAAQ,MAAM,kCAAkC,GAAO,EAAU;;CAGnE,oBAAoB;AAClB,OAAK,UAAU,OAAU;GACvB,UAAU;GACV,OAAO;GACP,YAAY,EAAK,aAAa;GAC/B,EAAE;;CAGL,SAAS;AACP,MAAI,KAAK,MAAM,UAAU;GACvB,IAAM,IAAW,KAAK,MAAM,aAAa;AACzC,UACE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD,EAAe,WAAU,iCAAkC,CAAA;MACvD,CAAA;KACN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBAA0C;OAAwB,CAAA,EAChF,kBAAC,KAAD;OAAG,WAAU;iBACV,IACG,4EACA;OACF,CAAA,CACA;;KACL,IACC,kBAAC,UAAD;MACE,MAAK;MACL,SAAS,KAAK;MACd,WAAU;gBAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA,EAAA,YAEzB;UAET,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,OAAO,SAAS,QAAQ;MACvC,WAAU;gBAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA,EAAA,eAEzB;;KAEP;;;AAIV,SAAO,kBAAC,GAAD,EAAA,UAAuC,KAAK,MAAM,UAAoB,EAAvD,KAAK,MAAM,WAA4C"}
|
|
@@ -24,17 +24,17 @@ function g() {
|
|
|
24
24
|
children: /* @__PURE__ */ p("div", {
|
|
25
25
|
className: "text-center",
|
|
26
26
|
children: [/* @__PURE__ */ f("h2", {
|
|
27
|
-
className: "text-lg font-semibold text-
|
|
27
|
+
className: "text-lg font-semibold text-text-primary mb-2",
|
|
28
28
|
children: _("analyticsDashboard.accessDenied", "Access Denied")
|
|
29
29
|
}), /* @__PURE__ */ f("p", {
|
|
30
|
-
className: "text-sm text-
|
|
30
|
+
className: "text-sm text-text-secondary",
|
|
31
31
|
children: _("analyticsDashboard.noPermission", "You do not have permission to view tour analytics.")
|
|
32
32
|
})]
|
|
33
33
|
})
|
|
34
34
|
}) : C ? /* @__PURE__ */ f("div", {
|
|
35
35
|
className: "flex items-center justify-center h-full",
|
|
36
36
|
children: /* @__PURE__ */ f("p", {
|
|
37
|
-
className: "text-
|
|
37
|
+
className: "text-text-secondary",
|
|
38
38
|
children: _("analyticsDashboard.loading", "Loading analytics...")
|
|
39
39
|
})
|
|
40
40
|
}) : S ? /* @__PURE__ */ p("div", {
|
|
@@ -44,24 +44,24 @@ function g() {
|
|
|
44
44
|
className: "text-xl sm:text-2xl font-bold",
|
|
45
45
|
children: _("analyticsDashboard.title", "Tour Analytics")
|
|
46
46
|
}), /* @__PURE__ */ f("p", {
|
|
47
|
-
className: "text-sm text-
|
|
47
|
+
className: "text-sm text-text-secondary",
|
|
48
48
|
children: _("analyticsDashboard.subtitle", "Performance metrics and insights")
|
|
49
49
|
})] }),
|
|
50
50
|
/* @__PURE__ */ p("div", {
|
|
51
51
|
className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4",
|
|
52
52
|
children: [
|
|
53
53
|
/* @__PURE__ */ p("div", {
|
|
54
|
-
className: "border border-border rounded-md p-4 bg-
|
|
54
|
+
className: "border border-border-subtle rounded-md p-4 bg-bg-surface",
|
|
55
55
|
children: [/* @__PURE__ */ p("div", {
|
|
56
56
|
className: "flex items-center gap-3 mb-2",
|
|
57
57
|
children: [/* @__PURE__ */ f("div", {
|
|
58
|
-
className: "p-2 bg-primary/10 rounded-md",
|
|
58
|
+
className: "p-2 bg-action-primary-bg/10 rounded-md",
|
|
59
59
|
children: /* @__PURE__ */ f(d, {
|
|
60
60
|
className: "text-primary",
|
|
61
61
|
size: 20
|
|
62
62
|
})
|
|
63
63
|
}), /* @__PURE__ */ f("span", {
|
|
64
|
-
className: "text-sm text-
|
|
64
|
+
className: "text-sm text-text-secondary",
|
|
65
65
|
children: _("analyticsDashboard.totalViews", "Total Views")
|
|
66
66
|
})]
|
|
67
67
|
}), /* @__PURE__ */ f("div", {
|
|
@@ -70,7 +70,7 @@ function g() {
|
|
|
70
70
|
})]
|
|
71
71
|
}),
|
|
72
72
|
/* @__PURE__ */ p("div", {
|
|
73
|
-
className: "border border-border rounded-md p-4 bg-
|
|
73
|
+
className: "border border-border-subtle rounded-md p-4 bg-bg-surface",
|
|
74
74
|
children: [/* @__PURE__ */ p("div", {
|
|
75
75
|
className: "flex items-center gap-3 mb-2",
|
|
76
76
|
children: [/* @__PURE__ */ f("div", {
|
|
@@ -80,7 +80,7 @@ function g() {
|
|
|
80
80
|
size: 20
|
|
81
81
|
})
|
|
82
82
|
}), /* @__PURE__ */ f("span", {
|
|
83
|
-
className: "text-sm text-
|
|
83
|
+
className: "text-sm text-text-secondary",
|
|
84
84
|
children: _("analyticsDashboard.completionRate", "Completion Rate")
|
|
85
85
|
})]
|
|
86
86
|
}), /* @__PURE__ */ f("div", {
|
|
@@ -89,7 +89,7 @@ function g() {
|
|
|
89
89
|
})]
|
|
90
90
|
}),
|
|
91
91
|
/* @__PURE__ */ p("div", {
|
|
92
|
-
className: "border border-border rounded-md p-4 bg-
|
|
92
|
+
className: "border border-border-subtle rounded-md p-4 bg-bg-surface",
|
|
93
93
|
children: [/* @__PURE__ */ p("div", {
|
|
94
94
|
className: "flex items-center gap-3 mb-2",
|
|
95
95
|
children: [/* @__PURE__ */ f("div", {
|
|
@@ -99,7 +99,7 @@ function g() {
|
|
|
99
99
|
size: 20
|
|
100
100
|
})
|
|
101
101
|
}), /* @__PURE__ */ f("span", {
|
|
102
|
-
className: "text-sm text-
|
|
102
|
+
className: "text-sm text-text-secondary",
|
|
103
103
|
children: _("analyticsDashboard.skipRate", "Skip Rate")
|
|
104
104
|
})]
|
|
105
105
|
}), /* @__PURE__ */ f("div", {
|
|
@@ -108,7 +108,7 @@ function g() {
|
|
|
108
108
|
})]
|
|
109
109
|
}),
|
|
110
110
|
/* @__PURE__ */ p("div", {
|
|
111
|
-
className: "border border-border rounded-md p-4 bg-
|
|
111
|
+
className: "border border-border-subtle rounded-md p-4 bg-bg-surface",
|
|
112
112
|
children: [/* @__PURE__ */ p("div", {
|
|
113
113
|
className: "flex items-center gap-3 mb-2",
|
|
114
114
|
children: [/* @__PURE__ */ f("div", {
|
|
@@ -118,7 +118,7 @@ function g() {
|
|
|
118
118
|
size: 20
|
|
119
119
|
})
|
|
120
120
|
}), /* @__PURE__ */ f("span", {
|
|
121
|
-
className: "text-sm text-
|
|
121
|
+
className: "text-sm text-text-secondary",
|
|
122
122
|
children: _("analyticsDashboard.avgDuration", "Avg Duration")
|
|
123
123
|
})]
|
|
124
124
|
}), /* @__PURE__ */ f("div", {
|
|
@@ -129,7 +129,7 @@ function g() {
|
|
|
129
129
|
]
|
|
130
130
|
}),
|
|
131
131
|
/* @__PURE__ */ p("div", {
|
|
132
|
-
className: "border border-border rounded-md p-4 sm:p-6 bg-
|
|
132
|
+
className: "border border-border-subtle rounded-md p-4 sm:p-6 bg-bg-surface",
|
|
133
133
|
children: [/* @__PURE__ */ f("h2", {
|
|
134
134
|
className: "text-lg font-semibold mb-4",
|
|
135
135
|
children: _("analyticsDashboard.deviceBreakdown", "Device Breakdown")
|
|
@@ -141,7 +141,7 @@ function g() {
|
|
|
141
141
|
className: "text-sm capitalize",
|
|
142
142
|
children: e.deviceType
|
|
143
143
|
}), /* @__PURE__ */ p("span", {
|
|
144
|
-
className: "text-sm text-
|
|
144
|
+
className: "text-sm text-text-secondary",
|
|
145
145
|
children: [
|
|
146
146
|
i(e.count),
|
|
147
147
|
" views (",
|
|
@@ -150,16 +150,16 @@ function g() {
|
|
|
150
150
|
]
|
|
151
151
|
})]
|
|
152
152
|
}), /* @__PURE__ */ f("div", {
|
|
153
|
-
className: "w-full bg-
|
|
153
|
+
className: "w-full bg-bg-sunken rounded-full h-2",
|
|
154
154
|
children: /* @__PURE__ */ f("div", {
|
|
155
|
-
className: "bg-primary h-2 rounded-full",
|
|
155
|
+
className: "bg-action-primary-bg h-2 rounded-full",
|
|
156
156
|
style: { width: `${e.completionRate}%` }
|
|
157
157
|
})
|
|
158
158
|
})] }, e.deviceType))
|
|
159
159
|
})]
|
|
160
160
|
}),
|
|
161
161
|
S.stepAnalytics && S.stepAnalytics.length > 0 && /* @__PURE__ */ p("div", {
|
|
162
|
-
className: "border border-border rounded-md p-4 sm:p-6 bg-
|
|
162
|
+
className: "border border-border-subtle rounded-md p-4 sm:p-6 bg-bg-surface",
|
|
163
163
|
children: [/* @__PURE__ */ f("h2", {
|
|
164
164
|
className: "text-lg font-semibold mb-4",
|
|
165
165
|
children: _("analyticsDashboard.stepPerformance", "Step Performance")
|
|
@@ -168,7 +168,7 @@ function g() {
|
|
|
168
168
|
children: /* @__PURE__ */ p("table", {
|
|
169
169
|
className: "w-full",
|
|
170
170
|
children: [/* @__PURE__ */ f("thead", { children: /* @__PURE__ */ p("tr", {
|
|
171
|
-
className: "border-b border-border",
|
|
171
|
+
className: "border-b border-border-subtle",
|
|
172
172
|
children: [
|
|
173
173
|
/* @__PURE__ */ f("th", {
|
|
174
174
|
className: "text-left py-2 px-4 text-sm font-medium",
|
|
@@ -188,7 +188,7 @@ function g() {
|
|
|
188
188
|
})
|
|
189
189
|
]
|
|
190
190
|
}) }), /* @__PURE__ */ f("tbody", { children: S.stepAnalytics.map((e) => /* @__PURE__ */ p("tr", {
|
|
191
|
-
className: "border-b border-border",
|
|
191
|
+
className: "border-b border-border-subtle",
|
|
192
192
|
children: [
|
|
193
193
|
/* @__PURE__ */ p("td", {
|
|
194
194
|
className: "py-2 px-4 text-sm",
|
|
@@ -219,7 +219,7 @@ function g() {
|
|
|
219
219
|
}) : /* @__PURE__ */ f("div", {
|
|
220
220
|
className: "flex items-center justify-center h-full",
|
|
221
221
|
children: /* @__PURE__ */ f("p", {
|
|
222
|
-
className: "text-
|
|
222
|
+
className: "text-text-secondary",
|
|
223
223
|
children: _("analyticsDashboard.noData", "No analytics data available")
|
|
224
224
|
})
|
|
225
225
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsDashboardPage.js","names":[],"sources":["../../../src/tours/pages/AnalyticsDashboardPage.tsx"],"sourcesContent":["import { useCallback, useEffect } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { BarChart3, TrendingUp, Users, Timer } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useTourAnalytics } from '../hooks/useTourAnalytics';\nimport { useToursPermissionAccess } from '../hooks/useToursPermissionAccess';\nimport { TOUR_PERMISSIONS } from '../../constants/permissions';\nimport { formatNumber, formatPercentage, formatDuration } from '../utils';\n\n/**\n * Analytics dashboard page\n * Display tour performance metrics and charts\n */\nexport function AnalyticsDashboardPage() {\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n },\n [t]\n );\n\n const { tourId } = useParams();\n const { hasPermission, isLoading: permissionsLoading } = useToursPermissionAccess();\n const canViewAnalytics = hasPermission(TOUR_PERMISSIONS.TOUR_ANALYTICS_READ);\n\n const { analytics, loading, fetchAnalytics, getCompletionRate, getSkipRate } =\n useTourAnalytics(tourId);\n\n useEffect(() => {\n if (permissionsLoading || !canViewAnalytics) return;\n fetchAnalytics();\n }, [fetchAnalytics, permissionsLoading, canViewAnalytics]);\n\n if (!permissionsLoading && !canViewAnalytics) {\n return (\n <div className=\"flex items-center justify-center h-full p-3 sm:p-6\">\n <div className=\"text-center\">\n <h2 className=\"text-lg font-semibold text-foreground mb-2\">\n {tr('analyticsDashboard.accessDenied', 'Access Denied')}\n </h2>\n <p className=\"text-sm text-muted-foreground\">\n {tr(\n 'analyticsDashboard.noPermission',\n 'You do not have permission to view tour analytics.'\n )}\n </p>\n </div>\n </div>\n );\n }\n\n if (loading) {\n return (\n <div className=\"flex items-center justify-center h-full\">\n <p className=\"text-muted-foreground\">\n {tr('analyticsDashboard.loading', 'Loading analytics...')}\n </p>\n </div>\n );\n }\n\n if (!analytics) {\n return (\n <div className=\"flex items-center justify-center h-full\">\n <p className=\"text-muted-foreground\">\n {tr('analyticsDashboard.noData', 'No analytics data available')}\n </p>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold\">\n {tr('analyticsDashboard.title', 'Tour Analytics')}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {tr('analyticsDashboard.subtitle', 'Performance metrics and insights')}\n </p>\n </div>\n\n {/* Summary Cards */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4\">\n <div className=\"border border-border rounded-md p-4 bg-card\">\n <div className=\"flex items-center gap-3 mb-2\">\n <div className=\"p-2 bg-primary/10 rounded-md\">\n <Users className=\"text-primary\" size={20} />\n </div>\n <span className=\"text-sm text-muted-foreground\">\n {tr('analyticsDashboard.totalViews', 'Total Views')}\n </span>\n </div>\n <div className=\"text-2xl font-bold\">{formatNumber(analytics.totalViews)}</div>\n </div>\n\n <div className=\"border border-border rounded-md p-4 bg-card\">\n <div className=\"flex items-center gap-3 mb-2\">\n <div className=\"p-2 bg-success/10 rounded-md\">\n <TrendingUp className=\"text-success\" size={20} />\n </div>\n <span className=\"text-sm text-muted-foreground\">\n {tr('analyticsDashboard.completionRate', 'Completion Rate')}\n </span>\n </div>\n <div className=\"text-2xl font-bold\">{formatPercentage(getCompletionRate())}</div>\n </div>\n\n <div className=\"border border-border rounded-md p-4 bg-card\">\n <div className=\"flex items-center gap-3 mb-2\">\n <div className=\"p-2 bg-warning/10 rounded-md\">\n <BarChart3 className=\"text-warning\" size={20} />\n </div>\n <span className=\"text-sm text-muted-foreground\">\n {tr('analyticsDashboard.skipRate', 'Skip Rate')}\n </span>\n </div>\n <div className=\"text-2xl font-bold\">{formatPercentage(getSkipRate())}</div>\n </div>\n\n <div className=\"border border-border rounded-md p-4 bg-card\">\n <div className=\"flex items-center gap-3 mb-2\">\n <div className=\"p-2 bg-info/10 rounded-md\">\n <Timer className=\"text-info\" size={20} />\n </div>\n <span className=\"text-sm text-muted-foreground\">\n {tr('analyticsDashboard.avgDuration', 'Avg Duration')}\n </span>\n </div>\n <div className=\"text-2xl font-bold\">{formatDuration(analytics.averageDuration)}</div>\n </div>\n </div>\n\n {/* Device Breakdown */}\n <div className=\"border border-border rounded-md p-4 sm:p-6 bg-card\">\n <h2 className=\"text-lg font-semibold mb-4\">\n {tr('analyticsDashboard.deviceBreakdown', 'Device Breakdown')}\n </h2>\n <div className=\"space-y-3\">\n {analytics.byDeviceType.map((device) => (\n <div key={device.deviceType}>\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm capitalize\">{device.deviceType}</span>\n <span className=\"text-sm text-muted-foreground\">\n {formatNumber(device.count)} views ({formatPercentage(device.completionRate)})\n </span>\n </div>\n <div className=\"w-full bg-muted rounded-full h-2\">\n <div\n className=\"bg-primary h-2 rounded-full\"\n style={{ width: `${device.completionRate}%` }}\n />\n </div>\n </div>\n ))}\n </div>\n </div>\n\n {/* Step Analytics */}\n {analytics.stepAnalytics && analytics.stepAnalytics.length > 0 && (\n <div className=\"border border-border rounded-md p-4 sm:p-6 bg-card\">\n <h2 className=\"text-lg font-semibold mb-4\">\n {tr('analyticsDashboard.stepPerformance', 'Step Performance')}\n </h2>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full\">\n <thead>\n <tr className=\"border-b border-border\">\n <th className=\"text-left py-2 px-4 text-sm font-medium\">\n {tr('analyticsDashboard.tableStep', 'Step')}\n </th>\n <th className=\"text-right py-2 px-4 text-sm font-medium\">\n {tr('analyticsDashboard.tableViews', 'Views')}\n </th>\n <th className=\"text-right py-2 px-4 text-sm font-medium\">\n {tr('analyticsDashboard.tableCompletions', 'Completions')}\n </th>\n <th className=\"text-right py-2 px-4 text-sm font-medium\">\n {tr('analyticsDashboard.tableDropOffRate', 'Drop-off Rate')}\n </th>\n </tr>\n </thead>\n <tbody>\n {analytics.stepAnalytics.map((step) => (\n <tr key={step.stepId} className=\"border-b border-border\">\n <td className=\"py-2 px-4 text-sm\">\n {tr('analyticsDashboard.stepLabel', 'Step')} {step.stepOrder}\n </td>\n <td className=\"text-right py-2 px-4 text-sm\">{formatNumber(step.views)}</td>\n <td className=\"text-right py-2 px-4 text-sm\">\n {formatNumber(step.completions)}\n </td>\n <td className=\"text-right py-2 px-4 text-sm\">\n {formatPercentage(step.dropOffRate)}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AnalyticsDashboardPage.js","names":[],"sources":["../../../src/tours/pages/AnalyticsDashboardPage.tsx"],"sourcesContent":["import { useCallback, useEffect } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { BarChart3, TrendingUp, Users, Timer } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useTourAnalytics } from '../hooks/useTourAnalytics';\nimport { useToursPermissionAccess } from '../hooks/useToursPermissionAccess';\nimport { TOUR_PERMISSIONS } from '../../constants/permissions';\nimport { formatNumber, formatPercentage, formatDuration } from '../utils';\n\n/**\n * Analytics dashboard page\n * Display tour performance metrics and charts\n */\nexport function AnalyticsDashboardPage() {\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n },\n [t]\n );\n\n const { tourId } = useParams();\n const { hasPermission, isLoading: permissionsLoading } = useToursPermissionAccess();\n const canViewAnalytics = hasPermission(TOUR_PERMISSIONS.TOUR_ANALYTICS_READ);\n\n const { analytics, loading, fetchAnalytics, getCompletionRate, getSkipRate } =\n useTourAnalytics(tourId);\n\n useEffect(() => {\n if (permissionsLoading || !canViewAnalytics) return;\n fetchAnalytics();\n }, [fetchAnalytics, permissionsLoading, canViewAnalytics]);\n\n if (!permissionsLoading && !canViewAnalytics) {\n return (\n <div className=\"flex items-center justify-center h-full p-3 sm:p-6\">\n <div className=\"text-center\">\n <h2 className=\"text-lg font-semibold text-text-primary mb-2\">\n {tr('analyticsDashboard.accessDenied', 'Access Denied')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'analyticsDashboard.noPermission',\n 'You do not have permission to view tour analytics.'\n )}\n </p>\n </div>\n </div>\n );\n }\n\n if (loading) {\n return (\n <div className=\"flex items-center justify-center h-full\">\n <p className=\"text-text-secondary\">\n {tr('analyticsDashboard.loading', 'Loading analytics...')}\n </p>\n </div>\n );\n }\n\n if (!analytics) {\n return (\n <div className=\"flex items-center justify-center h-full\">\n <p className=\"text-text-secondary\">\n {tr('analyticsDashboard.noData', 'No analytics data available')}\n </p>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold\">\n {tr('analyticsDashboard.title', 'Tour Analytics')}\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {tr('analyticsDashboard.subtitle', 'Performance metrics and insights')}\n </p>\n </div>\n\n {/* Summary Cards */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4\">\n <div className=\"border border-border-subtle rounded-md p-4 bg-bg-surface\">\n <div className=\"flex items-center gap-3 mb-2\">\n <div className=\"p-2 bg-action-primary-bg/10 rounded-md\">\n <Users className=\"text-primary\" size={20} />\n </div>\n <span className=\"text-sm text-text-secondary\">\n {tr('analyticsDashboard.totalViews', 'Total Views')}\n </span>\n </div>\n <div className=\"text-2xl font-bold\">{formatNumber(analytics.totalViews)}</div>\n </div>\n\n <div className=\"border border-border-subtle rounded-md p-4 bg-bg-surface\">\n <div className=\"flex items-center gap-3 mb-2\">\n <div className=\"p-2 bg-success/10 rounded-md\">\n <TrendingUp className=\"text-success\" size={20} />\n </div>\n <span className=\"text-sm text-text-secondary\">\n {tr('analyticsDashboard.completionRate', 'Completion Rate')}\n </span>\n </div>\n <div className=\"text-2xl font-bold\">{formatPercentage(getCompletionRate())}</div>\n </div>\n\n <div className=\"border border-border-subtle rounded-md p-4 bg-bg-surface\">\n <div className=\"flex items-center gap-3 mb-2\">\n <div className=\"p-2 bg-warning/10 rounded-md\">\n <BarChart3 className=\"text-warning\" size={20} />\n </div>\n <span className=\"text-sm text-text-secondary\">\n {tr('analyticsDashboard.skipRate', 'Skip Rate')}\n </span>\n </div>\n <div className=\"text-2xl font-bold\">{formatPercentage(getSkipRate())}</div>\n </div>\n\n <div className=\"border border-border-subtle rounded-md p-4 bg-bg-surface\">\n <div className=\"flex items-center gap-3 mb-2\">\n <div className=\"p-2 bg-info/10 rounded-md\">\n <Timer className=\"text-info\" size={20} />\n </div>\n <span className=\"text-sm text-text-secondary\">\n {tr('analyticsDashboard.avgDuration', 'Avg Duration')}\n </span>\n </div>\n <div className=\"text-2xl font-bold\">{formatDuration(analytics.averageDuration)}</div>\n </div>\n </div>\n\n {/* Device Breakdown */}\n <div className=\"border border-border-subtle rounded-md p-4 sm:p-6 bg-bg-surface\">\n <h2 className=\"text-lg font-semibold mb-4\">\n {tr('analyticsDashboard.deviceBreakdown', 'Device Breakdown')}\n </h2>\n <div className=\"space-y-3\">\n {analytics.byDeviceType.map((device) => (\n <div key={device.deviceType}>\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm capitalize\">{device.deviceType}</span>\n <span className=\"text-sm text-text-secondary\">\n {formatNumber(device.count)} views ({formatPercentage(device.completionRate)})\n </span>\n </div>\n <div className=\"w-full bg-bg-sunken rounded-full h-2\">\n <div\n className=\"bg-action-primary-bg h-2 rounded-full\"\n style={{ width: `${device.completionRate}%` }}\n />\n </div>\n </div>\n ))}\n </div>\n </div>\n\n {/* Step Analytics */}\n {analytics.stepAnalytics && analytics.stepAnalytics.length > 0 && (\n <div className=\"border border-border-subtle rounded-md p-4 sm:p-6 bg-bg-surface\">\n <h2 className=\"text-lg font-semibold mb-4\">\n {tr('analyticsDashboard.stepPerformance', 'Step Performance')}\n </h2>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full\">\n <thead>\n <tr className=\"border-b border-border-subtle\">\n <th className=\"text-left py-2 px-4 text-sm font-medium\">\n {tr('analyticsDashboard.tableStep', 'Step')}\n </th>\n <th className=\"text-right py-2 px-4 text-sm font-medium\">\n {tr('analyticsDashboard.tableViews', 'Views')}\n </th>\n <th className=\"text-right py-2 px-4 text-sm font-medium\">\n {tr('analyticsDashboard.tableCompletions', 'Completions')}\n </th>\n <th className=\"text-right py-2 px-4 text-sm font-medium\">\n {tr('analyticsDashboard.tableDropOffRate', 'Drop-off Rate')}\n </th>\n </tr>\n </thead>\n <tbody>\n {analytics.stepAnalytics.map((step) => (\n <tr key={step.stepId} className=\"border-b border-border-subtle\">\n <td className=\"py-2 px-4 text-sm\">\n {tr('analyticsDashboard.stepLabel', 'Step')} {step.stepOrder}\n </td>\n <td className=\"text-right py-2 px-4 text-sm\">{formatNumber(step.views)}</td>\n <td className=\"text-right py-2 px-4 text-sm\">\n {formatNumber(step.completions)}\n </td>\n <td className=\"text-right py-2 px-4 text-sm\">\n {formatPercentage(step.dropOffRate)}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -50,12 +50,12 @@ function x() {
|
|
|
50
50
|
className: "text-xl sm:text-2xl font-bold",
|
|
51
51
|
children: S("mediaLibrary.title", "Media Library")
|
|
52
52
|
}), /* @__PURE__ */ _("p", {
|
|
53
|
-
className: "text-sm text-
|
|
53
|
+
className: "text-sm text-text-secondary",
|
|
54
54
|
children: S("mediaLibrary.subtitle", "Upload and manage media for tour steps")
|
|
55
55
|
})] }), /* @__PURE__ */ _(l, {
|
|
56
56
|
permission: n.TOUR_MEDIA_CREATE,
|
|
57
57
|
children: /* @__PURE__ */ v("label", {
|
|
58
|
-
className: "w-full sm:w-auto px-4 py-2 bg-primary text-primary-
|
|
58
|
+
className: "w-full sm:w-auto px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 cursor-pointer flex items-center justify-center gap-2",
|
|
59
59
|
children: [
|
|
60
60
|
/* @__PURE__ */ _(h, { size: 20 }),
|
|
61
61
|
S("mediaLibrary.uploadMedia", "Upload Media"),
|
|
@@ -76,20 +76,20 @@ function x() {
|
|
|
76
76
|
children: [/* @__PURE__ */ v("div", {
|
|
77
77
|
className: "flex-1 relative",
|
|
78
78
|
children: [/* @__PURE__ */ _(p, {
|
|
79
|
-
className: "absolute left-3 top-1/2 -translate-y-1/2 text-
|
|
79
|
+
className: "absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary",
|
|
80
80
|
size: 20
|
|
81
81
|
}), /* @__PURE__ */ _("input", {
|
|
82
82
|
type: "text",
|
|
83
83
|
placeholder: S("mediaLibrary.searchPlaceholder", "Search media..."),
|
|
84
84
|
value: P,
|
|
85
85
|
onChange: (e) => F(e.target.value),
|
|
86
|
-
className: "w-full pl-[3.25rem] pr-4 py-2 min-h-[44px] border border-border rounded-md bg-
|
|
86
|
+
className: "w-full pl-[3.25rem] pr-4 py-2 min-h-[44px] border border-border-subtle rounded-md bg-bg-surface",
|
|
87
87
|
"aria-label": S("mediaLibrary.searchPlaceholder", "Search media...")
|
|
88
88
|
})]
|
|
89
89
|
}), /* @__PURE__ */ v("select", {
|
|
90
90
|
value: I,
|
|
91
91
|
onChange: (e) => L(e.target.value),
|
|
92
|
-
className: "px-4 py-2 min-h-[44px] border border-border rounded-md bg-
|
|
92
|
+
className: "px-4 py-2 min-h-[44px] border border-border-subtle rounded-md bg-bg-surface",
|
|
93
93
|
"aria-label": S("mediaLibrary.filterByType", "Filter by media type"),
|
|
94
94
|
children: [
|
|
95
95
|
/* @__PURE__ */ _("option", {
|
|
@@ -118,20 +118,20 @@ function x() {
|
|
|
118
118
|
Object.keys(N).length > 0 && /* @__PURE__ */ _("div", {
|
|
119
119
|
className: "space-y-2",
|
|
120
120
|
children: Object.entries(N).map(([e, t]) => /* @__PURE__ */ v("div", {
|
|
121
|
-
className: "border border-border rounded-md p-3",
|
|
121
|
+
className: "border border-border-subtle rounded-md p-3",
|
|
122
122
|
children: [/* @__PURE__ */ v("div", {
|
|
123
123
|
className: "flex items-center justify-between mb-2",
|
|
124
124
|
children: [/* @__PURE__ */ _("span", {
|
|
125
125
|
className: "text-sm",
|
|
126
126
|
children: S("mediaLibrary.uploading", "Uploading...")
|
|
127
127
|
}), /* @__PURE__ */ v("span", {
|
|
128
|
-
className: "text-sm text-
|
|
128
|
+
className: "text-sm text-text-secondary",
|
|
129
129
|
children: [t, "%"]
|
|
130
130
|
})]
|
|
131
131
|
}), /* @__PURE__ */ _("div", {
|
|
132
|
-
className: "w-full bg-
|
|
132
|
+
className: "w-full bg-bg-sunken rounded-full h-2",
|
|
133
133
|
children: /* @__PURE__ */ _("div", {
|
|
134
|
-
className: "bg-primary h-2 rounded-full transition-all",
|
|
134
|
+
className: "bg-action-primary-bg h-2 rounded-full transition-all",
|
|
135
135
|
style: { width: `${t}%` }
|
|
136
136
|
})
|
|
137
137
|
})]
|
|
@@ -140,15 +140,15 @@ function x() {
|
|
|
140
140
|
k.length > 0 ? /* @__PURE__ */ _("div", {
|
|
141
141
|
className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4",
|
|
142
142
|
children: k.map((e) => /* @__PURE__ */ v("div", {
|
|
143
|
-
className: "border border-border rounded-md overflow-hidden hover:shadow-md transition-shadow",
|
|
143
|
+
className: "border border-border-subtle rounded-md overflow-hidden hover:shadow-md transition-shadow",
|
|
144
144
|
children: [/* @__PURE__ */ _("div", {
|
|
145
|
-
className: "aspect-video bg-
|
|
145
|
+
className: "aspect-video bg-bg-sunken flex items-center justify-center",
|
|
146
146
|
children: e.type === "IMAGE" ? /* @__PURE__ */ _("img", {
|
|
147
147
|
src: e.cdnUrl,
|
|
148
148
|
alt: e.altText || e.name,
|
|
149
149
|
className: "w-full h-full object-cover"
|
|
150
150
|
}) : /* @__PURE__ */ _("div", {
|
|
151
|
-
className: "text-
|
|
151
|
+
className: "text-text-secondary",
|
|
152
152
|
children: z(e.type)
|
|
153
153
|
})
|
|
154
154
|
}), /* @__PURE__ */ v("div", {
|
|
@@ -159,7 +159,7 @@ function x() {
|
|
|
159
159
|
children: e.name
|
|
160
160
|
}),
|
|
161
161
|
/* @__PURE__ */ v("div", {
|
|
162
|
-
className: "flex items-center justify-between mt-2 text-xs text-
|
|
162
|
+
className: "flex items-center justify-between mt-2 text-xs text-text-secondary",
|
|
163
163
|
children: [/* @__PURE__ */ _("span", { children: e.type }), /* @__PURE__ */ _("span", { children: i(e.size) })]
|
|
164
164
|
}),
|
|
165
165
|
O && /* @__PURE__ */ v("button", {
|
|
@@ -167,25 +167,25 @@ function x() {
|
|
|
167
167
|
window.confirm(S("mediaLibrary.deleteConfirm", "Delete this media?")) && M(e.id);
|
|
168
168
|
},
|
|
169
169
|
type: "button",
|
|
170
|
-
className: "mt-2 w-full p-2 min-h-[44px] text-xs text-
|
|
170
|
+
className: "mt-2 w-full p-2 min-h-[44px] text-xs text-status-error-text border border-status-error-border/30 rounded hover:bg-status-error-bg/10 flex items-center justify-center gap-1",
|
|
171
171
|
children: [/* @__PURE__ */ _(m, { size: 12 }), S("mediaLibrary.delete", "Delete")]
|
|
172
172
|
})
|
|
173
173
|
]
|
|
174
174
|
})]
|
|
175
175
|
}, e.id))
|
|
176
176
|
}) : /* @__PURE__ */ v("div", {
|
|
177
|
-
className: "text-center py-12 border border-dashed border-border rounded-md",
|
|
177
|
+
className: "text-center py-12 border border-dashed border-border-subtle rounded-md",
|
|
178
178
|
children: [
|
|
179
179
|
/* @__PURE__ */ _(h, {
|
|
180
|
-
className: "mx-auto mb-4 text-
|
|
180
|
+
className: "mx-auto mb-4 text-text-secondary",
|
|
181
181
|
size: 48
|
|
182
182
|
}),
|
|
183
183
|
/* @__PURE__ */ _("p", {
|
|
184
|
-
className: "text-
|
|
184
|
+
className: "text-text-secondary mb-2",
|
|
185
185
|
children: S("mediaLibrary.noMedia", "No media files")
|
|
186
186
|
}),
|
|
187
187
|
/* @__PURE__ */ _("p", {
|
|
188
|
-
className: "text-sm text-
|
|
188
|
+
className: "text-sm text-text-secondary",
|
|
189
189
|
children: S("mediaLibrary.uploadFirstMedia", "Upload your first media file to get started")
|
|
190
190
|
})
|
|
191
191
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MediaLibraryPage.js","names":[],"sources":["../../../src/tours/pages/MediaLibraryPage.tsx"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\nimport { Upload, Search, Image as ImageIcon, Video, Music, FileText, Trash2 } from 'lucide-react';\nimport { PermissionGate } from '@burdenoff/fe-libs/shared/components';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useMediaLibrary } from '../hooks/useMediaLibrary';\nimport { useToursPermissionAccess } from '../hooks/useToursPermissionAccess';\nimport { useToursContext } from '../providers/ToursProvider';\nimport { TOUR_PERMISSIONS } from '../../constants/permissions';\nimport { formatFileSize } from '../utils';\nimport type { TourMediaType } from '../types';\nimport { emitToursApiFailed } from '../telemetry';\n\n/**\n * Media library page\n * Browse, upload, and manage tour media\n */\nexport function MediaLibraryPage() {\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n },\n [t]\n );\n\n const toursContext = useToursContext();\n const eventBus = useEventBus();\n\n const { hasPermission, isLoading: permissionsLoading } = useToursPermissionAccess();\n const canListMedia = hasPermission(TOUR_PERMISSIONS.TOUR_MEDIA_LIST);\n const canDeleteMedia = hasPermission(TOUR_PERMISSIONS.TOUR_MEDIA_DELETE);\n\n const { media, fetchMedia, uploadMedia, deleteMedia, uploadProgress } = useMediaLibrary();\n\n const [searchQuery, setSearchQuery] = useState('');\n const [typeFilter, setTypeFilter] = useState<TourMediaType | 'all'>('all');\n\n useEffect(() => {\n if (permissionsLoading || !canListMedia) return;\n fetchMedia(typeFilter === 'all' ? undefined : typeFilter, searchQuery);\n }, [fetchMedia, typeFilter, searchQuery, permissionsLoading, canListMedia]);\n\n const handleFileUpload = async (files: FileList | null) => {\n if (!files) return;\n\n await Promise.all(\n Array.from(files).map(async (file) => {\n try {\n await uploadMedia(file);\n } catch {\n emitToursApiFailed(eventBus, 'media.upload', toursContext);\n }\n })\n );\n\n // Refresh media list\n fetchMedia(typeFilter === 'all' ? undefined : typeFilter, searchQuery);\n };\n\n const getMediaIcon = (type: TourMediaType) => {\n switch (type) {\n case 'IMAGE':\n return <ImageIcon size={20} />;\n case 'VIDEO':\n return <Video size={20} />;\n case 'AUDIO':\n return <Music size={20} />;\n case 'DOCUMENT':\n return <FileText size={20} />;\n }\n };\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3\">\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold\">\n {tr('mediaLibrary.title', 'Media Library')}\n </h1>\n <p className=\"text-sm text-muted-foreground\">\n {tr('mediaLibrary.subtitle', 'Upload and manage media for tour steps')}\n </p>\n </div>\n <PermissionGate permission={TOUR_PERMISSIONS.TOUR_MEDIA_CREATE}>\n <label className=\"w-full sm:w-auto px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 cursor-pointer flex items-center justify-center gap-2\">\n <Upload size={20} />\n {tr('mediaLibrary.uploadMedia', 'Upload Media')}\n <input\n type=\"file\"\n multiple\n accept=\"image/*,video/*,audio/*,.pdf,.doc,.docx\"\n onChange={(e) => handleFileUpload(e.target.files)}\n className=\"hidden\"\n aria-label={tr('mediaLibrary.uploadMedia', 'Upload Media')}\n />\n </label>\n </PermissionGate>\n </div>\n\n {/* Filters */}\n <div className=\"flex flex-col sm:flex-row gap-3\">\n <div className=\"flex-1 relative\">\n <Search\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground\"\n size={20}\n />\n <input\n type=\"text\"\n placeholder={tr('mediaLibrary.searchPlaceholder', 'Search media...')}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full pl-[3.25rem] pr-4 py-2 min-h-[44px] border border-border rounded-md bg-background\"\n aria-label={tr('mediaLibrary.searchPlaceholder', 'Search media...')}\n />\n </div>\n <select\n value={typeFilter}\n onChange={(e) => setTypeFilter(e.target.value as TourMediaType | 'all')}\n className=\"px-4 py-2 min-h-[44px] border border-border rounded-md bg-background\"\n aria-label={tr('mediaLibrary.filterByType', 'Filter by media type')}\n >\n <option value=\"all\">{tr('mediaLibrary.allTypes', 'All Types')}</option>\n <option value=\"IMAGE\">{tr('mediaLibrary.typeImages', 'Images')}</option>\n <option value=\"VIDEO\">{tr('mediaLibrary.typeVideos', 'Videos')}</option>\n <option value=\"AUDIO\">{tr('mediaLibrary.typeAudio', 'Audio')}</option>\n <option value=\"DOCUMENT\">{tr('mediaLibrary.typeDocuments', 'Documents')}</option>\n </select>\n </div>\n\n {/* Upload Progress */}\n {Object.keys(uploadProgress).length > 0 && (\n <div className=\"space-y-2\">\n {Object.entries(uploadProgress).map(([fileId, progress]) => (\n <div key={fileId} className=\"border border-border rounded-md p-3\">\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm\">{tr('mediaLibrary.uploading', 'Uploading...')}</span>\n <span className=\"text-sm text-muted-foreground\">{progress}%</span>\n </div>\n <div className=\"w-full bg-muted rounded-full h-2\">\n <div\n className=\"bg-primary h-2 rounded-full transition-all\"\n style={{ width: `${progress}%` }}\n />\n </div>\n </div>\n ))}\n </div>\n )}\n\n {/* Media Grid */}\n {media.length > 0 ? (\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4\">\n {media.map((item) => (\n <div\n key={item.id}\n className=\"border border-border rounded-md overflow-hidden hover:shadow-md transition-shadow\"\n >\n {/* Thumbnail */}\n <div className=\"aspect-video bg-muted flex items-center justify-center\">\n {item.type === 'IMAGE' ? (\n <img\n src={item.cdnUrl}\n alt={item.altText || item.name}\n className=\"w-full h-full object-cover\"\n />\n ) : (\n <div className=\"text-muted-foreground\">{getMediaIcon(item.type)}</div>\n )}\n </div>\n\n {/* Info */}\n <div className=\"p-3\">\n <h3 className=\"font-medium text-sm truncate\">{item.name}</h3>\n <div className=\"flex items-center justify-between mt-2 text-xs text-muted-foreground\">\n <span>{item.type}</span>\n <span>{formatFileSize(item.size)}</span>\n </div>\n {canDeleteMedia && (\n <button\n onClick={() => {\n if (window.confirm(tr('mediaLibrary.deleteConfirm', 'Delete this media?')))\n deleteMedia(item.id);\n }}\n type=\"button\"\n className=\"mt-2 w-full p-2 min-h-[44px] text-xs text-destructive border border-destructive/30 rounded hover:bg-destructive/10 flex items-center justify-center gap-1\"\n >\n <Trash2 size={12} />\n {tr('mediaLibrary.delete', 'Delete')}\n </button>\n )}\n </div>\n </div>\n ))}\n </div>\n ) : (\n <div className=\"text-center py-12 border border-dashed border-border rounded-md\">\n <Upload className=\"mx-auto mb-4 text-muted-foreground\" size={48} />\n <p className=\"text-muted-foreground mb-2\">\n {tr('mediaLibrary.noMedia', 'No media files')}\n </p>\n <p className=\"text-sm text-muted-foreground\">\n {tr('mediaLibrary.uploadFirstMedia', 'Upload your first media file to get started')}\n </p>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MediaLibraryPage.js","names":[],"sources":["../../../src/tours/pages/MediaLibraryPage.tsx"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react';\nimport { Upload, Search, Image as ImageIcon, Video, Music, FileText, Trash2 } from 'lucide-react';\nimport { PermissionGate } from '@burdenoff/fe-libs/shared/components';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useMediaLibrary } from '../hooks/useMediaLibrary';\nimport { useToursPermissionAccess } from '../hooks/useToursPermissionAccess';\nimport { useToursContext } from '../providers/ToursProvider';\nimport { TOUR_PERMISSIONS } from '../../constants/permissions';\nimport { formatFileSize } from '../utils';\nimport type { TourMediaType } from '../types';\nimport { emitToursApiFailed } from '../telemetry';\n\n/**\n * Media library page\n * Browse, upload, and manage tour media\n */\nexport function MediaLibraryPage() {\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n },\n [t]\n );\n\n const toursContext = useToursContext();\n const eventBus = useEventBus();\n\n const { hasPermission, isLoading: permissionsLoading } = useToursPermissionAccess();\n const canListMedia = hasPermission(TOUR_PERMISSIONS.TOUR_MEDIA_LIST);\n const canDeleteMedia = hasPermission(TOUR_PERMISSIONS.TOUR_MEDIA_DELETE);\n\n const { media, fetchMedia, uploadMedia, deleteMedia, uploadProgress } = useMediaLibrary();\n\n const [searchQuery, setSearchQuery] = useState('');\n const [typeFilter, setTypeFilter] = useState<TourMediaType | 'all'>('all');\n\n useEffect(() => {\n if (permissionsLoading || !canListMedia) return;\n fetchMedia(typeFilter === 'all' ? undefined : typeFilter, searchQuery);\n }, [fetchMedia, typeFilter, searchQuery, permissionsLoading, canListMedia]);\n\n const handleFileUpload = async (files: FileList | null) => {\n if (!files) return;\n\n await Promise.all(\n Array.from(files).map(async (file) => {\n try {\n await uploadMedia(file);\n } catch {\n emitToursApiFailed(eventBus, 'media.upload', toursContext);\n }\n })\n );\n\n // Refresh media list\n fetchMedia(typeFilter === 'all' ? undefined : typeFilter, searchQuery);\n };\n\n const getMediaIcon = (type: TourMediaType) => {\n switch (type) {\n case 'IMAGE':\n return <ImageIcon size={20} />;\n case 'VIDEO':\n return <Video size={20} />;\n case 'AUDIO':\n return <Music size={20} />;\n case 'DOCUMENT':\n return <FileText size={20} />;\n }\n };\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3\">\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold\">\n {tr('mediaLibrary.title', 'Media Library')}\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {tr('mediaLibrary.subtitle', 'Upload and manage media for tour steps')}\n </p>\n </div>\n <PermissionGate permission={TOUR_PERMISSIONS.TOUR_MEDIA_CREATE}>\n <label className=\"w-full sm:w-auto px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 cursor-pointer flex items-center justify-center gap-2\">\n <Upload size={20} />\n {tr('mediaLibrary.uploadMedia', 'Upload Media')}\n <input\n type=\"file\"\n multiple\n accept=\"image/*,video/*,audio/*,.pdf,.doc,.docx\"\n onChange={(e) => handleFileUpload(e.target.files)}\n className=\"hidden\"\n aria-label={tr('mediaLibrary.uploadMedia', 'Upload Media')}\n />\n </label>\n </PermissionGate>\n </div>\n\n {/* Filters */}\n <div className=\"flex flex-col sm:flex-row gap-3\">\n <div className=\"flex-1 relative\">\n <Search\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary\"\n size={20}\n />\n <input\n type=\"text\"\n placeholder={tr('mediaLibrary.searchPlaceholder', 'Search media...')}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full pl-[3.25rem] pr-4 py-2 min-h-[44px] border border-border-subtle rounded-md bg-bg-surface\"\n aria-label={tr('mediaLibrary.searchPlaceholder', 'Search media...')}\n />\n </div>\n <select\n value={typeFilter}\n onChange={(e) => setTypeFilter(e.target.value as TourMediaType | 'all')}\n className=\"px-4 py-2 min-h-[44px] border border-border-subtle rounded-md bg-bg-surface\"\n aria-label={tr('mediaLibrary.filterByType', 'Filter by media type')}\n >\n <option value=\"all\">{tr('mediaLibrary.allTypes', 'All Types')}</option>\n <option value=\"IMAGE\">{tr('mediaLibrary.typeImages', 'Images')}</option>\n <option value=\"VIDEO\">{tr('mediaLibrary.typeVideos', 'Videos')}</option>\n <option value=\"AUDIO\">{tr('mediaLibrary.typeAudio', 'Audio')}</option>\n <option value=\"DOCUMENT\">{tr('mediaLibrary.typeDocuments', 'Documents')}</option>\n </select>\n </div>\n\n {/* Upload Progress */}\n {Object.keys(uploadProgress).length > 0 && (\n <div className=\"space-y-2\">\n {Object.entries(uploadProgress).map(([fileId, progress]) => (\n <div key={fileId} className=\"border border-border-subtle rounded-md p-3\">\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm\">{tr('mediaLibrary.uploading', 'Uploading...')}</span>\n <span className=\"text-sm text-text-secondary\">{progress}%</span>\n </div>\n <div className=\"w-full bg-bg-sunken rounded-full h-2\">\n <div\n className=\"bg-action-primary-bg h-2 rounded-full transition-all\"\n style={{ width: `${progress}%` }}\n />\n </div>\n </div>\n ))}\n </div>\n )}\n\n {/* Media Grid */}\n {media.length > 0 ? (\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4\">\n {media.map((item) => (\n <div\n key={item.id}\n className=\"border border-border-subtle rounded-md overflow-hidden hover:shadow-md transition-shadow\"\n >\n {/* Thumbnail */}\n <div className=\"aspect-video bg-bg-sunken flex items-center justify-center\">\n {item.type === 'IMAGE' ? (\n <img\n src={item.cdnUrl}\n alt={item.altText || item.name}\n className=\"w-full h-full object-cover\"\n />\n ) : (\n <div className=\"text-text-secondary\">{getMediaIcon(item.type)}</div>\n )}\n </div>\n\n {/* Info */}\n <div className=\"p-3\">\n <h3 className=\"font-medium text-sm truncate\">{item.name}</h3>\n <div className=\"flex items-center justify-between mt-2 text-xs text-text-secondary\">\n <span>{item.type}</span>\n <span>{formatFileSize(item.size)}</span>\n </div>\n {canDeleteMedia && (\n <button\n onClick={() => {\n if (window.confirm(tr('mediaLibrary.deleteConfirm', 'Delete this media?')))\n deleteMedia(item.id);\n }}\n type=\"button\"\n className=\"mt-2 w-full p-2 min-h-[44px] text-xs text-status-error-text border border-status-error-border/30 rounded hover:bg-status-error-bg/10 flex items-center justify-center gap-1\"\n >\n <Trash2 size={12} />\n {tr('mediaLibrary.delete', 'Delete')}\n </button>\n )}\n </div>\n </div>\n ))}\n </div>\n ) : (\n <div className=\"text-center py-12 border border-dashed border-border-subtle rounded-md\">\n <Upload className=\"mx-auto mb-4 text-text-secondary\" size={48} />\n <p className=\"text-text-secondary mb-2\">\n {tr('mediaLibrary.noMedia', 'No media files')}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {tr('mediaLibrary.uploadFirstMedia', 'Upload your first media file to get started')}\n </p>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -43,35 +43,35 @@ function _() {
|
|
|
43
43
|
return /* @__PURE__ */ u("div", {
|
|
44
44
|
className: "space-y-4 sm:space-y-6 p-3 sm:p-6",
|
|
45
45
|
children: [/* @__PURE__ */ u("div", { children: [/* @__PURE__ */ l("h1", {
|
|
46
|
-
className: "text-xl sm:text-2xl font-bold text-
|
|
46
|
+
className: "text-xl sm:text-2xl font-bold text-text-primary",
|
|
47
47
|
children: a("myTours.title")
|
|
48
48
|
}), /* @__PURE__ */ l("p", {
|
|
49
|
-
className: "text-sm sm:text-base text-
|
|
49
|
+
className: "text-sm sm:text-base text-text-secondary",
|
|
50
50
|
children: a("myTours.subtitle")
|
|
51
51
|
})] }), E && T.length === 0 ? /* @__PURE__ */ l("output", {
|
|
52
|
-
className: "text-sm text-
|
|
52
|
+
className: "text-sm text-text-secondary",
|
|
53
53
|
children: a("myTours.loading")
|
|
54
54
|
}) : T.length === 0 ? /* @__PURE__ */ l("div", {
|
|
55
|
-
className: "rounded-lg border border-dashed border-border p-8 text-center",
|
|
55
|
+
className: "rounded-lg border border-dashed border-border-subtle p-8 text-center",
|
|
56
56
|
children: /* @__PURE__ */ l("output", {
|
|
57
|
-
className: "block text-sm text-
|
|
57
|
+
className: "block text-sm text-text-secondary",
|
|
58
58
|
children: a("myTours.empty")
|
|
59
59
|
})
|
|
60
60
|
}) : /* @__PURE__ */ l("ul", {
|
|
61
61
|
className: "grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
|
|
62
62
|
children: T.map((e) => /* @__PURE__ */ u("li", {
|
|
63
|
-
className: "rounded-lg border border-border bg-
|
|
63
|
+
className: "rounded-lg border border-border-subtle bg-bg-surface p-4 flex flex-col gap-2",
|
|
64
64
|
children: [/* @__PURE__ */ u("div", {
|
|
65
65
|
className: "flex items-start justify-between gap-2",
|
|
66
66
|
children: [/* @__PURE__ */ l("h2", {
|
|
67
|
-
className: "font-semibold text-
|
|
67
|
+
className: "font-semibold text-text-primary line-clamp-2",
|
|
68
68
|
children: e.name
|
|
69
69
|
}), /* @__PURE__ */ l(v, {
|
|
70
70
|
status: e.myProgress?.status,
|
|
71
71
|
tr: a
|
|
72
72
|
})]
|
|
73
73
|
}), e.description ? /* @__PURE__ */ l("p", {
|
|
74
|
-
className: "text-sm text-
|
|
74
|
+
className: "text-sm text-text-secondary line-clamp-3",
|
|
75
75
|
children: e.description
|
|
76
76
|
}) : null]
|
|
77
77
|
}, e.id))
|
|
@@ -90,11 +90,11 @@ function v({ status: e, tr: t }) {
|
|
|
90
90
|
});
|
|
91
91
|
case "SKIPPED":
|
|
92
92
|
case "DISMISSED": return /* @__PURE__ */ u("span", {
|
|
93
|
-
className: "inline-flex items-center gap-1 text-xs text-
|
|
93
|
+
className: "inline-flex items-center gap-1 text-xs text-text-secondary",
|
|
94
94
|
children: [/* @__PURE__ */ l(s, { size: 14 }), t("myTours.status.skipped")]
|
|
95
95
|
});
|
|
96
96
|
default: return /* @__PURE__ */ u("span", {
|
|
97
|
-
className: "inline-flex items-center gap-1 text-xs text-
|
|
97
|
+
className: "inline-flex items-center gap-1 text-xs text-text-secondary",
|
|
98
98
|
children: [/* @__PURE__ */ l(o, { size: 14 }), t("myTours.status.notStarted")]
|
|
99
99
|
});
|
|
100
100
|
}
|