@burdenoff/microfe-tours 2026.624.1 → 2026.625.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/pages/AnalyticsDashboardPage.js +69 -63
- package/dist/tours/pages/AnalyticsDashboardPage.js.map +1 -1
- package/dist/tours/pages/MediaLibraryPage.js +49 -47
- package/dist/tours/pages/MediaLibraryPage.js.map +1 -1
- package/dist/tours/pages/MyToursPage.js +28 -26
- package/dist/tours/pages/MyToursPage.js.map +1 -1
- package/dist/tours/pages/MyToursProgressPage.js +50 -46
- package/dist/tours/pages/MyToursProgressPage.js.map +1 -1
- package/dist/tours/pages/SchedulerPage.js +91 -85
- package/dist/tours/pages/SchedulerPage.js.map +1 -1
- package/dist/tours/pages/StepEditorPage.js +217 -216
- package/dist/tours/pages/StepEditorPage.js.map +1 -1
- package/dist/tours/pages/SystemAnalyticsPage.js +47 -41
- package/dist/tours/pages/SystemAnalyticsPage.js.map +1 -1
- package/dist/tours/pages/TemplateLibraryPage.js +53 -51
- package/dist/tours/pages/TemplateLibraryPage.js.map +1 -1
- package/dist/tours/pages/TourBuilderPage.js +152 -165
- package/dist/tours/pages/TourBuilderPage.js.map +1 -1
- package/dist/tours/pages/TourPreviewPage.js +71 -68
- package/dist/tours/pages/TourPreviewPage.js.map +1 -1
- package/dist/tours/pages/ToursListPage.js +157 -201
- package/dist/tours/pages/ToursListPage.js.map +1 -1
- package/package.json +2 -2
|
@@ -6,52 +6,54 @@ import { useCallback as o, useEffect as s } from "react";
|
|
|
6
6
|
import { BarChart3 as c, Timer as l, TrendingUp as u, Users as d } from "lucide-react";
|
|
7
7
|
import { jsx as f, jsxs as p } from "react/jsx-runtime";
|
|
8
8
|
import { useParams as m } from "react-router-dom";
|
|
9
|
-
import {
|
|
9
|
+
import { EmphasisPanel as h, GlassCard as g } from "@burdenoff/fe-libs/ui";
|
|
10
|
+
import { useI18n as _ } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
10
11
|
//#region src/tours/pages/AnalyticsDashboardPage.tsx
|
|
11
|
-
function
|
|
12
|
-
let { t:
|
|
13
|
-
let n =
|
|
12
|
+
function v() {
|
|
13
|
+
let { t: v } = _(), y = o((e, t) => {
|
|
14
|
+
let n = v(e);
|
|
14
15
|
return n === e ? t : n;
|
|
15
|
-
}, [
|
|
16
|
+
}, [v]), { tourId: b } = m(), { hasPermission: x, isLoading: S } = e(), C = x(t.TOUR_ANALYTICS_READ), { analytics: w, loading: T, fetchAnalytics: E, getCompletionRate: D, getSkipRate: O } = n(b);
|
|
16
17
|
return s(() => {
|
|
17
|
-
|
|
18
|
+
S || !C || E();
|
|
18
19
|
}, [
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
]), !
|
|
20
|
+
E,
|
|
21
|
+
S,
|
|
22
|
+
C
|
|
23
|
+
]), !S && !C ? /* @__PURE__ */ f("div", {
|
|
23
24
|
className: "flex items-center justify-center h-full p-3 sm:p-6",
|
|
24
25
|
children: /* @__PURE__ */ p("div", {
|
|
25
26
|
className: "text-center",
|
|
26
27
|
children: [/* @__PURE__ */ f("h2", {
|
|
27
28
|
className: "text-lg font-semibold text-text-primary mb-2",
|
|
28
|
-
children:
|
|
29
|
+
children: y("analyticsDashboard.accessDenied", "Access Denied")
|
|
29
30
|
}), /* @__PURE__ */ f("p", {
|
|
30
31
|
className: "text-sm text-text-secondary",
|
|
31
|
-
children:
|
|
32
|
+
children: y("analyticsDashboard.noPermission", "You do not have permission to view tour analytics.")
|
|
32
33
|
})]
|
|
33
34
|
})
|
|
34
|
-
}) :
|
|
35
|
+
}) : T ? /* @__PURE__ */ f("div", {
|
|
35
36
|
className: "flex items-center justify-center h-full",
|
|
36
37
|
children: /* @__PURE__ */ f("p", {
|
|
37
38
|
className: "text-text-secondary",
|
|
38
|
-
children:
|
|
39
|
+
children: y("analyticsDashboard.loading", "Loading analytics...")
|
|
39
40
|
})
|
|
40
|
-
}) :
|
|
41
|
+
}) : w ? /* @__PURE__ */ p("div", {
|
|
41
42
|
className: "space-y-4 sm:space-y-6 p-3 sm:p-6",
|
|
42
43
|
children: [
|
|
43
44
|
/* @__PURE__ */ p("div", { children: [/* @__PURE__ */ f("h1", {
|
|
44
45
|
className: "text-xl sm:text-2xl font-bold",
|
|
45
|
-
children:
|
|
46
|
+
children: y("analyticsDashboard.title", "Tour Analytics")
|
|
46
47
|
}), /* @__PURE__ */ f("p", {
|
|
47
48
|
className: "text-sm text-text-secondary",
|
|
48
|
-
children:
|
|
49
|
+
children: y("analyticsDashboard.subtitle", "Performance metrics and insights")
|
|
49
50
|
})] }),
|
|
50
51
|
/* @__PURE__ */ p("div", {
|
|
51
52
|
className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4",
|
|
52
53
|
children: [
|
|
53
|
-
/* @__PURE__ */ p(
|
|
54
|
-
|
|
54
|
+
/* @__PURE__ */ p(g, {
|
|
55
|
+
treatment: "glass",
|
|
56
|
+
className: "p-4",
|
|
55
57
|
children: [/* @__PURE__ */ p("div", {
|
|
56
58
|
className: "flex items-center gap-3 mb-2",
|
|
57
59
|
children: [/* @__PURE__ */ f("div", {
|
|
@@ -62,15 +64,16 @@ function g() {
|
|
|
62
64
|
})
|
|
63
65
|
}), /* @__PURE__ */ f("span", {
|
|
64
66
|
className: "text-sm text-text-secondary",
|
|
65
|
-
children:
|
|
67
|
+
children: y("analyticsDashboard.totalViews", "Total Views")
|
|
66
68
|
})]
|
|
67
69
|
}), /* @__PURE__ */ f("div", {
|
|
68
|
-
className: "text-2xl font-bold",
|
|
69
|
-
children: i(
|
|
70
|
+
className: "text-2xl font-bold tabular-nums",
|
|
71
|
+
children: i(w.totalViews)
|
|
70
72
|
})]
|
|
71
73
|
}),
|
|
72
|
-
/* @__PURE__ */ p(
|
|
73
|
-
|
|
74
|
+
/* @__PURE__ */ p(g, {
|
|
75
|
+
treatment: "metric",
|
|
76
|
+
className: "p-4",
|
|
74
77
|
children: [/* @__PURE__ */ p("div", {
|
|
75
78
|
className: "flex items-center gap-3 mb-2",
|
|
76
79
|
children: [/* @__PURE__ */ f("div", {
|
|
@@ -81,15 +84,16 @@ function g() {
|
|
|
81
84
|
})
|
|
82
85
|
}), /* @__PURE__ */ f("span", {
|
|
83
86
|
className: "text-sm text-text-secondary",
|
|
84
|
-
children:
|
|
87
|
+
children: y("analyticsDashboard.completionRate", "Completion Rate")
|
|
85
88
|
})]
|
|
86
89
|
}), /* @__PURE__ */ f("div", {
|
|
87
|
-
className: "text-2xl font-bold",
|
|
88
|
-
children: a(
|
|
90
|
+
className: "text-2xl font-bold tabular-nums",
|
|
91
|
+
children: a(D())
|
|
89
92
|
})]
|
|
90
93
|
}),
|
|
91
|
-
/* @__PURE__ */ p(
|
|
92
|
-
|
|
94
|
+
/* @__PURE__ */ p(g, {
|
|
95
|
+
treatment: "metric",
|
|
96
|
+
className: "p-4",
|
|
93
97
|
children: [/* @__PURE__ */ p("div", {
|
|
94
98
|
className: "flex items-center gap-3 mb-2",
|
|
95
99
|
children: [/* @__PURE__ */ f("div", {
|
|
@@ -100,15 +104,16 @@ function g() {
|
|
|
100
104
|
})
|
|
101
105
|
}), /* @__PURE__ */ f("span", {
|
|
102
106
|
className: "text-sm text-text-secondary",
|
|
103
|
-
children:
|
|
107
|
+
children: y("analyticsDashboard.skipRate", "Skip Rate")
|
|
104
108
|
})]
|
|
105
109
|
}), /* @__PURE__ */ f("div", {
|
|
106
|
-
className: "text-2xl font-bold",
|
|
107
|
-
children: a(
|
|
110
|
+
className: "text-2xl font-bold tabular-nums",
|
|
111
|
+
children: a(O())
|
|
108
112
|
})]
|
|
109
113
|
}),
|
|
110
|
-
/* @__PURE__ */ p(
|
|
111
|
-
|
|
114
|
+
/* @__PURE__ */ p(g, {
|
|
115
|
+
treatment: "metric",
|
|
116
|
+
className: "p-4",
|
|
112
117
|
children: [/* @__PURE__ */ p("div", {
|
|
113
118
|
className: "flex items-center gap-3 mb-2",
|
|
114
119
|
children: [/* @__PURE__ */ f("div", {
|
|
@@ -119,29 +124,29 @@ function g() {
|
|
|
119
124
|
})
|
|
120
125
|
}), /* @__PURE__ */ f("span", {
|
|
121
126
|
className: "text-sm text-text-secondary",
|
|
122
|
-
children:
|
|
127
|
+
children: y("analyticsDashboard.avgDuration", "Avg Duration")
|
|
123
128
|
})]
|
|
124
129
|
}), /* @__PURE__ */ f("div", {
|
|
125
|
-
className: "text-2xl font-bold",
|
|
126
|
-
children: r(
|
|
130
|
+
className: "text-2xl font-bold tabular-nums",
|
|
131
|
+
children: r(w.averageDuration)
|
|
127
132
|
})]
|
|
128
133
|
})
|
|
129
134
|
]
|
|
130
135
|
}),
|
|
131
|
-
/* @__PURE__ */ p(
|
|
132
|
-
className: "
|
|
136
|
+
/* @__PURE__ */ p(h, {
|
|
137
|
+
className: "p-4 sm:p-6",
|
|
133
138
|
children: [/* @__PURE__ */ f("h2", {
|
|
134
139
|
className: "text-lg font-semibold mb-4",
|
|
135
|
-
children:
|
|
140
|
+
children: y("analyticsDashboard.deviceBreakdown", "Device Breakdown")
|
|
136
141
|
}), /* @__PURE__ */ f("div", {
|
|
137
142
|
className: "space-y-3",
|
|
138
|
-
children:
|
|
143
|
+
children: w.byDeviceType.map((e) => /* @__PURE__ */ p("div", { children: [/* @__PURE__ */ p("div", {
|
|
139
144
|
className: "flex items-center justify-between mb-2",
|
|
140
145
|
children: [/* @__PURE__ */ f("span", {
|
|
141
146
|
className: "text-sm capitalize",
|
|
142
147
|
children: e.deviceType
|
|
143
148
|
}), /* @__PURE__ */ p("span", {
|
|
144
|
-
className: "text-sm text-text-secondary",
|
|
149
|
+
className: "text-sm text-text-secondary tabular-nums",
|
|
145
150
|
children: [
|
|
146
151
|
i(e.count),
|
|
147
152
|
" views (",
|
|
@@ -152,62 +157,63 @@ function g() {
|
|
|
152
157
|
}), /* @__PURE__ */ f("div", {
|
|
153
158
|
className: "w-full bg-bg-sunken rounded-full h-2",
|
|
154
159
|
children: /* @__PURE__ */ f("div", {
|
|
155
|
-
className: "bg-action-primary-bg h-2 rounded-full",
|
|
160
|
+
className: "bg-action-primary-bg h-2 rounded-full shadow-[var(--shadow-pop)]",
|
|
156
161
|
style: { width: `${e.completionRate}%` }
|
|
157
162
|
})
|
|
158
163
|
})] }, e.deviceType))
|
|
159
164
|
})]
|
|
160
165
|
}),
|
|
161
|
-
|
|
162
|
-
|
|
166
|
+
w.stepAnalytics && w.stepAnalytics.length > 0 && /* @__PURE__ */ p(g, {
|
|
167
|
+
treatment: "plain",
|
|
168
|
+
className: "p-4 sm:p-6 border-border-seam",
|
|
163
169
|
children: [/* @__PURE__ */ f("h2", {
|
|
164
170
|
className: "text-lg font-semibold mb-4",
|
|
165
|
-
children:
|
|
171
|
+
children: y("analyticsDashboard.stepPerformance", "Step Performance")
|
|
166
172
|
}), /* @__PURE__ */ f("div", {
|
|
167
173
|
className: "overflow-x-auto",
|
|
168
174
|
children: /* @__PURE__ */ p("table", {
|
|
169
175
|
className: "w-full",
|
|
170
176
|
children: [/* @__PURE__ */ f("thead", { children: /* @__PURE__ */ p("tr", {
|
|
171
|
-
className: "border-b border-border-
|
|
177
|
+
className: "border-b border-border-seam",
|
|
172
178
|
children: [
|
|
173
179
|
/* @__PURE__ */ f("th", {
|
|
174
|
-
className: "text-left py-2 px-4 text-sm font-medium",
|
|
175
|
-
children:
|
|
180
|
+
className: "text-left py-2 px-4 text-sm font-medium text-text-secondary",
|
|
181
|
+
children: y("analyticsDashboard.tableStep", "Step")
|
|
176
182
|
}),
|
|
177
183
|
/* @__PURE__ */ f("th", {
|
|
178
|
-
className: "text-right py-2 px-4 text-sm font-medium",
|
|
179
|
-
children:
|
|
184
|
+
className: "text-right py-2 px-4 text-sm font-medium text-text-secondary",
|
|
185
|
+
children: y("analyticsDashboard.tableViews", "Views")
|
|
180
186
|
}),
|
|
181
187
|
/* @__PURE__ */ f("th", {
|
|
182
|
-
className: "text-right py-2 px-4 text-sm font-medium",
|
|
183
|
-
children:
|
|
188
|
+
className: "text-right py-2 px-4 text-sm font-medium text-text-secondary",
|
|
189
|
+
children: y("analyticsDashboard.tableCompletions", "Completions")
|
|
184
190
|
}),
|
|
185
191
|
/* @__PURE__ */ f("th", {
|
|
186
|
-
className: "text-right py-2 px-4 text-sm font-medium",
|
|
187
|
-
children:
|
|
192
|
+
className: "text-right py-2 px-4 text-sm font-medium text-text-secondary",
|
|
193
|
+
children: y("analyticsDashboard.tableDropOffRate", "Drop-off Rate")
|
|
188
194
|
})
|
|
189
195
|
]
|
|
190
|
-
}) }), /* @__PURE__ */ f("tbody", { children:
|
|
191
|
-
className: "border-b border-border-
|
|
196
|
+
}) }), /* @__PURE__ */ f("tbody", { children: w.stepAnalytics.map((e) => /* @__PURE__ */ p("tr", {
|
|
197
|
+
className: "border-b border-border-seam",
|
|
192
198
|
children: [
|
|
193
199
|
/* @__PURE__ */ p("td", {
|
|
194
200
|
className: "py-2 px-4 text-sm",
|
|
195
201
|
children: [
|
|
196
|
-
|
|
202
|
+
y("analyticsDashboard.stepLabel", "Step"),
|
|
197
203
|
" ",
|
|
198
204
|
e.stepOrder
|
|
199
205
|
]
|
|
200
206
|
}),
|
|
201
207
|
/* @__PURE__ */ f("td", {
|
|
202
|
-
className: "text-right py-2 px-4 text-sm",
|
|
208
|
+
className: "text-right py-2 px-4 text-sm tabular-nums",
|
|
203
209
|
children: i(e.views)
|
|
204
210
|
}),
|
|
205
211
|
/* @__PURE__ */ f("td", {
|
|
206
|
-
className: "text-right py-2 px-4 text-sm",
|
|
212
|
+
className: "text-right py-2 px-4 text-sm tabular-nums",
|
|
207
213
|
children: i(e.completions)
|
|
208
214
|
}),
|
|
209
215
|
/* @__PURE__ */ f("td", {
|
|
210
|
-
className: "text-right py-2 px-4 text-sm",
|
|
216
|
+
className: "text-right py-2 px-4 text-sm tabular-nums",
|
|
211
217
|
children: a(e.dropOffRate)
|
|
212
218
|
})
|
|
213
219
|
]
|
|
@@ -220,11 +226,11 @@ function g() {
|
|
|
220
226
|
className: "flex items-center justify-center h-full",
|
|
221
227
|
children: /* @__PURE__ */ f("p", {
|
|
222
228
|
className: "text-text-secondary",
|
|
223
|
-
children:
|
|
229
|
+
children: y("analyticsDashboard.noData", "No analytics data available")
|
|
224
230
|
})
|
|
225
231
|
});
|
|
226
232
|
}
|
|
227
233
|
//#endregion
|
|
228
|
-
export {
|
|
234
|
+
export { v as AnalyticsDashboardPage };
|
|
229
235
|
|
|
230
236
|
//# sourceMappingURL=AnalyticsDashboardPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsDashboardPage.js","names":[],"sources":["../../../src/tours/pages/AnalyticsDashboardPage.tsx"],"sourcesContent":["import { useCallback, useEffect } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { BarChart3, TrendingUp, Users, Timer } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useTourAnalytics } from '../hooks/useTourAnalytics';\nimport { useToursPermissionAccess } from '../hooks/useToursPermissionAccess';\nimport { TOUR_PERMISSIONS } from '../../constants/permissions';\nimport { formatNumber, formatPercentage, formatDuration } from '../utils';\n\n/**\n * Analytics dashboard page\n * Display tour performance metrics and charts\n */\nexport function AnalyticsDashboardPage() {\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n },\n [t]\n );\n\n const { tourId } = useParams();\n const { hasPermission, isLoading: permissionsLoading } = useToursPermissionAccess();\n const canViewAnalytics = hasPermission(TOUR_PERMISSIONS.TOUR_ANALYTICS_READ);\n\n const { analytics, loading, fetchAnalytics, getCompletionRate, getSkipRate } =\n useTourAnalytics(tourId);\n\n useEffect(() => {\n if (permissionsLoading || !canViewAnalytics) return;\n fetchAnalytics();\n }, [fetchAnalytics, permissionsLoading, canViewAnalytics]);\n\n if (!permissionsLoading && !canViewAnalytics) {\n return (\n <div className=\"flex items-center justify-center h-full p-3 sm:p-6\">\n <div className=\"text-center\">\n <h2 className=\"text-lg font-semibold text-text-primary mb-2\">\n {tr('analyticsDashboard.accessDenied', 'Access Denied')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'analyticsDashboard.noPermission',\n 'You do not have permission to view tour analytics.'\n )}\n </p>\n </div>\n </div>\n );\n }\n\n if (loading) {\n return (\n <div className=\"flex items-center justify-center h-full\">\n <p className=\"text-text-secondary\">\n {tr('analyticsDashboard.loading', 'Loading analytics...')}\n </p>\n </div>\n );\n }\n\n if (!analytics) {\n return (\n <div className=\"flex items-center justify-center h-full\">\n <p className=\"text-text-secondary\">\n {tr('analyticsDashboard.noData', 'No analytics data available')}\n </p>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold\">\n {tr('analyticsDashboard.title', 'Tour Analytics')}\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {tr('analyticsDashboard.subtitle', 'Performance metrics and insights')}\n </p>\n </div>\n\n {/* Summary Cards */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4\">\n <div className=\"border border-border-subtle rounded-md p-4 bg-bg-surface\">\n <div className=\"flex items-center gap-3 mb-2\">\n <div className=\"p-2 bg-action-primary-bg/10 rounded-md\">\n <Users className=\"text-primary\" size={20} />\n </div>\n <span className=\"text-sm text-text-secondary\">\n {tr('analyticsDashboard.totalViews', 'Total Views')}\n </span>\n </div>\n <div className=\"text-2xl font-bold\">{formatNumber(analytics.totalViews)}</div>\n </div>\n\n <div className=\"border border-border-subtle rounded-md p-4 bg-bg-surface\">\n <div className=\"flex items-center gap-3 mb-2\">\n <div className=\"p-2 bg-success/10 rounded-md\">\n <TrendingUp className=\"text-success\" size={20} />\n </div>\n <span className=\"text-sm text-text-secondary\">\n {tr('analyticsDashboard.completionRate', 'Completion Rate')}\n </span>\n </div>\n <div className=\"text-2xl font-bold\">{formatPercentage(getCompletionRate())}</div>\n </div>\n\n <div className=\"border border-border-subtle rounded-md p-4 bg-bg-surface\">\n <div className=\"flex items-center gap-3 mb-2\">\n <div className=\"p-2 bg-warning/10 rounded-md\">\n <BarChart3 className=\"text-warning\" size={20} />\n </div>\n <span className=\"text-sm text-text-secondary\">\n {tr('analyticsDashboard.skipRate', 'Skip Rate')}\n </span>\n </div>\n <div className=\"text-2xl font-bold\">{formatPercentage(getSkipRate())}</div>\n </div>\n\n <div className=\"border border-border-subtle rounded-md p-4 bg-bg-surface\">\n <div className=\"flex items-center gap-3 mb-2\">\n <div className=\"p-2 bg-info/10 rounded-md\">\n <Timer className=\"text-info\" size={20} />\n </div>\n <span className=\"text-sm text-text-secondary\">\n {tr('analyticsDashboard.avgDuration', 'Avg Duration')}\n </span>\n </div>\n <div className=\"text-2xl font-bold\">{formatDuration(analytics.averageDuration)}</div>\n </div>\n </div>\n\n {/* Device Breakdown */}\n <div className=\"border border-border-subtle rounded-md p-4 sm:p-6 bg-bg-surface\">\n <h2 className=\"text-lg font-semibold mb-4\">\n {tr('analyticsDashboard.deviceBreakdown', 'Device Breakdown')}\n </h2>\n <div className=\"space-y-3\">\n {analytics.byDeviceType.map((device) => (\n <div key={device.deviceType}>\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm capitalize\">{device.deviceType}</span>\n <span className=\"text-sm text-text-secondary\">\n {formatNumber(device.count)} views ({formatPercentage(device.completionRate)})\n </span>\n </div>\n <div className=\"w-full bg-bg-sunken rounded-full h-2\">\n <div\n className=\"bg-action-primary-bg h-2 rounded-full\"\n style={{ width: `${device.completionRate}%` }}\n />\n </div>\n </div>\n ))}\n </div>\n </div>\n\n {/* Step Analytics */}\n {analytics.stepAnalytics && analytics.stepAnalytics.length > 0 && (\n <div className=\"border border-border-subtle rounded-md p-4 sm:p-6 bg-bg-surface\">\n <h2 className=\"text-lg font-semibold mb-4\">\n {tr('analyticsDashboard.stepPerformance', 'Step Performance')}\n </h2>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full\">\n <thead>\n <tr className=\"border-b border-border-subtle\">\n <th className=\"text-left py-2 px-4 text-sm font-medium\">\n {tr('analyticsDashboard.tableStep', 'Step')}\n </th>\n <th className=\"text-right py-2 px-4 text-sm font-medium\">\n {tr('analyticsDashboard.tableViews', 'Views')}\n </th>\n <th className=\"text-right py-2 px-4 text-sm font-medium\">\n {tr('analyticsDashboard.tableCompletions', 'Completions')}\n </th>\n <th className=\"text-right py-2 px-4 text-sm font-medium\">\n {tr('analyticsDashboard.tableDropOffRate', 'Drop-off Rate')}\n </th>\n </tr>\n </thead>\n <tbody>\n {analytics.stepAnalytics.map((step) => (\n <tr key={step.stepId} className=\"border-b border-border-subtle\">\n <td className=\"py-2 px-4 text-sm\">\n {tr('analyticsDashboard.stepLabel', 'Step')} {step.stepOrder}\n </td>\n <td className=\"text-right py-2 px-4 text-sm\">{formatNumber(step.views)}</td>\n <td className=\"text-right py-2 px-4 text-sm\">\n {formatNumber(step.completions)}\n </td>\n <td className=\"text-right py-2 px-4 text-sm\">\n {formatPercentage(step.dropOffRate)}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;AAaA,SAAgB,IAAyB;CACvC,IAAM,EAAE,SAAM,GAAS,EACjB,IAAK,GACR,GAAa,MAA6B;EACzC,IAAM,IAAQ,EAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;IAEpC,CAAC,EAAE,CACJ,EAEK,EAAE,cAAW,GAAW,EACxB,EAAE,kBAAe,WAAW,MAAuB,GAA0B,EAC7E,IAAmB,EAAc,EAAiB,oBAAoB,EAEtE,EAAE,cAAW,YAAS,mBAAgB,sBAAmB,mBAC7D,EAAiB,EAAO;AA6C1B,QA3CA,QAAgB;AACV,OAAsB,CAAC,KAC3B,GAAgB;IACf;EAAC;EAAgB;EAAoB;EAAiB,CAAC,EAEtD,CAAC,KAAsB,CAAC,IAExB,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,MAAD;IAAI,WAAU;cACX,EAAG,mCAAmC,gBAAgB;IACpD,CAAA,EACL,kBAAC,KAAD;IAAG,WAAU;cACV,EACC,mCACA,qDACD;IACC,CAAA,CACA;;EACF,CAAA,GAIN,IAEA,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,KAAD;GAAG,WAAU;aACV,EAAG,8BAA8B,uBAAuB;GACvD,CAAA;EACA,CAAA,GAIL,IAWH,kBAAC,OAAD;EAAK,WAAU;YAAf;GAEE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;IAAI,WAAU;cACX,EAAG,4BAA4B,iBAAiB;IAC9C,CAAA,EACL,kBAAC,KAAD;IAAG,WAAU;cACV,EAAG,+BAA+B,mCAAmC;IACpE,CAAA,CACA,EAAA,CAAA;GAGN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,GAAD;SAAO,WAAU;SAAe,MAAM;SAAM,CAAA;QACxC,CAAA,EACN,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAG,iCAAiC,cAAc;QAC9C,CAAA,CACH;UACN,kBAAC,OAAD;OAAK,WAAU;iBAAsB,EAAa,EAAU,WAAW;OAAO,CAAA,CAC1E;;KAEN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,GAAD;SAAY,WAAU;SAAe,MAAM;SAAM,CAAA;QAC7C,CAAA,EACN,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAG,qCAAqC,kBAAkB;QACtD,CAAA,CACH;UACN,kBAAC,OAAD;OAAK,WAAU;iBAAsB,EAAiB,GAAmB,CAAC;OAAO,CAAA,CAC7E;;KAEN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,GAAD;SAAW,WAAU;SAAe,MAAM;SAAM,CAAA;QAC5C,CAAA,EACN,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAG,+BAA+B,YAAY;QAC1C,CAAA,CACH;UACN,kBAAC,OAAD;OAAK,WAAU;iBAAsB,EAAiB,GAAa,CAAC;OAAO,CAAA,CACvE;;KAEN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,GAAD;SAAO,WAAU;SAAY,MAAM;SAAM,CAAA;QACrC,CAAA,EACN,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAG,kCAAkC,eAAe;QAChD,CAAA,CACH;UACN,kBAAC,OAAD;OAAK,WAAU;iBAAsB,EAAe,EAAU,gBAAgB;OAAO,CAAA,CACjF;;KACF;;GAGN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,sCAAsC,mBAAmB;KAC1D,CAAA,EACL,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAU,aAAa,KAAK,MAC3B,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAsB,EAAO;OAAkB,CAAA,EAC/D,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACG,EAAa,EAAO,MAAM;QAAC;QAAS,EAAiB,EAAO,eAAe;QAAC;QACxE;SACH;SACN,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,OAAD;OACE,WAAU;OACV,OAAO,EAAE,OAAO,GAAG,EAAO,eAAe,IAAI;OAC7C,CAAA;MACE,CAAA,CACF,EAAA,EAbI,EAAO,WAaX,CACN;KACE,CAAA,CACF;;GAGL,EAAU,iBAAiB,EAAU,cAAc,SAAS,KAC3D,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,sCAAsC,mBAAmB;KAC1D,CAAA,EACL,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,SAAD;MAAO,WAAU;gBAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;OAAI,WAAU;iBAAd;QACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAG,gCAAgC,OAAO;SACxC,CAAA;QACL,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAG,iCAAiC,QAAQ;SAC1C,CAAA;QACL,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAG,uCAAuC,cAAc;SACtD,CAAA;QACL,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAG,uCAAuC,gBAAgB;SACxD,CAAA;QACF;UACC,CAAA,EACR,kBAAC,SAAD,EAAA,UACG,EAAU,cAAc,KAAK,MAC5B,kBAAC,MAAD;OAAsB,WAAU;iBAAhC;QACE,kBAAC,MAAD;SAAI,WAAU;mBAAd;UACG,EAAG,gCAAgC,OAAO;UAAC;UAAE,EAAK;UAChD;;QACL,kBAAC,MAAD;SAAI,WAAU;mBAAgC,EAAa,EAAK,MAAM;SAAM,CAAA;QAC5E,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAa,EAAK,YAAY;SAC5B,CAAA;QACL,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAiB,EAAK,YAAY;SAChC,CAAA;QACF;SAXI,EAAK,OAWT,CACL,EACI,CAAA,CACF;;KACJ,CAAA,CACF;;GAEJ;MA5IJ,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,KAAD;GAAG,WAAU;aACV,EAAG,6BAA6B,8BAA8B;GAC7D,CAAA;EACA,CAAA"}
|
|
1
|
+
{"version":3,"file":"AnalyticsDashboardPage.js","names":[],"sources":["../../../src/tours/pages/AnalyticsDashboardPage.tsx"],"sourcesContent":["import { useCallback, useEffect } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { BarChart3, TrendingUp, Users, Timer } from 'lucide-react';\nimport { GlassCard, EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useTourAnalytics } from '../hooks/useTourAnalytics';\nimport { useToursPermissionAccess } from '../hooks/useToursPermissionAccess';\nimport { TOUR_PERMISSIONS } from '../../constants/permissions';\nimport { formatNumber, formatPercentage, formatDuration } from '../utils';\n\n/**\n * Analytics dashboard page\n * Display tour performance metrics and charts\n */\nexport function AnalyticsDashboardPage() {\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n },\n [t]\n );\n\n const { tourId } = useParams();\n const { hasPermission, isLoading: permissionsLoading } = useToursPermissionAccess();\n const canViewAnalytics = hasPermission(TOUR_PERMISSIONS.TOUR_ANALYTICS_READ);\n\n const { analytics, loading, fetchAnalytics, getCompletionRate, getSkipRate } =\n useTourAnalytics(tourId);\n\n useEffect(() => {\n if (permissionsLoading || !canViewAnalytics) return;\n fetchAnalytics();\n }, [fetchAnalytics, permissionsLoading, canViewAnalytics]);\n\n if (!permissionsLoading && !canViewAnalytics) {\n return (\n <div className=\"flex items-center justify-center h-full p-3 sm:p-6\">\n <div className=\"text-center\">\n <h2 className=\"text-lg font-semibold text-text-primary mb-2\">\n {tr('analyticsDashboard.accessDenied', 'Access Denied')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'analyticsDashboard.noPermission',\n 'You do not have permission to view tour analytics.'\n )}\n </p>\n </div>\n </div>\n );\n }\n\n if (loading) {\n return (\n <div className=\"flex items-center justify-center h-full\">\n <p className=\"text-text-secondary\">\n {tr('analyticsDashboard.loading', 'Loading analytics...')}\n </p>\n </div>\n );\n }\n\n if (!analytics) {\n return (\n <div className=\"flex items-center justify-center h-full\">\n <p className=\"text-text-secondary\">\n {tr('analyticsDashboard.noData', 'No analytics data available')}\n </p>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold\">\n {tr('analyticsDashboard.title', 'Tour Analytics')}\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {tr('analyticsDashboard.subtitle', 'Performance metrics and insights')}\n </p>\n </div>\n\n {/* Summary Cards */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4\">\n <GlassCard treatment=\"glass\" className=\"p-4\">\n <div className=\"flex items-center gap-3 mb-2\">\n <div className=\"p-2 bg-action-primary-bg/10 rounded-md\">\n <Users className=\"text-primary\" size={20} />\n </div>\n <span className=\"text-sm text-text-secondary\">\n {tr('analyticsDashboard.totalViews', 'Total Views')}\n </span>\n </div>\n <div className=\"text-2xl font-bold tabular-nums\">{formatNumber(analytics.totalViews)}</div>\n </GlassCard>\n\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-3 mb-2\">\n <div className=\"p-2 bg-success/10 rounded-md\">\n <TrendingUp className=\"text-success\" size={20} />\n </div>\n <span className=\"text-sm text-text-secondary\">\n {tr('analyticsDashboard.completionRate', 'Completion Rate')}\n </span>\n </div>\n <div className=\"text-2xl font-bold tabular-nums\">\n {formatPercentage(getCompletionRate())}\n </div>\n </GlassCard>\n\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-3 mb-2\">\n <div className=\"p-2 bg-warning/10 rounded-md\">\n <BarChart3 className=\"text-warning\" size={20} />\n </div>\n <span className=\"text-sm text-text-secondary\">\n {tr('analyticsDashboard.skipRate', 'Skip Rate')}\n </span>\n </div>\n <div className=\"text-2xl font-bold tabular-nums\">{formatPercentage(getSkipRate())}</div>\n </GlassCard>\n\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-3 mb-2\">\n <div className=\"p-2 bg-info/10 rounded-md\">\n <Timer className=\"text-info\" size={20} />\n </div>\n <span className=\"text-sm text-text-secondary\">\n {tr('analyticsDashboard.avgDuration', 'Avg Duration')}\n </span>\n </div>\n <div className=\"text-2xl font-bold tabular-nums\">\n {formatDuration(analytics.averageDuration)}\n </div>\n </GlassCard>\n </div>\n\n {/* Device Breakdown — the key soft-glow data-viz zone */}\n <EmphasisPanel className=\"p-4 sm:p-6\">\n <h2 className=\"text-lg font-semibold mb-4\">\n {tr('analyticsDashboard.deviceBreakdown', 'Device Breakdown')}\n </h2>\n <div className=\"space-y-3\">\n {analytics.byDeviceType.map((device) => (\n <div key={device.deviceType}>\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm capitalize\">{device.deviceType}</span>\n <span className=\"text-sm text-text-secondary tabular-nums\">\n {formatNumber(device.count)} views ({formatPercentage(device.completionRate)})\n </span>\n </div>\n <div className=\"w-full bg-bg-sunken rounded-full h-2\">\n <div\n className=\"bg-action-primary-bg h-2 rounded-full shadow-[var(--shadow-pop)]\"\n style={{ width: `${device.completionRate}%` }}\n />\n </div>\n </div>\n ))}\n </div>\n </EmphasisPanel>\n\n {/* Step Analytics */}\n {analytics.stepAnalytics && analytics.stepAnalytics.length > 0 && (\n <GlassCard treatment=\"plain\" className=\"p-4 sm:p-6 border-border-seam\">\n <h2 className=\"text-lg font-semibold mb-4\">\n {tr('analyticsDashboard.stepPerformance', 'Step Performance')}\n </h2>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full\">\n <thead>\n <tr className=\"border-b border-border-seam\">\n <th className=\"text-left py-2 px-4 text-sm font-medium text-text-secondary\">\n {tr('analyticsDashboard.tableStep', 'Step')}\n </th>\n <th className=\"text-right py-2 px-4 text-sm font-medium text-text-secondary\">\n {tr('analyticsDashboard.tableViews', 'Views')}\n </th>\n <th className=\"text-right py-2 px-4 text-sm font-medium text-text-secondary\">\n {tr('analyticsDashboard.tableCompletions', 'Completions')}\n </th>\n <th className=\"text-right py-2 px-4 text-sm font-medium text-text-secondary\">\n {tr('analyticsDashboard.tableDropOffRate', 'Drop-off Rate')}\n </th>\n </tr>\n </thead>\n <tbody>\n {analytics.stepAnalytics.map((step) => (\n <tr key={step.stepId} className=\"border-b border-border-seam\">\n <td className=\"py-2 px-4 text-sm\">\n {tr('analyticsDashboard.stepLabel', 'Step')} {step.stepOrder}\n </td>\n <td className=\"text-right py-2 px-4 text-sm tabular-nums\">\n {formatNumber(step.views)}\n </td>\n <td className=\"text-right py-2 px-4 text-sm tabular-nums\">\n {formatNumber(step.completions)}\n </td>\n <td className=\"text-right py-2 px-4 text-sm tabular-nums\">\n {formatPercentage(step.dropOffRate)}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </GlassCard>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -8,32 +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 {
|
|
12
|
-
import {
|
|
11
|
+
import { GlassCard as y } from "@burdenoff/fe-libs/ui";
|
|
12
|
+
import { useI18n as b } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
13
|
+
import { useEventBus as x } from "@burdenoff/fe-libs/shared/events";
|
|
13
14
|
//#region src/tours/pages/MediaLibraryPage.tsx
|
|
14
|
-
function
|
|
15
|
-
let { t:
|
|
16
|
-
let n =
|
|
15
|
+
function S() {
|
|
16
|
+
let { t: S } = b(), C = o((e, t) => {
|
|
17
|
+
let n = S(e);
|
|
17
18
|
return n === e ? t : n;
|
|
18
|
-
}, [
|
|
19
|
+
}, [S]), w = e(), T = x(), { hasPermission: E, isLoading: D } = t(), O = E(n.TOUR_MEDIA_LIST), k = E(n.TOUR_MEDIA_DELETE), { media: A, fetchMedia: j, uploadMedia: M, deleteMedia: N, uploadProgress: P } = a(), [F, I] = c(""), [L, R] = c("all");
|
|
19
20
|
s(() => {
|
|
20
|
-
|
|
21
|
+
D || !O || j(L === "all" ? void 0 : L, F);
|
|
21
22
|
}, [
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
j,
|
|
24
|
+
L,
|
|
25
|
+
F,
|
|
26
|
+
D,
|
|
27
|
+
O
|
|
27
28
|
]);
|
|
28
|
-
let
|
|
29
|
+
let z = async (e) => {
|
|
29
30
|
e && (await Promise.all(Array.from(e).map(async (e) => {
|
|
30
31
|
try {
|
|
31
|
-
await
|
|
32
|
+
await M(e);
|
|
32
33
|
} catch {
|
|
33
|
-
r(
|
|
34
|
+
r(T, "media.upload", w);
|
|
34
35
|
}
|
|
35
|
-
})),
|
|
36
|
-
},
|
|
36
|
+
})), j(L === "all" ? void 0 : L, F));
|
|
37
|
+
}, B = (e) => {
|
|
37
38
|
switch (e) {
|
|
38
39
|
case "IMAGE": return /* @__PURE__ */ _(d, { size: 20 });
|
|
39
40
|
case "VIDEO": return /* @__PURE__ */ _(g, { size: 20 });
|
|
@@ -48,24 +49,24 @@ function x() {
|
|
|
48
49
|
className: "flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3",
|
|
49
50
|
children: [/* @__PURE__ */ v("div", { children: [/* @__PURE__ */ _("h1", {
|
|
50
51
|
className: "text-xl sm:text-2xl font-bold",
|
|
51
|
-
children:
|
|
52
|
+
children: C("mediaLibrary.title", "Media Library")
|
|
52
53
|
}), /* @__PURE__ */ _("p", {
|
|
53
54
|
className: "text-sm text-text-secondary",
|
|
54
|
-
children:
|
|
55
|
+
children: C("mediaLibrary.subtitle", "Upload and manage media for tour steps")
|
|
55
56
|
})] }), /* @__PURE__ */ _(l, {
|
|
56
57
|
permission: n.TOUR_MEDIA_CREATE,
|
|
57
58
|
children: /* @__PURE__ */ v("label", {
|
|
58
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",
|
|
59
60
|
children: [
|
|
60
61
|
/* @__PURE__ */ _(h, { size: 20 }),
|
|
61
|
-
|
|
62
|
+
C("mediaLibrary.uploadMedia", "Upload Media"),
|
|
62
63
|
/* @__PURE__ */ _("input", {
|
|
63
64
|
type: "file",
|
|
64
65
|
multiple: !0,
|
|
65
66
|
accept: "image/*,video/*,audio/*,.pdf,.doc,.docx",
|
|
66
|
-
onChange: (e) =>
|
|
67
|
+
onChange: (e) => z(e.target.files),
|
|
67
68
|
className: "hidden",
|
|
68
|
-
"aria-label":
|
|
69
|
+
"aria-label": C("mediaLibrary.uploadMedia", "Upload Media")
|
|
69
70
|
})
|
|
70
71
|
]
|
|
71
72
|
})
|
|
@@ -80,50 +81,50 @@ function x() {
|
|
|
80
81
|
size: 20
|
|
81
82
|
}), /* @__PURE__ */ _("input", {
|
|
82
83
|
type: "text",
|
|
83
|
-
placeholder:
|
|
84
|
-
value:
|
|
85
|
-
onChange: (e) =>
|
|
84
|
+
placeholder: C("mediaLibrary.searchPlaceholder", "Search media..."),
|
|
85
|
+
value: F,
|
|
86
|
+
onChange: (e) => I(e.target.value),
|
|
86
87
|
className: "w-full pl-[3.25rem] pr-4 py-2 min-h-[44px] border border-border-subtle rounded-md bg-bg-surface",
|
|
87
|
-
"aria-label":
|
|
88
|
+
"aria-label": C("mediaLibrary.searchPlaceholder", "Search media...")
|
|
88
89
|
})]
|
|
89
90
|
}), /* @__PURE__ */ v("select", {
|
|
90
|
-
value:
|
|
91
|
-
onChange: (e) =>
|
|
91
|
+
value: L,
|
|
92
|
+
onChange: (e) => R(e.target.value),
|
|
92
93
|
className: "px-4 py-2 min-h-[44px] border border-border-subtle rounded-md bg-bg-surface",
|
|
93
|
-
"aria-label":
|
|
94
|
+
"aria-label": C("mediaLibrary.filterByType", "Filter by media type"),
|
|
94
95
|
children: [
|
|
95
96
|
/* @__PURE__ */ _("option", {
|
|
96
97
|
value: "all",
|
|
97
|
-
children:
|
|
98
|
+
children: C("mediaLibrary.allTypes", "All Types")
|
|
98
99
|
}),
|
|
99
100
|
/* @__PURE__ */ _("option", {
|
|
100
101
|
value: "IMAGE",
|
|
101
|
-
children:
|
|
102
|
+
children: C("mediaLibrary.typeImages", "Images")
|
|
102
103
|
}),
|
|
103
104
|
/* @__PURE__ */ _("option", {
|
|
104
105
|
value: "VIDEO",
|
|
105
|
-
children:
|
|
106
|
+
children: C("mediaLibrary.typeVideos", "Videos")
|
|
106
107
|
}),
|
|
107
108
|
/* @__PURE__ */ _("option", {
|
|
108
109
|
value: "AUDIO",
|
|
109
|
-
children:
|
|
110
|
+
children: C("mediaLibrary.typeAudio", "Audio")
|
|
110
111
|
}),
|
|
111
112
|
/* @__PURE__ */ _("option", {
|
|
112
113
|
value: "DOCUMENT",
|
|
113
|
-
children:
|
|
114
|
+
children: C("mediaLibrary.typeDocuments", "Documents")
|
|
114
115
|
})
|
|
115
116
|
]
|
|
116
117
|
})]
|
|
117
118
|
}),
|
|
118
|
-
Object.keys(
|
|
119
|
+
Object.keys(P).length > 0 && /* @__PURE__ */ _("div", {
|
|
119
120
|
className: "space-y-2",
|
|
120
|
-
children: Object.entries(
|
|
121
|
+
children: Object.entries(P).map(([e, t]) => /* @__PURE__ */ v("div", {
|
|
121
122
|
className: "border border-border-subtle rounded-md p-3",
|
|
122
123
|
children: [/* @__PURE__ */ v("div", {
|
|
123
124
|
className: "flex items-center justify-between mb-2",
|
|
124
125
|
children: [/* @__PURE__ */ _("span", {
|
|
125
126
|
className: "text-sm",
|
|
126
|
-
children:
|
|
127
|
+
children: C("mediaLibrary.uploading", "Uploading...")
|
|
127
128
|
}), /* @__PURE__ */ v("span", {
|
|
128
129
|
className: "text-sm text-text-secondary",
|
|
129
130
|
children: [t, "%"]
|
|
@@ -137,10 +138,11 @@ function x() {
|
|
|
137
138
|
})]
|
|
138
139
|
}, e))
|
|
139
140
|
}),
|
|
140
|
-
|
|
141
|
+
A.length > 0 ? /* @__PURE__ */ _("div", {
|
|
141
142
|
className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4",
|
|
142
|
-
children:
|
|
143
|
-
|
|
143
|
+
children: A.map((e) => /* @__PURE__ */ v(y, {
|
|
144
|
+
treatment: "plain",
|
|
145
|
+
className: "overflow-hidden p-0 border-border-seam shadow-[var(--shadow-pop)]",
|
|
144
146
|
children: [/* @__PURE__ */ _("div", {
|
|
145
147
|
className: "aspect-video bg-bg-sunken flex items-center justify-center",
|
|
146
148
|
children: e.type === "IMAGE" ? /* @__PURE__ */ _("img", {
|
|
@@ -149,7 +151,7 @@ function x() {
|
|
|
149
151
|
className: "w-full h-full object-cover"
|
|
150
152
|
}) : /* @__PURE__ */ _("div", {
|
|
151
153
|
className: "text-text-secondary",
|
|
152
|
-
children:
|
|
154
|
+
children: B(e.type)
|
|
153
155
|
})
|
|
154
156
|
}), /* @__PURE__ */ v("div", {
|
|
155
157
|
className: "p-3",
|
|
@@ -162,13 +164,13 @@ function x() {
|
|
|
162
164
|
className: "flex items-center justify-between mt-2 text-xs text-text-secondary",
|
|
163
165
|
children: [/* @__PURE__ */ _("span", { children: e.type }), /* @__PURE__ */ _("span", { children: i(e.size) })]
|
|
164
166
|
}),
|
|
165
|
-
|
|
167
|
+
k && /* @__PURE__ */ v("button", {
|
|
166
168
|
onClick: () => {
|
|
167
|
-
window.confirm(
|
|
169
|
+
window.confirm(C("mediaLibrary.deleteConfirm", "Delete this media?")) && N(e.id);
|
|
168
170
|
},
|
|
169
171
|
type: "button",
|
|
170
172
|
className: "mt-2 w-full p-2 min-h-[44px] text-xs text-status-error-text border border-status-error-border/30 rounded hover:bg-status-error-bg/10 flex items-center justify-center gap-1",
|
|
171
|
-
children: [/* @__PURE__ */ _(m, { size: 12 }),
|
|
173
|
+
children: [/* @__PURE__ */ _(m, { size: 12 }), C("mediaLibrary.delete", "Delete")]
|
|
172
174
|
})
|
|
173
175
|
]
|
|
174
176
|
})]
|
|
@@ -182,11 +184,11 @@ function x() {
|
|
|
182
184
|
}),
|
|
183
185
|
/* @__PURE__ */ _("p", {
|
|
184
186
|
className: "text-text-secondary mb-2",
|
|
185
|
-
children:
|
|
187
|
+
children: C("mediaLibrary.noMedia", "No media files")
|
|
186
188
|
}),
|
|
187
189
|
/* @__PURE__ */ _("p", {
|
|
188
190
|
className: "text-sm text-text-secondary",
|
|
189
|
-
children:
|
|
191
|
+
children: C("mediaLibrary.uploadFirstMedia", "Upload your first media file to get started")
|
|
190
192
|
})
|
|
191
193
|
]
|
|
192
194
|
})
|
|
@@ -194,6 +196,6 @@ function x() {
|
|
|
194
196
|
});
|
|
195
197
|
}
|
|
196
198
|
//#endregion
|
|
197
|
-
export {
|
|
199
|
+
export { S as MediaLibraryPage };
|
|
198
200
|
|
|
199
201
|
//# 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 { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useMediaLibrary } from '../hooks/useMediaLibrary';\nimport { useToursPermissionAccess } from '../hooks/useToursPermissionAccess';\nimport { useToursContext } from '../providers/ToursProvider';\nimport { TOUR_PERMISSIONS } from '../../constants/permissions';\nimport { formatFileSize } from '../utils';\nimport type { TourMediaType } from '../types';\nimport { emitToursApiFailed } from '../telemetry';\n\n/**\n * Media library page\n * Browse, upload, and manage tour media\n */\nexport function MediaLibraryPage() {\n const { t } = useI18n();\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n },\n [t]\n );\n\n const toursContext = useToursContext();\n const eventBus = useEventBus();\n\n const { hasPermission, isLoading: permissionsLoading } = useToursPermissionAccess();\n const canListMedia = hasPermission(TOUR_PERMISSIONS.TOUR_MEDIA_LIST);\n const canDeleteMedia = hasPermission(TOUR_PERMISSIONS.TOUR_MEDIA_DELETE);\n\n const { media, fetchMedia, uploadMedia, deleteMedia, uploadProgress } = useMediaLibrary();\n\n const [searchQuery, setSearchQuery] = useState('');\n const [typeFilter, setTypeFilter] = useState<TourMediaType | 'all'>('all');\n\n useEffect(() => {\n if (permissionsLoading || !canListMedia) return;\n fetchMedia(typeFilter === 'all' ? undefined : typeFilter, searchQuery);\n }, [fetchMedia, typeFilter, searchQuery, permissionsLoading, canListMedia]);\n\n const handleFileUpload = async (files: FileList | null) => {\n if (!files) return;\n\n await Promise.all(\n Array.from(files).map(async (file) => {\n try {\n await uploadMedia(file);\n } catch {\n emitToursApiFailed(eventBus, 'media.upload', toursContext);\n }\n })\n );\n\n // Refresh media list\n fetchMedia(typeFilter === 'all' ? undefined : typeFilter, searchQuery);\n };\n\n const getMediaIcon = (type: TourMediaType) => {\n switch (type) {\n case 'IMAGE':\n return <ImageIcon size={20} />;\n case 'VIDEO':\n return <Video size={20} />;\n case 'AUDIO':\n return <Music size={20} />;\n case 'DOCUMENT':\n return <FileText size={20} />;\n }\n };\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3\">\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold\">\n {tr('mediaLibrary.title', 'Media Library')}\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {tr('mediaLibrary.subtitle', 'Upload and manage media for tour steps')}\n </p>\n </div>\n <PermissionGate permission={TOUR_PERMISSIONS.TOUR_MEDIA_CREATE}>\n <label className=\"w-full sm:w-auto px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 cursor-pointer flex items-center justify-center gap-2\">\n <Upload size={20} />\n {tr('mediaLibrary.uploadMedia', 'Upload Media')}\n <input\n type=\"file\"\n multiple\n accept=\"image/*,video/*,audio/*,.pdf,.doc,.docx\"\n onChange={(e) => handleFileUpload(e.target.files)}\n className=\"hidden\"\n aria-label={tr('mediaLibrary.uploadMedia', 'Upload Media')}\n />\n </label>\n </PermissionGate>\n </div>\n\n {/* Filters */}\n <div className=\"flex flex-col sm:flex-row gap-3\">\n <div className=\"flex-1 relative\">\n <Search\n className=\"absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary\"\n size={20}\n />\n <input\n type=\"text\"\n placeholder={tr('mediaLibrary.searchPlaceholder', 'Search media...')}\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full pl-[3.25rem] pr-4 py-2 min-h-[44px] border border-border-subtle rounded-md bg-bg-surface\"\n aria-label={tr('mediaLibrary.searchPlaceholder', 'Search media...')}\n />\n </div>\n <select\n value={typeFilter}\n onChange={(e) => setTypeFilter(e.target.value as TourMediaType | 'all')}\n className=\"px-4 py-2 min-h-[44px] border border-border-subtle rounded-md bg-bg-surface\"\n aria-label={tr('mediaLibrary.filterByType', 'Filter by media type')}\n >\n <option value=\"all\">{tr('mediaLibrary.allTypes', 'All Types')}</option>\n <option value=\"IMAGE\">{tr('mediaLibrary.typeImages', 'Images')}</option>\n <option value=\"VIDEO\">{tr('mediaLibrary.typeVideos', 'Videos')}</option>\n <option value=\"AUDIO\">{tr('mediaLibrary.typeAudio', 'Audio')}</option>\n <option value=\"DOCUMENT\">{tr('mediaLibrary.typeDocuments', 'Documents')}</option>\n </select>\n </div>\n\n {/* Upload Progress */}\n {Object.keys(uploadProgress).length > 0 && (\n <div className=\"space-y-2\">\n {Object.entries(uploadProgress).map(([fileId, progress]) => (\n <div key={fileId} className=\"border border-border-subtle rounded-md p-3\">\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm\">{tr('mediaLibrary.uploading', 'Uploading...')}</span>\n <span className=\"text-sm text-text-secondary\">{progress}%</span>\n </div>\n <div className=\"w-full bg-bg-sunken rounded-full h-2\">\n <div\n className=\"bg-action-primary-bg h-2 rounded-full transition-all\"\n style={{ width: `${progress}%` }}\n />\n </div>\n </div>\n ))}\n </div>\n )}\n\n {/* Media Grid */}\n {media.length > 0 ? (\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4\">\n {media.map((item) => (\n <div\n key={item.id}\n className=\"border border-border-subtle rounded-md overflow-hidden hover:shadow-md transition-shadow\"\n >\n {/* Thumbnail */}\n <div className=\"aspect-video bg-bg-sunken flex items-center justify-center\">\n {item.type === 'IMAGE' ? (\n <img\n src={item.cdnUrl}\n alt={item.altText || item.name}\n className=\"w-full h-full object-cover\"\n />\n ) : (\n <div className=\"text-text-secondary\">{getMediaIcon(item.type)}</div>\n )}\n </div>\n\n {/* Info */}\n <div className=\"p-3\">\n <h3 className=\"font-medium text-sm truncate\">{item.name}</h3>\n <div className=\"flex items-center justify-between mt-2 text-xs text-text-secondary\">\n <span>{item.type}</span>\n <span>{formatFileSize(item.size)}</span>\n </div>\n {canDeleteMedia && (\n <button\n onClick={() => {\n if (window.confirm(tr('mediaLibrary.deleteConfirm', 'Delete this media?')))\n deleteMedia(item.id);\n }}\n type=\"button\"\n className=\"mt-2 w-full p-2 min-h-[44px] text-xs text-status-error-text border border-status-error-border/30 rounded hover:bg-status-error-bg/10 flex items-center justify-center gap-1\"\n >\n <Trash2 size={12} />\n {tr('mediaLibrary.delete', 'Delete')}\n </button>\n )}\n </div>\n </div>\n ))}\n </div>\n ) : (\n <div className=\"text-center py-12 border border-dashed border-border-subtle rounded-md\">\n <Upload className=\"mx-auto mb-4 text-text-secondary\" size={48} />\n <p className=\"text-text-secondary mb-2\">\n {tr('mediaLibrary.noMedia', 'No media files')}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {tr('mediaLibrary.uploadFirstMedia', 'Upload your first media file to get started')}\n </p>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
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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"}
|