@burdenoff/microfe-tours 2026.625.1 → 2026.626.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/i18n.js +19 -1
- package/dist/tours/i18n.js.map +1 -1
- package/dist/tours/pages/AnalyticsDashboardPage.js +43 -41
- package/dist/tours/pages/AnalyticsDashboardPage.js.map +1 -1
- package/dist/tours/pages/MediaLibraryPage.js +66 -59
- package/dist/tours/pages/MediaLibraryPage.js.map +1 -1
- package/dist/tours/pages/MyToursPage.js +76 -57
- package/dist/tours/pages/MyToursPage.js.map +1 -1
- package/dist/tours/pages/MyToursProgressPage.js +81 -74
- package/dist/tours/pages/MyToursProgressPage.js.map +1 -1
- package/dist/tours/pages/SchedulerPage.js +78 -78
- package/dist/tours/pages/SchedulerPage.js.map +1 -1
- package/dist/tours/pages/StepEditorPage.js +570 -575
- package/dist/tours/pages/StepEditorPage.js.map +1 -1
- package/dist/tours/pages/SystemAnalyticsPage.js +128 -118
- package/dist/tours/pages/SystemAnalyticsPage.js.map +1 -1
- package/dist/tours/pages/TemplateLibraryPage.js +54 -65
- package/dist/tours/pages/TemplateLibraryPage.js.map +1 -1
- package/dist/tours/pages/TourBuilderPage.js +307 -272
- package/dist/tours/pages/TourBuilderPage.js.map +1 -1
- package/dist/tours/pages/TourPreviewPage.js +114 -122
- package/dist/tours/pages/TourPreviewPage.js.map +1 -1
- package/dist/tours/pages/ToursListPage.js +130 -122
- package/dist/tours/pages/ToursListPage.js.map +1 -1
- package/package.json +2 -2
|
@@ -8,33 +8,33 @@ import { useCallback as o, useEffect as s, useState as c } from "react";
|
|
|
8
8
|
import { PermissionGate as l } from "@burdenoff/fe-libs/shared/components";
|
|
9
9
|
import { FileText as u, Image as d, Music as f, Search as p, Trash2 as m, Upload as h, Video as g } from "lucide-react";
|
|
10
10
|
import { jsx as _, jsxs as v } from "react/jsx-runtime";
|
|
11
|
-
import { GlassCard as y } from "@burdenoff/fe-libs/ui";
|
|
12
|
-
import { useI18n as
|
|
13
|
-
import { useEventBus as
|
|
11
|
+
import { GlassCard as y, IllustratedEmptyState as b, PagePurpose as x } from "@burdenoff/fe-libs/ui";
|
|
12
|
+
import { useI18n as S } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
13
|
+
import { useEventBus as C } from "@burdenoff/fe-libs/shared/events";
|
|
14
14
|
//#region src/tours/pages/MediaLibraryPage.tsx
|
|
15
|
-
function
|
|
16
|
-
let { t:
|
|
17
|
-
let n =
|
|
15
|
+
function w() {
|
|
16
|
+
let { t: w } = S(), T = o((e, t) => {
|
|
17
|
+
let n = w(e);
|
|
18
18
|
return n === e ? t : n;
|
|
19
|
-
}, [
|
|
19
|
+
}, [w]), E = e(), D = C(), { hasPermission: O, isLoading: k } = t(), A = O(n.TOUR_MEDIA_LIST), j = O(n.TOUR_MEDIA_DELETE), { media: M, fetchMedia: N, uploadMedia: P, deleteMedia: F, uploadProgress: I } = a(), [L, R] = c(""), [z, B] = c("all");
|
|
20
20
|
s(() => {
|
|
21
|
-
|
|
21
|
+
k || !A || N(z === "all" ? void 0 : z, L);
|
|
22
22
|
}, [
|
|
23
|
-
|
|
23
|
+
N,
|
|
24
|
+
z,
|
|
24
25
|
L,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
O
|
|
26
|
+
k,
|
|
27
|
+
A
|
|
28
28
|
]);
|
|
29
|
-
let
|
|
29
|
+
let V = async (e) => {
|
|
30
30
|
e && (await Promise.all(Array.from(e).map(async (e) => {
|
|
31
31
|
try {
|
|
32
|
-
await
|
|
32
|
+
await P(e);
|
|
33
33
|
} catch {
|
|
34
|
-
r(
|
|
34
|
+
r(D, "media.upload", E);
|
|
35
35
|
}
|
|
36
|
-
})),
|
|
37
|
-
},
|
|
36
|
+
})), N(z === "all" ? void 0 : z, L));
|
|
37
|
+
}, H = (e) => {
|
|
38
38
|
switch (e) {
|
|
39
39
|
case "IMAGE": return /* @__PURE__ */ _(d, { size: 20 });
|
|
40
40
|
case "VIDEO": return /* @__PURE__ */ _(g, { size: 20 });
|
|
@@ -49,29 +49,30 @@ function S() {
|
|
|
49
49
|
className: "flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3",
|
|
50
50
|
children: [/* @__PURE__ */ v("div", { children: [/* @__PURE__ */ _("h1", {
|
|
51
51
|
className: "text-xl sm:text-2xl font-bold",
|
|
52
|
-
children:
|
|
52
|
+
children: T("mediaLibrary.title", "Media Library")
|
|
53
53
|
}), /* @__PURE__ */ _("p", {
|
|
54
54
|
className: "text-sm text-text-secondary",
|
|
55
|
-
children:
|
|
55
|
+
children: T("mediaLibrary.subtitle", "Upload and manage media for tour steps")
|
|
56
56
|
})] }), /* @__PURE__ */ _(l, {
|
|
57
57
|
permission: n.TOUR_MEDIA_CREATE,
|
|
58
58
|
children: /* @__PURE__ */ v("label", {
|
|
59
59
|
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",
|
|
60
60
|
children: [
|
|
61
61
|
/* @__PURE__ */ _(h, { size: 20 }),
|
|
62
|
-
|
|
62
|
+
T("mediaLibrary.uploadMedia", "Upload Media"),
|
|
63
63
|
/* @__PURE__ */ _("input", {
|
|
64
64
|
type: "file",
|
|
65
65
|
multiple: !0,
|
|
66
66
|
accept: "image/*,video/*,audio/*,.pdf,.doc,.docx",
|
|
67
|
-
onChange: (e) =>
|
|
67
|
+
onChange: (e) => V(e.target.files),
|
|
68
68
|
className: "hidden",
|
|
69
|
-
"aria-label":
|
|
69
|
+
"aria-label": T("mediaLibrary.uploadMedia", "Upload Media")
|
|
70
70
|
})
|
|
71
71
|
]
|
|
72
72
|
})
|
|
73
73
|
})]
|
|
74
74
|
}),
|
|
75
|
+
/* @__PURE__ */ _(x, { children: T("mediaLibrary.purpose", "Store and organise the images, videos, audio, and documents you attach to tour steps. Upload an asset here once, then reference it from any step — handy for product screenshots, short demo clips, or callout graphics that make a walkthrough clearer.") }),
|
|
75
76
|
/* @__PURE__ */ v("div", {
|
|
76
77
|
className: "flex flex-col sm:flex-row gap-3",
|
|
77
78
|
children: [/* @__PURE__ */ v("div", {
|
|
@@ -81,50 +82,50 @@ function S() {
|
|
|
81
82
|
size: 20
|
|
82
83
|
}), /* @__PURE__ */ _("input", {
|
|
83
84
|
type: "text",
|
|
84
|
-
placeholder:
|
|
85
|
-
value:
|
|
86
|
-
onChange: (e) =>
|
|
85
|
+
placeholder: T("mediaLibrary.searchPlaceholder", "Search media..."),
|
|
86
|
+
value: L,
|
|
87
|
+
onChange: (e) => R(e.target.value),
|
|
87
88
|
className: "w-full pl-[3.25rem] pr-4 py-2 min-h-[44px] border border-border-subtle rounded-md bg-bg-surface",
|
|
88
|
-
"aria-label":
|
|
89
|
+
"aria-label": T("mediaLibrary.searchPlaceholder", "Search media...")
|
|
89
90
|
})]
|
|
90
91
|
}), /* @__PURE__ */ v("select", {
|
|
91
|
-
value:
|
|
92
|
-
onChange: (e) =>
|
|
92
|
+
value: z,
|
|
93
|
+
onChange: (e) => B(e.target.value),
|
|
93
94
|
className: "px-4 py-2 min-h-[44px] border border-border-subtle rounded-md bg-bg-surface",
|
|
94
|
-
"aria-label":
|
|
95
|
+
"aria-label": T("mediaLibrary.filterByType", "Filter by media type"),
|
|
95
96
|
children: [
|
|
96
97
|
/* @__PURE__ */ _("option", {
|
|
97
98
|
value: "all",
|
|
98
|
-
children:
|
|
99
|
+
children: T("mediaLibrary.allTypes", "All Types")
|
|
99
100
|
}),
|
|
100
101
|
/* @__PURE__ */ _("option", {
|
|
101
102
|
value: "IMAGE",
|
|
102
|
-
children:
|
|
103
|
+
children: T("mediaLibrary.typeImages", "Images")
|
|
103
104
|
}),
|
|
104
105
|
/* @__PURE__ */ _("option", {
|
|
105
106
|
value: "VIDEO",
|
|
106
|
-
children:
|
|
107
|
+
children: T("mediaLibrary.typeVideos", "Videos")
|
|
107
108
|
}),
|
|
108
109
|
/* @__PURE__ */ _("option", {
|
|
109
110
|
value: "AUDIO",
|
|
110
|
-
children:
|
|
111
|
+
children: T("mediaLibrary.typeAudio", "Audio")
|
|
111
112
|
}),
|
|
112
113
|
/* @__PURE__ */ _("option", {
|
|
113
114
|
value: "DOCUMENT",
|
|
114
|
-
children:
|
|
115
|
+
children: T("mediaLibrary.typeDocuments", "Documents")
|
|
115
116
|
})
|
|
116
117
|
]
|
|
117
118
|
})]
|
|
118
119
|
}),
|
|
119
|
-
Object.keys(
|
|
120
|
+
Object.keys(I).length > 0 && /* @__PURE__ */ _("div", {
|
|
120
121
|
className: "space-y-2",
|
|
121
|
-
children: Object.entries(
|
|
122
|
+
children: Object.entries(I).map(([e, t]) => /* @__PURE__ */ v("div", {
|
|
122
123
|
className: "border border-border-subtle rounded-md p-3",
|
|
123
124
|
children: [/* @__PURE__ */ v("div", {
|
|
124
125
|
className: "flex items-center justify-between mb-2",
|
|
125
126
|
children: [/* @__PURE__ */ _("span", {
|
|
126
127
|
className: "text-sm",
|
|
127
|
-
children:
|
|
128
|
+
children: T("mediaLibrary.uploading", "Uploading...")
|
|
128
129
|
}), /* @__PURE__ */ v("span", {
|
|
129
130
|
className: "text-sm text-text-secondary",
|
|
130
131
|
children: [t, "%"]
|
|
@@ -138,9 +139,9 @@ function S() {
|
|
|
138
139
|
})]
|
|
139
140
|
}, e))
|
|
140
141
|
}),
|
|
141
|
-
|
|
142
|
+
M.length > 0 ? /* @__PURE__ */ _("div", {
|
|
142
143
|
className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4",
|
|
143
|
-
children:
|
|
144
|
+
children: M.map((e) => /* @__PURE__ */ v(y, {
|
|
144
145
|
treatment: "plain",
|
|
145
146
|
className: "overflow-hidden p-0 border-border-seam shadow-[var(--shadow-pop)]",
|
|
146
147
|
children: [/* @__PURE__ */ _("div", {
|
|
@@ -151,7 +152,7 @@ function S() {
|
|
|
151
152
|
className: "w-full h-full object-cover"
|
|
152
153
|
}) : /* @__PURE__ */ _("div", {
|
|
153
154
|
className: "text-text-secondary",
|
|
154
|
-
children:
|
|
155
|
+
children: H(e.type)
|
|
155
156
|
})
|
|
156
157
|
}), /* @__PURE__ */ v("div", {
|
|
157
158
|
className: "p-3",
|
|
@@ -164,38 +165,44 @@ function S() {
|
|
|
164
165
|
className: "flex items-center justify-between mt-2 text-xs text-text-secondary",
|
|
165
166
|
children: [/* @__PURE__ */ _("span", { children: e.type }), /* @__PURE__ */ _("span", { children: i(e.size) })]
|
|
166
167
|
}),
|
|
167
|
-
|
|
168
|
+
j && /* @__PURE__ */ v("button", {
|
|
168
169
|
onClick: () => {
|
|
169
|
-
window.confirm(
|
|
170
|
+
window.confirm(T("mediaLibrary.deleteConfirm", "Delete this media?")) && F(e.id);
|
|
170
171
|
},
|
|
171
172
|
type: "button",
|
|
172
173
|
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",
|
|
173
|
-
children: [/* @__PURE__ */ _(m, { size: 12 }),
|
|
174
|
+
children: [/* @__PURE__ */ _(m, { size: 12 }), T("mediaLibrary.delete", "Delete")]
|
|
174
175
|
})
|
|
175
176
|
]
|
|
176
177
|
})]
|
|
177
178
|
}, e.id))
|
|
178
|
-
}) : /* @__PURE__ */
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
179
|
+
}) : /* @__PURE__ */ _(b, {
|
|
180
|
+
illustration: "empty-files",
|
|
181
|
+
title: T("mediaLibrary.noMedia", "No media files"),
|
|
182
|
+
description: T("mediaLibrary.uploadFirstMedia", "Upload your first media file to get started"),
|
|
183
|
+
action: /* @__PURE__ */ _(l, {
|
|
184
|
+
permission: n.TOUR_MEDIA_CREATE,
|
|
185
|
+
children: /* @__PURE__ */ v("label", {
|
|
186
|
+
className: "px-4 py-2 min-h-[44px] bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 cursor-pointer inline-flex items-center justify-center gap-2",
|
|
187
|
+
children: [
|
|
188
|
+
/* @__PURE__ */ _(h, { size: 20 }),
|
|
189
|
+
T("mediaLibrary.uploadMedia", "Upload Media"),
|
|
190
|
+
/* @__PURE__ */ _("input", {
|
|
191
|
+
type: "file",
|
|
192
|
+
multiple: !0,
|
|
193
|
+
accept: "image/*,video/*,audio/*,.pdf,.doc,.docx",
|
|
194
|
+
onChange: (e) => V(e.target.files),
|
|
195
|
+
className: "hidden",
|
|
196
|
+
"aria-label": T("mediaLibrary.uploadMedia", "Upload Media")
|
|
197
|
+
})
|
|
198
|
+
]
|
|
192
199
|
})
|
|
193
|
-
|
|
200
|
+
})
|
|
194
201
|
})
|
|
195
202
|
]
|
|
196
203
|
});
|
|
197
204
|
}
|
|
198
205
|
//#endregion
|
|
199
|
-
export {
|
|
206
|
+
export { w as MediaLibraryPage };
|
|
200
207
|
|
|
201
208
|
//# sourceMappingURL=MediaLibraryPage.js.map
|
|
@@ -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 { GlassCard } from '@burdenoff/fe-libs/ui';\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 <GlassCard\n key={item.id}\n treatment=\"plain\"\n className=\"overflow-hidden p-0 border-border-seam shadow-[var(--shadow-pop)]\"\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 </GlassCard>\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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"}
|
|
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 { GlassCard, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\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 <PagePurpose>\n {tr(\n 'mediaLibrary.purpose',\n 'Store and organise the images, videos, audio, and documents you attach to tour steps. Upload an asset here once, then reference it from any step — handy for product screenshots, short demo clips, or callout graphics that make a walkthrough clearer.'\n )}\n </PagePurpose>\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 <GlassCard\n key={item.id}\n treatment=\"plain\"\n className=\"overflow-hidden p-0 border-border-seam shadow-[var(--shadow-pop)]\"\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 </GlassCard>\n ))}\n </div>\n ) : (\n <IllustratedEmptyState\n illustration=\"empty-files\"\n title={tr('mediaLibrary.noMedia', 'No media files')}\n description={tr(\n 'mediaLibrary.uploadFirstMedia',\n 'Upload your first media file to get started'\n )}\n action={\n <PermissionGate permission={TOUR_PERMISSIONS.TOUR_MEDIA_CREATE}>\n <label className=\"px-4 py-2 min-h-[44px] bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 cursor-pointer inline-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 }\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -5,89 +5,108 @@ import { useEffect as r, useRef as i } from "react";
|
|
|
5
5
|
import { CheckCircle2 as a, Circle as o, MinusCircle as s, Play as c } from "lucide-react";
|
|
6
6
|
import { jsx as l, jsxs as u } from "react/jsx-runtime";
|
|
7
7
|
import { useSearchParams as d } from "react-router-dom";
|
|
8
|
-
import {
|
|
9
|
-
import { useEventBus as
|
|
8
|
+
import { EmphasisPanel as f, GlassCard as p, GradientText as m, IllustratedEmptyState as h, PagePurpose as g } from "@burdenoff/fe-libs/ui";
|
|
9
|
+
import { useEventBus as _ } from "@burdenoff/fe-libs/shared/events";
|
|
10
10
|
//#region src/tours/pages/MyToursPage.tsx
|
|
11
|
-
var
|
|
12
|
-
function
|
|
13
|
-
return e &&
|
|
11
|
+
var v = /^[a-zA-Z0-9_-]+$/, y = /^[a-zA-Z0-9_\-/]+$/;
|
|
12
|
+
function b(e, t) {
|
|
13
|
+
return e && v.test(e) ? e : t;
|
|
14
14
|
}
|
|
15
|
-
function
|
|
16
|
-
return e &&
|
|
15
|
+
function x(e, t) {
|
|
16
|
+
return e && y.test(e) ? e : t;
|
|
17
17
|
}
|
|
18
|
-
function
|
|
19
|
-
let { tr: a } = n(), { productId: o, moduleId: s, pageId: c, workspaceId:
|
|
18
|
+
function S() {
|
|
19
|
+
let { tr: a } = n(), { productId: o, moduleId: s, pageId: c, workspaceId: v } = e(), [y] = d(), S = _(), w = i(!1);
|
|
20
20
|
r(() => {
|
|
21
|
-
|
|
21
|
+
w.current || (w.current = !0, S.emit("tours.page.viewed", {
|
|
22
22
|
page: "/tours",
|
|
23
23
|
productId: o,
|
|
24
24
|
moduleId: s,
|
|
25
25
|
pageId: c,
|
|
26
|
-
workspaceId:
|
|
26
|
+
workspaceId: v
|
|
27
27
|
}));
|
|
28
28
|
}, [
|
|
29
|
-
|
|
29
|
+
S,
|
|
30
30
|
o,
|
|
31
31
|
s,
|
|
32
32
|
c,
|
|
33
|
-
|
|
33
|
+
v
|
|
34
34
|
]);
|
|
35
|
-
let
|
|
36
|
-
variables:
|
|
37
|
-
productId:
|
|
38
|
-
moduleId:
|
|
39
|
-
pageId:
|
|
35
|
+
let T = b(o, void 0) ?? b(y.get("productId"), void 0), E = b(s, void 0) ?? b(y.get("moduleId"), void 0), D = x(c, void 0) ?? x(y.get("pageId"), void 0), { data: O, loading: k } = t({
|
|
36
|
+
variables: T ? { context: {
|
|
37
|
+
productId: T,
|
|
38
|
+
moduleId: E,
|
|
39
|
+
pageId: D
|
|
40
40
|
} } : void 0,
|
|
41
|
-
skip: !
|
|
41
|
+
skip: !T,
|
|
42
42
|
fetchPolicy: "cache-and-network"
|
|
43
|
-
}),
|
|
43
|
+
}), A = O?.toursForContext ?? [], j = k, M = A.find((e) => e.myProgress?.status === "IN_PROGRESS") ?? A.find((e) => !e.myProgress?.status || e.myProgress.status !== "COMPLETED" && e.myProgress.status !== "SKIPPED" && e.myProgress.status !== "DISMISSED"), N = A.filter((e) => e.id !== M?.id);
|
|
44
44
|
return /* @__PURE__ */ u("div", {
|
|
45
45
|
className: "space-y-4 sm:space-y-6 p-3 sm:p-6",
|
|
46
|
-
children: [
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
children: a("myTours.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
46
|
+
children: [
|
|
47
|
+
/* @__PURE__ */ u("div", { children: [/* @__PURE__ */ l("h1", {
|
|
48
|
+
className: "text-xl sm:text-2xl font-bold text-text-primary",
|
|
49
|
+
children: /* @__PURE__ */ l(m, { children: a("myTours.title") })
|
|
50
|
+
}), /* @__PURE__ */ l("p", {
|
|
51
|
+
className: "text-sm sm:text-base text-text-secondary",
|
|
52
|
+
children: a("myTours.subtitle")
|
|
53
|
+
})] }),
|
|
54
|
+
/* @__PURE__ */ l(g, { children: a("myTours.purpose") }),
|
|
55
|
+
j && A.length === 0 ? /* @__PURE__ */ l("output", {
|
|
56
|
+
className: "text-sm text-text-secondary",
|
|
57
|
+
children: a("myTours.loading")
|
|
58
|
+
}) : A.length === 0 ? /* @__PURE__ */ l(h, {
|
|
59
|
+
illustration: "empty-generic",
|
|
60
|
+
title: a("myTours.empty")
|
|
61
|
+
}) : /* @__PURE__ */ u("div", {
|
|
62
|
+
className: "space-y-4 sm:space-y-6",
|
|
63
|
+
children: [M ? /* @__PURE__ */ u(f, {
|
|
64
|
+
className: "p-4 sm:p-5",
|
|
65
|
+
children: [/* @__PURE__ */ u("div", {
|
|
66
|
+
className: "flex items-start justify-between gap-3",
|
|
67
|
+
children: [/* @__PURE__ */ l("h2", {
|
|
68
|
+
className: "text-lg font-semibold text-text-primary line-clamp-2",
|
|
69
|
+
children: M.name
|
|
70
|
+
}), /* @__PURE__ */ l(C, {
|
|
71
|
+
status: M.myProgress?.status,
|
|
72
|
+
tr: a
|
|
73
|
+
})]
|
|
74
|
+
}), M.description ? /* @__PURE__ */ l("p", {
|
|
75
|
+
className: "mt-2 text-sm text-text-secondary line-clamp-3",
|
|
76
|
+
children: M.description
|
|
77
|
+
}) : null]
|
|
78
|
+
}) : null, N.length > 0 ? /* @__PURE__ */ l("ul", {
|
|
79
|
+
className: "grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
|
|
80
|
+
children: N.map((e) => /* @__PURE__ */ l("li", { children: /* @__PURE__ */ u(p, {
|
|
81
|
+
treatment: "plain",
|
|
82
|
+
className: "h-full p-4 flex flex-col gap-2 border-border-seam shadow-[var(--shadow-pop)]",
|
|
83
|
+
children: [/* @__PURE__ */ u("div", {
|
|
84
|
+
className: "flex items-start justify-between gap-2",
|
|
85
|
+
children: [/* @__PURE__ */ l("h2", {
|
|
86
|
+
className: "font-semibold text-text-primary line-clamp-2",
|
|
87
|
+
children: e.name
|
|
88
|
+
}), /* @__PURE__ */ l(C, {
|
|
89
|
+
status: e.myProgress?.status,
|
|
90
|
+
tr: a
|
|
91
|
+
})]
|
|
92
|
+
}), e.description ? /* @__PURE__ */ l("p", {
|
|
93
|
+
className: "text-sm text-text-secondary line-clamp-3",
|
|
94
|
+
children: e.description
|
|
95
|
+
}) : null]
|
|
96
|
+
}) }, e.id))
|
|
78
97
|
}) : null]
|
|
79
|
-
})
|
|
80
|
-
|
|
98
|
+
})
|
|
99
|
+
]
|
|
81
100
|
});
|
|
82
101
|
}
|
|
83
|
-
function
|
|
102
|
+
function C({ status: e, tr: t }) {
|
|
84
103
|
switch (e) {
|
|
85
104
|
case "COMPLETED": return /* @__PURE__ */ u("span", {
|
|
86
105
|
className: "inline-flex items-center gap-1 text-xs text-success",
|
|
87
106
|
children: [/* @__PURE__ */ l(a, { size: 14 }), t("myTours.status.completed")]
|
|
88
107
|
});
|
|
89
108
|
case "IN_PROGRESS": return /* @__PURE__ */ u("span", {
|
|
90
|
-
className: "inline-flex items-center gap-1 text-xs text-warning",
|
|
109
|
+
className: "inline-flex items-center gap-1 text-xs text-warning motion-safe:animate-vc-glow-pulse",
|
|
91
110
|
children: [/* @__PURE__ */ l(c, { size: 14 }), t("myTours.status.inProgress")]
|
|
92
111
|
});
|
|
93
112
|
case "SKIPPED":
|
|
@@ -102,6 +121,6 @@ function y({ status: e, tr: t }) {
|
|
|
102
121
|
}
|
|
103
122
|
}
|
|
104
123
|
//#endregion
|
|
105
|
-
export {
|
|
124
|
+
export { S as MyToursPage };
|
|
106
125
|
|
|
107
126
|
//# sourceMappingURL=MyToursPage.js.map
|
|
@@ -1 +1 @@
|
|
|
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';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\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-text-primary\">{tr('myTours.title')}</h1>\n <p className=\"text-sm sm:text-base text-text-secondary\">{tr('myTours.subtitle')}</p>\n </div>\n\n {isLoading && tours.length === 0 ? (\n <output className=\"text-sm text-text-secondary\">{tr('myTours.loading')}</output>\n ) : tours.length === 0 ? (\n <div className=\"rounded-lg border border-dashed border-border-subtle p-8 text-center\">\n <output className=\"block text-sm text-text-secondary\">{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 key={tour.id}>\n <GlassCard\n treatment=\"plain\"\n className=\"h-full p-4 flex flex-col gap-2 border-border-seam shadow-[var(--shadow-pop)]\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <h2 className=\"font-semibold text-text-primary 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-text-secondary line-clamp-3\">{tour.description}</p>\n ) : null}\n </GlassCard>\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-text-secondary\">\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-text-secondary\">\n <Circle size={14} />\n {tr('myTours.status.notStarted')}\n </span>\n );\n }\n}\n"],"mappings":"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"}
|
|
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';\nimport {\n EmphasisPanel,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\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 // Surface a single, clearly-emphasised \"pick up where you left off / start\n // here\" tour: prefer one already in progress, then the first not-yet-started\n // tour, so a new user always has one obvious next walkthrough to take.\n const featuredTour =\n tours.find((tour: TourSummary) => tour.myProgress?.status === 'IN_PROGRESS') ??\n tours.find(\n (tour: TourSummary) =>\n !tour.myProgress?.status ||\n (tour.myProgress.status !== 'COMPLETED' &&\n tour.myProgress.status !== 'SKIPPED' &&\n tour.myProgress.status !== 'DISMISSED')\n );\n const restTours = tours.filter((tour: TourSummary) => tour.id !== featuredTour?.id);\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-text-primary\">\n <GradientText>{tr('myTours.title')}</GradientText>\n </h1>\n <p className=\"text-sm sm:text-base text-text-secondary\">{tr('myTours.subtitle')}</p>\n </div>\n\n <PagePurpose>{tr('myTours.purpose')}</PagePurpose>\n\n {isLoading && tours.length === 0 ? (\n <output className=\"text-sm text-text-secondary\">{tr('myTours.loading')}</output>\n ) : tours.length === 0 ? (\n <IllustratedEmptyState illustration=\"empty-generic\" title={tr('myTours.empty')} />\n ) : (\n <div className=\"space-y-4 sm:space-y-6\">\n {/* Featured \"start here\" tour — the single emphasis zone on this page. */}\n {featuredTour ? (\n <EmphasisPanel className=\"p-4 sm:p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <h2 className=\"text-lg font-semibold text-text-primary line-clamp-2\">\n {featuredTour.name}\n </h2>\n <ProgressBadge status={featuredTour.myProgress?.status} tr={tr} />\n </div>\n {featuredTour.description ? (\n <p className=\"mt-2 text-sm text-text-secondary line-clamp-3\">\n {featuredTour.description}\n </p>\n ) : null}\n </EmphasisPanel>\n ) : null}\n\n {restTours.length > 0 ? (\n <ul className=\"grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\">\n {restTours.map((tour: TourSummary) => (\n <li key={tour.id}>\n <GlassCard\n treatment=\"plain\"\n className=\"h-full p-4 flex flex-col gap-2 border-border-seam shadow-[var(--shadow-pop)]\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <h2 className=\"font-semibold text-text-primary 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-text-secondary line-clamp-3\">{tour.description}</p>\n ) : null}\n </GlassCard>\n </li>\n ))}\n </ul>\n ) : null}\n </div>\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 motion-safe:animate-vc-glow-pulse\">\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-text-secondary\">\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-text-secondary\">\n <Circle size={14} />\n {tr('myTours.status.notStarted')}\n </span>\n );\n }\n}\n"],"mappings":"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"}
|