rimelight-components 1.2.1 → 1.3.0
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 +6 -0
- package/dist/runtime/components/{feedback/Feedback.d.vue.ts → swatches/ColorSwatch.d.vue.ts} +5 -4
- package/dist/runtime/components/swatches/ColorSwatch.vue +90 -0
- package/dist/runtime/components/{feedback/Feedback.vue.d.ts → swatches/ColorSwatch.vue.d.ts} +5 -4
- package/package.json +2 -2
- package/dist/runtime/components/feedback/Feedback.vue +0 -207
- package/dist/runtime/components/feedback/FeedbackChart.d.vue.ts +0 -18
- package/dist/runtime/components/feedback/FeedbackChart.vue +0 -604
- package/dist/runtime/components/feedback/FeedbackChart.vue.d.ts +0 -18
- package/dist/runtime/components/feedback/FeedbackDatePicker.d.vue.ts +0 -3
- package/dist/runtime/components/feedback/FeedbackDatePicker.vue +0 -149
- package/dist/runtime/components/feedback/FeedbackDatePicker.vue.d.ts +0 -3
- package/dist/runtime/components/feedback/FeedbackItem.d.vue.ts +0 -10
- package/dist/runtime/components/feedback/FeedbackItem.vue +0 -77
- package/dist/runtime/components/feedback/FeedbackItem.vue.d.ts +0 -10
- package/dist/runtime/components/feedback/FeedbackStatCard.d.vue.ts +0 -17
- package/dist/runtime/components/feedback/FeedbackStatCard.vue +0 -66
- package/dist/runtime/components/feedback/FeedbackStatCard.vue.d.ts +0 -17
- package/dist/runtime/composables/useFeedback.d.ts +0 -50
- package/dist/runtime/composables/useFeedback.js +0 -237
- package/dist/runtime/composables/useFeedbackExports.d.ts +0 -4
- package/dist/runtime/composables/useFeedbackExports.js +0 -150
- package/dist/runtime/types/index.d.ts +0 -14
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { getLocalTimeZone, CalendarDate, today } from "@internationalized/date";
|
|
3
|
-
import { formatDateRange } from "little-date";
|
|
4
|
-
const { dateRange, setDateRange, setPresetRange } = useDateRange();
|
|
5
|
-
const formattedDateRange = computed(() => {
|
|
6
|
-
if (dateRange.value.start && dateRange.value.end) {
|
|
7
|
-
return formatDateRange(dateRange.value.start, dateRange.value.end, {
|
|
8
|
-
includeTime: false
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
return null;
|
|
12
|
-
});
|
|
13
|
-
const ranges = [
|
|
14
|
-
{
|
|
15
|
-
label: `Last 7 days`,
|
|
16
|
-
preset: `week`
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
label: `Last 30 days`,
|
|
20
|
-
preset: `month`
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
label: `Last 3 months`,
|
|
24
|
-
preset: `3months`
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
label: `Last 6 months`,
|
|
28
|
-
preset: `6months`
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
label: `Last year`,
|
|
32
|
-
preset: `year`
|
|
33
|
-
}
|
|
34
|
-
];
|
|
35
|
-
const toCalendarDate = (date) => {
|
|
36
|
-
return new CalendarDate(
|
|
37
|
-
date.getFullYear(),
|
|
38
|
-
date.getMonth() + 1,
|
|
39
|
-
date.getDate()
|
|
40
|
-
);
|
|
41
|
-
};
|
|
42
|
-
const calendarRange = computed({
|
|
43
|
-
get: () => ({
|
|
44
|
-
start: dateRange.value.start ? toCalendarDate(dateRange.value.start) : void 0,
|
|
45
|
-
end: dateRange.value.end ? toCalendarDate(dateRange.value.end) : void 0
|
|
46
|
-
}),
|
|
47
|
-
set: (newValue) => {
|
|
48
|
-
if (newValue.start && newValue.end) {
|
|
49
|
-
setDateRange({
|
|
50
|
-
start: newValue.start.toDate(getLocalTimeZone()),
|
|
51
|
-
end: newValue.end.toDate(getLocalTimeZone())
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
const isRangeSelected = (preset) => {
|
|
57
|
-
if (!dateRange.value.start || !dateRange.value.end) return false;
|
|
58
|
-
const currentDate = today(getLocalTimeZone());
|
|
59
|
-
let startDate = currentDate.copy();
|
|
60
|
-
switch (preset) {
|
|
61
|
-
case `week`:
|
|
62
|
-
startDate = startDate.subtract({
|
|
63
|
-
days: 7
|
|
64
|
-
});
|
|
65
|
-
break;
|
|
66
|
-
case `month`:
|
|
67
|
-
startDate = startDate.subtract({
|
|
68
|
-
days: 30
|
|
69
|
-
});
|
|
70
|
-
break;
|
|
71
|
-
case `3months`:
|
|
72
|
-
startDate = startDate.subtract({
|
|
73
|
-
months: 3
|
|
74
|
-
});
|
|
75
|
-
break;
|
|
76
|
-
case `6months`:
|
|
77
|
-
startDate = startDate.subtract({
|
|
78
|
-
months: 6
|
|
79
|
-
});
|
|
80
|
-
break;
|
|
81
|
-
case `year`:
|
|
82
|
-
startDate = startDate.subtract({
|
|
83
|
-
years: 1
|
|
84
|
-
});
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
const selectedStart = toCalendarDate(dateRange.value.start);
|
|
88
|
-
const selectedEnd = toCalendarDate(dateRange.value.end);
|
|
89
|
-
return Math.abs(
|
|
90
|
-
selectedStart.toDate(getLocalTimeZone()).getTime() - startDate.toDate(getLocalTimeZone()).getTime()
|
|
91
|
-
) < 24 * 60 * 60 * 1e3 && Math.abs(
|
|
92
|
-
selectedEnd.toDate(getLocalTimeZone()).getTime() - currentDate.toDate(getLocalTimeZone()).getTime()
|
|
93
|
-
) < 24 * 60 * 60 * 1e3;
|
|
94
|
-
};
|
|
95
|
-
</script>
|
|
96
|
-
|
|
97
|
-
<template>
|
|
98
|
-
<div class="mb-4 flex w-full items-center justify-center">
|
|
99
|
-
<UPopover :content="{ align: 'center' }" :modal="true">
|
|
100
|
-
<UButton
|
|
101
|
-
color="neutral"
|
|
102
|
-
variant="outline"
|
|
103
|
-
icon="i-lucide-calendar"
|
|
104
|
-
class="group min-w-fit data-[state=open]:bg-elevated"
|
|
105
|
-
>
|
|
106
|
-
<span class="truncate">
|
|
107
|
-
<template v-if="formattedDateRange">
|
|
108
|
-
{{ formattedDateRange }}
|
|
109
|
-
</template>
|
|
110
|
-
<template v-else> Pick a date range </template>
|
|
111
|
-
</span>
|
|
112
|
-
|
|
113
|
-
<template #trailing>
|
|
114
|
-
<UIcon
|
|
115
|
-
name="i-lucide-chevron-down"
|
|
116
|
-
class="size-5 shrink-0 text-dimmed transition-transform duration-200 group-data-[state=open]:rotate-180"
|
|
117
|
-
/>
|
|
118
|
-
</template>
|
|
119
|
-
</UButton>
|
|
120
|
-
|
|
121
|
-
<template #content>
|
|
122
|
-
<div class="flex items-stretch divide-default sm:divide-x">
|
|
123
|
-
<div class="hidden min-w-[140px] flex-col justify-center sm:flex">
|
|
124
|
-
<UButton
|
|
125
|
-
v-for="(range, index) in ranges"
|
|
126
|
-
:key="index"
|
|
127
|
-
:label="range.label"
|
|
128
|
-
color="neutral"
|
|
129
|
-
variant="ghost"
|
|
130
|
-
class="justify-start rounded-none px-4"
|
|
131
|
-
:class="[
|
|
132
|
-
isRangeSelected(range.preset) ? 'bg-elevated' : 'hover:bg-elevated/50'
|
|
133
|
-
]"
|
|
134
|
-
truncate
|
|
135
|
-
@click="setPresetRange(range.preset)"
|
|
136
|
-
/>
|
|
137
|
-
</div>
|
|
138
|
-
|
|
139
|
-
<UCalendar
|
|
140
|
-
v-model="calendarRange"
|
|
141
|
-
class="p-2"
|
|
142
|
-
:number-of-months="2"
|
|
143
|
-
range
|
|
144
|
-
/>
|
|
145
|
-
</div>
|
|
146
|
-
</template>
|
|
147
|
-
</UPopover>
|
|
148
|
-
</div>
|
|
149
|
-
</template>
|
|
@@ -1,3 +0,0 @@
|
|
|
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;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
type __VLS_Props = {
|
|
2
|
-
feedback: FeedbackItem;
|
|
3
|
-
};
|
|
4
|
-
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
5
|
-
delete: (id: number) => any;
|
|
6
|
-
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
7
|
-
onDelete?: ((id: number) => any) | undefined;
|
|
8
|
-
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
9
|
-
declare const _default: typeof __VLS_export;
|
|
10
|
-
export default _default;
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
const {} = defineProps({
|
|
3
|
-
feedback: { type: null, required: true }
|
|
4
|
-
});
|
|
5
|
-
const emit = defineEmits([]);
|
|
6
|
-
const { getRatingFromFeedback, getScoreColor } = useFeedbackRatings();
|
|
7
|
-
const rating = computed(() => getRatingFromFeedback(props.feedback));
|
|
8
|
-
const isDeleting = ref(false);
|
|
9
|
-
async function handleDelete() {
|
|
10
|
-
if (!confirm(`Are you sure you want to delete this feedback?`)) {
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
isDeleting.value = true;
|
|
14
|
-
try {
|
|
15
|
-
emit(`delete`, props.feedback.id);
|
|
16
|
-
} finally {
|
|
17
|
-
isDeleting.value = false;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
</script>
|
|
21
|
-
|
|
22
|
-
<template>
|
|
23
|
-
<div class="rounded-lg border border-default p-4">
|
|
24
|
-
<div class="mb-3 flex items-start justify-between">
|
|
25
|
-
<div class="flex items-center gap-3">
|
|
26
|
-
<div class="flex flex-col items-center">
|
|
27
|
-
<span class="text-2xl">{{ rating.emoji }}</span>
|
|
28
|
-
<span class="text-xs font-bold" :class="getScoreColor(rating.score)">
|
|
29
|
-
{{ rating.score }}/4
|
|
30
|
-
</span>
|
|
31
|
-
</div>
|
|
32
|
-
<div>
|
|
33
|
-
<div class="mb-1 flex items-center gap-2">
|
|
34
|
-
<span class="text-sm font-medium">{{ rating.label }}</span>
|
|
35
|
-
</div>
|
|
36
|
-
<div class="flex items-center gap-3 text-xs text-muted">
|
|
37
|
-
<span class="flex items-center gap-1">
|
|
38
|
-
<UIcon name="i-lucide-calendar" class="size-3" />
|
|
39
|
-
{{
|
|
40
|
-
new Date(feedback.updatedAt).toLocaleDateString("en-US", {
|
|
41
|
-
month: "short",
|
|
42
|
-
day: "numeric",
|
|
43
|
-
year: "numeric",
|
|
44
|
-
hour: "2-digit",
|
|
45
|
-
minute: "2-digit"
|
|
46
|
-
})
|
|
47
|
-
}}
|
|
48
|
-
</span>
|
|
49
|
-
<span v-if="feedback.country" class="flex items-center gap-1">
|
|
50
|
-
<UIcon name="i-lucide-map-pin" class="size-3" />
|
|
51
|
-
{{ feedback.country }}
|
|
52
|
-
</span>
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
<UButton
|
|
57
|
-
color="error"
|
|
58
|
-
variant="ghost"
|
|
59
|
-
size="sm"
|
|
60
|
-
icon="i-lucide-trash-2"
|
|
61
|
-
:loading="isDeleting"
|
|
62
|
-
:disabled="isDeleting"
|
|
63
|
-
@click="handleDelete"
|
|
64
|
-
/>
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
<div
|
|
68
|
-
v-if="feedback.feedback"
|
|
69
|
-
class="rounded bg-muted/30 p-3 text-sm leading-relaxed"
|
|
70
|
-
>
|
|
71
|
-
"{{ feedback.feedback }}"
|
|
72
|
-
</div>
|
|
73
|
-
<div v-else class="text-sm text-muted italic">
|
|
74
|
-
No additional comment provided
|
|
75
|
-
</div>
|
|
76
|
-
</div>
|
|
77
|
-
</template>
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
type __VLS_Props = {
|
|
2
|
-
feedback: FeedbackItem;
|
|
3
|
-
};
|
|
4
|
-
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
5
|
-
delete: (id: number) => any;
|
|
6
|
-
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
7
|
-
onDelete?: ((id: number) => any) | undefined;
|
|
8
|
-
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
9
|
-
declare const _default: typeof __VLS_export;
|
|
10
|
-
export default _default;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
type __VLS_Props = {
|
|
2
|
-
icon: string;
|
|
3
|
-
iconColor?: string;
|
|
4
|
-
value: number | string;
|
|
5
|
-
label: string;
|
|
6
|
-
description?: string;
|
|
7
|
-
descriptionColor?: string;
|
|
8
|
-
popoverStats?: {
|
|
9
|
-
percentage?: string;
|
|
10
|
-
trend?: string;
|
|
11
|
-
lastPeriod?: string;
|
|
12
|
-
details?: string;
|
|
13
|
-
};
|
|
14
|
-
};
|
|
15
|
-
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>;
|
|
16
|
-
declare const _default: typeof __VLS_export;
|
|
17
|
-
export default _default;
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
const { iconColor = `text-primary`, descriptionColor = `text-muted` } = defineProps({
|
|
3
|
-
icon: { type: String, required: true },
|
|
4
|
-
iconColor: { type: String, required: false },
|
|
5
|
-
value: { type: [Number, String], required: true },
|
|
6
|
-
label: { type: String, required: true },
|
|
7
|
-
description: { type: String, required: false },
|
|
8
|
-
descriptionColor: { type: String, required: false },
|
|
9
|
-
popoverStats: { type: Object, required: false }
|
|
10
|
-
});
|
|
11
|
-
</script>
|
|
12
|
-
|
|
13
|
-
<template>
|
|
14
|
-
<UPopover mode="hover" arrow :open-delay="300" :close-delay="200">
|
|
15
|
-
<div
|
|
16
|
-
class="flex cursor-pointer items-center gap-3 rounded-lg border border-default bg-muted/20 px-4 py-3 transition-colors hover:bg-muted/30"
|
|
17
|
-
>
|
|
18
|
-
<UIcon :name="icon" :class="`size-6 ${iconColor} shrink-0`" />
|
|
19
|
-
<div>
|
|
20
|
-
<div class="text-xl font-bold">
|
|
21
|
-
{{ value }}
|
|
22
|
-
</div>
|
|
23
|
-
<div class="text-sm text-muted">
|
|
24
|
-
{{ label }}
|
|
25
|
-
</div>
|
|
26
|
-
</div>
|
|
27
|
-
</div>
|
|
28
|
-
|
|
29
|
-
<template #content>
|
|
30
|
-
<div class="min-w-64 p-4">
|
|
31
|
-
<div class="mb-3 flex items-center gap-2">
|
|
32
|
-
<UIcon :name="icon" :class="`size-5 ${iconColor}`" />
|
|
33
|
-
<h3 class="font-semibold">{{ label }} Details</h3>
|
|
34
|
-
</div>
|
|
35
|
-
|
|
36
|
-
<div class="space-y-2 text-sm">
|
|
37
|
-
<div v-if="popoverStats?.percentage" class="flex justify-between">
|
|
38
|
-
<span class="text-muted">Percentage:</span>
|
|
39
|
-
<span class="font-medium">{{ popoverStats.percentage }}</span>
|
|
40
|
-
</div>
|
|
41
|
-
|
|
42
|
-
<div v-if="popoverStats?.trend" class="flex justify-between">
|
|
43
|
-
<span class="text-muted">Status:</span>
|
|
44
|
-
<span class="font-medium">
|
|
45
|
-
{{ popoverStats.trend }}
|
|
46
|
-
</span>
|
|
47
|
-
</div>
|
|
48
|
-
|
|
49
|
-
<div v-if="popoverStats?.lastPeriod" class="flex justify-between">
|
|
50
|
-
<span class="text-muted">Last 7 days:</span>
|
|
51
|
-
<span class="font-medium">{{ popoverStats.lastPeriod }}</span>
|
|
52
|
-
</div>
|
|
53
|
-
|
|
54
|
-
<div
|
|
55
|
-
v-if="popoverStats?.details"
|
|
56
|
-
class="border-t border-default pt-2"
|
|
57
|
-
>
|
|
58
|
-
<p class="text-xs text-muted">
|
|
59
|
-
{{ popoverStats.details }}
|
|
60
|
-
</p>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
</template>
|
|
65
|
-
</UPopover>
|
|
66
|
-
</template>
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
type __VLS_Props = {
|
|
2
|
-
icon: string;
|
|
3
|
-
iconColor?: string;
|
|
4
|
-
value: number | string;
|
|
5
|
-
label: string;
|
|
6
|
-
description?: string;
|
|
7
|
-
descriptionColor?: string;
|
|
8
|
-
popoverStats?: {
|
|
9
|
-
percentage?: string;
|
|
10
|
-
trend?: string;
|
|
11
|
-
lastPeriod?: string;
|
|
12
|
-
details?: string;
|
|
13
|
-
};
|
|
14
|
-
};
|
|
15
|
-
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>;
|
|
16
|
-
declare const _default: typeof __VLS_export;
|
|
17
|
-
export default _default;
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
export declare function useFeedbackRatings(): {
|
|
2
|
-
ratingConfig: any;
|
|
3
|
-
getScoreColor: (score: number) => string;
|
|
4
|
-
getRatingFromFeedback: (feedback: {
|
|
5
|
-
rating: FeedbackRating;
|
|
6
|
-
}) => any;
|
|
7
|
-
calculateStats: (feedbacks: {
|
|
8
|
-
rating: FeedbackRating;
|
|
9
|
-
}[]) => {
|
|
10
|
-
total: number;
|
|
11
|
-
positive: number;
|
|
12
|
-
negative: number;
|
|
13
|
-
averageScore: number;
|
|
14
|
-
positivePercentage: number;
|
|
15
|
-
};
|
|
16
|
-
};
|
|
17
|
-
export declare function useFeedbackData(rawFeedback: Ref<FeedbackItem[] | null>): {
|
|
18
|
-
feedbackData: any;
|
|
19
|
-
globalStats: any;
|
|
20
|
-
pageAnalytics: any;
|
|
21
|
-
};
|
|
22
|
-
export declare function useFeedbackModal(): {
|
|
23
|
-
selectedPage: any;
|
|
24
|
-
showFeedbackModal: any;
|
|
25
|
-
currentPage: any;
|
|
26
|
-
itemsPerPage: number;
|
|
27
|
-
paginatedFeedback: any;
|
|
28
|
-
totalPages: any;
|
|
29
|
-
viewPageDetails: (page: PageAnalytic) => void;
|
|
30
|
-
closeFeedbackModal: () => void;
|
|
31
|
-
};
|
|
32
|
-
export declare function useFeedbackDelete(): {
|
|
33
|
-
deleteFeedback: (id: number) => Promise<boolean>;
|
|
34
|
-
};
|
|
35
|
-
interface UseFeedbackFormOptions {
|
|
36
|
-
page: {
|
|
37
|
-
title: string;
|
|
38
|
-
stem: string;
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
export declare function useFeedbackForm(options: UseFeedbackFormOptions): {
|
|
42
|
-
formState: any;
|
|
43
|
-
isExpanded: any;
|
|
44
|
-
isSubmitted: any;
|
|
45
|
-
isSubmitting: any;
|
|
46
|
-
handleRatingSelect: (rating: FeedbackRating) => void;
|
|
47
|
-
submitFeedback: () => Promise<void>;
|
|
48
|
-
resetFeedback: () => void;
|
|
49
|
-
};
|
|
50
|
-
export {};
|
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
export function useFeedbackRatings() {
|
|
2
|
-
const ratingConfig = computed(() => {
|
|
3
|
-
return FEEDBACK_OPTIONS.reduce(
|
|
4
|
-
(acc, option) => {
|
|
5
|
-
acc[option.value] = option;
|
|
6
|
-
return acc;
|
|
7
|
-
},
|
|
8
|
-
{}
|
|
9
|
-
);
|
|
10
|
-
});
|
|
11
|
-
function getScoreColor(score) {
|
|
12
|
-
if (score >= 4) return `text-success`;
|
|
13
|
-
if (score >= 3) return `text-warning`;
|
|
14
|
-
return `text-error`;
|
|
15
|
-
}
|
|
16
|
-
function getRatingFromFeedback(feedback) {
|
|
17
|
-
return ratingConfig.value[feedback.rating];
|
|
18
|
-
}
|
|
19
|
-
function calculateStats(feedbacks) {
|
|
20
|
-
const total = feedbacks.length;
|
|
21
|
-
const positive = feedbacks.filter(
|
|
22
|
-
(f) => [`very-helpful`, `helpful`].includes(f.rating)
|
|
23
|
-
).length;
|
|
24
|
-
const negative = feedbacks.filter(
|
|
25
|
-
(f) => [`not-helpful`, `confusing`].includes(f.rating)
|
|
26
|
-
).length;
|
|
27
|
-
const totalScore = feedbacks.reduce(
|
|
28
|
-
(sum, item) => sum + ratingConfig.value[item.rating].score,
|
|
29
|
-
0
|
|
30
|
-
);
|
|
31
|
-
const averageScore = total > 0 ? Number((totalScore / total).toFixed(1)) : 0;
|
|
32
|
-
const positivePercentage = total > 0 ? Math.round(positive / total * 100) : 0;
|
|
33
|
-
return {
|
|
34
|
-
total,
|
|
35
|
-
positive,
|
|
36
|
-
negative,
|
|
37
|
-
averageScore,
|
|
38
|
-
positivePercentage
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
return {
|
|
42
|
-
ratingConfig,
|
|
43
|
-
getScoreColor,
|
|
44
|
-
getRatingFromFeedback,
|
|
45
|
-
calculateStats
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
export function useFeedbackData(rawFeedback) {
|
|
49
|
-
const { calculateStats } = useFeedbackRatings();
|
|
50
|
-
const { filterFeedbackByDateRange } = useDateRange();
|
|
51
|
-
const allFeedbackData = computed(
|
|
52
|
-
() => rawFeedback.value?.map((item) => ({
|
|
53
|
-
...item,
|
|
54
|
-
createdAt: new Date(item.createdAt),
|
|
55
|
-
updatedAt: new Date(item.updatedAt)
|
|
56
|
-
})) || []
|
|
57
|
-
);
|
|
58
|
-
const feedbackData = computed(
|
|
59
|
-
() => filterFeedbackByDateRange(allFeedbackData.value)
|
|
60
|
-
);
|
|
61
|
-
const globalStats = computed(() => calculateStats(feedbackData.value));
|
|
62
|
-
const pageAnalytics = computed(() => {
|
|
63
|
-
const filteredFeedback = filterFeedbackByDateRange(allFeedbackData.value);
|
|
64
|
-
const pageGroups = filteredFeedback.reduce(
|
|
65
|
-
(acc, item) => {
|
|
66
|
-
if (!acc[item.path]) {
|
|
67
|
-
acc[item.path] = [];
|
|
68
|
-
}
|
|
69
|
-
acc[item.path].push(item);
|
|
70
|
-
return acc;
|
|
71
|
-
},
|
|
72
|
-
{}
|
|
73
|
-
);
|
|
74
|
-
return Object.entries(pageGroups).map(([path, feedback]) => {
|
|
75
|
-
const stats = calculateStats(feedback);
|
|
76
|
-
const sortedFeedback = feedback.sort(
|
|
77
|
-
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
78
|
-
);
|
|
79
|
-
const oldestFeedback = feedback.reduce((oldest, current) => {
|
|
80
|
-
if (new Date(current.createdAt) < new Date(oldest.createdAt)) {
|
|
81
|
-
return current;
|
|
82
|
-
} else {
|
|
83
|
-
return oldest;
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
return {
|
|
87
|
-
path,
|
|
88
|
-
...stats,
|
|
89
|
-
feedback,
|
|
90
|
-
lastFeedback: sortedFeedback[0],
|
|
91
|
-
createdAt: new Date(oldestFeedback.createdAt),
|
|
92
|
-
updatedAt: new Date(sortedFeedback[0].updatedAt)
|
|
93
|
-
};
|
|
94
|
-
}).sort((a, b) => b.total - a.total);
|
|
95
|
-
});
|
|
96
|
-
return {
|
|
97
|
-
feedbackData,
|
|
98
|
-
globalStats,
|
|
99
|
-
pageAnalytics
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
export function useFeedbackModal() {
|
|
103
|
-
const selectedPage = ref(null);
|
|
104
|
-
const showFeedbackModal = ref(false);
|
|
105
|
-
const currentPage = ref(1);
|
|
106
|
-
const itemsPerPage = 5;
|
|
107
|
-
const paginatedFeedback = computed(() => {
|
|
108
|
-
if (!selectedPage.value) return [];
|
|
109
|
-
const sortedFeedback = [...selectedPage.value.feedback].sort(
|
|
110
|
-
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
111
|
-
);
|
|
112
|
-
const startIndex = (currentPage.value - 1) * itemsPerPage;
|
|
113
|
-
const endIndex = startIndex + itemsPerPage;
|
|
114
|
-
return sortedFeedback.slice(startIndex, endIndex);
|
|
115
|
-
});
|
|
116
|
-
const totalPages = computed(() => {
|
|
117
|
-
if (!selectedPage.value) return 0;
|
|
118
|
-
return Math.ceil(selectedPage.value.feedback.length / itemsPerPage);
|
|
119
|
-
});
|
|
120
|
-
function viewPageDetails(page) {
|
|
121
|
-
selectedPage.value = page;
|
|
122
|
-
currentPage.value = 1;
|
|
123
|
-
showFeedbackModal.value = true;
|
|
124
|
-
}
|
|
125
|
-
function closeFeedbackModal() {
|
|
126
|
-
showFeedbackModal.value = false;
|
|
127
|
-
selectedPage.value = null;
|
|
128
|
-
}
|
|
129
|
-
return {
|
|
130
|
-
selectedPage: readonly(selectedPage),
|
|
131
|
-
showFeedbackModal,
|
|
132
|
-
currentPage,
|
|
133
|
-
itemsPerPage,
|
|
134
|
-
paginatedFeedback,
|
|
135
|
-
totalPages,
|
|
136
|
-
viewPageDetails,
|
|
137
|
-
closeFeedbackModal
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
export function useFeedbackDelete() {
|
|
141
|
-
const toast = useToast();
|
|
142
|
-
async function deleteFeedback(id) {
|
|
143
|
-
try {
|
|
144
|
-
await $fetch(`/api/feedback/${id}`, {
|
|
145
|
-
method: `DELETE`
|
|
146
|
-
});
|
|
147
|
-
toast.add({
|
|
148
|
-
title: `Feedback deleted`,
|
|
149
|
-
description: `The feedback has been successfully removed`,
|
|
150
|
-
color: `success`,
|
|
151
|
-
icon: `i-lucide-check`
|
|
152
|
-
});
|
|
153
|
-
return true;
|
|
154
|
-
} catch (error) {
|
|
155
|
-
console.error(`Failed to delete feedback:`, error);
|
|
156
|
-
toast.add({
|
|
157
|
-
title: `Failed to delete feedback`,
|
|
158
|
-
description: `Please try again later`,
|
|
159
|
-
color: `error`,
|
|
160
|
-
icon: `i-lucide-circle-alert`
|
|
161
|
-
});
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return {
|
|
166
|
-
deleteFeedback
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
export function useFeedbackForm(options) {
|
|
170
|
-
const route = useRoute();
|
|
171
|
-
const toast = useToast();
|
|
172
|
-
const formState = reactive({
|
|
173
|
-
rating: null,
|
|
174
|
-
feedback: ``
|
|
175
|
-
});
|
|
176
|
-
const isExpanded = ref(false);
|
|
177
|
-
const isSubmitted = ref(false);
|
|
178
|
-
const isSubmitting = ref(false);
|
|
179
|
-
function cancelFeedback() {
|
|
180
|
-
formState.rating = null;
|
|
181
|
-
formState.feedback = ``;
|
|
182
|
-
isExpanded.value = false;
|
|
183
|
-
}
|
|
184
|
-
function handleRatingSelect(rating) {
|
|
185
|
-
if (isSubmitted.value) return;
|
|
186
|
-
if (isExpanded.value && rating === formState.rating) {
|
|
187
|
-
cancelFeedback();
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
formState.rating = rating;
|
|
191
|
-
isExpanded.value = true;
|
|
192
|
-
}
|
|
193
|
-
async function submitFeedback() {
|
|
194
|
-
if (!formState.rating) return;
|
|
195
|
-
isSubmitting.value = true;
|
|
196
|
-
const submission = {
|
|
197
|
-
rating: formState.rating,
|
|
198
|
-
feedback: formState.feedback.trim() || void 0,
|
|
199
|
-
path: route.path,
|
|
200
|
-
title: options.page.title,
|
|
201
|
-
stem: options.page.stem
|
|
202
|
-
};
|
|
203
|
-
try {
|
|
204
|
-
await $fetch(`/api/feedback`, {
|
|
205
|
-
method: `POST`,
|
|
206
|
-
body: submission
|
|
207
|
-
});
|
|
208
|
-
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
209
|
-
isSubmitted.value = true;
|
|
210
|
-
} catch {
|
|
211
|
-
toast.add({
|
|
212
|
-
title: `Failed to submit feedback`,
|
|
213
|
-
description: `Please try again later`,
|
|
214
|
-
color: `error`,
|
|
215
|
-
icon: `i-lucide-circle-alert`
|
|
216
|
-
});
|
|
217
|
-
} finally {
|
|
218
|
-
isSubmitting.value = false;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
function resetFeedback() {
|
|
222
|
-
isSubmitted.value = false;
|
|
223
|
-
isExpanded.value = false;
|
|
224
|
-
formState.rating = null;
|
|
225
|
-
formState.feedback = ``;
|
|
226
|
-
}
|
|
227
|
-
watch(route, resetFeedback);
|
|
228
|
-
return {
|
|
229
|
-
formState,
|
|
230
|
-
isExpanded: readonly(isExpanded),
|
|
231
|
-
isSubmitted: readonly(isSubmitted),
|
|
232
|
-
isSubmitting: readonly(isSubmitting),
|
|
233
|
-
handleRatingSelect,
|
|
234
|
-
submitFeedback,
|
|
235
|
-
resetFeedback
|
|
236
|
-
};
|
|
237
|
-
}
|