Skip to content

Commit cfb424a

Browse files
authored
Add a hint field feature
A feature in which you can add a hint next to each question.
2 parents 75ee517 + e9223e9 commit cfb424a

File tree

12 files changed

+151
-23
lines changed

12 files changed

+151
-23
lines changed

contracts/evoting/types/ballots.go

+3
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ type Select struct {
423423
MaxN uint
424424
MinN uint
425425
Choices []string
426+
Hint string
426427
}
427428

428429
// GetID implements Question
@@ -488,6 +489,7 @@ type Rank struct {
488489
MaxN uint
489490
MinN uint
490491
Choices []string
492+
Hint string
491493
}
492494

493495
func (r Rank) GetID() string {
@@ -562,6 +564,7 @@ type Text struct {
562564
MaxLength uint
563565
Regex string
564566
Choices []string
567+
Hint string
565568
}
566569

567570
func (t Text) GetID() string {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { FC, Fragment, useRef } from 'react';
2+
import { QuestionMarkCircleIcon } from '@heroicons/react/outline/';
3+
import { Popover, Transition } from '@headlessui/react';
4+
5+
type HintButtonProps = {
6+
text: string;
7+
};
8+
9+
const HintButton: FC<HintButtonProps> = ({ text }) => {
10+
const buttonRef = useRef(null);
11+
const timeoutDuration = 200;
12+
let timeout;
13+
14+
const closePopover = () => {
15+
return buttonRef.current?.dispatchEvent(
16+
new KeyboardEvent('keydown', {
17+
key: 'Escape',
18+
bubbles: true,
19+
cancelable: true,
20+
})
21+
);
22+
};
23+
24+
const onMouseEnter = (open) => {
25+
clearTimeout(timeout);
26+
if (open) return;
27+
return buttonRef.current?.click();
28+
};
29+
30+
const onMouseLeave = (open) => {
31+
if (!open) return;
32+
timeout = setTimeout(() => closePopover(), timeoutDuration);
33+
};
34+
35+
return (
36+
text.length !== 0 && (
37+
<Popover className="relative ">
38+
{({ open }) => {
39+
return (
40+
<>
41+
<div onMouseLeave={onMouseLeave.bind(null, open)}>
42+
<Popover.Button
43+
ref={buttonRef}
44+
onMouseEnter={onMouseEnter.bind(null, open)}
45+
onMouseLeave={onMouseLeave.bind(null, open)}>
46+
<div className="text-gray-600 hover:text-blue-600">
47+
<QuestionMarkCircleIcon className="color-gray-900 mt-2 h-4 w-4" />
48+
</div>
49+
</Popover.Button>
50+
<Transition
51+
as={Fragment}
52+
enter="transition ease-out duration-100"
53+
enterFrom="transform opacity-0 scale-95"
54+
enterTo="transform opacity-100 scale-100"
55+
leave="transition ease-in duration-75"
56+
leaveFrom="transform opacity-100 scale-100"
57+
leaveTo="transform opacity-0 scale-95">
58+
<Popover.Panel
59+
onMouseEnter={onMouseEnter.bind(null, open)}
60+
onMouseLeave={onMouseLeave.bind(null, open)}
61+
className="z-30 absolute flex justify-end w-[300px] right-0">
62+
{
63+
<div className="text-sm p-3 text-justify rounded-md bg-white rounded-lg shadow-lg ring-1 ring-black ring-opacity-5">
64+
{text}
65+
</div>
66+
}
67+
</Popover.Panel>
68+
</Transition>
69+
</div>
70+
</>
71+
);
72+
}}
73+
</Popover>
74+
)
75+
);
76+
};
77+
78+
export default HintButton;

web/frontend/src/language/en.json

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"enterMaxN": "Enter the MaxN",
4242
"enterRegex": "Enter your regex",
4343
"enterTitle": "Enter your Title",
44+
"enterHint": "Enter your Hint (optionnal)",
4445
"mainProperties": "Main properties",
4546
"additionalProperties": "Additional properties",
4647
"removeSubject": "Remove subject",

web/frontend/src/pages/ballot/components/Rank.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { FC } from 'react';
22
import { Draggable, DropResult, Droppable } from 'react-beautiful-dnd';
33
import { Answers, ID, RankQuestion } from 'types/configuration';
44
import { answersFrom } from 'types/getObjectType';
5+
import HintButton from 'components/buttons/HintButton';
56

67
export const handleOnDragEnd = (
78
result: DropResult,
@@ -78,7 +79,14 @@ const Rank: FC<RankProps> = ({ rank, answers }) => {
7879

7980
return (
8081
<div className="mb-6">
81-
<h3 className="text-lg break-words text-gray-600">{rank.Title}</h3>
82+
<div className="grid grid-rows-1 grid-flow-col">
83+
<div>
84+
<h3 className="text-lg break-words text-gray-600 w-96">{rank.Title}</h3>
85+
</div>
86+
<div>
87+
<HintButton text={rank.Hint} />
88+
</div>
89+
</div>
8290
<div className="mt-5 px-4 max-w-[300px] sm:pl-8 sm:max-w-md">
8391
<>
8492
<Droppable droppableId={String(rank.ID)}>

web/frontend/src/pages/ballot/components/Select.tsx

+17-9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { FC } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { Answers, SelectQuestion } from 'types/configuration';
44
import { answersFrom } from 'types/getObjectType';
5+
import HintButton from 'components/buttons/HintButton';
56

67
type SelectProps = {
78
select: SelectQuestion;
@@ -33,26 +34,26 @@ const Select: FC<SelectProps> = ({ select, answers, setAnswers }) => {
3334
setAnswers(newAnswers);
3435
};
3536

36-
const hintDisplay = () => {
37-
let hint = '';
37+
const requirementsDisplay = () => {
38+
let requirements = '';
3839
const max = select.MaxN;
3940
const min = select.MinN;
4041

4142
if (max === min) {
42-
hint =
43+
requirements =
4344
max > 1
4445
? t('selectMin', { minSelect: min, singularPlural: t('pluralAnswers') })
4546
: t('selectMin', { minSelect: min, singularPlural: t('singularAnswer') });
4647
} else if (min === 0) {
47-
hint =
48+
requirements =
4849
max > 1
4950
? t('selectMax', { maxSelect: max, singularPlural: t('pluralAnswers') })
5051
: t('selectMax', { maxSelect: max, singularPlural: t('singularAnswer') });
5152
} else {
52-
hint = t('selectBetween', { minSelect: min, maxSelect: max });
53+
requirements = t('selectBetween', { minSelect: min, maxSelect: max });
5354
}
5455

55-
return <div className="text-sm pl-2 pb-2 sm:pl-4 text-gray-400">{hint}</div>;
56+
return <div className="text-sm pl-2 pb-2 sm:pl-4 text-gray-400">{requirements}</div>;
5657
};
5758

5859
const choiceDisplay = (isChecked: boolean, choice: string, choiceIndex: number) => {
@@ -75,9 +76,16 @@ const Select: FC<SelectProps> = ({ select, answers, setAnswers }) => {
7576

7677
return (
7778
<div>
78-
<h3 className="text-lg break-words text-gray-600">{select.Title}</h3>
79-
{hintDisplay()}
80-
<div className="sm:pl-8 pl-6">
79+
<div className="grid grid-rows-1 grid-flow-col">
80+
<div>
81+
<h3 className="text-lg break-words text-gray-600 w-96">{select.Title}</h3>
82+
</div>
83+
<div>
84+
<HintButton text={select.Hint} />
85+
</div>
86+
</div>
87+
<div className="pt-1">{requirementsDisplay()}</div>
88+
<div className="sm:pl-8 mt-2 pl-6">
8189
{Array.from(answers.SelectAnswers.get(select.ID).entries()).map(
8290
([choiceIndex, isChecked]) =>
8391
choiceDisplay(isChecked, select.Choices[choiceIndex], choiceIndex)

web/frontend/src/pages/ballot/components/Text.tsx

+15-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { FC, useEffect, useState } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { Answers, TextQuestion } from 'types/configuration';
44
import { answersFrom } from 'types/getObjectType';
5+
import HintButton from 'components/buttons/HintButton';
56

67
type TextProps = {
78
text: TextQuestion;
@@ -13,23 +14,23 @@ const Text: FC<TextProps> = ({ text, answers, setAnswers }) => {
1314
const { t } = useTranslation();
1415
const [charCounts, setCharCounts] = useState(new Array<number>(text.Choices.length).fill(0));
1516

16-
const hintDisplay = () => {
17-
let hint = '';
17+
const requirementsDisplay = () => {
18+
let requirements = '';
1819
const min = text.MinN;
1920
const max = text.MaxN;
2021

2122
if (min !== max) {
22-
hint =
23+
requirements =
2324
min > 1
2425
? t('minText', { minText: min, singularPlural: t('pluralAnswers') })
2526
: t('minText', { minText: min, singularPlural: t('singularAnswer') });
2627
} else {
27-
hint =
28+
requirements =
2829
min > 1
2930
? t('fillText', { minText: min, singularPlural: t('pluralAnswers') })
3031
: t('fillText', { minText: min, singularPlural: t('singularAnswer') });
3132
}
32-
return <div className="text-sm pl-2 pb-2 text-gray-400">{hint}</div>;
33+
return <div className="text-sm pl-2 pb-2 sm:pl-4 text-gray-400">{requirements}</div>;
3334
};
3435

3536
const handleTextInput = (e: React.ChangeEvent<HTMLTextAreaElement>, choiceIndex: number) => {
@@ -98,8 +99,15 @@ const Text: FC<TextProps> = ({ text, answers, setAnswers }) => {
9899

99100
return (
100101
<div>
101-
<h3 className="text-lg break-words text-gray-600">{text.Title}</h3>
102-
{hintDisplay()}
102+
<div className="grid grid-rows-1 grid-flow-col">
103+
<div>
104+
<h3 className="text-lg break-words text-gray-600 w-96">{text.Title}</h3>
105+
</div>
106+
<div>
107+
<HintButton text={text.Hint} />
108+
</div>
109+
</div>
110+
<div className="pt-1">{requirementsDisplay()}</div>
103111
<div className="sm:pl-8 mt-2 pl-6">
104112
{text.Choices.map((choice, index) => choiceDisplay(choice, index))}
105113
</div>

web/frontend/src/pages/form/components/AddQuestionModal.tsx

+12-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const AddQuestionModal: FC<AddQuestionModalProps> = ({
4343
updateChoice,
4444
} = useQuestionForm(question);
4545

46-
const { Title, MaxN, MinN, Choices } = values;
46+
const { Title, MaxN, MinN, Choices, Hint } = values;
4747
const [errors, setErrors] = useState([]);
4848

4949
const handleSave = async () => {
@@ -189,6 +189,17 @@ const AddQuestionModal: FC<AddQuestionModalProps> = ({
189189
<div key={i}>{v}</div>
190190
))}
191191
</div>
192+
<div>
193+
<label className="block text-md mt font-medium text-gray-500">Hint</label>
194+
<input
195+
value={Hint}
196+
onChange={handleChange()}
197+
name="Hint"
198+
type="text"
199+
placeholder={t('enterHint')}
200+
className="my-1 px-1 w-60 ml-1 border rounded-md"
201+
/>
202+
</div>
192203
<label className="flex pt-2 text-md font-medium text-gray-500">
193204
{Type !== TEXT ? t('choices') : t('answers')}
194205
</label>

web/frontend/src/pages/form/components/UploadFile.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ const UploadFile = ({ updateForm, setShowModal, setTextModal }) => {
2323
var reader = new FileReader();
2424

2525
reader.onload = async function (param) {
26-
const result: any = JSON.parse(param.target.result.toString());
27-
26+
const result: string = JSON.parse(param.target.result.toString());
2827
if (!validateJSONSchema(result)) {
2928
setTextModal('Invalid schema JSON file');
3029
setShowModal(true);

web/frontend/src/schema/configurationValidation.ts

+3
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ const selectsSchema = yup.object({
9999
},
100100
})
101101
.required(),
102+
Hint: yup.lazy(() => yup.string()),
102103
});
103104

104105
const ranksSchema = yup.object({
@@ -196,6 +197,7 @@ const ranksSchema = yup.object({
196197
},
197198
})
198199
.required(),
200+
Hint: yup.lazy(() => yup.string()),
199201
});
200202

201203
const textsSchema = yup.object({
@@ -309,6 +311,7 @@ const textsSchema = yup.object({
309311
},
310312
})
311313
.required(),
314+
Hint: yup.lazy(() => yup.string()),
312315
});
313316

314317
const subjectSchema = yup.object({

web/frontend/src/schema/form_conf.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
"Choices": {
3131
"type": "array",
3232
"items": { "type": "string" }
33-
}
33+
},
34+
"Hint": { "type": "string" }
3435
},
3536
"required": ["ID", "Title", "MaxN", "MinN", "Choices"],
3637
"additionalProperties": false
@@ -48,7 +49,8 @@
4849
"Choices": {
4950
"type": "array",
5051
"items": { "type": "string" }
51-
}
52+
},
53+
"Hint": { "type": "string" }
5254
},
5355
"required": ["ID", "Title", "MaxN", "MinN", "Choices"],
5456
"additionalProperties": false
@@ -68,7 +70,8 @@
6870
"Choices": {
6971
"type": "array",
7072
"items": { "type": "string" }
71-
}
73+
},
74+
"Hint": { "type": "string" }
7275
},
7376
"required": ["ID", "Title", "MaxN", "MinN", "Regex", "MaxLength", "Choices"],
7477
"additionalProperties": false

web/frontend/src/types/configuration.ts

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface RankQuestion extends SubjectElement {
1616
MaxN: number;
1717
MinN: number;
1818
Choices: string[];
19+
Hint: string;
1920
}
2021

2122
// Text describes a "text" question, which allows the user to enter free text.
@@ -25,6 +26,7 @@ interface TextQuestion extends SubjectElement {
2526
MaxLength: number;
2627
Regex: string;
2728
Choices: string[];
29+
Hint: string;
2830
}
2931

3032
// Select describes a "select" question, which requires the user to select one
@@ -33,6 +35,7 @@ interface SelectQuestion extends SubjectElement {
3335
MaxN: number;
3436
MinN: number;
3537
Choices: string[];
38+
Hint: string;
3639
}
3740

3841
interface Subject extends SubjectElement {

web/frontend/src/types/getObjectType.ts

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const newRank = (): types.RankQuestion => {
2626
MinN: 2,
2727
Choices: ['', ''],
2828
Type: RANK,
29+
Hint: '',
2930
};
3031
};
3132

@@ -37,6 +38,7 @@ const newSelect = (): types.SelectQuestion => {
3738
MinN: 1,
3839
Choices: [''],
3940
Type: SELECT,
41+
Hint: '',
4042
};
4143
};
4244

@@ -50,6 +52,7 @@ const newText = (): types.TextQuestion => {
5052
Regex: '',
5153
Choices: [''],
5254
Type: TEXT,
55+
Hint: '',
5356
};
5457
};
5558

0 commit comments

Comments
 (0)