rimelight-components 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.mjs +8 -1
- package/dist/runtime/components/app/Header.d.vue.ts +6 -6
- package/dist/runtime/components/app/Header.vue +44 -24
- package/dist/runtime/components/app/Header.vue.d.ts +6 -6
- package/dist/runtime/components/{app/ConstructionBanner.d.vue.ts → feedback/Feedback.d.vue.ts} +4 -1
- package/dist/runtime/components/feedback/Feedback.vue +207 -0
- package/dist/runtime/components/{app/ConstructionBanner.vue.d.ts → feedback/Feedback.vue.d.ts} +4 -1
- package/dist/runtime/components/feedback/FeedbackChart.d.vue.ts +18 -0
- package/dist/runtime/components/feedback/FeedbackChart.vue +604 -0
- package/dist/runtime/components/feedback/FeedbackChart.vue.d.ts +18 -0
- package/dist/runtime/components/feedback/FeedbackDatePicker.d.vue.ts +3 -0
- package/dist/runtime/components/feedback/FeedbackDatePicker.vue +149 -0
- package/dist/runtime/components/feedback/FeedbackDatePicker.vue.d.ts +3 -0
- package/dist/runtime/components/feedback/FeedbackItem.d.vue.ts +10 -0
- package/dist/runtime/components/feedback/FeedbackItem.vue +77 -0
- package/dist/runtime/components/feedback/FeedbackItem.vue.d.ts +10 -0
- package/dist/runtime/components/feedback/FeedbackStatCard.d.vue.ts +17 -0
- package/dist/runtime/components/feedback/FeedbackStatCard.vue +66 -0
- package/dist/runtime/components/feedback/FeedbackStatCard.vue.d.ts +17 -0
- package/package.json +21 -10
- package/dist/runtime/components/app/ConstructionBanner.vue +0 -23
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const {} = defineProps({
|
|
3
|
+
pageAnalytics: { type: Array, required: true }
|
|
4
|
+
});
|
|
5
|
+
const { dateRange } = useDateRange();
|
|
6
|
+
const chartType = ref(`line`);
|
|
7
|
+
const selectedPagePaths = ref([]);
|
|
8
|
+
const showPageSelector = ref(false);
|
|
9
|
+
const pageSearchQuery = ref(``);
|
|
10
|
+
const hasValidData = computed(() => {
|
|
11
|
+
return props.pageAnalytics && props.pageAnalytics.length > 0 && props.pageAnalytics.some((p) => p && p.total > 0);
|
|
12
|
+
});
|
|
13
|
+
const availablePages = computed(() => {
|
|
14
|
+
if (!props.pageAnalytics) return [];
|
|
15
|
+
const pages = props.pageAnalytics.filter((p) => p && p.total > 0).sort((a, b) => b.total - a.total).map((page) => ({
|
|
16
|
+
path: page.path,
|
|
17
|
+
title: page.lastFeedback?.title || page.path,
|
|
18
|
+
total: page.total,
|
|
19
|
+
score: page.averageScore
|
|
20
|
+
}));
|
|
21
|
+
if (!pageSearchQuery.value.trim()) {
|
|
22
|
+
return pages;
|
|
23
|
+
}
|
|
24
|
+
const searchTerm = pageSearchQuery.value.toLowerCase().trim();
|
|
25
|
+
return pages.filter(
|
|
26
|
+
(page) => page.title.toLowerCase().includes(searchTerm) || page.path.toLowerCase().includes(searchTerm)
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
watch(
|
|
30
|
+
() => props.pageAnalytics,
|
|
31
|
+
(analytics) => {
|
|
32
|
+
if (selectedPagePaths.value.length === 0 && analytics && analytics.length > 0) {
|
|
33
|
+
const validAnalytics = analytics.filter((p) => p && p.total > 0);
|
|
34
|
+
if (validAnalytics.length > 0) {
|
|
35
|
+
const topPages = validAnalytics.sort((a, b) => b.total - a.total).slice(0, Math.min(5, validAnalytics.length));
|
|
36
|
+
selectedPagePaths.value = topPages.map((p) => p.path);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
immediate: true
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
const overallChartData = computed(() => {
|
|
45
|
+
const data = [];
|
|
46
|
+
const endDate = dateRange.value.end;
|
|
47
|
+
const startDate = dateRange.value.start;
|
|
48
|
+
const daysDiff = Math.ceil(
|
|
49
|
+
(endDate.getTime() - startDate.getTime()) / (1e3 * 60 * 60 * 24)
|
|
50
|
+
);
|
|
51
|
+
const dailyScores = {};
|
|
52
|
+
if (hasValidData.value && props.pageAnalytics) {
|
|
53
|
+
props.pageAnalytics.forEach((page) => {
|
|
54
|
+
if (!page || !page.feedback) return;
|
|
55
|
+
page.feedback.forEach((feedback) => {
|
|
56
|
+
const feedbackDate = new Date(feedback.createdAt);
|
|
57
|
+
if (feedbackDate >= startDate && feedbackDate <= endDate) {
|
|
58
|
+
const dateStr = feedbackDate.toISOString().split(`T`)[0];
|
|
59
|
+
if (!dailyScores[dateStr]) {
|
|
60
|
+
dailyScores[dateStr] = [];
|
|
61
|
+
}
|
|
62
|
+
const ratingScore = FEEDBACK_OPTIONS.find((opt) => opt.value === feedback.rating)?.score || 0;
|
|
63
|
+
dailyScores[dateStr].push(ratingScore);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
let lastKnownValue = 0;
|
|
69
|
+
for (let i = daysDiff - 1; i >= 0; i--) {
|
|
70
|
+
const date = new Date(endDate);
|
|
71
|
+
date.setDate(date.getDate() - i);
|
|
72
|
+
const dateStr = date.toISOString().split(`T`)[0];
|
|
73
|
+
const entry = {
|
|
74
|
+
date: dateStr,
|
|
75
|
+
day: date.toLocaleDateString(`en-US`, {
|
|
76
|
+
month: `short`,
|
|
77
|
+
day: `numeric`
|
|
78
|
+
})
|
|
79
|
+
};
|
|
80
|
+
if (hasValidData.value && dailyScores[dateStr] && dailyScores[dateStr].length > 0) {
|
|
81
|
+
const dayAverage = dailyScores[dateStr].reduce((sum, score) => sum + score, 0) / dailyScores[dateStr].length;
|
|
82
|
+
lastKnownValue = Math.min(4, Number(dayAverage.toFixed(2)));
|
|
83
|
+
}
|
|
84
|
+
entry.average = lastKnownValue;
|
|
85
|
+
data.push(entry);
|
|
86
|
+
}
|
|
87
|
+
return data;
|
|
88
|
+
});
|
|
89
|
+
const timeBasedChartData = computed(() => {
|
|
90
|
+
const data = [];
|
|
91
|
+
const endDate = dateRange.value.end;
|
|
92
|
+
const startDate = dateRange.value.start;
|
|
93
|
+
const daysDiff = Math.ceil(
|
|
94
|
+
(endDate.getTime() - startDate.getTime()) / (1e3 * 60 * 60 * 24)
|
|
95
|
+
);
|
|
96
|
+
if (!hasValidData.value || selectedPagePaths.value.length === 0 || !props.pageAnalytics) {
|
|
97
|
+
for (let i = daysDiff - 1; i >= 0; i--) {
|
|
98
|
+
const date = new Date(endDate);
|
|
99
|
+
date.setDate(date.getDate() - i);
|
|
100
|
+
data.push({
|
|
101
|
+
date: date.toISOString().split(`T`)[0],
|
|
102
|
+
day: date.toLocaleDateString(`en-US`, {
|
|
103
|
+
month: `short`,
|
|
104
|
+
day: `numeric`
|
|
105
|
+
}),
|
|
106
|
+
placeholder: 0
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return data;
|
|
110
|
+
}
|
|
111
|
+
const dailyScores = {};
|
|
112
|
+
const selectedPages = props.pageAnalytics.filter(
|
|
113
|
+
(p) => p && selectedPagePaths.value.includes(p.path)
|
|
114
|
+
);
|
|
115
|
+
selectedPages.forEach((page) => {
|
|
116
|
+
if (!page || !page.feedback) return;
|
|
117
|
+
const pageKey = page.path.split(`/`).pop()?.replace(/[^a-z0-9]/gi, ``) || `page`;
|
|
118
|
+
page.feedback.forEach((feedback) => {
|
|
119
|
+
const feedbackDate = new Date(feedback.createdAt);
|
|
120
|
+
if (feedbackDate >= startDate && feedbackDate <= endDate) {
|
|
121
|
+
const dateStr = feedbackDate.toISOString().split(`T`)[0];
|
|
122
|
+
if (!dailyScores[dateStr]) {
|
|
123
|
+
dailyScores[dateStr] = {};
|
|
124
|
+
}
|
|
125
|
+
if (!dailyScores[dateStr][pageKey]) {
|
|
126
|
+
dailyScores[dateStr][pageKey] = [];
|
|
127
|
+
}
|
|
128
|
+
const ratingScore = FEEDBACK_OPTIONS.find((opt) => opt.value === feedback.rating)?.score || 0;
|
|
129
|
+
dailyScores[dateStr][pageKey].push(ratingScore);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
const lastKnownValues = {};
|
|
134
|
+
for (let i = daysDiff - 1; i >= 0; i--) {
|
|
135
|
+
const date = new Date(endDate);
|
|
136
|
+
date.setDate(date.getDate() - i);
|
|
137
|
+
const dateStr = date.toISOString().split(`T`)[0];
|
|
138
|
+
const entry = {
|
|
139
|
+
date: dateStr,
|
|
140
|
+
day: date.toLocaleDateString(`en-US`, {
|
|
141
|
+
month: `short`,
|
|
142
|
+
day: `numeric`
|
|
143
|
+
})
|
|
144
|
+
};
|
|
145
|
+
selectedPages.forEach((page) => {
|
|
146
|
+
if (!page) return;
|
|
147
|
+
const pageKey = page.path.split(`/`).pop()?.replace(/[^a-z0-9]/gi, ``) || `page`;
|
|
148
|
+
if (dailyScores[dateStr] && dailyScores[dateStr][pageKey] && dailyScores[dateStr][pageKey].length > 0) {
|
|
149
|
+
const dayAverage = dailyScores[dateStr][pageKey].reduce((sum, score) => sum + score, 0) / dailyScores[dateStr][pageKey].length;
|
|
150
|
+
lastKnownValues[pageKey] = Math.min(4, Number(dayAverage.toFixed(2)));
|
|
151
|
+
}
|
|
152
|
+
entry[pageKey] = lastKnownValues[pageKey] || 0;
|
|
153
|
+
});
|
|
154
|
+
data.push(entry);
|
|
155
|
+
}
|
|
156
|
+
return data;
|
|
157
|
+
});
|
|
158
|
+
const comparisonChartData = computed(() => {
|
|
159
|
+
if (!hasValidData.value || selectedPagePaths.value.length === 0 || !props.pageAnalytics) {
|
|
160
|
+
return [
|
|
161
|
+
{
|
|
162
|
+
page: `No Data`,
|
|
163
|
+
positive: 0,
|
|
164
|
+
negative: 0
|
|
165
|
+
}
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
return props.pageAnalytics.filter((p) => p && selectedPagePaths.value.includes(p.path)).map((page) => {
|
|
169
|
+
const title = page.lastFeedback?.title || page.path;
|
|
170
|
+
const shortTitle = title.length > 15 ? title.substring(0, 15) + `...` : title;
|
|
171
|
+
return {
|
|
172
|
+
page: shortTitle,
|
|
173
|
+
positive: page.positive,
|
|
174
|
+
negative: page.negative
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
const chartData = computed(() => {
|
|
179
|
+
if (chartType.value === `compare`) return comparisonChartData.value;
|
|
180
|
+
if (chartType.value === `overall`) return overallChartData.value;
|
|
181
|
+
return timeBasedChartData.value;
|
|
182
|
+
});
|
|
183
|
+
const chartCategories = computed(() => {
|
|
184
|
+
if (!hasValidData.value) {
|
|
185
|
+
return {
|
|
186
|
+
placeholder: {
|
|
187
|
+
name: `No Data Available`,
|
|
188
|
+
color: `#6b7280`
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (chartType.value === `compare`) {
|
|
193
|
+
return {
|
|
194
|
+
positive: {
|
|
195
|
+
name: `Positive`,
|
|
196
|
+
color: `var(--ui-success)`
|
|
197
|
+
},
|
|
198
|
+
negative: {
|
|
199
|
+
name: `Negative`,
|
|
200
|
+
color: `var(--ui-error)`
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (chartType.value === `overall`) {
|
|
205
|
+
return {
|
|
206
|
+
average: {
|
|
207
|
+
name: `Overall Rating`,
|
|
208
|
+
color: `#3b82f6`
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (selectedPagePaths.value.length === 0 || !props.pageAnalytics) {
|
|
213
|
+
return {
|
|
214
|
+
placeholder: {
|
|
215
|
+
name: `No Pages Selected`,
|
|
216
|
+
color: `#6b7280`
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const selectedPages = props.pageAnalytics.filter(
|
|
221
|
+
(p) => p && selectedPagePaths.value.includes(p.path)
|
|
222
|
+
);
|
|
223
|
+
const colors = [
|
|
224
|
+
`#3b82f6`,
|
|
225
|
+
`#10b981`,
|
|
226
|
+
`#f59e0b`,
|
|
227
|
+
`#ef4444`,
|
|
228
|
+
`#8b5cf6`,
|
|
229
|
+
`#06b6d4`,
|
|
230
|
+
`#84cc16`,
|
|
231
|
+
`#f97316`
|
|
232
|
+
];
|
|
233
|
+
return selectedPages.reduce(
|
|
234
|
+
(acc, page, index) => {
|
|
235
|
+
if (!page) return acc;
|
|
236
|
+
const key = page.path.split(`/`).pop()?.replace(/[^a-z0-9]/gi, ``) || `page`;
|
|
237
|
+
const title = page.lastFeedback?.title || page.path;
|
|
238
|
+
acc[key] = {
|
|
239
|
+
name: title.length > 25 ? title.substring(0, 25) + `...` : title,
|
|
240
|
+
color: colors[index % colors.length]
|
|
241
|
+
};
|
|
242
|
+
return acc;
|
|
243
|
+
},
|
|
244
|
+
{}
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
const xFormatter = (index) => {
|
|
248
|
+
if (chartType.value === `compare`) {
|
|
249
|
+
return comparisonChartData.value[index]?.page || ``;
|
|
250
|
+
}
|
|
251
|
+
if (chartType.value === `overall`) {
|
|
252
|
+
return overallChartData.value[index]?.day || ``;
|
|
253
|
+
}
|
|
254
|
+
return timeBasedChartData.value[index]?.day || ``;
|
|
255
|
+
};
|
|
256
|
+
const yFormatter = (value) => {
|
|
257
|
+
if (chartType.value === `compare`) {
|
|
258
|
+
return Math.round(value).toString();
|
|
259
|
+
}
|
|
260
|
+
return value === 0 ? `0` : `${Number(value).toFixed(1)}/4`;
|
|
261
|
+
};
|
|
262
|
+
const dateRangeLabel = computed(() => {
|
|
263
|
+
if (chartType.value === `compare`) {
|
|
264
|
+
return `Selected Pages (${selectedPagePaths.value.length})`;
|
|
265
|
+
}
|
|
266
|
+
const daysDiff = Math.ceil(
|
|
267
|
+
(dateRange.value.end.getTime() - dateRange.value.start.getTime()) / (1e3 * 60 * 60 * 24)
|
|
268
|
+
);
|
|
269
|
+
if (daysDiff <= 7) return `Last ${daysDiff} days`;
|
|
270
|
+
if (daysDiff <= 31) return `Last ${daysDiff} days`;
|
|
271
|
+
if (daysDiff <= 93) return `Last ${Math.round(daysDiff / 30)} months`;
|
|
272
|
+
if (daysDiff <= 186) return `Last ${Math.round(daysDiff / 30)} months`;
|
|
273
|
+
return `Last ${Math.round(daysDiff / 365)} year${daysDiff > 730 ? `s` : ``}`;
|
|
274
|
+
});
|
|
275
|
+
const chartTitle = computed(() => {
|
|
276
|
+
switch (chartType.value) {
|
|
277
|
+
case `line`:
|
|
278
|
+
return `Rating Evolution`;
|
|
279
|
+
case `compare`:
|
|
280
|
+
return `Page Comparison`;
|
|
281
|
+
case `overall`:
|
|
282
|
+
return `Overall Documentation Rating`;
|
|
283
|
+
default:
|
|
284
|
+
return `Rating Evolution`;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
const chartDescription = computed(() => {
|
|
288
|
+
switch (chartType.value) {
|
|
289
|
+
case `line`:
|
|
290
|
+
return `Track selected pages satisfaction over time`;
|
|
291
|
+
case `compare`:
|
|
292
|
+
return `Compare feedback distribution across pages`;
|
|
293
|
+
case `overall`:
|
|
294
|
+
return `Global documentation satisfaction evolution`;
|
|
295
|
+
default:
|
|
296
|
+
return `Track selected pages satisfaction over time`;
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
const chartIcon = computed(() => {
|
|
300
|
+
switch (chartType.value) {
|
|
301
|
+
case `line`:
|
|
302
|
+
return `i-lucide-trending-up`;
|
|
303
|
+
case `compare`:
|
|
304
|
+
return `i-lucide-bar-chart-4`;
|
|
305
|
+
case `overall`:
|
|
306
|
+
return `i-lucide-activity`;
|
|
307
|
+
default:
|
|
308
|
+
return `i-lucide-trending-up`;
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
const availableChartTypes = [
|
|
312
|
+
{
|
|
313
|
+
value: `line`,
|
|
314
|
+
label: `Line`,
|
|
315
|
+
icon: `i-lucide-trending-up`
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
value: `compare`,
|
|
319
|
+
label: `Compare`,
|
|
320
|
+
icon: `i-lucide-bar-chart-4`
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
value: `overall`,
|
|
324
|
+
label: `Overall`,
|
|
325
|
+
icon: `i-lucide-activity`
|
|
326
|
+
}
|
|
327
|
+
];
|
|
328
|
+
function togglePageSelection(pagePath) {
|
|
329
|
+
const index = selectedPagePaths.value.indexOf(pagePath);
|
|
330
|
+
if (index > -1) {
|
|
331
|
+
selectedPagePaths.value.splice(index, 1);
|
|
332
|
+
} else {
|
|
333
|
+
selectedPagePaths.value.push(pagePath);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function selectTopPages(count) {
|
|
337
|
+
if (!props.pageAnalytics) return;
|
|
338
|
+
const pages = props.pageAnalytics.filter((p) => p && p.total > 0).sort((a, b) => b.total - a.total).slice(0, count);
|
|
339
|
+
selectedPagePaths.value = pages.map((p) => p.path);
|
|
340
|
+
}
|
|
341
|
+
function selectBestRatedPages(count) {
|
|
342
|
+
if (!props.pageAnalytics) return;
|
|
343
|
+
const pages = props.pageAnalytics.filter((p) => p && p.total > 0).sort((a, b) => b.averageScore - a.averageScore).slice(0, count);
|
|
344
|
+
selectedPagePaths.value = pages.map((p) => p.path);
|
|
345
|
+
}
|
|
346
|
+
function selectWorstPages(count) {
|
|
347
|
+
if (!props.pageAnalytics) return;
|
|
348
|
+
const pages = props.pageAnalytics.filter((p) => p && p.total > 0).sort((a, b) => a.averageScore - b.averageScore).slice(0, count);
|
|
349
|
+
selectedPagePaths.value = pages.map((p) => p.path);
|
|
350
|
+
}
|
|
351
|
+
</script>
|
|
352
|
+
|
|
353
|
+
<template>
|
|
354
|
+
<div class="space-y-6">
|
|
355
|
+
<div
|
|
356
|
+
class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"
|
|
357
|
+
>
|
|
358
|
+
<Motion
|
|
359
|
+
:key="`header-${chartType}`"
|
|
360
|
+
:initial="{ opacity: 0, y: 10 }"
|
|
361
|
+
:animate="{ opacity: 1, y: 0 }"
|
|
362
|
+
:transition="{ duration: 0.4, ease: 'easeInOut' }"
|
|
363
|
+
class="flex items-center gap-3"
|
|
364
|
+
>
|
|
365
|
+
<UIcon
|
|
366
|
+
:name="chartIcon"
|
|
367
|
+
class="size-6 shrink-0 text-primary sm:size-5"
|
|
368
|
+
/>
|
|
369
|
+
<div class="min-w-0">
|
|
370
|
+
<h3 class="truncate text-lg font-semibold">
|
|
371
|
+
{{ chartTitle }}
|
|
372
|
+
</h3>
|
|
373
|
+
<p class="text-sm text-muted">
|
|
374
|
+
{{ chartDescription }}
|
|
375
|
+
</p>
|
|
376
|
+
</div>
|
|
377
|
+
</Motion>
|
|
378
|
+
|
|
379
|
+
<div
|
|
380
|
+
class="flex flex-wrap items-center gap-2 max-sm:flex-row-reverse max-sm:justify-end"
|
|
381
|
+
>
|
|
382
|
+
<AnimatePresence mode="wait">
|
|
383
|
+
<Motion
|
|
384
|
+
v-if="chartType !== 'overall'"
|
|
385
|
+
:initial="{ opacity: 0, scale: 0.9 }"
|
|
386
|
+
:animate="{ opacity: 1, scale: 1 }"
|
|
387
|
+
:exit="{ opacity: 0, scale: 0.9 }"
|
|
388
|
+
:transition="{ duration: 0.3 }"
|
|
389
|
+
>
|
|
390
|
+
<UChip :text="selectedPagePaths.length" size="3xl">
|
|
391
|
+
<UButton
|
|
392
|
+
color="neutral"
|
|
393
|
+
variant="outline"
|
|
394
|
+
icon="i-lucide-settings"
|
|
395
|
+
@click="showPageSelector = true"
|
|
396
|
+
/>
|
|
397
|
+
</UChip>
|
|
398
|
+
</Motion>
|
|
399
|
+
</AnimatePresence>
|
|
400
|
+
|
|
401
|
+
<div
|
|
402
|
+
class="flex items-center gap-1 rounded-lg border border-default p-1"
|
|
403
|
+
>
|
|
404
|
+
<UButton
|
|
405
|
+
v-for="type in availableChartTypes"
|
|
406
|
+
:key="type.value"
|
|
407
|
+
:color="chartType === type.value ? 'primary' : 'neutral'"
|
|
408
|
+
:variant="chartType === type.value ? 'solid' : 'ghost'"
|
|
409
|
+
size="sm"
|
|
410
|
+
:icon="type.icon"
|
|
411
|
+
:label="type.label"
|
|
412
|
+
@click="chartType = type.value"
|
|
413
|
+
/>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
|
|
418
|
+
<div class="relative mb-8 overflow-hidden rounded-xl">
|
|
419
|
+
<div class="dot-pattern -top-5 right-0 left-0 h-[300px]" />
|
|
420
|
+
|
|
421
|
+
<LineChart
|
|
422
|
+
v-if="chartType === 'overall'"
|
|
423
|
+
:data="chartData"
|
|
424
|
+
:categories="chartCategories"
|
|
425
|
+
:x-formatter="xFormatter"
|
|
426
|
+
:y-formatter="yFormatter"
|
|
427
|
+
:x-label="dateRangeLabel"
|
|
428
|
+
y-label="Rating (out of 4)"
|
|
429
|
+
:show-tooltip="true"
|
|
430
|
+
/>
|
|
431
|
+
|
|
432
|
+
<LineChart
|
|
433
|
+
v-else-if="chartType === 'line'"
|
|
434
|
+
:data="chartData"
|
|
435
|
+
:categories="chartCategories"
|
|
436
|
+
:x-formatter="xFormatter"
|
|
437
|
+
:y-formatter="yFormatter"
|
|
438
|
+
:x-label="dateRangeLabel"
|
|
439
|
+
y-label="Rating (out of 4)"
|
|
440
|
+
:show-tooltip="true"
|
|
441
|
+
class="min-h-[300px]"
|
|
442
|
+
/>
|
|
443
|
+
|
|
444
|
+
<BarChart
|
|
445
|
+
v-else-if="chartType === 'compare'"
|
|
446
|
+
:data="chartData"
|
|
447
|
+
:categories="chartCategories"
|
|
448
|
+
:y-axis="['positive', 'negative']"
|
|
449
|
+
:stacked="true"
|
|
450
|
+
:x-formatter="xFormatter"
|
|
451
|
+
:y-formatter="yFormatter"
|
|
452
|
+
:x-label="dateRangeLabel"
|
|
453
|
+
y-label="Feedback Count"
|
|
454
|
+
:height="300"
|
|
455
|
+
:bar-padding="0.2"
|
|
456
|
+
:y-grid-line="false"
|
|
457
|
+
class="min-h-[300px]"
|
|
458
|
+
/>
|
|
459
|
+
</div>
|
|
460
|
+
|
|
461
|
+
<div v-if="!hasValidData" class="py-4 text-center">
|
|
462
|
+
<p class="text-sm text-muted">
|
|
463
|
+
Chart shows no data - will display real trends once feedback is
|
|
464
|
+
collected
|
|
465
|
+
</p>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
<UModal v-model:open="showPageSelector" :ui="{ content: 'max-w-2xl' }">
|
|
469
|
+
<template #content>
|
|
470
|
+
<UCard>
|
|
471
|
+
<template #header>
|
|
472
|
+
<UButton
|
|
473
|
+
size="sm"
|
|
474
|
+
variant="ghost"
|
|
475
|
+
color="neutral"
|
|
476
|
+
icon="i-lucide-x"
|
|
477
|
+
class="absolute top-2 right-2"
|
|
478
|
+
@click="showPageSelector = false"
|
|
479
|
+
/>
|
|
480
|
+
<div class="space-y-3">
|
|
481
|
+
<h3 class="text-lg font-semibold">
|
|
482
|
+
Select Pages to {{ chartType === "line" ? "Track" : "Compare" }}
|
|
483
|
+
</h3>
|
|
484
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
485
|
+
<span class="text-sm font-medium text-muted"
|
|
486
|
+
>Quick select:</span
|
|
487
|
+
>
|
|
488
|
+
<UButton
|
|
489
|
+
size="sm"
|
|
490
|
+
variant="soft"
|
|
491
|
+
color="neutral"
|
|
492
|
+
label="Best Rated 5"
|
|
493
|
+
@click="selectBestRatedPages(5)"
|
|
494
|
+
/>
|
|
495
|
+
<UButton
|
|
496
|
+
size="sm"
|
|
497
|
+
variant="soft"
|
|
498
|
+
color="neutral"
|
|
499
|
+
label="Most Popular 5"
|
|
500
|
+
@click="selectTopPages(5)"
|
|
501
|
+
/>
|
|
502
|
+
<UButton
|
|
503
|
+
size="sm"
|
|
504
|
+
variant="soft"
|
|
505
|
+
color="neutral"
|
|
506
|
+
label="Worst Rated 5"
|
|
507
|
+
@click="selectWorstPages(5)"
|
|
508
|
+
/>
|
|
509
|
+
<UButton
|
|
510
|
+
size="sm"
|
|
511
|
+
variant="soft"
|
|
512
|
+
color="neutral"
|
|
513
|
+
label="Worst Rated 10"
|
|
514
|
+
@click="selectWorstPages(10)"
|
|
515
|
+
/>
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
</template>
|
|
519
|
+
|
|
520
|
+
<div class="mb-4">
|
|
521
|
+
<UInput
|
|
522
|
+
v-model="pageSearchQuery"
|
|
523
|
+
placeholder="Search pages..."
|
|
524
|
+
icon="i-lucide-search"
|
|
525
|
+
class="w-full"
|
|
526
|
+
:ui="{ trailing: 'pe-1' }"
|
|
527
|
+
>
|
|
528
|
+
<template v-if="pageSearchQuery?.length" #trailing>
|
|
529
|
+
<UButton
|
|
530
|
+
color="neutral"
|
|
531
|
+
variant="link"
|
|
532
|
+
size="sm"
|
|
533
|
+
icon="i-lucide-circle-x"
|
|
534
|
+
aria-label="Clear input"
|
|
535
|
+
@click="pageSearchQuery = ''"
|
|
536
|
+
/>
|
|
537
|
+
</template>
|
|
538
|
+
</UInput>
|
|
539
|
+
</div>
|
|
540
|
+
|
|
541
|
+
<div class="max-h-96 space-y-3 overflow-y-auto">
|
|
542
|
+
<div
|
|
543
|
+
v-for="page in availablePages"
|
|
544
|
+
:key="page.path"
|
|
545
|
+
class="flex cursor-pointer items-center justify-between rounded-lg border border-default p-3 transition-colors hover:bg-muted/50"
|
|
546
|
+
:class="{
|
|
547
|
+
'border-primary/20 bg-primary/5 hover:bg-primary/10': selectedPagePaths.includes(page.path)
|
|
548
|
+
}"
|
|
549
|
+
@click="togglePageSelection(page.path)"
|
|
550
|
+
>
|
|
551
|
+
<div class="flex min-w-0 flex-1 items-center gap-3">
|
|
552
|
+
<UCheckbox
|
|
553
|
+
:model-value="selectedPagePaths.includes(page.path)"
|
|
554
|
+
@update:model-value="togglePageSelection(page.path)"
|
|
555
|
+
/>
|
|
556
|
+
<div class="min-w-0 flex-1">
|
|
557
|
+
<div class="truncate text-sm font-medium">
|
|
558
|
+
{{ page.title }}
|
|
559
|
+
</div>
|
|
560
|
+
<code class="block truncate text-xs text-muted">{{
|
|
561
|
+
page.path
|
|
562
|
+
}}</code>
|
|
563
|
+
</div>
|
|
564
|
+
</div>
|
|
565
|
+
<div class="flex shrink-0 items-center gap-2 text-sm sm:gap-4">
|
|
566
|
+
<div class="text-center">
|
|
567
|
+
<div class="font-semibold">
|
|
568
|
+
{{ page.total }}
|
|
569
|
+
</div>
|
|
570
|
+
<div class="text-xs text-muted">resp.</div>
|
|
571
|
+
</div>
|
|
572
|
+
<div class="text-center">
|
|
573
|
+
<div
|
|
574
|
+
class="font-semibold"
|
|
575
|
+
:class="
|
|
576
|
+
page.score >= 3.5 ? 'text-success' : page.score >= 3 ? 'text-warning' : 'text-error'
|
|
577
|
+
"
|
|
578
|
+
>
|
|
579
|
+
{{ page.score.toFixed(1) }}/4
|
|
580
|
+
</div>
|
|
581
|
+
<div class="text-xs text-muted">score</div>
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
</div>
|
|
585
|
+
|
|
586
|
+
<div v-if="availablePages.length === 0" class="py-8 text-center">
|
|
587
|
+
<UIcon
|
|
588
|
+
name="i-lucide-search-x"
|
|
589
|
+
class="mx-auto mb-2 size-8 text-muted"
|
|
590
|
+
/>
|
|
591
|
+
<p class="text-sm text-muted">
|
|
592
|
+
No pages found matching your search
|
|
593
|
+
</p>
|
|
594
|
+
</div>
|
|
595
|
+
</div>
|
|
596
|
+
</UCard>
|
|
597
|
+
</template>
|
|
598
|
+
</UModal>
|
|
599
|
+
</div>
|
|
600
|
+
</template>
|
|
601
|
+
|
|
602
|
+
<style>
|
|
603
|
+
:root{--vis-tooltip-background-color:hsla(0,0%,100%,.95)!important;--vis-tooltip-border-color:rgba(0,0,0,.1)!important;--vis-tooltip-text-color:rgba(0,0,0,.9)!important;--vis-tooltip-label-color:rgba(0,0,0,.7)!important;--vis-tooltip-value-color:#000!important;--vis-axis-grid-color:hsla(0,0%,100%,.1)!important;--vis-axis-tick-label-color:var(--ui-text-muted)!important;--vis-axis-label-color:var(--ui-text-toned)!important;--vis-legend-label-color:var(--ui-text-muted)!important;--dot-pattern-color:#111827}.dark{--vis-tooltip-background-color:rgba(15,23,42,.95)!important;--vis-tooltip-border-color:hsla(0,0%,100%,.1)!important;--vis-tooltip-text-color:hsla(0,0%,100%,.9)!important;--vis-tooltip-label-color:hsla(0,0%,100%,.7)!important;--vis-tooltip-value-color:#fff!important;--dot-pattern-color:#9ca3af}.dot-pattern{background-image:radial-gradient(var(--dot-pattern-color) 1px,transparent 1px);background-position:-8.5px -8.5px;background-size:7px 7px;-webkit-mask-image:radial-gradient(ellipse at center,#000,transparent 75%);mask-image:radial-gradient(ellipse at center,#000,transparent 75%);opacity:20%;position:absolute}
|
|
604
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type PageAnalytic = {
|
|
2
|
+
path: string;
|
|
3
|
+
total: number;
|
|
4
|
+
positive: number;
|
|
5
|
+
negative: number;
|
|
6
|
+
averageScore: number;
|
|
7
|
+
positivePercentage: number;
|
|
8
|
+
feedback: any[];
|
|
9
|
+
lastFeedback: any;
|
|
10
|
+
createdAt: Date;
|
|
11
|
+
updatedAt: Date;
|
|
12
|
+
};
|
|
13
|
+
type __VLS_Props = {
|
|
14
|
+
pageAnalytics: PageAnalytic[];
|
|
15
|
+
};
|
|
16
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
17
|
+
declare const _default: typeof __VLS_export;
|
|
18
|
+
export default _default;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|