Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/Choice question #200

Merged
merged 2 commits into from
Aug 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions locales/en/survey.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"unSuccessfullSubmit": "Error occured",
"surveyInactive": "The survey is no longer active.",
"choseEmojiBeforeSend": "Please select an emoji before sending.",
"choseAnswerBeforeSend": "Please select an answer before sending.",
"requiredField": "This answer is required.",
"fillRequiredFields": "Please fill all required fields",
"atLeastOneAnswer": "Please select at least one answer."
Expand Down
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,5 @@ model AnswerData {
enum QuestionType {
EMOJI
INPUT
CHOICE
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { QuestionType } from '@prisma/client';
import ListAnswersComponent from 'features/surveys/components/AnswersComponent/ListAnswersComponent';
import TextAnswersComponent from 'features/surveys/components/AnswersComponent/TextAnswersComponent';
import ChoiceComponent from 'features/surveys/components/AnswersComponent/ChoiceComponent/ChoiceComponent';

interface AnswersComponentFactoryProps {
type: QuestionType;
Expand All @@ -22,6 +23,7 @@ export const AnswersComponentFactory = (
<h2 className="mb-4 text-lg font-semibold">{props.question}</h2>
{type === QuestionType.EMOJI && <ListAnswersComponent {...props} />}
{type === QuestionType.INPUT && <TextAnswersComponent {...props} />}
{type === QuestionType.CHOICE && <ChoiceComponent {...props} />}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import clsx from 'clsx';
import React from 'react';
import useTranslation from 'next-translate/useTranslation';

interface ChoiceComponentProps {
options: string[];
handleAnswerChange: (answer: string, questionId: string) => void;
answer?: string;
questionId: string;
isSubmitted: boolean;
isRequired: boolean;
}

export default function ChoiceComponent({
handleAnswerChange,
isRequired,
isSubmitted,
options,
questionId,
answer,
}: ChoiceComponentProps) {
const { t } = useTranslation('survey');

return (
<div>
{options.map((option, idx) => (
<button
key={idx}
className={clsx(
'mb-2 w-full rounded border p-4 text-center text-sm font-medium hover:bg-gray-100',
answer === option && 'bg-gray-200'
)}
onClick={() => handleAnswerChange(option, questionId)}
>
{option}
</button>
))}
{isSubmitted && !answer && isRequired && (
<p className="mt-2 text-sm text-red-500">
{t('choseAnswerBeforeSend')}
</p>
)}
</div>
);
}
17 changes: 13 additions & 4 deletions src/features/surveys/components/EmojiPicker/EmojiPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,17 @@ interface EmojiPickerProps {
index?: number;
pickedEmoji?: string;
addEmoji?: boolean;
onEmotePick?: (idx: number, newValue: string, questionIndex: number) => void;
onEmoteAdd?: (newValue: string, questionIndex: number) => void;
onEmotePick?: (
idx: number,
newValue: string,
questionIndex: number,
blockDuplicates?: boolean
) => void;
onEmoteAdd?: (
newValue: string,
questionIndex: number,
blockDuplicates?: boolean
) => void;
onEmoteRemove?: (idx: number, questionIndex: number) => void;
questionIndex: number;
}
Expand All @@ -29,12 +38,12 @@ function EmojiPicker({
const [displayPicker, setDisplayPicker] = useState(false);

const onEmojiClick = (emojiObject: EmojiObject) => {
onEmotePick?.(index, emojiObject.shortcodes, questionIndex);
onEmotePick?.(index, emojiObject.shortcodes, questionIndex, true);
setDisplayPicker(false);
};

const onEmojiClickAdd = (emojiObject: EmojiObject) => {
onEmoteAdd?.(emojiObject.shortcodes, questionIndex);
onEmoteAdd?.(emojiObject.shortcodes, questionIndex, true);
setDisplayPicker(false);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,36 @@ export default function NewQuestionModal({
});
};

const addChoiceQuestion = () => {
closeModal();
onSuccess?.({
id: v4(),
type: QuestionType.CHOICE,
title: '',
isRequired: false,
options: ['', '', ''],
});
};

return (
<StyledDialog
centerTitle
isOpen={isOpened}
onClose={closeModal}
title={'Choose question type'}
content={
<div className="mt-6 flex gap-2">
<Button onClick={addEmojiQuestion} className="w-[120px] p-6">
<div className="mt-6 flex flex-col gap-2 sm:flex-row">
<Button onClick={addEmojiQuestion} className="p-6 sm:w-[120px]">
Emoji
</Button>

<Button onClick={addInputQuestion} className="w-[120px] p-6">
<Button onClick={addInputQuestion} className="p-6 sm:w-[120px]">
Text
</Button>

<Button onClick={addChoiceQuestion} className="p-6 sm:w-[120px]">
Choice
</Button>
</div>
}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { PlusIcon, TrashIcon } from '@heroicons/react/outline';
import Button, { ButtonVariant } from 'shared/components/Button/Button';
import Input from 'shared/components/Input/Input';
import { MAX_OPTIONS, MIN_OPTIONS } from 'shared/constants/surveysConfig';
import useTranslation from 'next-translate/useTranslation';

interface ChoiceQuestionBlockProps {
options: string[];
handleOptionChange: (
index: number,
newOption: string,
questionIndex: number,
blockDuplicates?: boolean
) => void;
handleOptionRemove: (index: number, questionIndex: number) => void;
handleAddingNewOption: (
newOption: string,
questionIndex: number,
blockDuplicates?: boolean
) => void;
questionIndex: number;
isSubmitted: boolean;
}

export default function ChoiceQuestionBlock({
options,
handleAddingNewOption,
handleOptionChange,
handleOptionRemove,
questionIndex,
isSubmitted,
}: ChoiceQuestionBlockProps) {
const { t } = useTranslation('surveyCreate');

const getAnswerError = (option: string) => {
if (isSubmitted && option.length === 0) {
return t('required');
}

return undefined;
};

return (
<div className="mt-2">
{options.map((option, index) => (
<div className="flex w-full" key={index}>
<div className="mr-4 block flex-grow">
<Input
type="text"
value={option}
placeholder="Answer..."
error={getAnswerError(option)}
onChange={(e) =>
handleOptionChange(
index,
(e.target as HTMLInputElement).value,
questionIndex
)
}
/>
</div>
{options.length > MIN_OPTIONS && (
<Button
className="mt-[8px] h-[44px] w-[60px]"
variant={ButtonVariant.DANGER}
icon={<TrashIcon className="h-4 w-4" />}
onClick={() => handleOptionRemove(index, questionIndex)}
/>
)}
</div>
))}
{options.length < MAX_OPTIONS && (
<div className="mt-2">
<Button
className="ml-auto"
onClick={() => handleAddingNewOption('', questionIndex)}
>
Add New Option <PlusIcon className="ml-2 h-4 w-4" />
</Button>
</div>
)}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ interface EmojiQuestionBlockProps {
handleEmotePick: (
index: number,
newEmote: string,
questionIndex: number
questionIndex: number,
blockDuplicates?: boolean
) => void;
handleEmoteRemove: (index: number, questionIndex: number) => void;
handleAddingNewEmote: (newEmote: string, questionIndex: number) => void;
handleAddingNewEmote: (
newEmote: string,
questionIndex: number,
blockDuplicates?: boolean
) => void;
questionIndex: number;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@ import { QuestionType } from '@prisma/client';
import EmojiQuestionBlock from 'features/surveys/components/QuestionBlocks/EmojiQuestionBlock/EmojiQuestionBlock';
import QuestionBlockWrapper from 'features/surveys/components/QuestionBlocks/QuestionBlockWrapper/QuestionBlockWrapper';
import InputQuestionBlock from 'features/surveys/components/QuestionBlocks/InputQuestionBlock/InputQuestionBlock';
import ChoiceQuestionBlock from 'features/surveys/components/QuestionBlocks/ChoiceQuestionBlock/ChoiceQuestionBlock';

interface QuestionBlockFactoryProps {
type: QuestionType;
pack: string[];
handleEmotePick: (
options: string[];
handleOptionChange: (
index: number,
newEmote: string,
questionIndex: number
questionIndex: number,
blockDuplicates?: boolean
) => void;
handleOptionRemove: (index: number, questionIndex: number) => void;
handleAddingNewOption: (
newEmote: string,
questionIndex: number,
blockDuplicates?: boolean
) => void;
handleEmoteRemove: (index: number, questionIndex: number) => void;
handleAddingNewEmote: (newEmote: string, questionIndex: number) => void;
onQuestionRemove: (index: number) => void;
questionIndex: number;
updateQuestion: (newQuestion: string, questionIndex: number) => void;
Expand All @@ -24,15 +30,15 @@ interface QuestionBlockFactoryProps {
}

export default function QuestionBlockFactory({
handleEmotePick,
handleEmoteRemove,
handleAddingNewEmote,
handleOptionChange,
handleOptionRemove,
handleAddingNewOption,
questionIndex,
questionTitle,
onQuestionRemove,
type,
updateQuestion,
pack,
options,
isSubmitted,
isRemovingPossible,
isRequired,
Expand All @@ -50,12 +56,22 @@ export default function QuestionBlockFactory({
updateQuestionRequired={updateQuestionRequired}
>
{type === QuestionType.INPUT && <InputQuestionBlock />}
{type === QuestionType.CHOICE && (
<ChoiceQuestionBlock
handleAddingNewOption={handleAddingNewOption}
handleOptionChange={handleOptionChange}
handleOptionRemove={handleOptionRemove}
options={options}
questionIndex={questionIndex}
isSubmitted={isSubmitted}
/>
)}
{type === QuestionType.EMOJI && (
<EmojiQuestionBlock
handleAddingNewEmote={handleAddingNewEmote}
handleEmotePick={handleEmotePick}
pack={pack}
handleEmoteRemove={handleEmoteRemove}
handleAddingNewEmote={handleAddingNewOption}
handleEmotePick={handleOptionChange}
pack={options}
handleEmoteRemove={handleOptionRemove}
questionIndex={questionIndex}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Cell,
Tooltip,
ResponsiveContainer,
Legend,
} from 'recharts';

import { CustomTooltip } from 'features/surveys/components/ResultsComponents/BarChart/CustomTooltip';
Expand All @@ -14,6 +15,7 @@ import useTranslation from 'next-translate/useTranslation';

interface BarChartProps {
data: BarChartData[];
emojiLabels?: boolean;
}

export interface BarChartData {
Expand All @@ -32,7 +34,7 @@ const COLORS = [
'#52525B',
];

export default function BarChart({ data }: BarChartProps) {
export default function BarChart({ data, emojiLabels }: BarChartProps) {
const { t } = useTranslation('surveyAnswer');

const getMaxValue = () => {
Expand Down Expand Up @@ -62,7 +64,9 @@ export default function BarChart({ data }: BarChartProps) {
<XAxis
dataKey="name"
interval={0}
tick={(props) => <CustomXAxisTick {...props} />}
tick={
emojiLabels ? (props) => <CustomXAxisTick {...props} /> : false
}
/>

<YAxis
Expand All @@ -73,13 +77,31 @@ export default function BarChart({ data }: BarChartProps) {
<Tooltip
wrapperStyle={{ outline: 'none' }}
cursor={false}
content={(props) => <CustomTooltip {...props} />}
content={(props) => (
<CustomTooltip emojiLabels={emojiLabels} {...props} />
)}
/>
<Bar dataKey="value" fill="#8884d8" radius={[8, 8, 0, 0]}>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index]} />
))}
</Bar>

{!emojiLabels && (
<Legend
wrapperStyle={{
paddingLeft: '38px',
lineHeight: '28px',
}}
layout="vertical"
payload={data.map((item, index) => ({
id: item.name,
type: 'circle',
value: `${item.name} (${item.value})`,
color: COLORS[index],
}))}
/>
)}
</Chart>
</ResponsiveContainer>
</div>
Expand Down
Loading