mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48:08 +00:00
feat: Improve CSAT responses (#11485)
# Pull Request Template ## Description This PR introduces basic customization options for the CSAT survey: * **Display Type**: Option to use star ratings instead of emojis. * **Message Text**: Customize the survey message (up to 200 characters). * **Survey Rules**: Send surveys based on labels — trigger when a conversation has or doesn't have a specific label. Fixes https://linear.app/chatwoot/document/improve-csat-responses-a61cf30e054e ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? ### Loom videos **Website Channel (Widget)** https://www.loom.com/share/7f47836cde7940ae9d17b7997d060a18?sid=aad2ad0a-140a-4a09-8829-e01fa2e102c5 **Email Channel (Survey link)** https://www.loom.com/share/e92f4c4c0f73417ba300a25885e093ce?sid=4bb006f0-1c2a-4352-a232-8bf684e3d757 ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
@@ -25,6 +25,10 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: 'faded',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const selected = defineModel({
|
||||
@@ -56,7 +60,7 @@ const updateSelected = newValue => {
|
||||
:variant
|
||||
:icon="iconToRender"
|
||||
:trailing-icon="selectedOption.icon ? false : true"
|
||||
:label="hideLabel ? null : selectedOption.label"
|
||||
:label="label || (hideLabel ? null : selectedOption.label)"
|
||||
@click="toggle"
|
||||
/>
|
||||
</slot>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import { computed } from 'vue';
|
||||
import BaseBubble from './Base.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { CSAT_RATINGS } from 'shared/constants/messages';
|
||||
import { CSAT_RATINGS, CSAT_DISPLAY_TYPES } from 'shared/constants/messages';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
|
||||
const { contentAttributes } = useMessageContext();
|
||||
const { contentAttributes, content } = useMessageContext();
|
||||
const { t } = useI18n();
|
||||
|
||||
const response = computed(() => {
|
||||
@@ -16,6 +16,14 @@ const isRatingSubmitted = computed(() => {
|
||||
return !!response.value.rating;
|
||||
});
|
||||
|
||||
const displayType = computed(() => {
|
||||
return contentAttributes.value?.displayType || CSAT_DISPLAY_TYPES.EMOJI;
|
||||
});
|
||||
|
||||
const isStarRating = computed(() => {
|
||||
return displayType.value === CSAT_DISPLAY_TYPES.STAR;
|
||||
});
|
||||
|
||||
const rating = computed(() => {
|
||||
if (isRatingSubmitted.value) {
|
||||
return CSAT_RATINGS.find(
|
||||
@@ -25,16 +33,33 @@ const rating = computed(() => {
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const starRatingValue = computed(() => {
|
||||
return response.value.rating || 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble class="px-4 py-3" data-bubble-name="csat">
|
||||
<h4>{{ t('CONVERSATION.CSAT_REPLY_MESSAGE') }}</h4>
|
||||
<h4>{{ content || t('CONVERSATION.CSAT_REPLY_MESSAGE') }}</h4>
|
||||
<dl v-if="isRatingSubmitted" class="mt-4">
|
||||
<dt class="text-n-slate-11 italic">
|
||||
{{ t('CONVERSATION.RATING_TITLE') }}
|
||||
</dt>
|
||||
<dd>{{ t(rating.translationKey) }}</dd>
|
||||
<dd v-if="!isStarRating">
|
||||
{{ t(rating.translationKey) }}
|
||||
</dd>
|
||||
<dd v-else class="flex mt-1">
|
||||
<span v-for="n in 5" :key="n" class="text-2xl mr-1">
|
||||
<i
|
||||
:class="[
|
||||
n <= starRatingValue
|
||||
? 'i-ri-star-fill text-n-amber-9'
|
||||
: 'i-ri-star-line text-n-slate-10',
|
||||
]"
|
||||
/>
|
||||
</span>
|
||||
</dd>
|
||||
|
||||
<dt v-if="response.feedbackMessage" class="text-n-slate-11 italic mt-2">
|
||||
{{ t('CONVERSATION.FEEDBACK_TITLE') }}
|
||||
|
||||
@@ -481,7 +481,8 @@
|
||||
"PRE_CHAT_FORM": "Pre Chat Form",
|
||||
"BUSINESS_HOURS": "Business Hours",
|
||||
"WIDGET_BUILDER": "Widget Builder",
|
||||
"BOT_CONFIGURATION": "Bot Configuration"
|
||||
"BOT_CONFIGURATION": "Bot Configuration",
|
||||
"CSAT": "CSAT"
|
||||
},
|
||||
"SETTINGS": "Settings",
|
||||
"FEATURES": {
|
||||
@@ -502,9 +503,7 @@
|
||||
"ENABLE_EMAIL_COLLECT_BOX": "Enable email collect box",
|
||||
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "Enable or disable email collect box on new conversation",
|
||||
"AUTO_ASSIGNMENT": "Enable auto assignment",
|
||||
"ENABLE_CSAT": "Enable CSAT",
|
||||
"SENDER_NAME_SECTION": "Enable Agent Name in Email",
|
||||
"ENABLE_CSAT_SUB_TEXT": "Enable/Disable CSAT(Customer satisfaction) survey after resolving a conversation",
|
||||
"SENDER_NAME_SECTION_TEXT": "Enable/Disable showing Agent's name in email, if disabled it will show business name",
|
||||
"ENABLE_CONTINUITY_VIA_EMAIL": "Enable conversation continuity via email",
|
||||
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "Conversations will continue over email if the contact email address is available.",
|
||||
@@ -578,6 +577,32 @@
|
||||
"LABEL": "Visitors should provide their name and email address before starting the chat"
|
||||
}
|
||||
},
|
||||
"CSAT": {
|
||||
"TITLE": "Enable CSAT",
|
||||
"SUBTITLE": "Automatically trigger CSAT surveys at the end of conversations to understand how customers feel about their support experience. Track satisfaction trends and identify areas for improvement over time.",
|
||||
"DISPLAY_TYPE": {
|
||||
"LABEL": "Display type"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"LABEL": "Message",
|
||||
"PLACEHOLDER": "Please enter a message to show users with the form"
|
||||
},
|
||||
"SURVEY_RULE": {
|
||||
"LABEL": "Survey rule",
|
||||
"DESCRIPTION_PREFIX": "Send the survey if the conversation",
|
||||
"DESCRIPTION_SUFFIX": "any of the labels",
|
||||
"OPERATOR": {
|
||||
"CONTAINS": "contains",
|
||||
"DOES_NOT_CONTAINS": "does not contain"
|
||||
},
|
||||
"SELECT_PLACEHOLDER": "select labels"
|
||||
},
|
||||
"NOTE": "Note: CSAT surveys are sent only once per conversation",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "CSAT settings updated successfully",
|
||||
"ERROR_MESSAGE": "We couldn't update CSAT settings. Please try again later."
|
||||
}
|
||||
},
|
||||
"BUSINESS_HOURS": {
|
||||
"TITLE": "Set your availability",
|
||||
"SUBTITLE": "Set your availability on your livechat widget",
|
||||
|
||||
@@ -15,6 +15,7 @@ import PreChatFormSettings from './PreChatForm/Settings.vue';
|
||||
import WeeklyAvailability from './components/WeeklyAvailability.vue';
|
||||
import GreetingsEditor from 'shared/components/GreetingsEditor.vue';
|
||||
import ConfigurationPage from './settingsPage/ConfigurationPage.vue';
|
||||
import CustomerSatisfactionPage from './settingsPage/CustomerSatisfactionPage.vue';
|
||||
import CollaboratorsPage from './settingsPage/CollaboratorsPage.vue';
|
||||
import WidgetBuilder from './WidgetBuilder.vue';
|
||||
import BotConfiguration from './components/BotConfiguration.vue';
|
||||
@@ -28,6 +29,7 @@ export default {
|
||||
BotConfiguration,
|
||||
CollaboratorsPage,
|
||||
ConfigurationPage,
|
||||
CustomerSatisfactionPage,
|
||||
FacebookReauthorize,
|
||||
GreetingsEditor,
|
||||
PreChatFormSettings,
|
||||
@@ -53,7 +55,6 @@ export default {
|
||||
greetingEnabled: true,
|
||||
greetingMessage: '',
|
||||
emailCollectEnabled: false,
|
||||
csatSurveyEnabled: false,
|
||||
senderNameType: 'friendly',
|
||||
businessName: '',
|
||||
locktoSingleConversation: false,
|
||||
@@ -107,6 +108,10 @@ export default {
|
||||
key: 'businesshours',
|
||||
name: this.$t('INBOX_MGMT.TABS.BUSINESS_HOURS'),
|
||||
},
|
||||
{
|
||||
key: 'csat',
|
||||
name: this.$t('INBOX_MGMT.TABS.CSAT'),
|
||||
},
|
||||
];
|
||||
|
||||
if (this.isAWebWidgetInbox) {
|
||||
@@ -277,7 +282,6 @@ export default {
|
||||
this.greetingEnabled = this.inbox.greeting_enabled || false;
|
||||
this.greetingMessage = this.inbox.greeting_message || '';
|
||||
this.emailCollectEnabled = this.inbox.enable_email_collect;
|
||||
this.csatSurveyEnabled = this.inbox.csat_survey_enabled;
|
||||
this.senderNameType = this.inbox.sender_name_type;
|
||||
this.businessName = this.inbox.business_name;
|
||||
this.allowMessagesAfterResolved =
|
||||
@@ -300,7 +304,6 @@ export default {
|
||||
id: this.currentInboxId,
|
||||
name: this.selectedInboxName,
|
||||
enable_email_collect: this.emailCollectEnabled,
|
||||
csat_survey_enabled: this.csatSurveyEnabled,
|
||||
allow_messages_after_resolved: this.allowMessagesAfterResolved,
|
||||
greeting_enabled: this.greetingEnabled,
|
||||
greeting_message: this.greetingMessage || '',
|
||||
@@ -589,21 +592,6 @@ export default {
|
||||
</p>
|
||||
</label>
|
||||
|
||||
<label class="pb-4">
|
||||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.ENABLE_CSAT') }}
|
||||
<select v-model="csatSurveyEnabled">
|
||||
<option :value="true">
|
||||
{{ $t('INBOX_MGMT.EDIT.ENABLE_CSAT.ENABLED') }}
|
||||
</option>
|
||||
<option :value="false">
|
||||
{{ $t('INBOX_MGMT.EDIT.ENABLE_CSAT.DISABLED') }}
|
||||
</option>
|
||||
</select>
|
||||
<p class="pb-1 text-sm not-italic text-n-slate-11">
|
||||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.ENABLE_CSAT_SUB_TEXT') }}
|
||||
</p>
|
||||
</label>
|
||||
|
||||
<label v-if="isAWebWidgetInbox" class="pb-4">
|
||||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.ALLOW_MESSAGES_AFTER_RESOLVED') }}
|
||||
<select v-model="allowMessagesAfterResolved">
|
||||
@@ -802,6 +790,9 @@ export default {
|
||||
<div v-if="selectedTabKey === 'configuration'">
|
||||
<ConfigurationPage :inbox="inbox" />
|
||||
</div>
|
||||
<div v-if="selectedTabKey === 'csat'">
|
||||
<CustomerSatisfactionPage :inbox="inbox" />
|
||||
</div>
|
||||
<div v-if="selectedTabKey === 'preChatForm'">
|
||||
<PreChatFormSettings :inbox="inbox" />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
<script setup>
|
||||
import { reactive, onMounted, ref, defineProps, watch, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { CSAT_DISPLAY_TYPES } from 'shared/constants/messages';
|
||||
|
||||
import WithLabel from 'v3/components/Form/WithLabel.vue';
|
||||
import SectionLayout from 'dashboard/routes/dashboard/settings/account/components/SectionLayout.vue';
|
||||
import CSATDisplayTypeSelector from './components/CSATDisplayTypeSelector.vue';
|
||||
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||
import FilterSelect from 'dashboard/components-next/filter/inputs/FilterSelect.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import Switch from 'next/switch/Switch.vue';
|
||||
|
||||
const props = defineProps({
|
||||
inbox: { type: Object, required: true },
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const labels = useMapGetter('labels/getLabels');
|
||||
|
||||
const isUpdating = ref(false);
|
||||
const selectedLabelValues = ref([]);
|
||||
const currentLabel = ref('');
|
||||
|
||||
const state = reactive({
|
||||
csatSurveyEnabled: false,
|
||||
displayType: 'emoji',
|
||||
message: '',
|
||||
surveyRuleOperator: 'contains',
|
||||
});
|
||||
|
||||
const filterTypes = [
|
||||
{
|
||||
label: t('INBOX_MGMT.CSAT.SURVEY_RULE.OPERATOR.CONTAINS'),
|
||||
value: 'contains',
|
||||
},
|
||||
{
|
||||
label: t('INBOX_MGMT.CSAT.SURVEY_RULE.OPERATOR.DOES_NOT_CONTAINS'),
|
||||
value: 'does_not_contain',
|
||||
},
|
||||
];
|
||||
|
||||
const labelOptions = computed(() =>
|
||||
labels.value?.length
|
||||
? labels.value
|
||||
.map(label => ({ label: label.title, value: label.title }))
|
||||
.filter(label => !selectedLabelValues.value.includes(label.value))
|
||||
: []
|
||||
);
|
||||
|
||||
const initializeState = () => {
|
||||
if (!props.inbox) return;
|
||||
|
||||
const { csat_survey_enabled, csat_config } = props.inbox;
|
||||
|
||||
state.csatSurveyEnabled = csat_survey_enabled || false;
|
||||
|
||||
if (!csat_config) return;
|
||||
|
||||
const {
|
||||
display_type: displayType = CSAT_DISPLAY_TYPES.EMOJI,
|
||||
message = '',
|
||||
survey_rules: surveyRules = {},
|
||||
} = csat_config;
|
||||
|
||||
state.displayType = displayType;
|
||||
state.message = message;
|
||||
state.surveyRuleOperator = surveyRules.operator || 'contains';
|
||||
|
||||
selectedLabelValues.value = Array.isArray(surveyRules.values)
|
||||
? [...surveyRules.values]
|
||||
: [];
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initializeState();
|
||||
if (!labels.value?.length) store.dispatch('labels/get');
|
||||
});
|
||||
|
||||
watch(() => props.inbox, initializeState, { immediate: true });
|
||||
|
||||
const handleLabelSelect = value => {
|
||||
if (!value || selectedLabelValues.value.includes(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectedLabelValues.value.push(value);
|
||||
};
|
||||
|
||||
const updateDisplayType = type => {
|
||||
state.displayType = type;
|
||||
};
|
||||
|
||||
const updateSurveyRuleOperator = operator => {
|
||||
state.surveyRuleOperator = operator;
|
||||
};
|
||||
|
||||
const removeLabel = label => {
|
||||
const index = selectedLabelValues.value.indexOf(label);
|
||||
if (index !== -1) {
|
||||
selectedLabelValues.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const updateInbox = async attributes => {
|
||||
const payload = {
|
||||
id: props.inbox.id,
|
||||
formData: false,
|
||||
...attributes,
|
||||
};
|
||||
|
||||
return store.dispatch('inboxes/updateInbox', payload);
|
||||
};
|
||||
|
||||
const saveSettings = async () => {
|
||||
try {
|
||||
isUpdating.value = true;
|
||||
|
||||
const csatConfig = {
|
||||
display_type: state.displayType,
|
||||
message: state.message,
|
||||
survey_rules: {
|
||||
operator: state.surveyRuleOperator,
|
||||
values: selectedLabelValues.value,
|
||||
},
|
||||
};
|
||||
|
||||
await updateInbox({
|
||||
csat_survey_enabled: state.csatSurveyEnabled,
|
||||
csat_config: csatConfig,
|
||||
});
|
||||
|
||||
useAlert(t('INBOX_MGMT.CSAT.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
useAlert(t('INBOX_MGMT.CSAT.API.ERROR_MESSAGE'));
|
||||
} finally {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-8">
|
||||
<SectionLayout
|
||||
:title="$t('INBOX_MGMT.CSAT.TITLE')"
|
||||
:description="$t('INBOX_MGMT.CSAT.SUBTITLE')"
|
||||
>
|
||||
<template #headerActions>
|
||||
<div class="flex justify-end">
|
||||
<Switch v-model="state.csatSurveyEnabled" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="grid gap-5">
|
||||
<WithLabel
|
||||
:label="$t('INBOX_MGMT.CSAT.DISPLAY_TYPE.LABEL')"
|
||||
name="display_type"
|
||||
>
|
||||
<CSATDisplayTypeSelector
|
||||
:selected-type="state.displayType"
|
||||
@update="updateDisplayType"
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel :label="$t('INBOX_MGMT.CSAT.MESSAGE.LABEL')" name="message">
|
||||
<Editor
|
||||
v-model="state.message"
|
||||
:placeholder="$t('INBOX_MGMT.CSAT.MESSAGE.PLACEHOLDER')"
|
||||
:max-length="200"
|
||||
class="w-full"
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel
|
||||
:label="$t('INBOX_MGMT.CSAT.SURVEY_RULE.LABEL')"
|
||||
name="survey_rule"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<span
|
||||
class="inline-flex flex-wrap items-center gap-1.5 text-sm text-n-slate-12"
|
||||
>
|
||||
{{ $t('INBOX_MGMT.CSAT.SURVEY_RULE.DESCRIPTION_PREFIX') }}
|
||||
<FilterSelect
|
||||
v-model="state.surveyRuleOperator"
|
||||
variant="faded"
|
||||
:options="filterTypes"
|
||||
class="inline-flex shrink-0"
|
||||
@update:model-value="updateSurveyRuleOperator"
|
||||
/>
|
||||
{{ $t('INBOX_MGMT.CSAT.SURVEY_RULE.DESCRIPTION_SUFFIX') }}
|
||||
|
||||
<NextButton
|
||||
v-for="label in selectedLabelValues"
|
||||
:key="label"
|
||||
sm
|
||||
faded
|
||||
slate
|
||||
trailing-icon
|
||||
:label="label"
|
||||
icon="i-lucide-x"
|
||||
class="inline-flex shrink-0"
|
||||
@click="removeLabel(label)"
|
||||
/>
|
||||
<FilterSelect
|
||||
v-model="currentLabel"
|
||||
:options="labelOptions"
|
||||
:label="$t('INBOX_MGMT.CSAT.SURVEY_RULE.SELECT_PLACEHOLDER')"
|
||||
hide-label
|
||||
variant="faded"
|
||||
class="inline-flex shrink-0"
|
||||
@update:model-value="handleLabelSelect"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</WithLabel>
|
||||
<p class="text-sm italic text-n-slate-11">
|
||||
{{ $t('INBOX_MGMT.CSAT.NOTE') }}
|
||||
</p>
|
||||
<div>
|
||||
<NextButton
|
||||
type="submit"
|
||||
:label="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
|
||||
:is-loading="isUpdating"
|
||||
@click="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SectionLayout>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
import { CSAT_DISPLAY_TYPES } from 'shared/constants/messages';
|
||||
import CSATEmojiInput from './CSATEmojiInput.vue';
|
||||
import CSATStarInput from './CSATStarInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
selectedType: {
|
||||
type: String,
|
||||
default: CSAT_DISPLAY_TYPES.EMOJI,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['update']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap gap-6 mt-2">
|
||||
<CSATEmojiInput
|
||||
:selected="props.selectedType === CSAT_DISPLAY_TYPES.EMOJI"
|
||||
@update="emit('update', $event)"
|
||||
/>
|
||||
<CSATStarInput
|
||||
:selected="props.selectedType === CSAT_DISPLAY_TYPES.STAR"
|
||||
@update="emit('update', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { CSAT_RATINGS, CSAT_DISPLAY_TYPES } from 'shared/constants/messages';
|
||||
|
||||
const props = defineProps({
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const selectionClass = computed(() => {
|
||||
return props.selected
|
||||
? 'outline-n-brand bg-n-brand/5'
|
||||
: 'outline-n-weak bg-n-alpha-black2';
|
||||
});
|
||||
|
||||
const emojis = CSAT_RATINGS;
|
||||
const selectedEmoji = ref(5);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="flex items-center rounded-lg transition-all duration-500 cursor-pointer outline outline-1 px-4 py-2 gap-2 min-w-56"
|
||||
:class="selectionClass"
|
||||
@click="emit('update', CSAT_DISPLAY_TYPES.EMOJI)"
|
||||
>
|
||||
<div
|
||||
v-for="emoji in emojis"
|
||||
:key="emoji.key"
|
||||
class="rounded-full p-1 transition-transform duration-150 focus:outline-none flex items-center flex-shrink-0"
|
||||
>
|
||||
<span
|
||||
class="text-2xl"
|
||||
:class="selectedEmoji === emoji.value ? '' : 'grayscale opacity-60'"
|
||||
>
|
||||
{{ emoji.emoji }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup>
|
||||
import { CSAT_DISPLAY_TYPES } from 'shared/constants/messages';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const selectionClass = computed(() => {
|
||||
return props.selected
|
||||
? 'bg-n-brand/5 outline-n-brand'
|
||||
: 'bg-n-alpha-black2 outline-n-weak';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="flex items-center rounded-lg transition-all duration-300 cursor-pointer outline outline-1 px-4 py-2 gap-2 min-w-56"
|
||||
:class="selectionClass"
|
||||
@click="emit('update', CSAT_DISPLAY_TYPES.STAR)"
|
||||
>
|
||||
<div
|
||||
v-for="n in 5"
|
||||
:key="'star-' + n"
|
||||
class="rounded-full p-1 transition-transform duration-150 focus:outline-none flex items-center flex-shrink-0"
|
||||
:aria-label="`Star ${n}`"
|
||||
>
|
||||
<i class="i-ri-star-fill text-n-amber-9 text-2xl" />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
Reference in New Issue
Block a user