@burdenoff/microfe-tours 2026.527.2 → 2026.527.4
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 +2 -4
- package/dist/tours/ToursUserRoutes.js.map +1 -1
- package/dist/tours/components/RouteHelpers.js +4 -4
- package/dist/tours/components/RouteHelpers.js.map +1 -1
- package/dist/tours/hooks/useTourPreview.js +39 -39
- package/dist/tours/hooks/useTourPreview.js.map +1 -1
- package/dist/tours/hooks/useToursList.js.map +1 -1
- package/dist/tours/pages/MediaLibraryPage.js +11 -8
- package/dist/tours/pages/MediaLibraryPage.js.map +1 -1
- package/dist/tours/pages/MyToursPage.js +59 -57
- package/dist/tours/pages/MyToursPage.js.map +1 -1
- package/dist/tours/pages/MyToursProgressPage.js +65 -61
- package/dist/tours/pages/MyToursProgressPage.js.map +1 -1
- package/dist/tours/pages/SchedulerPage.js +18 -4
- package/dist/tours/pages/SchedulerPage.js.map +1 -1
- package/dist/tours/pages/StepEditorPage.js +330 -276
- package/dist/tours/pages/StepEditorPage.js.map +1 -1
- package/dist/tours/pages/TemplateLibraryPage.js +7 -2
- package/dist/tours/pages/TemplateLibraryPage.js.map +1 -1
- package/dist/tours/pages/TourBuilderPage.js +25 -11
- package/dist/tours/pages/TourBuilderPage.js.map +1 -1
- package/dist/tours/pages/TourPreviewPage.js +12 -4
- package/dist/tours/pages/TourPreviewPage.js.map +1 -1
- package/dist/tours/pages/ToursListPage.js +165 -149
- package/dist/tours/pages/ToursListPage.js.map +1 -1
- package/dist/tours/providers/ToursProvider.js +1 -1
- package/dist/tours/providers/ToursProvider.js.map +1 -1
- package/dist/tours/utils/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -50,11 +50,9 @@ function _() {
|
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
52
|
function v() {
|
|
53
|
-
return /* @__PURE__ */ u("
|
|
53
|
+
return /* @__PURE__ */ u("output", {
|
|
54
54
|
className: "flex min-h-[300px] items-center justify-center p-6 text-center",
|
|
55
|
-
|
|
56
|
-
"aria-live": "polite",
|
|
57
|
-
children: /* @__PURE__ */ u("p", {
|
|
55
|
+
children: /* @__PURE__ */ u("span", {
|
|
58
56
|
className: "max-w-md text-sm text-muted-foreground",
|
|
59
57
|
children: "Tours are not available for this workspace right now."
|
|
60
58
|
})
|
|
@@ -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-foreground\">{tr('myTours.notFoundTitle')}</h2>\n <p className=\"text-sm text-muted-foreground 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 <
|
|
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-foreground\">{tr('myTours.notFoundTitle')}</h2>\n <p className=\"text-sm text-muted-foreground 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-muted-foreground\">\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;aAAyC,EAAG,wBAAwB;GAAM,CAAA,EACxF,kBAAC,KAAD;GAAG,WAAU;aAA0C,EAAG,uBAAuB;GAAK,CAAA,CAClF;;;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;aAAyC;GAElD,CAAA;EACA,CAAA"}
|
|
@@ -5,7 +5,7 @@ import { jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
|
5
5
|
function s() {
|
|
6
6
|
return /* @__PURE__ */ a("div", {
|
|
7
7
|
className: "flex items-center justify-center min-h-[200px]",
|
|
8
|
-
children: /* @__PURE__ */ a(r, { className: "
|
|
8
|
+
children: /* @__PURE__ */ a(r, { className: "size-8 animate-spin text-primary" })
|
|
9
9
|
});
|
|
10
10
|
}
|
|
11
11
|
var c = 3, l = class extends e {
|
|
@@ -40,7 +40,7 @@ var c = 3, l = class extends e {
|
|
|
40
40
|
children: [
|
|
41
41
|
/* @__PURE__ */ a("div", {
|
|
42
42
|
className: "p-3 rounded-full bg-destructive/10",
|
|
43
|
-
children: /* @__PURE__ */ a(n, { className: "
|
|
43
|
+
children: /* @__PURE__ */ a(n, { className: "size-8 text-destructive" })
|
|
44
44
|
}),
|
|
45
45
|
/* @__PURE__ */ o("div", {
|
|
46
46
|
className: "space-y-2",
|
|
@@ -56,12 +56,12 @@ var c = 3, l = class extends e {
|
|
|
56
56
|
type: "button",
|
|
57
57
|
onClick: this.handleRetry,
|
|
58
58
|
className: "inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors",
|
|
59
|
-
children: [/* @__PURE__ */ a(i, { className: "
|
|
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
63
|
className: "inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors",
|
|
64
|
-
children: [/* @__PURE__ */ a(i, { className: "
|
|
64
|
+
children: [/* @__PURE__ */ a(i, { className: "size-4" }), "Refresh Page"]
|
|
65
65
|
})
|
|
66
66
|
]
|
|
67
67
|
});
|
|
@@ -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=\"
|
|
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-destructive/10\">\n <AlertTriangle className=\"size-8 text-destructive\" />\n </div>\n <div className=\"space-y-2\">\n <h3 className=\"text-lg font-semibold text-foreground\">Failed to load page</h3>\n <p className=\"text-sm text-muted-foreground 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-primary text-primary-foreground hover:bg-primary/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-primary text-primary-foreground hover:bg-primary/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,2BAA4B,CAAA;MACjD,CAAA;KACN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBAAwC;OAAwB,CAAA,EAC9E,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"}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import { useToursStore as e } from "../store/toursStore.js";
|
|
2
|
-
import { useCallback as t,
|
|
2
|
+
import { useCallback as t, useMemo as n, useState as r } from "react";
|
|
3
3
|
//#region src/tours/hooks/useTourPreview.ts
|
|
4
|
-
function
|
|
5
|
-
let [
|
|
6
|
-
|
|
7
|
-
}, []), h = t(() => {
|
|
8
|
-
a(!1), s(0);
|
|
4
|
+
function i(i) {
|
|
5
|
+
let [a, o] = r(!1), [s, c] = r(0), [l, u] = r(null), { devicePreview: d, setDevicePreview: f } = e(), p = n(() => i?.steps ?? [], [i?.steps]), m = p[s], h = t(() => {
|
|
6
|
+
o(!0), c(0);
|
|
9
7
|
}, []), g = t(() => {
|
|
10
|
-
o
|
|
11
|
-
}, [
|
|
12
|
-
|
|
13
|
-
}, [
|
|
14
|
-
|
|
15
|
-
}, [
|
|
16
|
-
|
|
17
|
-
}, [
|
|
8
|
+
o(!1), c(0);
|
|
9
|
+
}, []), _ = t(() => {
|
|
10
|
+
s < p.length - 1 && c((e) => e + 1);
|
|
11
|
+
}, [s, p.length]), v = t(() => {
|
|
12
|
+
s > 0 && c((e) => e - 1);
|
|
13
|
+
}, [s]), y = t((e) => {
|
|
14
|
+
e >= 0 && e < p.length && c(e);
|
|
15
|
+
}, [p.length]), b = t((e) => {
|
|
16
|
+
f(e);
|
|
17
|
+
}, [f]), x = t(() => {
|
|
18
18
|
let e = [], t = [];
|
|
19
|
-
if (!
|
|
19
|
+
if (!i) return e.push("No tour selected"), u({
|
|
20
20
|
valid: !1,
|
|
21
21
|
errors: e,
|
|
22
22
|
warnings: t
|
|
@@ -25,13 +25,13 @@ function r(r) {
|
|
|
25
25
|
errors: e,
|
|
26
26
|
warnings: t
|
|
27
27
|
};
|
|
28
|
-
|
|
29
|
-
n.placement !== "CENTER" && !n.targetSelector && e.push(`Step ${r + 1} has no selector`), (!n.content || n.content.trim() === "") && t.push(`Step ${r + 1} has empty content`),
|
|
28
|
+
p.length === 0 && e.push("Tour has no steps"), p.forEach((n, r) => {
|
|
29
|
+
n.placement !== "CENTER" && !n.targetSelector && e.push(`Step ${r + 1} has no selector`), (!n.content || n.content.trim() === "") && t.push(`Step ${r + 1} has empty content`), d === "mobile" && !n.targetSelectorMobile && !n.targetSelector && t.push(`Step ${r + 1} has no mobile selector`), d === "tablet" && !n.targetSelectorTablet && !n.targetSelector && t.push(`Step ${r + 1} has no tablet selector`), d === "desktop" && !n.targetSelectorDesktop && !n.targetSelector && t.push(`Step ${r + 1} has no desktop selector`), n.media && n.media.length > 0 && n.media.forEach((n, i) => {
|
|
30
30
|
n.media.cdnUrl || e.push(`Step ${r + 1}, media ${i + 1} has no URL`), n.media.type === "IMAGE" && !n.media.altText && t.push(`Step ${r + 1}, media ${i + 1} missing alt text`);
|
|
31
31
|
});
|
|
32
32
|
});
|
|
33
33
|
let n = e.length === 0;
|
|
34
|
-
return
|
|
34
|
+
return u({
|
|
35
35
|
valid: n,
|
|
36
36
|
errors: e,
|
|
37
37
|
warnings: t
|
|
@@ -41,30 +41,30 @@ function r(r) {
|
|
|
41
41
|
warnings: t
|
|
42
42
|
};
|
|
43
43
|
}, [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
]),
|
|
44
|
+
i,
|
|
45
|
+
p,
|
|
46
|
+
d
|
|
47
|
+
]), S = t(async () => i ? `${window.location.origin}/tours/${i.id}/preview` : null, [i]);
|
|
48
48
|
return {
|
|
49
|
-
isPreviewActive:
|
|
50
|
-
currentStepIndex:
|
|
51
|
-
currentStep:
|
|
52
|
-
devicePreview:
|
|
53
|
-
validationResults:
|
|
54
|
-
totalSteps:
|
|
55
|
-
canGoNext:
|
|
56
|
-
canGoPrevious:
|
|
57
|
-
startPreview:
|
|
58
|
-
stopPreview:
|
|
59
|
-
nextStep:
|
|
60
|
-
previousStep:
|
|
61
|
-
goToStep:
|
|
62
|
-
changeDevice:
|
|
63
|
-
validateForPreview:
|
|
64
|
-
generatePreviewLink:
|
|
49
|
+
isPreviewActive: a,
|
|
50
|
+
currentStepIndex: s,
|
|
51
|
+
currentStep: m,
|
|
52
|
+
devicePreview: d,
|
|
53
|
+
validationResults: l,
|
|
54
|
+
totalSteps: p.length,
|
|
55
|
+
canGoNext: s < p.length - 1,
|
|
56
|
+
canGoPrevious: s > 0,
|
|
57
|
+
startPreview: h,
|
|
58
|
+
stopPreview: g,
|
|
59
|
+
nextStep: _,
|
|
60
|
+
previousStep: v,
|
|
61
|
+
goToStep: y,
|
|
62
|
+
changeDevice: b,
|
|
63
|
+
validateForPreview: x,
|
|
64
|
+
generatePreviewLink: S
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
//#endregion
|
|
68
|
-
export {
|
|
68
|
+
export { i as useTourPreview };
|
|
69
69
|
|
|
70
70
|
//# sourceMappingURL=useTourPreview.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useTourPreview.js","names":[],"sources":["../../../src/tours/hooks/useTourPreview.ts"],"sourcesContent":["import { useState, useCallback } from 'react';\nimport { useToursStore } from '../store/toursStore';\nimport type { Tour, DeviceType } from '../types';\n\n/**\n * Hook for tour preview functionality\n * Manages preview mode, device simulation, and validation\n */\nexport function useTourPreview(tour?: Tour) {\n const [isPreviewActive, setIsPreviewActive] = useState(false);\n const [currentStepIndex, setCurrentStepIndex] = useState(0);\n const [validationResults, setValidationResults] = useState<{\n valid: boolean;\n errors: string[];\n warnings: string[];\n } | null>(null);\n\n const { devicePreview, setDevicePreview } = useToursStore();\n\n const steps = tour?.steps
|
|
1
|
+
{"version":3,"file":"useTourPreview.js","names":[],"sources":["../../../src/tours/hooks/useTourPreview.ts"],"sourcesContent":["import { useState, useCallback, useMemo } from 'react';\nimport { useToursStore } from '../store/toursStore';\nimport type { Tour, DeviceType } from '../types';\n\n/**\n * Hook for tour preview functionality\n * Manages preview mode, device simulation, and validation\n */\nexport function useTourPreview(tour?: Tour) {\n const [isPreviewActive, setIsPreviewActive] = useState(false);\n const [currentStepIndex, setCurrentStepIndex] = useState(0);\n const [validationResults, setValidationResults] = useState<{\n valid: boolean;\n errors: string[];\n warnings: string[];\n } | null>(null);\n\n const { devicePreview, setDevicePreview } = useToursStore();\n\n const steps = useMemo(() => tour?.steps ?? [], [tour?.steps]);\n const currentStep = steps[currentStepIndex];\n\n /**\n * Start preview mode\n */\n const startPreview = useCallback(() => {\n setIsPreviewActive(true);\n setCurrentStepIndex(0);\n }, []);\n\n /**\n * Stop preview mode\n */\n const stopPreview = useCallback(() => {\n setIsPreviewActive(false);\n setCurrentStepIndex(0);\n }, []);\n\n /**\n * Navigate to next step\n */\n const nextStep = useCallback(() => {\n if (currentStepIndex < steps.length - 1) {\n setCurrentStepIndex((prev) => prev + 1);\n }\n }, [currentStepIndex, steps.length]);\n\n /**\n * Navigate to previous step\n */\n const previousStep = useCallback(() => {\n if (currentStepIndex > 0) {\n setCurrentStepIndex((prev) => prev - 1);\n }\n }, [currentStepIndex]);\n\n /**\n * Jump to specific step\n */\n const goToStep = useCallback(\n (index: number) => {\n if (index >= 0 && index < steps.length) {\n setCurrentStepIndex(index);\n }\n },\n [steps.length]\n );\n\n /**\n * Change preview device\n */\n const changeDevice = useCallback(\n (device: DeviceType) => {\n setDevicePreview(device);\n },\n [setDevicePreview]\n );\n\n /**\n * Validate tour for preview\n * Checks if all selectors exist, media is accessible, etc.\n */\n const validateForPreview = useCallback(() => {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (!tour) {\n errors.push('No tour selected');\n setValidationResults({ valid: false, errors, warnings });\n return { valid: false, errors, warnings };\n }\n\n if (steps.length === 0) {\n errors.push('Tour has no steps');\n }\n\n steps.forEach((step, index) => {\n if (step.placement !== 'CENTER' && !step.targetSelector) {\n errors.push(`Step ${index + 1} has no selector`);\n }\n\n if (!step.content || step.content.trim() === '') {\n warnings.push(`Step ${index + 1} has empty content`);\n }\n\n // Check device-specific selectors\n if (devicePreview === 'mobile' && !step.targetSelectorMobile && !step.targetSelector) {\n warnings.push(`Step ${index + 1} has no mobile selector`);\n }\n if (devicePreview === 'tablet' && !step.targetSelectorTablet && !step.targetSelector) {\n warnings.push(`Step ${index + 1} has no tablet selector`);\n }\n if (devicePreview === 'desktop' && !step.targetSelectorDesktop && !step.targetSelector) {\n warnings.push(`Step ${index + 1} has no desktop selector`);\n }\n\n // Check media\n if (step.media && step.media.length > 0) {\n step.media.forEach((media, mediaIndex) => {\n if (!media.media.cdnUrl) {\n errors.push(`Step ${index + 1}, media ${mediaIndex + 1} has no URL`);\n }\n if (media.media.type === 'IMAGE' && !media.media.altText) {\n warnings.push(`Step ${index + 1}, media ${mediaIndex + 1} missing alt text`);\n }\n });\n }\n });\n\n const valid = errors.length === 0;\n setValidationResults({ valid, errors, warnings });\n return { valid, errors, warnings };\n }, [tour, steps, devicePreview]);\n\n /**\n * Generate shareable preview link\n */\n const generatePreviewLink = useCallback(async () => {\n if (!tour) return null;\n return `${window.location.origin}/tours/${tour.id}/preview`;\n }, [tour]);\n\n return {\n isPreviewActive,\n currentStepIndex,\n currentStep,\n devicePreview,\n validationResults,\n totalSteps: steps.length,\n canGoNext: currentStepIndex < steps.length - 1,\n canGoPrevious: currentStepIndex > 0,\n startPreview,\n stopPreview,\n nextStep,\n previousStep,\n goToStep,\n changeDevice,\n validateForPreview,\n generatePreviewLink,\n };\n}\n"],"mappings":";;;AAQA,SAAgB,EAAe,GAAa;CAC1C,IAAM,CAAC,GAAiB,KAAsB,EAAS,GAAM,EACvD,CAAC,GAAkB,KAAuB,EAAS,EAAE,EACrD,CAAC,GAAmB,KAAwB,EAIxC,KAAK,EAET,EAAE,kBAAe,wBAAqB,GAAe,EAErD,IAAQ,QAAc,GAAM,SAAS,EAAE,EAAE,CAAC,GAAM,MAAM,CAAC,EACvD,IAAc,EAAM,IAKpB,IAAe,QAAkB;AAErC,EADA,EAAmB,GAAK,EACxB,EAAoB,EAAE;IACrB,EAAE,CAAC,EAKA,IAAc,QAAkB;AAEpC,EADA,EAAmB,GAAM,EACzB,EAAoB,EAAE;IACrB,EAAE,CAAC,EAKA,IAAW,QAAkB;AACjC,EAAI,IAAmB,EAAM,SAAS,KACpC,GAAqB,MAAS,IAAO,EAAE;IAExC,CAAC,GAAkB,EAAM,OAAO,CAAC,EAK9B,IAAe,QAAkB;AACrC,EAAI,IAAmB,KACrB,GAAqB,MAAS,IAAO,EAAE;IAExC,CAAC,EAAiB,CAAC,EAKhB,IAAW,GACd,MAAkB;AACjB,EAAI,KAAS,KAAK,IAAQ,EAAM,UAC9B,EAAoB,EAAM;IAG9B,CAAC,EAAM,OAAO,CACf,EAKK,IAAe,GAClB,MAAuB;AACtB,IAAiB,EAAO;IAE1B,CAAC,EAAiB,CACnB,EAMK,IAAqB,QAAkB;EAC3C,IAAM,IAAmB,EAAE,EACrB,IAAqB,EAAE;AAE7B,MAAI,CAAC,EAGH,QAFA,EAAO,KAAK,mBAAmB,EAC/B,EAAqB;GAAE,OAAO;GAAO;GAAQ;GAAU,CAAC,EACjD;GAAE,OAAO;GAAO;GAAQ;GAAU;AAO3C,EAJI,EAAM,WAAW,KACnB,EAAO,KAAK,oBAAoB,EAGlC,EAAM,SAAS,GAAM,MAAU;AAqB7B,GApBI,EAAK,cAAc,YAAY,CAAC,EAAK,kBACvC,EAAO,KAAK,QAAQ,IAAQ,EAAE,kBAAkB,GAG9C,CAAC,EAAK,WAAW,EAAK,QAAQ,MAAM,KAAK,OAC3C,EAAS,KAAK,QAAQ,IAAQ,EAAE,oBAAoB,EAIlD,MAAkB,YAAY,CAAC,EAAK,wBAAwB,CAAC,EAAK,kBACpE,EAAS,KAAK,QAAQ,IAAQ,EAAE,yBAAyB,EAEvD,MAAkB,YAAY,CAAC,EAAK,wBAAwB,CAAC,EAAK,kBACpE,EAAS,KAAK,QAAQ,IAAQ,EAAE,yBAAyB,EAEvD,MAAkB,aAAa,CAAC,EAAK,yBAAyB,CAAC,EAAK,kBACtE,EAAS,KAAK,QAAQ,IAAQ,EAAE,0BAA0B,EAIxD,EAAK,SAAS,EAAK,MAAM,SAAS,KACpC,EAAK,MAAM,SAAS,GAAO,MAAe;AAIxC,IAHK,EAAM,MAAM,UACf,EAAO,KAAK,QAAQ,IAAQ,EAAE,UAAU,IAAa,EAAE,aAAa,EAElE,EAAM,MAAM,SAAS,WAAW,CAAC,EAAM,MAAM,WAC/C,EAAS,KAAK,QAAQ,IAAQ,EAAE,UAAU,IAAa,EAAE,mBAAmB;KAE9E;IAEJ;EAEF,IAAM,IAAQ,EAAO,WAAW;AAEhC,SADA,EAAqB;GAAE;GAAO;GAAQ;GAAU,CAAC,EAC1C;GAAE;GAAO;GAAQ;GAAU;IACjC;EAAC;EAAM;EAAO;EAAc,CAAC,EAK1B,IAAsB,EAAY,YACjC,IACE,GAAG,OAAO,SAAS,OAAO,SAAS,EAAK,GAAG,YADhC,MAEjB,CAAC,EAAK,CAAC;AAEV,QAAO;EACL;EACA;EACA;EACA;EACA;EACA,YAAY,EAAM;EAClB,WAAW,IAAmB,EAAM,SAAS;EAC7C,eAAe,IAAmB;EAClC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useToursList.js","names":[],"sources":["../../../src/tours/hooks/useToursList.ts"],"sourcesContent":["import { useCallback, useState } from 'react';\nimport { useGatewayClient } from '@burdenoff/fe-libs/shared/graphql';\nimport type {\n TourFilters as LocalTourFilters,\n TourPagination as LocalTourPagination,\n} from '../types';\nimport type { TourStatus } from '../../generated/global-types';\nimport { GetToursDocument } from '../../generated/global-operations';\nimport type { GetToursQuery, GetToursQueryVariables } from '../../generated/global-operations';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Tour = any;\n\n/**\n * Hook for fetching and managing tours list\n * Uses the gateway client from fe-libs\n * All tours are now global (no workspace scoping)\n */\nexport function useToursList(\n _initialFilters?: LocalTourFilters,\n _initialPagination?: LocalTourPagination\n) {\n const globalClient = useGatewayClient('global');\n\n const [tours, setTours] = useState<Tour[]>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const [total, setTotal] = useState(0);\n\n const fetchTours = useCallback(\n async (filters?: LocalTourFilters, pagination?: LocalTourPagination) => {\n setLoading(true);\n setError(null);\n\n try {\n const { data, error: queryError } = await globalClient.query<\n GetToursQuery,\n GetToursQueryVariables\n >({\n query: GetToursDocument,\n variables: {\n filters: {\n productId: filters?.productId,\n moduleId: filters?.moduleId,\n pageId: filters?.pageId,\n status:\n filters?.status === 'all' ? undefined : (filters?.status as TourStatus | undefined),\n },\n pagination: {\n limit: pagination?.limit ?? 50,\n offset: pagination?.page ? (pagination.page - 1) * (pagination?.limit ?? 50) : 0,\n },\n },\n fetchPolicy: 'network-only',\n });\n\n const fetchedTours = (data?.tours?.tours ?? []).filter((tour) => {\n if (!filters?.search) return true;\n\n const normalizedSearch = filters.search.toLowerCase();\n return (\n tour.name.toLowerCase().includes(normalizedSearch) ||\n (tour.description ?? '').toLowerCase().includes(normalizedSearch)\n );\n });\n\n setTours(fetchedTours);\n setTotal(data?.tours?.total ?? fetchedTours.length);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to fetch tours'));\n setTours([]);\n setTotal(0);\n } finally {\n setLoading(false);\n }\n },\n [globalClient]\n );\n\n return {\n tours,\n loading,\n error,\n total,\n hasMore: false,\n fetchTours,\n refetch: () => fetchTours(),\n };\n}\n
|
|
1
|
+
{"version":3,"file":"useToursList.js","names":[],"sources":["../../../src/tours/hooks/useToursList.ts"],"sourcesContent":["import { useCallback, useState } from 'react';\nimport { useGatewayClient } from '@burdenoff/fe-libs/shared/graphql';\nimport type {\n TourFilters as LocalTourFilters,\n TourPagination as LocalTourPagination,\n} from '../types';\nimport type { TourStatus } from '../../generated/global-types';\nimport { GetToursDocument } from '../../generated/global-operations';\nimport type { GetToursQuery, GetToursQueryVariables } from '../../generated/global-operations';\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Tour = any;\n\n/**\n * Hook for fetching and managing tours list\n * Uses the gateway client from fe-libs\n * All tours are now global (no workspace scoping)\n */\nexport function useToursList(\n _initialFilters?: LocalTourFilters,\n _initialPagination?: LocalTourPagination\n) {\n const globalClient = useGatewayClient('global');\n\n const [tours, setTours] = useState<Tour[]>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const [total, setTotal] = useState(0);\n\n const fetchTours = useCallback(\n async (filters?: LocalTourFilters, pagination?: LocalTourPagination) => {\n setLoading(true);\n setError(null);\n\n try {\n const { data, error: queryError } = await globalClient.query<\n GetToursQuery,\n GetToursQueryVariables\n >({\n query: GetToursDocument,\n variables: {\n filters: {\n productId: filters?.productId,\n moduleId: filters?.moduleId,\n pageId: filters?.pageId,\n status:\n filters?.status === 'all' ? undefined : (filters?.status as TourStatus | undefined),\n },\n pagination: {\n limit: pagination?.limit ?? 50,\n offset: pagination?.page ? (pagination.page - 1) * (pagination?.limit ?? 50) : 0,\n },\n },\n fetchPolicy: 'network-only',\n });\n\n const fetchedTours = (data?.tours?.tours ?? []).filter((tour) => {\n if (!filters?.search) return true;\n\n const normalizedSearch = filters.search.toLowerCase();\n return (\n tour.name.toLowerCase().includes(normalizedSearch) ||\n (tour.description ?? '').toLowerCase().includes(normalizedSearch)\n );\n });\n\n setTours(fetchedTours);\n setTotal(data?.tours?.total ?? fetchedTours.length);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to fetch tours'));\n setTours([]);\n setTotal(0);\n } finally {\n setLoading(false);\n }\n },\n [globalClient]\n );\n\n return {\n tours,\n loading,\n error,\n total,\n hasMore: false,\n fetchTours,\n refetch: () => fetchTours(),\n };\n}\n"],"mappings":";;;;AAkBA,SAAgB,EACd,GACA,GACA;CACA,IAAM,IAAe,EAAiB,SAAS,EAEzC,CAAC,GAAO,KAAY,EAAiB,EAAE,CAAC,EACxC,CAAC,GAAS,KAAc,EAAS,GAAM,EACvC,CAAC,GAAO,KAAY,EAAuB,KAAK,EAChD,CAAC,GAAO,KAAY,EAAS,EAAE,EAE/B,IAAa,EACjB,OAAO,GAA4B,MAAqC;AAEtE,EADA,EAAW,GAAK,EAChB,EAAS,KAAK;AAEd,MAAI;GACF,IAAM,EAAE,SAAM,OAAO,MAAe,MAAM,EAAa,MAGrD;IACA,OAAO;IACP,WAAW;KACT,SAAS;MACP,WAAW,GAAS;MACpB,UAAU,GAAS;MACnB,QAAQ,GAAS;MACjB,QACE,GAAS,WAAW,QAAQ,KAAA,IAAa,GAAS;MACrD;KACD,YAAY;MACV,OAAO,GAAY,SAAS;MAC5B,QAAQ,GAAY,QAAQ,EAAW,OAAO,MAAM,GAAY,SAAS,MAAM;MAChF;KACF;IACD,aAAa;IACd,CAAC,EAEI,KAAgB,GAAM,OAAO,SAAS,EAAE,EAAE,QAAQ,MAAS;AAC/D,QAAI,CAAC,GAAS,OAAQ,QAAO;IAE7B,IAAM,IAAmB,EAAQ,OAAO,aAAa;AACrD,WACE,EAAK,KAAK,aAAa,CAAC,SAAS,EAAiB,KACjD,EAAK,eAAe,IAAI,aAAa,CAAC,SAAS,EAAiB;KAEnE;AAGF,GADA,EAAS,EAAa,EACtB,EAAS,GAAM,OAAO,SAAS,EAAa,OAAO;WAC5C,GAAK;AAGZ,GAFA,EAAS,aAAe,QAAQ,IAAM,gBAAI,MAAM,wBAAwB,CAAC,EACzE,EAAS,EAAE,CAAC,EACZ,EAAS,EAAE;YACH;AACR,KAAW,GAAM;;IAGrB,CAAC,EAAa,CACf;AAED,QAAO;EACL;EACA;EACA;EACA;EACA,SAAS;EACT;EACA,eAAe,GAAY;EAC5B"}
|
|
@@ -26,14 +26,13 @@ function x() {
|
|
|
26
26
|
D
|
|
27
27
|
]);
|
|
28
28
|
let R = async (e) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
await j(e
|
|
29
|
+
e && (await Promise.all(Array.from(e).map(async (e) => {
|
|
30
|
+
try {
|
|
31
|
+
await j(e);
|
|
32
32
|
} catch {
|
|
33
33
|
r(w, "media.upload", C);
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
}
|
|
35
|
+
})), A(I === "all" ? void 0 : I, P));
|
|
37
36
|
}, z = (e) => {
|
|
38
37
|
switch (e) {
|
|
39
38
|
case "IMAGE": return /* @__PURE__ */ _(d, { size: 20 });
|
|
@@ -65,7 +64,8 @@ function x() {
|
|
|
65
64
|
multiple: !0,
|
|
66
65
|
accept: "image/*,video/*,audio/*,.pdf,.doc,.docx",
|
|
67
66
|
onChange: (e) => R(e.target.files),
|
|
68
|
-
className: "hidden"
|
|
67
|
+
className: "hidden",
|
|
68
|
+
"aria-label": S("mediaLibrary.uploadMedia", "Upload Media")
|
|
69
69
|
})
|
|
70
70
|
]
|
|
71
71
|
})
|
|
@@ -83,12 +83,14 @@ function x() {
|
|
|
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-background"
|
|
86
|
+
className: "w-full pl-[3.25rem] pr-4 py-2 min-h-[44px] border border-border rounded-md bg-background",
|
|
87
|
+
"aria-label": S("mediaLibrary.searchPlaceholder", "Search media...")
|
|
87
88
|
})]
|
|
88
89
|
}), /* @__PURE__ */ v("select", {
|
|
89
90
|
value: I,
|
|
90
91
|
onChange: (e) => L(e.target.value),
|
|
91
92
|
className: "px-4 py-2 min-h-[44px] border border-border rounded-md bg-background",
|
|
93
|
+
"aria-label": S("mediaLibrary.filterByType", "Filter by media type"),
|
|
92
94
|
children: [
|
|
93
95
|
/* @__PURE__ */ _("option", {
|
|
94
96
|
value: "all",
|
|
@@ -164,7 +166,8 @@ function x() {
|
|
|
164
166
|
onClick: () => {
|
|
165
167
|
window.confirm(S("mediaLibrary.deleteConfirm", "Delete this media?")) && M(e.id);
|
|
166
168
|
},
|
|
167
|
-
|
|
169
|
+
type: "button",
|
|
170
|
+
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",
|
|
168
171
|
children: [/* @__PURE__ */ _(m, { size: 12 }), S("mediaLibrary.delete", "Delete")]
|
|
169
172
|
})
|
|
170
173
|
]
|
|
@@ -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 for (let i = 0; i < files.length; i++) {\n try {\n await uploadMedia(files[i]);\n } catch {\n emitToursApiFailed(eventBus, 'media.upload', toursContext);\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 />\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 />\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 >\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 className=\"mt-2 w-full px-2 py-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-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,74 +1,76 @@
|
|
|
1
1
|
import { useToursContext as e } from "../providers/ToursProvider.js";
|
|
2
2
|
import { useGetToursForContextQuery as t } from "../../generated/global-operations.js";
|
|
3
3
|
import { useToursI18n as n } from "../i18n.js";
|
|
4
|
-
import { useEffect as r } from "react";
|
|
5
|
-
import { CheckCircle2 as
|
|
6
|
-
import { jsx as
|
|
7
|
-
import { useSearchParams as
|
|
8
|
-
import { useEventBus as
|
|
4
|
+
import { useEffect as r, useRef as i } from "react";
|
|
5
|
+
import { CheckCircle2 as a, Circle as o, MinusCircle as s, Play as c } from "lucide-react";
|
|
6
|
+
import { jsx as l, jsxs as u } from "react/jsx-runtime";
|
|
7
|
+
import { useSearchParams as d } from "react-router-dom";
|
|
8
|
+
import { useEventBus as f } from "@burdenoff/fe-libs/shared/events";
|
|
9
9
|
//#region src/tours/pages/MyToursPage.tsx
|
|
10
|
-
var
|
|
11
|
-
function m(e, t) {
|
|
12
|
-
return e && f.test(e) ? e : t;
|
|
13
|
-
}
|
|
10
|
+
var p = /^[a-zA-Z0-9_-]+$/, m = /^[a-zA-Z0-9_\-/]+$/;
|
|
14
11
|
function h(e, t) {
|
|
15
12
|
return e && p.test(e) ? e : t;
|
|
16
13
|
}
|
|
17
|
-
function g() {
|
|
18
|
-
|
|
14
|
+
function g(e, t) {
|
|
15
|
+
return e && m.test(e) ? e : t;
|
|
16
|
+
}
|
|
17
|
+
function _() {
|
|
18
|
+
let { tr: a } = n(), { productId: o, moduleId: s, pageId: c, workspaceId: p } = e(), [m] = d(), _ = f(), y = i(!1);
|
|
19
19
|
r(() => {
|
|
20
|
-
|
|
20
|
+
y.current || (y.current = !0, _.emit("tours.page.viewed", {
|
|
21
21
|
page: "/tours",
|
|
22
|
-
productId:
|
|
23
|
-
moduleId:
|
|
24
|
-
pageId:
|
|
25
|
-
workspaceId:
|
|
26
|
-
});
|
|
27
|
-
}, [
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
productId: o,
|
|
23
|
+
moduleId: s,
|
|
24
|
+
pageId: c,
|
|
25
|
+
workspaceId: p
|
|
26
|
+
}));
|
|
27
|
+
}, [
|
|
28
|
+
_,
|
|
29
|
+
o,
|
|
30
|
+
s,
|
|
31
|
+
c,
|
|
32
|
+
p
|
|
33
|
+
]);
|
|
34
|
+
let b = h(o, void 0) ?? h(m.get("productId"), void 0), x = h(s, void 0) ?? h(m.get("moduleId"), void 0), S = g(c, void 0) ?? g(m.get("pageId"), void 0), { data: C, loading: w } = t({
|
|
35
|
+
variables: b ? { context: {
|
|
36
|
+
productId: b,
|
|
37
|
+
moduleId: x,
|
|
38
|
+
pageId: S
|
|
33
39
|
} } : void 0,
|
|
34
|
-
skip: !
|
|
40
|
+
skip: !b,
|
|
35
41
|
fetchPolicy: "cache-and-network"
|
|
36
|
-
}),
|
|
37
|
-
return /* @__PURE__ */
|
|
42
|
+
}), T = C?.toursForContext ?? [], E = w;
|
|
43
|
+
return /* @__PURE__ */ u("div", {
|
|
38
44
|
className: "space-y-4 sm:space-y-6 p-3 sm:p-6",
|
|
39
|
-
children: [/* @__PURE__ */
|
|
45
|
+
children: [/* @__PURE__ */ u("div", { children: [/* @__PURE__ */ l("h1", {
|
|
40
46
|
className: "text-xl sm:text-2xl font-bold text-foreground",
|
|
41
|
-
children:
|
|
42
|
-
}), /* @__PURE__ */
|
|
47
|
+
children: a("myTours.title")
|
|
48
|
+
}), /* @__PURE__ */ l("p", {
|
|
43
49
|
className: "text-sm sm:text-base text-muted-foreground",
|
|
44
|
-
children:
|
|
45
|
-
})] }),
|
|
50
|
+
children: a("myTours.subtitle")
|
|
51
|
+
})] }), E && T.length === 0 ? /* @__PURE__ */ l("output", {
|
|
46
52
|
className: "text-sm text-muted-foreground",
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
children: i("myTours.loading")
|
|
50
|
-
}) : C.length === 0 ? /* @__PURE__ */ c("div", {
|
|
53
|
+
children: a("myTours.loading")
|
|
54
|
+
}) : T.length === 0 ? /* @__PURE__ */ l("div", {
|
|
51
55
|
className: "rounded-lg border border-dashed border-border p-8 text-center",
|
|
52
|
-
children: /* @__PURE__ */
|
|
53
|
-
className: "text-sm text-muted-foreground",
|
|
54
|
-
|
|
55
|
-
"aria-live": "polite",
|
|
56
|
-
children: i("myTours.empty")
|
|
56
|
+
children: /* @__PURE__ */ l("output", {
|
|
57
|
+
className: "block text-sm text-muted-foreground",
|
|
58
|
+
children: a("myTours.empty")
|
|
57
59
|
})
|
|
58
|
-
}) : /* @__PURE__ */
|
|
60
|
+
}) : /* @__PURE__ */ l("ul", {
|
|
59
61
|
className: "grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
|
|
60
|
-
children:
|
|
62
|
+
children: T.map((e) => /* @__PURE__ */ u("li", {
|
|
61
63
|
className: "rounded-lg border border-border bg-card p-4 flex flex-col gap-2",
|
|
62
|
-
children: [/* @__PURE__ */
|
|
64
|
+
children: [/* @__PURE__ */ u("div", {
|
|
63
65
|
className: "flex items-start justify-between gap-2",
|
|
64
|
-
children: [/* @__PURE__ */
|
|
66
|
+
children: [/* @__PURE__ */ l("h2", {
|
|
65
67
|
className: "font-semibold text-foreground line-clamp-2",
|
|
66
68
|
children: e.name
|
|
67
|
-
}), /* @__PURE__ */
|
|
69
|
+
}), /* @__PURE__ */ l(v, {
|
|
68
70
|
status: e.myProgress?.status,
|
|
69
|
-
tr:
|
|
71
|
+
tr: a
|
|
70
72
|
})]
|
|
71
|
-
}), e.description ? /* @__PURE__ */
|
|
73
|
+
}), e.description ? /* @__PURE__ */ l("p", {
|
|
72
74
|
className: "text-sm text-muted-foreground line-clamp-3",
|
|
73
75
|
children: e.description
|
|
74
76
|
}) : null]
|
|
@@ -76,28 +78,28 @@ function g() {
|
|
|
76
78
|
})]
|
|
77
79
|
});
|
|
78
80
|
}
|
|
79
|
-
function
|
|
81
|
+
function v({ status: e, tr: t }) {
|
|
80
82
|
switch (e) {
|
|
81
|
-
case "COMPLETED": return /* @__PURE__ */
|
|
83
|
+
case "COMPLETED": return /* @__PURE__ */ u("span", {
|
|
82
84
|
className: "inline-flex items-center gap-1 text-xs text-success",
|
|
83
|
-
children: [/* @__PURE__ */
|
|
85
|
+
children: [/* @__PURE__ */ l(a, { size: 14 }), t("myTours.status.completed")]
|
|
84
86
|
});
|
|
85
|
-
case "IN_PROGRESS": return /* @__PURE__ */
|
|
87
|
+
case "IN_PROGRESS": return /* @__PURE__ */ u("span", {
|
|
86
88
|
className: "inline-flex items-center gap-1 text-xs text-warning",
|
|
87
|
-
children: [/* @__PURE__ */ c
|
|
89
|
+
children: [/* @__PURE__ */ l(c, { size: 14 }), t("myTours.status.inProgress")]
|
|
88
90
|
});
|
|
89
91
|
case "SKIPPED":
|
|
90
|
-
case "DISMISSED": return /* @__PURE__ */
|
|
92
|
+
case "DISMISSED": return /* @__PURE__ */ u("span", {
|
|
91
93
|
className: "inline-flex items-center gap-1 text-xs text-muted-foreground",
|
|
92
|
-
children: [/* @__PURE__ */
|
|
94
|
+
children: [/* @__PURE__ */ l(s, { size: 14 }), t("myTours.status.skipped")]
|
|
93
95
|
});
|
|
94
|
-
default: return /* @__PURE__ */
|
|
96
|
+
default: return /* @__PURE__ */ u("span", {
|
|
95
97
|
className: "inline-flex items-center gap-1 text-xs text-muted-foreground",
|
|
96
|
-
children: [/* @__PURE__ */
|
|
98
|
+
children: [/* @__PURE__ */ l(o, { size: 14 }), t("myTours.status.notStarted")]
|
|
97
99
|
});
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
102
|
//#endregion
|
|
101
|
-
export {
|
|
103
|
+
export { _ as MyToursPage };
|
|
102
104
|
|
|
103
105
|
//# sourceMappingURL=MyToursPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MyToursPage.js","names":[],"sources":["../../../src/tours/pages/MyToursPage.tsx"],"sourcesContent":["import { useEffect } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport { CheckCircle2, Circle, MinusCircle, Play } from 'lucide-react';\nimport {\n useGetToursForContextQuery,\n type GetToursForContextQuery,\n} from '../../generated/global-operations';\nimport { useToursContext } from '../providers/ToursProvider';\nimport { useToursI18n, type ToursTranslationKey } from '../i18n';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\n\ntype TourSummary = GetToursForContextQuery['toursForContext'][number];\n\n// Plain slug validator for productId and moduleId.\nconst SLUG_PATTERN = /^[a-zA-Z0-9_-]+$/;\n\n// pageId is documented with path-style values like \"settings/team\" per\n// ToursRootProps — tighten to URL-safe characters only, but permit `/`.\nconst PAGE_PATH_PATTERN = /^[a-zA-Z0-9_\\-/]+$/;\n\nfunction safeSlug<T extends string | undefined>(\n raw: string | null | undefined,\n fallback: T\n): string | T {\n if (raw && SLUG_PATTERN.test(raw)) return raw;\n return fallback;\n}\n\nfunction safePageId<T extends string | undefined>(\n raw: string | null | undefined,\n fallback: T\n): string | T {\n if (raw && PAGE_PATH_PATTERN.test(raw)) return raw;\n return fallback;\n}\n\n/**\n * MyToursPage — user-facing browse for available tours.\n *\n * Lists tours returned by `toursForContext` (filtered to the current\n * product/module/page surface) and decorates each with the requesting\n * user's `TourUserProgress` status (Completed / In progress / Skipped /\n * Not started). Read-only — tours are actually played via the SDK overlay,\n * not via mutations on this page.\n */\nexport function MyToursPage() {\n const { tr } = useToursI18n();\n const {\n productId: shellProductId,\n moduleId: shellModuleId,\n pageId: shellPageId,\n workspaceId,\n } = useToursContext();\n const [searchParams] = useSearchParams();\n const eventBus = useEventBus();\n\n // Emit page view event
|
|
1
|
+
{"version":3,"file":"MyToursPage.js","names":[],"sources":["../../../src/tours/pages/MyToursPage.tsx"],"sourcesContent":["import { useEffect, useRef } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport { CheckCircle2, Circle, MinusCircle, Play } from 'lucide-react';\nimport {\n useGetToursForContextQuery,\n type GetToursForContextQuery,\n} from '../../generated/global-operations';\nimport { useToursContext } from '../providers/ToursProvider';\nimport { useToursI18n, type ToursTranslationKey } from '../i18n';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\n\ntype TourSummary = GetToursForContextQuery['toursForContext'][number];\n\n// Plain slug validator for productId and moduleId.\nconst SLUG_PATTERN = /^[a-zA-Z0-9_-]+$/;\n\n// pageId is documented with path-style values like \"settings/team\" per\n// ToursRootProps — tighten to URL-safe characters only, but permit `/`.\nconst PAGE_PATH_PATTERN = /^[a-zA-Z0-9_\\-/]+$/;\n\nfunction safeSlug<T extends string | undefined>(\n raw: string | null | undefined,\n fallback: T\n): string | T {\n if (raw && SLUG_PATTERN.test(raw)) return raw;\n return fallback;\n}\n\nfunction safePageId<T extends string | undefined>(\n raw: string | null | undefined,\n fallback: T\n): string | T {\n if (raw && PAGE_PATH_PATTERN.test(raw)) return raw;\n return fallback;\n}\n\n/**\n * MyToursPage — user-facing browse for available tours.\n *\n * Lists tours returned by `toursForContext` (filtered to the current\n * product/module/page surface) and decorates each with the requesting\n * user's `TourUserProgress` status (Completed / In progress / Skipped /\n * Not started). Read-only — tours are actually played via the SDK overlay,\n * not via mutations on this page.\n */\nexport function MyToursPage() {\n const { tr } = useToursI18n();\n const {\n productId: shellProductId,\n moduleId: shellModuleId,\n pageId: shellPageId,\n workspaceId,\n } = useToursContext();\n const [searchParams] = useSearchParams();\n const eventBus = useEventBus();\n\n // Emit page view event exactly once per mount. A ref guard keeps the effect\n // semantically mount-only while satisfying exhaustive-deps with the real\n // dependency list, so we don't have to disable the lint rule.\n const pageViewedRef = useRef(false);\n useEffect(() => {\n if (pageViewedRef.current) return;\n pageViewedRef.current = true;\n eventBus.emit('tours.page.viewed', {\n page: '/tours',\n productId: shellProductId,\n moduleId: shellModuleId,\n pageId: shellPageId,\n workspaceId,\n });\n }, [eventBus, shellProductId, shellModuleId, shellPageId, workspaceId]);\n\n // Preferred source is the shell's productId prop (the host app knows\n // which product it is — \"workspaces\", \"vibecontrols\", \"bigconsole\", etc.).\n // Fall back to a query-param override for deep-link scenarios, but only\n // after passing a plain-slug validator so we never forward arbitrary\n // strings into the GraphQL variables. No hardcoded product default —\n // if neither source provides one, skip the query entirely so non-\n // workspaces integrations don't get workspaces tours.\n const productId =\n safeSlug(shellProductId, undefined) ?? safeSlug(searchParams.get('productId'), undefined);\n const moduleId =\n safeSlug(shellModuleId, undefined) ?? safeSlug(searchParams.get('moduleId'), undefined);\n const pageId =\n safePageId(shellPageId, undefined) ?? safePageId(searchParams.get('pageId'), undefined);\n\n const { data: toursData, loading: toursLoading } = useGetToursForContextQuery({\n variables: productId ? { context: { productId, moduleId, pageId } } : undefined,\n skip: !productId,\n fetchPolicy: 'cache-and-network',\n });\n\n const tours = toursData?.toursForContext ?? [];\n const isLoading = toursLoading;\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold text-foreground\">{tr('myTours.title')}</h1>\n <p className=\"text-sm sm:text-base text-muted-foreground\">{tr('myTours.subtitle')}</p>\n </div>\n\n {isLoading && tours.length === 0 ? (\n <output className=\"text-sm text-muted-foreground\">{tr('myTours.loading')}</output>\n ) : tours.length === 0 ? (\n <div className=\"rounded-lg border border-dashed border-border p-8 text-center\">\n <output className=\"block text-sm text-muted-foreground\">{tr('myTours.empty')}</output>\n </div>\n ) : (\n <ul className=\"grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\">\n {tours.map((tour: TourSummary) => (\n <li\n key={tour.id}\n className=\"rounded-lg border border-border bg-card p-4 flex flex-col gap-2\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <h2 className=\"font-semibold text-foreground line-clamp-2\">{tour.name}</h2>\n <ProgressBadge status={tour.myProgress?.status} tr={tr} />\n </div>\n {tour.description ? (\n <p className=\"text-sm text-muted-foreground line-clamp-3\">{tour.description}</p>\n ) : null}\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nfunction ProgressBadge({\n status,\n tr,\n}: {\n status: string | undefined;\n tr: (key: ToursTranslationKey) => string;\n}) {\n switch (status) {\n case 'COMPLETED':\n return (\n <span className=\"inline-flex items-center gap-1 text-xs text-success\">\n <CheckCircle2 size={14} />\n {tr('myTours.status.completed')}\n </span>\n );\n case 'IN_PROGRESS':\n return (\n <span className=\"inline-flex items-center gap-1 text-xs text-warning\">\n <Play size={14} />\n {tr('myTours.status.inProgress')}\n </span>\n );\n case 'SKIPPED':\n case 'DISMISSED':\n return (\n <span className=\"inline-flex items-center gap-1 text-xs text-muted-foreground\">\n <MinusCircle size={14} />\n {tr('myTours.status.skipped')}\n </span>\n );\n default:\n return (\n <span className=\"inline-flex items-center gap-1 text-xs text-muted-foreground\">\n <Circle size={14} />\n {tr('myTours.status.notStarted')}\n </span>\n );\n }\n}\n"],"mappings":"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"}
|