diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/components/Modal.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/components/Modal.tsx index 7f66533c6e..11f5374014 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/components/Modal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/components/Modal.tsx @@ -52,7 +52,7 @@ export default function Modal({ return { transform: `scale(${scaleValue})`, - "transform-origin": placementClass, + transformOrigin: placementClass, }; }; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey.tsx index aba7be092f..06c2b98698 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey.tsx @@ -241,8 +241,9 @@ export default function PreviewSurvey({
-

- {previewType === "modal" ? "Your web app" : "Preview"} +

+

{previewType === "modal" ? "Your web app" : "Preview"}

+
{isFullScreenPreview ? (
-

+
{previewType === "modal" ? ( diff --git a/packages/ui/Input/index.tsx b/packages/ui/Input/index.tsx index 62d08787ea..58d0170ad3 100644 --- a/packages/ui/Input/index.tsx +++ b/packages/ui/Input/index.tsx @@ -11,13 +11,13 @@ export interface InputProps isInvalid?: boolean; } -const Input = React.forwardRef(({ className, ...props }, ref) => { +const Input = React.forwardRef(({ className, isInvalid, ...props }, ref) => { return ( ; } -const QuestionFormInput = ({ - localSurvey, - questionId, - questionIdx, - updateQuestion, - updateSurvey, - isInvalid, - environmentId, - type, -}: QuestionFormInputProps) => { - const isThankYouCard = questionId === "end"; - const question = useMemo(() => { - return isThankYouCard - ? localSurvey.thankYouCard - : localSurvey.questions.find((question) => question.id === questionId)!; - }, [isThankYouCard, localSurvey, questionId]); +const QuestionFormInput = React.forwardRef( + ( + { + localSurvey, + questionId, + questionIdx, + updateQuestion, + updateSurvey, + isInvalid, + environmentId, + type, + }: QuestionFormInputProps, + ref + ) => { + const isThankYouCard = questionId === "end"; + const question = useMemo(() => { + return isThankYouCard + ? localSurvey.thankYouCard + : localSurvey.questions.find((question) => question.id === questionId)!; + }, [isThankYouCard, localSurvey, questionId]); - const getQuestionTextBasedOnType = (): string => { - return question[type as keyof typeof question] || ""; - }; + const getQuestionTextBasedOnType = (): string => { + return question[type as keyof typeof question] || ""; + }; - const [text, setText] = useState(getQuestionTextBasedOnType() ?? ""); - const [renderedText, setRenderedText] = useState(); + const [text, setText] = useState(getQuestionTextBasedOnType() ?? ""); + const [renderedText, setRenderedText] = useState(); - const highlightContainerRef = useRef(null); - const fallbackInputRef = useRef(null); - const inputRef = useRef(null); - const [showImageUploader, setShowImageUploader] = useState( - questionId === "end" - ? localSurvey.thankYouCard.imageUrl - ? true - : false - : !!(question as TSurveyQuestion).imageUrl - ); - const [showQuestionSelect, setShowQuestionSelect] = useState(false); - const [showFallbackInput, setShowFallbackInput] = useState(false); - const [recallQuestions, setRecallQuestions] = useState( - text.includes("#recall:") ? getRecallQuestions(text, localSurvey) : [] - ); - const filteredRecallQuestions = Array.from(new Set(recallQuestions.map((q) => q.id))).map((id) => { - return recallQuestions.find((q) => q.id === id); - }); - const [fallbacks, setFallbacks] = useState<{ [type: string]: string }>( - text.includes("/fallback:") ? getFallbackValues(text) : {} - ); - - // Hook to synchronize the horizontal scroll position of highlightContainerRef and inputRef. - useSyncScroll(highlightContainerRef, inputRef, text); - - useEffect(() => { - // Generates an array of headlines from recallQuestions, replacing nested recall questions with '___' . - const recallQuestionHeadlines = recallQuestions.flatMap((recallQuestion) => { - if (!recallQuestion.headline.includes("#recall:")) { - return [recallQuestion.headline]; - } - const recallQuestionText = (recallQuestion[type as keyof typeof recallQuestion] as string) || ""; - const recallInfo = extractRecallInfo(recallQuestionText); - - if (recallInfo) { - const recallQuestionId = extractId(recallInfo); - const recallQuestion = localSurvey.questions.find((question) => question.id === recallQuestionId); - - if (recallQuestion) { - return [recallQuestionText.replace(recallInfo, `___`)]; - } - } - return []; + const highlightContainerRef = useRef(null); + const fallbackInputRef = useRef(null); + const inputRef = useRef(null); + const [showImageUploader, setShowImageUploader] = useState( + questionId === "end" + ? localSurvey.thankYouCard.imageUrl + ? true + : false + : !!(question as TSurveyQuestion).imageUrl + ); + const [showQuestionSelect, setShowQuestionSelect] = useState(false); + const [showFallbackInput, setShowFallbackInput] = useState(false); + const [recallQuestions, setRecallQuestions] = useState( + text.includes("#recall:") ? getRecallQuestions(text, localSurvey) : [] + ); + const filteredRecallQuestions = Array.from(new Set(recallQuestions.map((q) => q.id))).map((id) => { + return recallQuestions.find((q) => q.id === id); }); + const [fallbacks, setFallbacks] = useState<{ [type: string]: string }>( + text.includes("/fallback:") ? getFallbackValues(text) : {} + ); - // Constructs an array of JSX elements representing segmented parts of text, interspersed with special formatted spans for recall headlines. - const processInput = (): JSX.Element[] => { - const parts: JSX.Element[] = []; - let remainingText: string = text ?? ""; - remainingText = recallToHeadline(remainingText, localSurvey, false); - filterRecallQuestions(remainingText); - recallQuestionHeadlines.forEach((headline) => { - const index = remainingText.indexOf("@" + headline); - if (index !== -1) { - if (index > 0) { + // Hook to synchronize the horizontal scroll position of highlightContainerRef and inputRef. + useSyncScroll(highlightContainerRef, inputRef, text); + + useEffect(() => { + // Generates an array of headlines from recallQuestions, replacing nested recall questions with '___' . + const recallQuestionHeadlines = recallQuestions.flatMap((recallQuestion) => { + if (!recallQuestion.headline.includes("#recall:")) { + return [recallQuestion.headline]; + } + const recallQuestionText = (recallQuestion[type as keyof typeof recallQuestion] as string) || ""; + const recallInfo = extractRecallInfo(recallQuestionText); + + if (recallInfo) { + const recallQuestionId = extractId(recallInfo); + const recallQuestion = localSurvey.questions.find((question) => question.id === recallQuestionId); + + if (recallQuestion) { + return [recallQuestionText.replace(recallInfo, `___`)]; + } + } + return []; + }); + + // Constructs an array of JSX elements representing segmented parts of text, interspersed with special formatted spans for recall headlines. + const processInput = (): JSX.Element[] => { + const parts: JSX.Element[] = []; + let remainingText: string = text ?? ""; + remainingText = recallToHeadline(remainingText, localSurvey, false); + filterRecallQuestions(remainingText); + recallQuestionHeadlines.forEach((headline) => { + const index = remainingText.indexOf("@" + headline); + if (index !== -1) { + if (index > 0) { + parts.push( + + {remainingText.substring(0, index)} + + ); + } parts.push( - - {remainingText.substring(0, index)} + + {"@" + headline} ); + remainingText = remainingText.substring(index + headline.length + 1); } + }); + if (remainingText.length) { parts.push( - - {"@" + headline} + + {remainingText} ); - remainingText = remainingText.substring(index + headline.length + 1); + } + return parts; + }; + setRenderedText(processInput()); + }, [text]); + + useEffect(() => { + if (fallbackInputRef.current) { + fallbackInputRef.current.focus(); + } + }, [showFallbackInput]); + + const checkForRecallSymbol = () => { + const pattern = /(^|\s)@(\s|$)/; + if (pattern.test(text)) { + setShowQuestionSelect(true); + } else { + setShowQuestionSelect(false); + } + }; + + // Adds a new recall question to the recallQuestions array, updates fallbacks, modifies the text with recall details. + const addRecallQuestion = (recallQuestion: TSurveyQuestion) => { + let recallQuestionTemp = { ...recallQuestion }; + recallQuestionTemp = replaceRecallInfoWithUnderline(recallQuestionTemp); + setRecallQuestions((prevQuestions) => { + const updatedQuestions = [...prevQuestions, recallQuestionTemp]; + return updatedQuestions; + }); + if (!Object.keys(fallbacks).includes(recallQuestion.id)) { + setFallbacks((prevFallbacks) => ({ + ...prevFallbacks, + [recallQuestion.id]: "", + })); + } + setShowQuestionSelect(false); + const modifiedHeadlineWithId = getQuestionTextBasedOnType().replace( + "@", + `#recall:${recallQuestion.id}/fallback:# ` + ); + updateQuestionDetails(modifiedHeadlineWithId); + + const modifiedHeadlineWithName = recallToHeadline(modifiedHeadlineWithId, localSurvey, false); + setText(modifiedHeadlineWithName); + setShowFallbackInput(true); + }; + + // Filters and updates the list of recall questions based on their presence in the given text, also managing related text and fallback states. + const filterRecallQuestions = (text: string) => { + let includedQuestions: TSurveyQuestion[] = []; + recallQuestions.forEach((recallQuestion) => { + if (text.includes(`@${recallQuestion.headline}`)) { + includedQuestions.push(recallQuestion); + } else { + const questionToRemove = recallQuestion.headline.slice(0, -1); + const newText = text.replace(`@${questionToRemove}`, ""); + setText(newText); + updateQuestionDetails(newText); + let updatedFallback = { ...fallbacks }; + delete updatedFallback[recallQuestion.id]; + setFallbacks(updatedFallback); } }); - if (remainingText.length) { - parts.push( - - {remainingText} - - ); - } - return parts; + setRecallQuestions(includedQuestions); }; - setRenderedText(processInput()); - }, [text]); - useEffect(() => { - if (fallbackInputRef.current) { - fallbackInputRef.current.focus(); - } - }, [showFallbackInput]); + const addFallback = () => { + let headlineWithFallback = getQuestionTextBasedOnType(); + filteredRecallQuestions.forEach((recallQuestion) => { + if (recallQuestion) { + const recallInfo = findRecallInfoById(getQuestionTextBasedOnType(), recallQuestion!.id); + if (recallInfo) { + let fallBackValue = fallbacks[recallQuestion.id].trim(); + fallBackValue = fallBackValue.replace(/ /g, "nbsp"); + let updatedFallback = { ...fallbacks }; + updatedFallback[recallQuestion.id] = fallBackValue; + setFallbacks(updatedFallback); + headlineWithFallback = headlineWithFallback.replace( + recallInfo, + `#recall:${recallQuestion?.id}/fallback:${fallBackValue}#` + ); + updateQuestionDetails(headlineWithFallback); + } + } + }); + setShowFallbackInput(false); + inputRef.current?.focus(); + }; - const checkForRecallSymbol = () => { - const pattern = /(^|\s)@(\s|$)/; - if (pattern.test(text)) { - setShowQuestionSelect(true); - } else { - setShowQuestionSelect(false); - } - }; + useEffect(() => { + checkForRecallSymbol(); + }, [text]); - // Adds a new recall question to the recallQuestions array, updates fallbacks, modifies the text with recall details. - const addRecallQuestion = (recallQuestion: TSurveyQuestion) => { - let recallQuestionTemp = { ...recallQuestion }; - recallQuestionTemp = replaceRecallInfoWithUnderline(recallQuestionTemp); - setRecallQuestions((prevQuestions) => { - const updatedQuestions = [...prevQuestions, recallQuestionTemp]; - return updatedQuestions; - }); - if (!Object.keys(fallbacks).includes(recallQuestion.id)) { - setFallbacks((prevFallbacks) => ({ - ...prevFallbacks, - [recallQuestion.id]: "", - })); - } - setShowQuestionSelect(false); - const modifiedHeadlineWithId = getQuestionTextBasedOnType().replace( - "@", - `#recall:${recallQuestion.id}/fallback:# ` - ); - updateQuestionDetails(modifiedHeadlineWithId); - - const modifiedHeadlineWithName = recallToHeadline(modifiedHeadlineWithId, localSurvey, false); - setText(modifiedHeadlineWithName); - setShowFallbackInput(true); - }; - - // Filters and updates the list of recall questions based on their presence in the given text, also managing related text and fallback states. - const filterRecallQuestions = (text: string) => { - let includedQuestions: TSurveyQuestion[] = []; - recallQuestions.forEach((recallQuestion) => { - if (text.includes(`@${recallQuestion.headline}`)) { - includedQuestions.push(recallQuestion); + // updation of questions and Thank You Card is done in a different manner, so for question we use updateQuestion and for ThankYouCard we use updateSurvey + const updateQuestionDetails = (updatedText: string) => { + if (isThankYouCard) { + if (updateSurvey) { + updateSurvey({ [type]: updatedText }); + } } else { - const questionToRemove = recallQuestion.headline.slice(0, -1); - const newText = text.replace(`@${questionToRemove}`, ""); - setText(newText); - updateQuestionDetails(newText); - let updatedFallback = { ...fallbacks }; - delete updatedFallback[recallQuestion.id]; - setFallbacks(updatedFallback); - } - }); - setRecallQuestions(includedQuestions); - }; - - const addFallback = () => { - let headlineWithFallback = getQuestionTextBasedOnType(); - filteredRecallQuestions.forEach((recallQuestion) => { - if (recallQuestion) { - const recallInfo = findRecallInfoById(getQuestionTextBasedOnType(), recallQuestion!.id); - if (recallInfo) { - let fallBackValue = fallbacks[recallQuestion.id].trim(); - fallBackValue = fallBackValue.replace(/ /g, "nbsp"); - let updatedFallback = { ...fallbacks }; - updatedFallback[recallQuestion.id] = fallBackValue; - setFallbacks(updatedFallback); - headlineWithFallback = headlineWithFallback.replace( - recallInfo, - `#recall:${recallQuestion?.id}/fallback:${fallBackValue}#` - ); - updateQuestionDetails(headlineWithFallback); + if (updateQuestion) { + updateQuestion(questionIdx, { + [type]: updatedText, + }); } } - }); - setShowFallbackInput(false); - inputRef.current?.focus(); - }; + }; - useEffect(() => { - checkForRecallSymbol(); - }, [text]); - - // updation of questions and Thank You Card is done in a different manner, so for question we use updateQuestion and for ThankYouCard we use updateSurvey - const updateQuestionDetails = (updatedText: string) => { - if (isThankYouCard) { - if (updateSurvey) { - updateSurvey({ [type]: updatedText }); - } - } else { - if (updateQuestion) { - updateQuestion(questionIdx, { - [type]: updatedText, - }); - } - } - }; - - return ( -
- -
- {showImageUploader && type === "headline" && ( - { - if (isThankYouCard && updateSurvey && url) { - updateSurvey({ imageUrl: url[0] }); - } else if (updateQuestion && url) { - updateQuestion(questionIdx, { imageUrl: url[0] }); - } - }} - fileUrl={ - isThankYouCard ? localSurvey.thankYouCard.imageUrl : (question as TSurveyQuestion).imageUrl - } - /> - )} -
-
-
-
- {renderedText} -
- {getQuestionTextBasedOnType().includes("recall:") && ( - - )} - { - setText(recallToHeadline(e.target.value ?? "", localSurvey, false)); - updateQuestionDetails(headlineToRecall(e.target.value, recallQuestions, fallbacks)); + return ( +
+ +
+ {showImageUploader && type === "headline" && ( + { + if (isThankYouCard && updateSurvey && url) { + updateSurvey({ imageUrl: url[0] }); + } else if (updateQuestion && url) { + updateQuestion(questionIdx, { imageUrl: url[0] }); + } }} - isInvalid={isInvalid && text.trim() === ""} + fileUrl={ + isThankYouCard ? localSurvey.thankYouCard.imageUrl : (question as TSurveyQuestion).imageUrl + } /> - {!showQuestionSelect && showFallbackInput && recallQuestions.length > 0 && ( - +
+
+
+ {renderedText} +
+ {getQuestionTextBasedOnType().includes("recall:") && ( + + )} + | undefined} + id={type} + name={type} + aria-label={type === "headline" ? "Question" : "Description"} + autoComplete={showQuestionSelect ? "off" : "on"} + value={recallToHeadline(text ?? "", localSurvey, false)} + onChange={(e) => { + setText(recallToHeadline(e.target.value ?? "", localSurvey, false)); + updateQuestionDetails(headlineToRecall(e.target.value, recallQuestions, fallbacks)); + }} + isInvalid={isInvalid && text.trim() === ""} + /> + {!showQuestionSelect && showFallbackInput && recallQuestions.length > 0 && ( + + )} +
+ {type === "headline" && ( + setShowImageUploader((prev) => !prev)} /> )}
- {type === "headline" && ( - setShowImageUploader((prev) => !prev)} - /> - )}
+ {showQuestionSelect && ( + + )}
- {showQuestionSelect && ( - - )} -
- ); -}; + ); + } +); +QuestionFormInput.displayName = "QuestionFormInput"; + export default QuestionFormInput; diff --git a/packages/ui/SurveysList/index.tsx b/packages/ui/SurveysList/index.tsx index a9188ce1dc..01b20fafa2 100644 --- a/packages/ui/SurveysList/index.tsx +++ b/packages/ui/SurveysList/index.tsx @@ -28,12 +28,16 @@ export default function SurveysList({ userId, }: SurveysListProps) { const [filteredSurveys, setFilteredSurveys] = useState(surveys); - // Initialize orientation state from localStorage or default to 'grid' - const [orientation, setOrientation] = useState(() => localStorage.getItem("surveyOrientation") || "grid"); + // Initialize orientation state with a function that checks if window is defined + const [orientation, setOrientation] = useState(() => + typeof window !== "undefined" ? localStorage.getItem("surveyOrientation") || "grid" : "grid" + ); // Save orientation to localStorage useEffect(() => { - localStorage.setItem("surveyOrientation", orientation); + if (typeof window !== "undefined") { + localStorage.setItem("surveyOrientation", orientation); + } }, [orientation]); return (