mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-29 18:00:26 -06:00
Compare commits
1 Commits
main
...
fix/rating
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e2f51414b |
@@ -1171,7 +1171,6 @@ checksums:
|
|||||||
environments/surveys/edit/automatically_mark_the_survey_as_complete_after: c6ede2a5515a4ca72b36aec2583f43aa
|
environments/surveys/edit/automatically_mark_the_survey_as_complete_after: c6ede2a5515a4ca72b36aec2583f43aa
|
||||||
environments/surveys/edit/back_button_label: 25af945e77336724b5276de291cc92d9
|
environments/surveys/edit/back_button_label: 25af945e77336724b5276de291cc92d9
|
||||||
environments/surveys/edit/background_styling: 4e1e6fd2ec767bbff8767f6c0f68a731
|
environments/surveys/edit/background_styling: 4e1e6fd2ec767bbff8767f6c0f68a731
|
||||||
environments/surveys/edit/block_deleted: c682259eb138ad84f8b4441abfd9b572
|
|
||||||
environments/surveys/edit/block_duplicated: dc9e9fab2b1cd91f6c265324b34c6376
|
environments/surveys/edit/block_duplicated: dc9e9fab2b1cd91f6c265324b34c6376
|
||||||
environments/surveys/edit/bold: 4d7306bc355ed2befd6a9237c5452ee6
|
environments/surveys/edit/bold: 4d7306bc355ed2befd6a9237c5452ee6
|
||||||
environments/surveys/edit/brand_color: 84ddb5736deb9f5c081ffe4962a6c63e
|
environments/surveys/edit/brand_color: 84ddb5736deb9f5c081ffe4962a6c63e
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"test:coverage": "dotenv -e ../../.env -- vitest run --coverage",
|
"test:coverage": "dotenv -e ../../.env -- vitest run --coverage",
|
||||||
"generate-api-specs": "./scripts/openapi/generate.sh",
|
"generate-api-specs": "./scripts/openapi/generate.sh",
|
||||||
"merge-client-endpoints": "tsx ./scripts/openapi/merge-client-endpoints.ts",
|
"merge-client-endpoints": "tsx ./scripts/openapi/merge-client-endpoints.ts",
|
||||||
"generate-and-merge-api-specs": "pnpm run generate-api-specs && pnpm run merge-client-endpoints"
|
"generate-and-merge-api-specs": "pnpm run generate-api-specs && pnpm run merge-client-endpoints",
|
||||||
|
"i18n:generate": "npx lingo.dev@latest i18n"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.879.0",
|
"@aws-sdk/client-s3": "3.879.0",
|
||||||
|
|||||||
@@ -34,8 +34,9 @@
|
|||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"storybook": "turbo run storybook",
|
"storybook": "turbo run storybook",
|
||||||
"fb-migrate-dev": "pnpm --filter @formbricks/database create-migration && pnpm prisma generate",
|
"fb-migrate-dev": "pnpm --filter @formbricks/database create-migration && pnpm prisma generate",
|
||||||
"i18n:generate": " pnpm --filter @formbricks/surveys i18n:generate",
|
"i18n:surveys:generate": "pnpm --filter @formbricks/surveys i18n:generate",
|
||||||
"generate-translations": "cd apps/web && npx lingo.dev@latest i18n",
|
"i18n:web:generate": "pnpm --filter @formbricks/web i18n:generate",
|
||||||
|
"generate-translations": "pnpm i18n:web:generate && pnpm i18n:surveys:generate",
|
||||||
"scan-translations": "pnpm --filter @formbricks/i18n-utils scan-translations",
|
"scan-translations": "pnpm --filter @formbricks/i18n-utils scan-translations",
|
||||||
"i18n": "pnpm generate-translations && pnpm scan-translations",
|
"i18n": "pnpm generate-translations && pnpm scan-translations",
|
||||||
"i18n:validate": "pnpm scan-translations"
|
"i18n:validate": "pnpm scan-translations"
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ checksums:
|
|||||||
errors/please_fill_out_this_field: 88d4fd502ae8d423277aef723afcd1a7
|
errors/please_fill_out_this_field: 88d4fd502ae8d423277aef723afcd1a7
|
||||||
errors/please_rank_all_items_before_submitting: 24fb14a2550bd7ec3e253dda0997cea8
|
errors/please_rank_all_items_before_submitting: 24fb14a2550bd7ec3e253dda0997cea8
|
||||||
errors/please_select_a_date: 1abdc8ffb887dbbdcc0d05486cd84de7
|
errors/please_select_a_date: 1abdc8ffb887dbbdcc0d05486cd84de7
|
||||||
|
errors/please_select_a_rating: e871aa58c243589768f8b266ed6bb0aa
|
||||||
|
errors/please_select_a_value: 0c86021b2b819e94c99ae70bfccfd3f0
|
||||||
errors/please_upload_a_file: 4356dfca88553acb377664c923c2d6b7
|
errors/please_upload_a_file: 4356dfca88553acb377664c923c2d6b7
|
||||||
errors/recaptcha_error/message: b3f2c5950cbc0887f391f9e2bccb676e
|
errors/recaptcha_error/message: b3f2c5950cbc0887f391f9e2bccb676e
|
||||||
errors/recaptcha_error/title: 8e923ec38a92041569879a39c6467131
|
errors/recaptcha_error/title: 8e923ec38a92041569879a39c6467131
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
"please_fill_out_this_field": "يرجى ملء هذا الحقل",
|
"please_fill_out_this_field": "يرجى ملء هذا الحقل",
|
||||||
"please_rank_all_items_before_submitting": "يرجى ترتيب جميع العناصر قبل الإرسال",
|
"please_rank_all_items_before_submitting": "يرجى ترتيب جميع العناصر قبل الإرسال",
|
||||||
"please_select_a_date": "يرجى اختيار تاريخ",
|
"please_select_a_date": "يرجى اختيار تاريخ",
|
||||||
|
"please_select_a_rating": "يرجى اختيار تقييم",
|
||||||
|
"please_select_a_value": "يرجى اختيار قيمة",
|
||||||
"please_upload_a_file": "يرجى تحميل ملف",
|
"please_upload_a_file": "يرجى تحميل ملف",
|
||||||
"recaptcha_error": {
|
"recaptcha_error": {
|
||||||
"message": "تعذر إرسال ردك لأنه تم تصنيفه كنشاط آلي. إذا كنت تتنفس، يرجى المحاولة مرة أخرى.",
|
"message": "تعذر إرسال ردك لأنه تم تصنيفه كنشاط آلي. إذا كنت تتنفس، يرجى المحاولة مرة أخرى.",
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
"please_fill_out_this_field": "Bitte füllen Sie dieses Feld aus",
|
"please_fill_out_this_field": "Bitte füllen Sie dieses Feld aus",
|
||||||
"please_rank_all_items_before_submitting": "Bitte ordnen Sie alle Elemente vor dem Absenden",
|
"please_rank_all_items_before_submitting": "Bitte ordnen Sie alle Elemente vor dem Absenden",
|
||||||
"please_select_a_date": "Bitte wählen Sie ein Datum aus",
|
"please_select_a_date": "Bitte wählen Sie ein Datum aus",
|
||||||
|
"please_select_a_rating": "Bitte wählen Sie eine Bewertung aus",
|
||||||
|
"please_select_a_value": "Bitte wählen Sie einen Wert aus",
|
||||||
"please_upload_a_file": "Bitte laden Sie eine Datei hoch",
|
"please_upload_a_file": "Bitte laden Sie eine Datei hoch",
|
||||||
"recaptcha_error": {
|
"recaptcha_error": {
|
||||||
"message": "Ihre Antwort konnte nicht übermittelt werden, da sie als automatisierte Aktivität eingestuft wurde. Wenn Sie atmen, versuchen Sie es bitte erneut.",
|
"message": "Ihre Antwort konnte nicht übermittelt werden, da sie als automatisierte Aktivität eingestuft wurde. Wenn Sie atmen, versuchen Sie es bitte erneut.",
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
"please_fill_out_this_field": "Please fill out this field",
|
"please_fill_out_this_field": "Please fill out this field",
|
||||||
"please_rank_all_items_before_submitting": "Please rank all items before submitting",
|
"please_rank_all_items_before_submitting": "Please rank all items before submitting",
|
||||||
"please_select_a_date": "Please select a date",
|
"please_select_a_date": "Please select a date",
|
||||||
|
"please_select_a_rating": "Please select a rating",
|
||||||
|
"please_select_a_value": "Please select a value",
|
||||||
"please_upload_a_file": "Please upload a file",
|
"please_upload_a_file": "Please upload a file",
|
||||||
"recaptcha_error": {
|
"recaptcha_error": {
|
||||||
"message": "Your response could not be submitted because it was flagged as automated activity. If you breathe, please try again.",
|
"message": "Your response could not be submitted because it was flagged as automated activity. If you breathe, please try again.",
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
"please_fill_out_this_field": "Por favor, complete este campo",
|
"please_fill_out_this_field": "Por favor, complete este campo",
|
||||||
"please_rank_all_items_before_submitting": "Por favor, clasifique todos los elementos antes de enviar",
|
"please_rank_all_items_before_submitting": "Por favor, clasifique todos los elementos antes de enviar",
|
||||||
"please_select_a_date": "Por favor, seleccione una fecha",
|
"please_select_a_date": "Por favor, seleccione una fecha",
|
||||||
|
"please_select_a_rating": "Por favor selecciona una calificación",
|
||||||
|
"please_select_a_value": "Por favor selecciona un valor",
|
||||||
"please_upload_a_file": "Por favor, suba un archivo",
|
"please_upload_a_file": "Por favor, suba un archivo",
|
||||||
"recaptcha_error": {
|
"recaptcha_error": {
|
||||||
"message": "Su respuesta no pudo ser enviada porque fue marcada como actividad automatizada. Si respira, por favor inténtelo de nuevo.",
|
"message": "Su respuesta no pudo ser enviada porque fue marcada como actividad automatizada. Si respira, por favor inténtelo de nuevo.",
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
"please_fill_out_this_field": "Veuillez remplir ce champ",
|
"please_fill_out_this_field": "Veuillez remplir ce champ",
|
||||||
"please_rank_all_items_before_submitting": "Veuillez classer tous les éléments avant de soumettre",
|
"please_rank_all_items_before_submitting": "Veuillez classer tous les éléments avant de soumettre",
|
||||||
"please_select_a_date": "Veuillez sélectionner une date",
|
"please_select_a_date": "Veuillez sélectionner une date",
|
||||||
|
"please_select_a_rating": "Veuillez sélectionner une note",
|
||||||
|
"please_select_a_value": "Veuillez sélectionner une valeur",
|
||||||
"please_upload_a_file": "Veuillez télécharger un fichier",
|
"please_upload_a_file": "Veuillez télécharger un fichier",
|
||||||
"recaptcha_error": {
|
"recaptcha_error": {
|
||||||
"message": "Votre réponse n'a pas pu être soumise car elle a été signalée comme une activité automatisée. Si vous respirez, veuillez réessayer.",
|
"message": "Votre réponse n'a pas pu être soumise car elle a été signalée comme une activité automatisée. Si vous respirez, veuillez réessayer.",
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
"please_fill_out_this_field": "कृपया इस फील्ड को भरें",
|
"please_fill_out_this_field": "कृपया इस फील्ड को भरें",
|
||||||
"please_rank_all_items_before_submitting": "जमा करने से पहले कृपया सभी आइटम्स को रैंक करें",
|
"please_rank_all_items_before_submitting": "जमा करने से पहले कृपया सभी आइटम्स को रैंक करें",
|
||||||
"please_select_a_date": "कृपया एक तारीख चुनें",
|
"please_select_a_date": "कृपया एक तारीख चुनें",
|
||||||
|
"please_select_a_rating": "कृपया एक रेटिंग चुनें",
|
||||||
|
"please_select_a_value": "कृपया एक मान चुनें",
|
||||||
"please_upload_a_file": "कृपया एक फाइल अपलोड करें",
|
"please_upload_a_file": "कृपया एक फाइल अपलोड करें",
|
||||||
"recaptcha_error": {
|
"recaptcha_error": {
|
||||||
"message": "आपका प्रतिसाद जमा नहीं किया जा सका क्योंकि इसे स्वचालित गतिविधि के रूप में चिह्नित किया गया था। यदि आप सांस लेते हैं, तो कृपया पुनः प्रयास करें।",
|
"message": "आपका प्रतिसाद जमा नहीं किया जा सका क्योंकि इसे स्वचालित गतिविधि के रूप में चिह्नित किया गया था। यदि आप सांस लेते हैं, तो कृपया पुनः प्रयास करें।",
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
"please_fill_out_this_field": "Compila questo campo",
|
"please_fill_out_this_field": "Compila questo campo",
|
||||||
"please_rank_all_items_before_submitting": "Classifica tutti gli elementi prima di inviare",
|
"please_rank_all_items_before_submitting": "Classifica tutti gli elementi prima di inviare",
|
||||||
"please_select_a_date": "Seleziona una data",
|
"please_select_a_date": "Seleziona una data",
|
||||||
|
"please_select_a_rating": "Seleziona una valutazione",
|
||||||
|
"please_select_a_value": "Seleziona un valore",
|
||||||
"please_upload_a_file": "Carica un file",
|
"please_upload_a_file": "Carica un file",
|
||||||
"recaptcha_error": {
|
"recaptcha_error": {
|
||||||
"message": "La tua risposta non può essere inviata perché è stata segnalata come attività automatizzata. Se respiri, riprova.",
|
"message": "La tua risposta non può essere inviata perché è stata segnalata come attività automatizzata. Se respiri, riprova.",
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
"please_fill_out_this_field": "このフィールドに入力してください",
|
"please_fill_out_this_field": "このフィールドに入力してください",
|
||||||
"please_rank_all_items_before_submitting": "送信する前にすべての項目をランク付けしてください",
|
"please_rank_all_items_before_submitting": "送信する前にすべての項目をランク付けしてください",
|
||||||
"please_select_a_date": "日付を選択してください",
|
"please_select_a_date": "日付を選択してください",
|
||||||
|
"please_select_a_rating": "評価を選択してください",
|
||||||
|
"please_select_a_value": "値を選択してください",
|
||||||
"please_upload_a_file": "ファイルをアップロードしてください",
|
"please_upload_a_file": "ファイルをアップロードしてください",
|
||||||
"recaptcha_error": {
|
"recaptcha_error": {
|
||||||
"message": "自動化された活動としてフラグが立てられたため、回答を送信できませんでした。人間の方は、もう一度お試しください。",
|
"message": "自動化された活動としてフラグが立てられたため、回答を送信できませんでした。人間の方は、もう一度お試しください。",
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
"please_fill_out_this_field": "Vul dit veld in",
|
"please_fill_out_this_field": "Vul dit veld in",
|
||||||
"please_rank_all_items_before_submitting": "Rangschik alle items voordat u ze verzendt",
|
"please_rank_all_items_before_submitting": "Rangschik alle items voordat u ze verzendt",
|
||||||
"please_select_a_date": "Selecteer een datum",
|
"please_select_a_date": "Selecteer een datum",
|
||||||
|
"please_select_a_rating": "Selecteer een beoordeling",
|
||||||
|
"please_select_a_value": "Selecteer een waarde",
|
||||||
"please_upload_a_file": "Upload een bestand",
|
"please_upload_a_file": "Upload een bestand",
|
||||||
"recaptcha_error": {
|
"recaptcha_error": {
|
||||||
"message": "Uw reactie kan niet worden verzonden omdat deze is gemarkeerd als geautomatiseerde activiteit. Als u ademhaalt, probeer het dan opnieuw.",
|
"message": "Uw reactie kan niet worden verzonden omdat deze is gemarkeerd als geautomatiseerde activiteit. Als u ademhaalt, probeer het dan opnieuw.",
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
"please_fill_out_this_field": "Por favor, preencha este campo",
|
"please_fill_out_this_field": "Por favor, preencha este campo",
|
||||||
"please_rank_all_items_before_submitting": "Por favor, classifique todos os itens antes de enviar",
|
"please_rank_all_items_before_submitting": "Por favor, classifique todos os itens antes de enviar",
|
||||||
"please_select_a_date": "Por favor, selecione uma data",
|
"please_select_a_date": "Por favor, selecione uma data",
|
||||||
|
"please_select_a_rating": "Por favor, selecione uma classificação",
|
||||||
|
"please_select_a_value": "Por favor, selecione um valor",
|
||||||
"please_upload_a_file": "Por favor, carregue um arquivo",
|
"please_upload_a_file": "Por favor, carregue um arquivo",
|
||||||
"recaptcha_error": {
|
"recaptcha_error": {
|
||||||
"message": "Sua resposta não pôde ser enviada porque foi sinalizada como atividade automatizada. Se você respira, por favor tente novamente.",
|
"message": "Sua resposta não pôde ser enviada porque foi sinalizada como atividade automatizada. Se você respira, por favor tente novamente.",
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
"please_fill_out_this_field": "Vă rugăm să completați acest câmp",
|
"please_fill_out_this_field": "Vă rugăm să completați acest câmp",
|
||||||
"please_rank_all_items_before_submitting": "Vă rugăm să clasificați toate elementele înainte de a trimite",
|
"please_rank_all_items_before_submitting": "Vă rugăm să clasificați toate elementele înainte de a trimite",
|
||||||
"please_select_a_date": "Vă rugăm să selectați o dată",
|
"please_select_a_date": "Vă rugăm să selectați o dată",
|
||||||
|
"please_select_a_rating": "Vă rugăm să selectați o evaluare",
|
||||||
|
"please_select_a_value": "Vă rugăm să selectați o valoare",
|
||||||
"please_upload_a_file": "Vă rugăm să încărcați un fișier",
|
"please_upload_a_file": "Vă rugăm să încărcați un fișier",
|
||||||
"recaptcha_error": {
|
"recaptcha_error": {
|
||||||
"message": "Răspunsul dumneavoastră nu a putut fi trimis deoarece a fost marcat ca activitate automată. Dacă respirați, încercați din nou.",
|
"message": "Răspunsul dumneavoastră nu a putut fi trimis deoarece a fost marcat ca activitate automată. Dacă respirați, încercați din nou.",
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
"please_fill_out_this_field": "Пожалуйста, заполните это поле",
|
"please_fill_out_this_field": "Пожалуйста, заполните это поле",
|
||||||
"please_rank_all_items_before_submitting": "Пожалуйста, оцените все элементы перед отправкой",
|
"please_rank_all_items_before_submitting": "Пожалуйста, оцените все элементы перед отправкой",
|
||||||
"please_select_a_date": "Пожалуйста, выберите дату",
|
"please_select_a_date": "Пожалуйста, выберите дату",
|
||||||
|
"please_select_a_rating": "Пожалуйста, выберите оценку",
|
||||||
|
"please_select_a_value": "Пожалуйста, выберите значение",
|
||||||
"please_upload_a_file": "Пожалуйста, загрузите файл",
|
"please_upload_a_file": "Пожалуйста, загрузите файл",
|
||||||
"recaptcha_error": {
|
"recaptcha_error": {
|
||||||
"message": "Ваш ответ не может быть отправлен, так как он был помечен как автоматическая активность. Если вы дышите, попробуйте ещё раз.",
|
"message": "Ваш ответ не может быть отправлен, так как он был помечен как автоматическая активность. Если вы дышите, попробуйте ещё раз.",
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
"please_fill_out_this_field": "Iltimos, ushbu maydonni to'ldiring",
|
"please_fill_out_this_field": "Iltimos, ushbu maydonni to'ldiring",
|
||||||
"please_rank_all_items_before_submitting": "Iltimos, yuborishdan oldin barcha elementlarni baholang",
|
"please_rank_all_items_before_submitting": "Iltimos, yuborishdan oldin barcha elementlarni baholang",
|
||||||
"please_select_a_date": "Iltimos, sanani tanlang",
|
"please_select_a_date": "Iltimos, sanani tanlang",
|
||||||
|
"please_select_a_rating": "Iltimos, reytingni tanlang",
|
||||||
|
"please_select_a_value": "Iltimos, qiymatni tanlang",
|
||||||
"please_upload_a_file": "Iltimos, faylni yuklang",
|
"please_upload_a_file": "Iltimos, faylni yuklang",
|
||||||
"recaptcha_error": {
|
"recaptcha_error": {
|
||||||
"message": "Sizning javobingiz avtomatlashtirilgan faoliyat sifatida belgilanganligi sababli yuborilmadi. Agar siz nafas olayotgan bo'lsangiz, qayta urinib ko'ring.",
|
"message": "Sizning javobingiz avtomatlashtirilgan faoliyat sifatida belgilanganligi sababli yuborilmadi. Agar siz nafas olayotgan bo'lsangiz, qayta urinib ko'ring.",
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
"please_fill_out_this_field": "请填写此字段",
|
"please_fill_out_this_field": "请填写此字段",
|
||||||
"please_rank_all_items_before_submitting": "请在提交之前对所有项目进行排名",
|
"please_rank_all_items_before_submitting": "请在提交之前对所有项目进行排名",
|
||||||
"please_select_a_date": "请选择一个日期",
|
"please_select_a_date": "请选择一个日期",
|
||||||
|
"please_select_a_rating": "请选择一个评分",
|
||||||
|
"please_select_a_value": "请选择一个值",
|
||||||
"please_upload_a_file": "请上传一个文件",
|
"please_upload_a_file": "请上传一个文件",
|
||||||
"recaptcha_error": {
|
"recaptcha_error": {
|
||||||
"message": "您的响应未能提交,因为它被标记为自动活动。如果您是人类,请重试。",
|
"message": "您的响应未能提交,因为它被标记为自动活动。如果您是人类,请重试。",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||||
import type { TSurveyNPSElement } from "@formbricks/types/surveys/elements";
|
import type { TSurveyNPSElement } from "@formbricks/types/surveys/elements";
|
||||||
import { ElementMedia } from "@/components/general/element-media";
|
import { ElementMedia } from "@/components/general/element-media";
|
||||||
@@ -18,6 +19,8 @@ interface NPSElementProps {
|
|||||||
autoFocusEnabled: boolean;
|
autoFocusEnabled: boolean;
|
||||||
currentElementId: string;
|
currentElementId: string;
|
||||||
dir?: "ltr" | "rtl" | "auto";
|
dir?: "ltr" | "rtl" | "auto";
|
||||||
|
shouldAutoAdvance?: boolean;
|
||||||
|
onAutoSubmit?: (responseData: TResponseData, ttc: TResponseTtc) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NPSElement({
|
export function NPSElement({
|
||||||
@@ -29,17 +32,27 @@ export function NPSElement({
|
|||||||
setTtc,
|
setTtc,
|
||||||
currentElementId,
|
currentElementId,
|
||||||
dir = "auto",
|
dir = "auto",
|
||||||
|
shouldAutoAdvance,
|
||||||
|
onAutoSubmit,
|
||||||
}: Readonly<NPSElementProps>) {
|
}: Readonly<NPSElementProps>) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const [hoveredNumber, setHoveredNumber] = useState(-1);
|
const [hoveredNumber, setHoveredNumber] = useState(-1);
|
||||||
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||||
const isCurrent = element.id === currentElementId;
|
const isCurrent = element.id === currentElementId;
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
||||||
|
|
||||||
const handleClick = (number: number) => {
|
const handleClick = (number: number) => {
|
||||||
onChange({ [element.id]: number });
|
setErrorMessage("");
|
||||||
|
const responseData = { [element.id]: number };
|
||||||
|
onChange(responseData);
|
||||||
const updatedTtcObj = getUpdatedTtc(ttc, element.id, performance.now() - startTime);
|
const updatedTtcObj = getUpdatedTtc(ttc, element.id, performance.now() - startTime);
|
||||||
setTtc(updatedTtcObj);
|
setTtc(updatedTtcObj);
|
||||||
|
|
||||||
|
if (shouldAutoAdvance && onAutoSubmit) {
|
||||||
|
onAutoSubmit(responseData, updatedTtcObj);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNPSOptionColor = (idx: number) => {
|
const getNPSOptionColor = (idx: number) => {
|
||||||
@@ -53,6 +66,13 @@ export function NPSElement({
|
|||||||
key={element.id}
|
key={element.id}
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (element.required && value === undefined) {
|
||||||
|
setErrorMessage(t("errors.please_select_a_value"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrorMessage("");
|
||||||
const updatedTtcObj = getUpdatedTtc(ttc, element.id, performance.now() - startTime);
|
const updatedTtcObj = getUpdatedTtc(ttc, element.id, performance.now() - startTime);
|
||||||
setTtc(updatedTtcObj);
|
setTtc(updatedTtcObj);
|
||||||
}}>
|
}}>
|
||||||
@@ -66,6 +86,11 @@ export function NPSElement({
|
|||||||
subheader={element.subheader ? getLocalizedValue(element.subheader, languageCode) : ""}
|
subheader={element.subheader ? getLocalizedValue(element.subheader, languageCode) : ""}
|
||||||
elementId={element.id}
|
elementId={element.id}
|
||||||
/>
|
/>
|
||||||
|
{errorMessage && (
|
||||||
|
<div className="fb-mt-2 fb-text-sm fb-text-red-500" role="alert" aria-live="assertive">
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="fb-my-4">
|
<div className="fb-my-4">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend className="fb-sr-only">Options</legend>
|
<legend className="fb-sr-only">Options</legend>
|
||||||
@@ -121,7 +146,7 @@ export function NPSElement({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleClick(number);
|
handleClick(number);
|
||||||
}}
|
}}
|
||||||
required={element.required}
|
// Note: We handle required validation manually via onSubmit to show custom error messages
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
/>
|
/>
|
||||||
{number}
|
{number}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import type { JSX } from "react";
|
import type { JSX } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||||
import type { TSurveyRatingElement } from "@formbricks/types/surveys/elements";
|
import type { TSurveyRatingElement } from "@formbricks/types/surveys/elements";
|
||||||
import { ElementMedia } from "@/components/general/element-media";
|
import { ElementMedia } from "@/components/general/element-media";
|
||||||
@@ -31,6 +32,8 @@ interface RatingElementProps {
|
|||||||
autoFocusEnabled: boolean;
|
autoFocusEnabled: boolean;
|
||||||
currentElementId: string;
|
currentElementId: string;
|
||||||
dir?: "ltr" | "rtl" | "auto";
|
dir?: "ltr" | "rtl" | "auto";
|
||||||
|
shouldAutoAdvance?: boolean;
|
||||||
|
onAutoSubmit?: (responseData: TResponseData, ttc: TResponseTtc) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RatingElement({
|
export function RatingElement({
|
||||||
@@ -42,17 +45,28 @@ export function RatingElement({
|
|||||||
setTtc,
|
setTtc,
|
||||||
currentElementId,
|
currentElementId,
|
||||||
dir = "auto",
|
dir = "auto",
|
||||||
|
shouldAutoAdvance,
|
||||||
|
onAutoSubmit,
|
||||||
}: RatingElementProps) {
|
}: RatingElementProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [hoveredNumber, setHoveredNumber] = useState(0);
|
const [hoveredNumber, setHoveredNumber] = useState(0);
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||||
const isCurrent = element.id === currentElementId;
|
const isCurrent = element.id === currentElementId;
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
||||||
|
|
||||||
const handleSelect = (number: number) => {
|
const handleSelect = (number: number) => {
|
||||||
onChange({ [element.id]: number });
|
setErrorMessage("");
|
||||||
|
const responseData = { [element.id]: number };
|
||||||
|
onChange(responseData);
|
||||||
const updatedTtcObj = getUpdatedTtc(ttc, element.id, performance.now() - startTime);
|
const updatedTtcObj = getUpdatedTtc(ttc, element.id, performance.now() - startTime);
|
||||||
setTtc(updatedTtcObj);
|
setTtc(updatedTtcObj);
|
||||||
|
|
||||||
|
// Auto-advance if enabled (single required Rating element in block)
|
||||||
|
if (shouldAutoAdvance && onAutoSubmit) {
|
||||||
|
onAutoSubmit(responseData, updatedTtcObj);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function HiddenRadioInput({ number, id }: { number: number; id?: string }) {
|
function HiddenRadioInput({ number, id }: { number: number; id?: string }) {
|
||||||
@@ -66,7 +80,7 @@ export function RatingElement({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleSelect(number);
|
handleSelect(number);
|
||||||
}}
|
}}
|
||||||
required={element.required}
|
// Note: We handle required validation manually via handleFormSubmit to show custom error messages
|
||||||
checked={value === number}
|
checked={value === number}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -93,6 +107,13 @@ export function RatingElement({
|
|||||||
|
|
||||||
const handleFormSubmit = (e: Event) => {
|
const handleFormSubmit = (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (element.required && value === undefined) {
|
||||||
|
setErrorMessage(t("errors.please_select_a_rating"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrorMessage("");
|
||||||
const updatedTtcObj = getUpdatedTtc(ttc, element.id, performance.now() - startTime);
|
const updatedTtcObj = getUpdatedTtc(ttc, element.id, performance.now() - startTime);
|
||||||
setTtc(updatedTtcObj);
|
setTtc(updatedTtcObj);
|
||||||
};
|
};
|
||||||
@@ -246,6 +267,11 @@ export function RatingElement({
|
|||||||
subheader={element.subheader ? getLocalizedValue(element.subheader, languageCode) : ""}
|
subheader={element.subheader ? getLocalizedValue(element.subheader, languageCode) : ""}
|
||||||
elementId={element.id}
|
elementId={element.id}
|
||||||
/>
|
/>
|
||||||
|
{errorMessage && (
|
||||||
|
<div className="fb-mt-2 fb-text-sm fb-text-red-500" role="alert" aria-live="assertive">
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="fb-mb-4 fb-mt-6 fb-flex fb-items-center fb-justify-center">
|
<div className="fb-mb-4 fb-mt-6 fb-flex fb-items-center fb-justify-center">
|
||||||
<fieldset className="fb-w-full">
|
<fieldset className="fb-w-full">
|
||||||
<legend className="fb-sr-only">Choices</legend>
|
<legend className="fb-sr-only">Choices</legend>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from "preact/hooks";
|
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||||
import { type TJsFileUploadParams } from "@formbricks/types/js";
|
import { type TJsFileUploadParams } from "@formbricks/types/js";
|
||||||
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
|
||||||
import { type TUploadFileConfig } from "@formbricks/types/storage";
|
import { type TUploadFileConfig } from "@formbricks/types/storage";
|
||||||
@@ -67,6 +67,16 @@ export function BlockConditional({
|
|||||||
// Ref to collect TTC values synchronously (state updates are async)
|
// Ref to collect TTC values synchronously (state updates are async)
|
||||||
const ttcCollectorRef = useRef<TResponseTtc>({});
|
const ttcCollectorRef = useRef<TResponseTtc>({});
|
||||||
|
|
||||||
|
// Determine if we should auto-advance (single required NPS or Rating element in block)
|
||||||
|
const shouldAutoAdvance = useMemo(() => {
|
||||||
|
if (block.elements.length !== 1) return false;
|
||||||
|
const element = block.elements[0];
|
||||||
|
return (
|
||||||
|
(element.type === TSurveyElementTypeEnum.NPS || element.type === TSurveyElementTypeEnum.Rating) &&
|
||||||
|
element.required
|
||||||
|
);
|
||||||
|
}, [block.elements]);
|
||||||
|
|
||||||
// Handle change for an individual element
|
// Handle change for an individual element
|
||||||
const handleElementChange = (elementId: string, responseData: TResponseData) => {
|
const handleElementChange = (elementId: string, responseData: TResponseData) => {
|
||||||
// If user moved to a different element, we should track it
|
// If user moved to a different element, we should track it
|
||||||
@@ -278,37 +288,42 @@ export function BlockConditional({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onTtcCollect={handleTtcCollect}
|
onTtcCollect={handleTtcCollect}
|
||||||
|
shouldAutoAdvance={shouldAutoAdvance}
|
||||||
|
onAutoSubmit={onSubmit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
{/* Hide navigation buttons when auto-advancing (check the condition for auto-advance above) */}
|
||||||
className={cn(
|
{!shouldAutoAdvance && (
|
||||||
"fb-flex fb-w-full fb-flex-row-reverse fb-justify-between",
|
<div
|
||||||
fullSizeCards ? "fb-sticky fb-bottom-0 fb-bg-white" : ""
|
className={cn(
|
||||||
)}>
|
"fb-flex fb-w-full fb-flex-row-reverse fb-justify-between",
|
||||||
<div>
|
fullSizeCards ? "fb-sticky fb-bottom-0 fb-bg-white" : ""
|
||||||
<SubmitButton
|
)}>
|
||||||
buttonLabel={
|
<div>
|
||||||
block.buttonLabel ? getLocalizedValue(block.buttonLabel, languageCode) : undefined
|
<SubmitButton
|
||||||
}
|
buttonLabel={
|
||||||
isLastQuestion={isLastBlock}
|
block.buttonLabel ? getLocalizedValue(block.buttonLabel, languageCode) : undefined
|
||||||
onClick={handleBlockSubmit}
|
}
|
||||||
tabIndex={0}
|
isLastQuestion={isLastBlock}
|
||||||
/>
|
onClick={handleBlockSubmit}
|
||||||
|
tabIndex={0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!isFirstBlock && !isBackButtonHidden && (
|
||||||
|
<BackButton
|
||||||
|
backButtonLabel={
|
||||||
|
block.backButtonLabel ? getLocalizedValue(block.backButtonLabel, languageCode) : undefined
|
||||||
|
}
|
||||||
|
onClick={onBack}
|
||||||
|
tabIndex={0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!isFirstBlock && !isBackButtonHidden && (
|
)}
|
||||||
<BackButton
|
|
||||||
backButtonLabel={
|
|
||||||
block.backButtonLabel ? getLocalizedValue(block.backButtonLabel, languageCode) : undefined
|
|
||||||
}
|
|
||||||
onClick={onBack}
|
|
||||||
tabIndex={0}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ScrollableContainer>
|
</ScrollableContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ interface ElementConditionalProps {
|
|||||||
dir?: "ltr" | "rtl" | "auto";
|
dir?: "ltr" | "rtl" | "auto";
|
||||||
formRef?: (ref: HTMLFormElement | null) => void; // Callback to expose the form element
|
formRef?: (ref: HTMLFormElement | null) => void; // Callback to expose the form element
|
||||||
onTtcCollect?: (elementId: string, ttc: number) => void; // Callback to collect TTC synchronously
|
onTtcCollect?: (elementId: string, ttc: number) => void; // Callback to collect TTC synchronously
|
||||||
|
shouldAutoAdvance?: boolean;
|
||||||
|
onAutoSubmit?: (responseData: TResponseData, ttc: TResponseTtc) => void; // Ideally just calls onSubmit from the block conditional
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ElementConditional({
|
export function ElementConditional({
|
||||||
@@ -62,6 +64,8 @@ export function ElementConditional({
|
|||||||
dir,
|
dir,
|
||||||
formRef,
|
formRef,
|
||||||
onTtcCollect,
|
onTtcCollect,
|
||||||
|
shouldAutoAdvance,
|
||||||
|
onAutoSubmit,
|
||||||
}: ElementConditionalProps) {
|
}: ElementConditionalProps) {
|
||||||
// Ref to the container div, used to find and expose the form element inside
|
// Ref to the container div, used to find and expose the form element inside
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -184,6 +188,8 @@ export function ElementConditional({
|
|||||||
autoFocusEnabled={autoFocusEnabled}
|
autoFocusEnabled={autoFocusEnabled}
|
||||||
currentElementId={currentElementId}
|
currentElementId={currentElementId}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
|
shouldAutoAdvance={shouldAutoAdvance}
|
||||||
|
onAutoSubmit={onAutoSubmit}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case TSurveyElementTypeEnum.CTA:
|
case TSurveyElementTypeEnum.CTA:
|
||||||
@@ -214,6 +220,8 @@ export function ElementConditional({
|
|||||||
autoFocusEnabled={autoFocusEnabled}
|
autoFocusEnabled={autoFocusEnabled}
|
||||||
currentElementId={currentElementId}
|
currentElementId={currentElementId}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
|
shouldAutoAdvance={shouldAutoAdvance}
|
||||||
|
onAutoSubmit={onAutoSubmit}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case TSurveyElementTypeEnum.Consent:
|
case TSurveyElementTypeEnum.Consent:
|
||||||
|
|||||||
Reference in New Issue
Block a user