Add Rating Question (Numbers, Stars & Smileys) (#271)

Add new Star Rating Question Type with different ranges and variations.

---------

Co-authored-by: moritzrengert <moritz@rengert.de>
This commit is contained in:
Matti Nannt
2023-05-10 13:31:49 +02:00
committed by GitHub
parent 106afd12c3
commit 3206637e22
17 changed files with 1395 additions and 67 deletions

View File

@@ -0,0 +1,135 @@
import {
ConfusedFace,
FrowningFace,
GrinningFaceWithSmilingEyes,
GrinningSquintingFace,
NeutralFace,
PerseveringFace,
SlightlySmilingFace,
SmilingFaceWithSmilingEyes,
TiredFace,
WearyFace,
} from "@/components/Smileys";
import { StarIcon } from "@heroicons/react/24/solid";
interface RatingResponseProps {
scale?: "number" | "star" | "smiley";
range?: number;
answer: string;
}
export const RatingResponse: React.FC<RatingResponseProps> = ({ scale, range, answer }) => {
if (typeof answer !== "number") return null;
if (typeof scale === "undefined" || typeof range === "undefined") return answer;
if (scale === "star") {
// show number of stars according to answer value
const stars: any = [];
for (let i = 0; i < range; i++) {
if (i < parseInt(answer)) {
stars.push(<StarIcon className="h-7 text-yellow-400" />);
} else {
stars.push(<StarIcon className="h-7 text-gray-300" />);
}
}
return <div className="flex">{stars}</div>;
}
if (scale === "smiley") {
if (range === 10 && answer === 1) {
return (
<div className="h-10 w-10">
<TiredFace />
</div>
);
}
if ((range === 10 && answer === 2) || (range === 7 && answer === 1)) {
return (
<div className="h-10 w-10">
<WearyFace />
</div>
);
}
if (range === 10 && answer === 3) {
return (
<div className="h-10 w-10">
<PerseveringFace />
</div>
);
}
if ((range === 10 && answer === 4) || (range === 7 && answer === 2) || (range === 5 && answer === 1)) {
return (
<div className="h-10 w-10">
<FrowningFace />
</div>
);
}
if (
(range === 10 && answer === 5) ||
(range === 7 && answer === 3) ||
(range === 5 && answer === 2) ||
(range === 4 && answer === 1) ||
(range === 3 && answer === 1)
) {
return (
<div className="h-10 w-10">
<ConfusedFace />
</div>
);
}
if (
(range === 10 && answer === 6) ||
(range === 7 && answer === 4) ||
(range === 5 && answer === 3) ||
(range === 4 && answer === 2) ||
(range === 3 && answer === 2)
) {
return (
<div className="h-10 w-10">
<NeutralFace />
</div>
);
}
if (
(range === 10 && answer === 7) ||
(range === 7 && answer === 5) ||
(range === 5 && answer === 4) ||
(range === 4 && answer === 3)
) {
return (
<div className="h-10 w-10">
<SlightlySmilingFace />
</div>
);
}
if (
(range === 10 && answer === 8) ||
(range === 5 && answer === 5) ||
(range === 4 && answer === 4) ||
(range === 3 && answer === 3)
) {
return (
<div className="h-10 w-10">
<SmilingFaceWithSmilingEyes />
</div>
);
}
if ((range === 10 && answer === 9) || (range === 7 && answer === 6)) {
return (
<div className="h-10 w-10">
<GrinningFaceWithSmilingEyes />
</div>
);
}
if ((range === 10 && answer === 10) || (range === 7 && answer === 7)) {
return (
<div className="h-10 w-10">
<GrinningSquintingFace />
</div>
);
}
}
return answer;
};

View File

@@ -176,6 +176,7 @@ export default function QuestionCard({
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
survey={localSurvey}
/>
) : null}
<div className="mt-4 border-t border-slate-200">

View File

@@ -2,9 +2,11 @@ import type { RatingQuestion } from "@formbricks/types/questions";
import { Input, Label } from "@formbricks/ui";
import { FaceSmileIcon, HashtagIcon, StarIcon } from "@heroicons/react/24/outline";
import type { Survey } from "@formbricks/types/surveys";
import Dropdown from "./RatingTypeDropdown";
interface RatingQuestionFormProps {
survey: Survey;
question: RatingQuestion;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
@@ -50,8 +52,8 @@ export default function RatingQuestionForm({
<Dropdown
options={[
{ label: "Number", value: "number", icon: HashtagIcon },
{ label: "Star", value: "star", icon: StarIcon, disabled: true },
{ label: "Smiley", value: "smiley", icon: FaceSmileIcon, disabled: true },
{ label: "Star", value: "star", icon: StarIcon },
{ label: "Smiley", value: "smiley", icon: FaceSmileIcon },
]}
defaultValue={question.scale || "number"}
onSelect={(option) => updateQuestion(questionIdx, { scale: option.value })}
@@ -63,13 +65,14 @@ export default function RatingQuestionForm({
<div className="mt-2">
<Dropdown
options={[
{ label: "5 points (recommended)", value: "5" },
{ label: "3 points", value: "3" },
{ label: "4 points", value: "4" },
{ label: "7 points", value: "7" },
{ label: "10 points", value: "10" },
{ label: "5 points (recommended)", value: 5 },
{ label: "3 points", value: 3 },
{ label: "4 points", value: 4 },
{ label: "7 points", value: 7 },
{ label: "10 points", value: 10 },
]}
defaultValue={question.range || "5"}
/* disabled={survey.status !== "draft"} */
defaultValue={question.range || 5}
onSelect={(option) => updateQuestion(questionIdx, { range: option.value })}
/>
</div>

View File

@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { ChevronDownIcon } from "@heroicons/react/24/solid";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
@@ -11,15 +11,20 @@ type Option = {
type DropdownProps = {
options: Option[];
disabled?: boolean;
defaultValue: string | number;
onSelect: (option: Option) => any;
};
const Dropdown = ({ options, defaultValue, onSelect }: DropdownProps) => {
const Dropdown = ({ options, defaultValue, onSelect, disabled = false }: DropdownProps) => {
const [selectedOption, setSelectedOption] = useState<Option>(
options.filter((option) => option.value === defaultValue)[0] || options[0]
);
useEffect(() => {
setSelectedOption(options.filter((option) => option.value === defaultValue)[0] || options[0]);
}, [defaultValue, options]);
const handleSelect = (option) => {
setSelectedOption(option);
onSelect(option);
@@ -49,7 +54,7 @@ const Dropdown = ({ options, defaultValue, onSelect }: DropdownProps) => {
<DropdownMenu.Item
key={option.value}
className="flex cursor-pointer items-center p-3 hover:bg-gray-100 hover:outline-none data-[disabled]:cursor-default data-[disabled]:opacity-50"
disabled={option.disabled}
disabled={disabled || option.disabled}
onSelect={() => handleSelect(option)}>
{option.icon && <option.icon className="mr-3 h-5 w-5" />}
{option.label}

View File

@@ -24,14 +24,25 @@ export default function ResponseTimeline({ environmentId, surveyId }) {
// Replace question IDs with question headlines in response data
const updatedResponses = responses.map((response) => {
const updatedResponse: Array<{ question: string; answer: string }> = []; // Specify the type of updatedData
const updatedResponse: Array<{
question: string;
answer: string;
type: string;
scale?: "number" | "star" | "smiley";
range?: number;
}> = []; // Specify the type of updatedData
// iterate over survey questions and build the updated response
for (const question of survey.questions) {
const questionId = question.id;
const questionHeadline = question.headline;
const answer = response.data[questionId];
console.log(question);
const answer = response.data[question.id];
if (answer) {
updatedResponse.push({ question: questionHeadline, answer: answer as string });
updatedResponse.push({
question: question.headline,
type: question.type,
scale: question.scale,
range: question.range,
answer: answer as string,
});
}
}
return { ...response, responses: updatedResponse, person: response.person };

View File

@@ -2,6 +2,7 @@ import { timeSince } from "@formbricks/lib/time";
import { PersonAvatar } from "@formbricks/ui";
import { CheckCircleIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
import { RatingResponse } from "../RatingResponse";
interface OpenTextSummaryProps {
data: {
@@ -21,6 +22,9 @@ interface OpenTextSummaryProps {
id: string;
question: string;
answer: string | any[];
type: string;
scale?: "number" | "star" | "smiley";
range?: number;
}[];
};
environmentId: string;
@@ -35,6 +39,8 @@ export default function SingleResponse({ data, environmentId }: OpenTextSummaryP
const email = data.person && findEmail(data.person);
const displayIdentifier = email || data.personId;
console.log(data);
return (
<div className=" my-6 rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
<div className="space-y-2 px-6 pb-5 pt-6">
@@ -72,7 +78,13 @@ export default function SingleResponse({ data, environmentId }: OpenTextSummaryP
<div key={`${response.id}-${idx}`}>
<p className="text-sm text-slate-500">{response.question}</p>
{typeof response.answer !== "object" ? (
<p className="ph-no-capture my-1 font-semibold text-slate-700">{response.answer}</p>
response.type === "rating" ? (
<div className="h-8">
<RatingResponse scale={response.scale} answer={response.answer} range={response.range} />
</div>
) : (
<p className="ph-no-capture my-1 font-semibold text-slate-700">{response.answer}</p>
)
) : (
<p className="ph-no-capture my-1 font-semibold text-slate-700">{response.answer.join(", ")}</p>
)}

View File

@@ -1,10 +1,11 @@
import { CTAQuestion } from "@formbricks/types/questions";
import type { QuestionSummary } from "@formbricks/types/responses";
import { ProgressBar } from "@formbricks/ui";
import { InboxStackIcon } from "@heroicons/react/24/solid";
import { useMemo } from "react";
interface CTASummaryProps {
questionSummary: QuestionSummary;
questionSummary: QuestionSummary<CTAQuestion>;
}
interface ChoiceResult {

View File

@@ -1,10 +1,11 @@
import { MultipleChoiceMultiQuestion, MultipleChoiceSingleQuestion } from "@formbricks/types/questions";
import type { QuestionSummary } from "@formbricks/types/responses";
import { ProgressBar } from "@formbricks/ui";
import { InboxStackIcon } from "@heroicons/react/24/solid";
import { useMemo } from "react";
interface MultipleChoiceSummaryProps {
questionSummary: QuestionSummary;
questionSummary: QuestionSummary<MultipleChoiceMultiQuestion | MultipleChoiceSingleQuestion>;
}
interface ChoiceResult {

View File

@@ -1,10 +1,11 @@
import { NPSQuestion } from "@formbricks/types/questions";
import type { QuestionSummary } from "@formbricks/types/responses";
import { HalfCircle, ProgressBar } from "@formbricks/ui";
import { InboxStackIcon } from "@heroicons/react/24/solid";
import { useMemo } from "react";
interface NPSSummaryProps {
questionSummary: QuestionSummary;
questionSummary: QuestionSummary<NPSQuestion>;
}
interface Result {

View File

@@ -1,12 +1,13 @@
import { truncate } from "@/lib/utils";
import { timeSince } from "@formbricks/lib/time";
import { OpenTextQuestion } from "@formbricks/types/questions";
import type { QuestionSummary } from "@formbricks/types/responses";
import { PersonAvatar } from "@formbricks/ui";
import { InboxStackIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
interface OpenTextSummaryProps {
questionSummary: QuestionSummary;
questionSummary: QuestionSummary<OpenTextQuestion>;
environmentId: string;
}

View File

@@ -2,13 +2,15 @@ import type { QuestionSummary } from "@formbricks/types/responses";
import { ProgressBar } from "@formbricks/ui";
import { InboxStackIcon } from "@heroicons/react/24/solid";
import { useMemo } from "react";
import { RatingResponse } from "../RatingResponse";
import { RatingQuestion } from "@formbricks/types/questions";
interface RatingSummaryProps {
questionSummary: QuestionSummary;
questionSummary: QuestionSummary<RatingQuestion>;
}
interface ChoiceResult {
label: string;
label: number | string;
count: number;
percentage: number;
}
@@ -21,7 +23,7 @@ export default function RatingSummary({ questionSummary }: RatingSummaryProps) {
for (let i = 1; i <= questionSummary.question.range; i++) {
resultsDict[i.toString()] = {
count: 0,
label: i.toString(),
label: i,
percentage: 0,
};
}
@@ -71,6 +73,8 @@ export default function RatingSummary({ questionSummary }: RatingSummaryProps) {
return total;
}, [results]);
console.log(JSON.stringify(questionSummary.question, null, 2));
return (
<div className=" rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
<div className="space-y-2 px-6 pb-5 pt-6">
@@ -90,7 +94,13 @@ export default function RatingSummary({ questionSummary }: RatingSummaryProps) {
<div key={result.label}>
<div className="text flex justify-between px-2 pb-2">
<div className="mr-8 flex space-x-1">
<p className="font-semibold text-slate-700">{result.label}</p>
<div className="font-semibold text-slate-700">
<RatingResponse
scale={questionSummary.question.scale}
answer={result.label}
range={questionSummary.question.range}
/>
</div>
<div>
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
{Math.round(result.percentage * 100)}%

View File

@@ -12,6 +12,15 @@ import MultipleChoiceSummary from "./MultipleChoiceSummary";
import NPSSummary from "./NPSSummary";
import OpenTextSummary from "./OpenTextSummary";
import RatingSummary from "./RatingSummary";
import type {
CTAQuestion,
MultipleChoiceMultiQuestion,
MultipleChoiceSingleQuestion,
NPSQuestion,
OpenTextQuestion,
Question,
RatingQuestion,
} from "@formbricks/types/questions";
export default function SummaryList({ environmentId, surveyId }) {
const { responsesData, isLoadingResponses, isErrorResponses } = useResponses(environmentId, surveyId);
@@ -19,7 +28,7 @@ export default function SummaryList({ environmentId, surveyId }) {
const responses = responsesData?.responses;
const summaryData: QuestionSummary[] = useMemo(() => {
const summaryData: QuestionSummary<Question>[] = useMemo(() => {
if (survey && responses) {
return survey.questions.map((question) => {
const questionResponses = responses
@@ -64,7 +73,7 @@ export default function SummaryList({ environmentId, surveyId }) {
return (
<OpenTextSummary
key={questionSummary.question.id}
questionSummary={questionSummary}
questionSummary={questionSummary as QuestionSummary<OpenTextQuestion>}
environmentId={environmentId}
/>
);
@@ -76,18 +85,37 @@ export default function SummaryList({ environmentId, surveyId }) {
return (
<MultipleChoiceSummary
key={questionSummary.question.id}
questionSummary={questionSummary}
questionSummary={
questionSummary as QuestionSummary<
MultipleChoiceMultiQuestion | MultipleChoiceSingleQuestion
>
}
/>
);
}
if (questionSummary.question.type === "nps") {
return <NPSSummary key={questionSummary.question.id} questionSummary={questionSummary} />;
return (
<NPSSummary
key={questionSummary.question.id}
questionSummary={questionSummary as QuestionSummary<NPSQuestion>}
/>
);
}
if (questionSummary.question.type === "cta") {
return <CTASummary key={questionSummary.question.id} questionSummary={questionSummary} />;
return (
<CTASummary
key={questionSummary.question.id}
questionSummary={questionSummary as QuestionSummary<CTAQuestion>}
/>
);
}
if (questionSummary.question.type === "rating") {
return <RatingSummary key={questionSummary.question.id} questionSummary={questionSummary} />;
return (
<RatingSummary
key={questionSummary.question.id}
questionSummary={questionSummary as QuestionSummary<RatingQuestion>}
/>
);
}
return null;
})}

View File

@@ -0,0 +1,464 @@
export const TiredFace: React.FC<React.SVGProps<SVGCircleElement>> = (props) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeMiterlimit="10"
strokeWidth="2"
{...props}
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="m21.88 23.92c5.102-0.06134 7.273-1.882 8.383-3.346"
/>
<path
stroke="currentColor"
strokeMiterlimit="10"
strokeWidth="2"
d="m46.24 47.56c0-2.592-2.867-7.121-10.25-6.93-6.974 0.1812-10.22 4.518-10.22 7.111s4.271-1.611 10.05-1.492c6.317 0.13 10.43 3.903 10.43 1.311z"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="m23.16 28.47c5.215 1.438 5.603 0.9096 8.204 1.207 1.068 0.1221-2.03 2.67-7.282 4.397"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="m50.12 23.92c-5.102-0.06134-7.273-1.882-8.383-3.346"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="m48.84 28.47c-5.215 1.438-5.603 0.9096-8.204 1.207-1.068 0.1221 2.03 2.67 7.282 4.397"
/>
</g>
</svg>
);
};
export const WearyFace: React.FC<React.SVGProps<SVGCircleElement>> = (props) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeMiterlimit="10"
strokeWidth="2"
{...props}
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="m22.88 23.92c5.102-0.06134 7.273-1.882 8.383-3.346"
/>
<path
stroke="currentColor"
strokeMiterlimit="10"
strokeWidth="2"
d="m46.24 47.56c0-2.592-2.867-7.121-10.25-6.93-6.974 0.1812-10.22 4.518-10.22 7.111s4.271-1.611 10.05-1.492c6.317 0.13 10.43 3.903 10.43 1.311z"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="m49.12 23.92c-5.102-0.06134-7.273-1.882-8.383-3.346"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="m48.24 30.51c-6.199 1.47-7.079 1.059-8.868-1.961"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="m23.76 30.51c6.199 1.47 7.079 1.059 8.868-1.961"
/>
</g>
</svg>
);
};
export const PerseveringFace: React.FC<React.SVGProps<SVGCircleElement>> = (props) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeMiterlimit="10"
strokeWidth="2"
{...props}
/>
<line
x1="44.5361"
x2="50.9214"
y1="21.4389"
y2="24.7158"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
/>
<line
x1="26.9214"
x2="20.5361"
y1="21.4389"
y2="24.7158"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M24,28c2.3334,1.3333,4.6666,2.6667,7,4c-2.3334,1.3333-4.6666,2.6667-7,4"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M48,28c-2.3334,1.3333-4.6666,2.6667-7,4c2.3334,1.3333,4.6666,2.6667,7,4"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M28,51c0.2704-0.3562,1-8,8.4211-8.0038C43,42.9929,43.6499,50.5372,44,51C38.6667,51,33.3333,51,28,51z"
/>
</g>
</svg>
);
};
export const FrowningFace: React.FC<React.SVGProps<SVGCircleElement>> = (props) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeMiterlimit="10"
strokeWidth="2"
{...props}
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M26.5,48c1.8768-3.8326,5.8239-6.1965,10-6c3.8343,0.1804,7.2926,2.4926,9,6"
/>
<path d="M30,31c0,1.6568-1.3448,3-3,3c-1.6553,0-3-1.3433-3-3c0-1.6552,1.3447-3,3-3C28.6552,28,30,29.3448,30,31" />
<path d="M48,31c0,1.6568-1.3447,3-3,3s-3-1.3433-3-3c0-1.6552,1.3447-3,3-3S48,29.3448,48,31" />
</g>
</svg>
);
};
export const ConfusedFace: React.FC<React.SVGProps<SVGCircleElement>> = (props) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeMiterlimit="10"
strokeWidth="2"
{...props}
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="m44.7 43.92c-6.328-1.736-11.41-0.906-17.4 1.902"
/>
<path d="M30,31c0,1.6568-1.3448,3-3,3c-1.6553,0-3-1.3433-3-3c0-1.6552,1.3447-3,3-3C28.6552,28,30,29.3448,30,31" />
<path d="M48,31c0,1.6568-1.3447,3-3,3s-3-1.3433-3-3c0-1.6552,1.3447-3,3-3S48,29.3448,48,31" />
</g>
</svg>
);
};
export const NeutralFace: React.FC<React.SVGProps<SVGCircleElement>> = (props) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeMiterlimit="10"
strokeWidth="2"
{...props}
/>
<line
x1="27"
x2="45"
y1="43"
y2="43"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
/>
<path d="M30,31c0,1.6568-1.3448,3-3,3c-1.6553,0-3-1.3433-3-3c0-1.6552,1.3447-3,3-3C28.6552,28,30,29.3448,30,31" />
<path d="M48,31c0,1.6568-1.3447,3-3,3s-3-1.3433-3-3c0-1.6552,1.3447-3,3-3S48,29.3448,48,31" />
</g>
</svg>
);
};
export const SlightlySmilingFace: React.FC<React.SVGProps<SVGCircleElement>> = (props) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
{...props}
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M45.8149,44.9293 c-2.8995,1.6362-6.2482,2.5699-9.8149,2.5699s-6.9153-0.9336-9.8149-2.5699"
/>
<path d="M30,31c0,1.6568-1.3448,3-3,3c-1.6553,0-3-1.3433-3-3c0-1.6552,1.3447-3,3-3C28.6552,28,30,29.3448,30,31" />
<path d="M48,31c0,1.6568-1.3447,3-3,3s-3-1.3433-3-3c0-1.6552,1.3447-3,3-3S48,29.3448,48,31" />
</g>
</svg>
);
};
export const SmilingFaceWithSmilingEyes: React.FC<React.SVGProps<SVGCircleElement>> = (props) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
{...props}
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M45.8147,45.2268a15.4294,15.4294,0,0,1-19.6294,0"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M31.6941,33.4036a4.7262,4.7262,0,0,0-8.6382,0"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M48.9441,33.4036a4.7262,4.7262,0,0,0-8.6382,0"
/>
</g>
</svg>
);
};
export const GrinningFaceWithSmilingEyes: React.FC<React.SVGProps<SVGCircleElement>> = (props) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
{...props}
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M50.595,41.64a11.5554,11.5554,0,0,1-.87,4.49c-12.49,3.03-25.43.34-27.49-.13a11.4347,11.4347,0,0,1-.83-4.36h.11s14.8,3.59,28.89.07Z"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M49.7251,46.13c-1.79,4.27-6.35,7.23-13.69,7.23-7.41,0-12.03-3.03-13.8-7.36C24.2951,46.47,37.235,49.16,49.7251,46.13Z"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M31.6941,32.4036a4.7262,4.7262,0,0,0-8.6382,0"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M48.9441,32.4036a4.7262,4.7262,0,0,0-8.6382,0"
/>
</g>
</svg>
);
};
export const GrinningSquintingFace: React.FC<React.SVGProps<SVGCircleElement>> = (props) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
{...props}
/>
<polyline
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
points="25.168 27.413 31.755 31.427 25.168 35.165"
/>
<polyline
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
points="46.832 27.413 40.245 31.427 46.832 35.165"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M50.595,41.64a11.5554,11.5554,0,0,1-.87,4.49c-12.49,3.03-25.43.34-27.49-.13a11.4347,11.4347,0,0,1-.83-4.36h.11s14.8,3.59,28.89.07Z"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M49.7251,46.13c-1.79,4.27-6.35,7.23-13.69,7.23-7.41,0-12.03-3.03-13.8-7.36C24.2951,46.47,37.235,49.16,49.7251,46.13Z"
/>
</g>
</svg>
);
};
export let icons = [<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg"></svg>];

View File

@@ -3,6 +3,20 @@ import { cn } from "@formbricks/lib/cn";
import type { RatingQuestion } from "@formbricks/types/questions";
import Headline from "./Headline";
import Subheader from "./Subheader";
import { StarIcon } from "@heroicons/react/24/outline";
import { StarIcon as FilledStarIcon } from "@heroicons/react/24/solid";
import {
ConfusedFace,
FrowningFace,
GrinningFaceWithSmilingEyes,
GrinningSquintingFace,
NeutralFace,
PerseveringFace,
SlightlySmilingFace,
SmilingFaceWithSmilingEyes,
TiredFace,
WearyFace,
} from "../Smileys";
interface RatingQuestionProps {
question: RatingQuestion;
@@ -18,6 +32,8 @@ export default function RatingQuestion({
brandColor,
}: RatingQuestionProps) {
const [selectedChoice, setSelectedChoice] = useState<number | null>(null);
const [hoveredNumber, setHoveredNumber] = useState(0);
// const icons = RatingSmileyList(question.range);
const handleSelect = (number: number) => {
setSelectedChoice(number);
@@ -29,6 +45,17 @@ export default function RatingQuestion({
}
};
const HiddenRadioInput = ({ number }) => (
<input
type="radio"
name="rating"
value={number}
className="absolute h-full w-full cursor-pointer opacity-0"
onChange={() => handleSelect(number)}
required={question.required}
/>
);
return (
<form
onSubmit={(e) => {
@@ -45,34 +72,61 @@ export default function RatingQuestion({
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
<div className="my-4">
<fieldset>
<fieldset className="max-w-full">
<legend className="sr-only">Options</legend>
<div className="flex">
{Array.from({ length: question.range }, (_, i) => i + 1).map((number) => (
<label
{Array.from({ length: question.range }, (_, i) => i + 1).map((number, i, a) => (
<span
key={number}
className={cn(
selectedChoice === number ? "z-10 border-slate-400 bg-slate-50" : "",
"relative h-10 flex-1 cursor-pointer border bg-white text-center text-sm leading-10 first:rounded-l-md last:rounded-r-md hover:bg-gray-100 focus:outline-none"
)}>
<input
type="radio"
name="rating"
value={number}
className="absolute h-full w-full cursor-pointer opacity-0"
onChange={() => handleSelect(number)}
required={question.required}
/>
{number}
</label>
onMouseOver={() => setHoveredNumber(number)}
onMouseLeave={() => setHoveredNumber(0)}
className="relative max-h-10 flex-1 cursor-pointer bg-white text-center text-sm leading-10">
{question.scale === "number" ? (
<label
className={cn(
selectedChoice === number ? "z-10 border-slate-400 bg-slate-50" : "",
a.length === number ? "rounded-r-md" : "",
number === 1 ? "rounded-l-md" : "",
"box-border block h-full w-full border hover:bg-gray-100 focus:outline-none"
)}>
<HiddenRadioInput number={number} />
{number}
</label>
) : question.scale === "star" ? (
<label
className={cn(
number <= hoveredNumber ? "text-yellow-500" : "",
"flex h-full w-full justify-center"
)}>
<HiddenRadioInput number={number} />
{selectedChoice && selectedChoice >= number ? (
<FilledStarIcon className="max-h-full text-yellow-300" />
) : (
<StarIcon className="max-h-full " />
)}
</label>
) : (
<label className="flex h-full w-full justify-center">
<HiddenRadioInput number={number} />
<RatingSmiley
active={selectedChoice == number || hoveredNumber == number}
idx={i}
range={question.range}
/>
</label>
)}
</span>
))}
</div>
<div className="flex justify-between px-1.5 text-xs leading-6 text-slate-500">
<p>{question.lowerLabel}</p>
<p>{question.upperLabel}</p>
</div>
</fieldset>
</div>
{!question.required && (
<div className="mt-4 flex w-full justify-between">
<div></div>
@@ -87,3 +141,32 @@ export default function RatingQuestion({
</form>
);
}
interface RatingSmileyProps {
active: boolean;
idx: number;
range: number;
}
function RatingSmiley({ active, idx, range }: RatingSmileyProps): JSX.Element {
const activeColor = "fill-yellow-500";
const inactiveColor = "fill-none";
let icons = [
<TiredFace className={active ? activeColor : inactiveColor} />,
<WearyFace className={active ? activeColor : inactiveColor} />,
<PerseveringFace className={active ? activeColor : inactiveColor} />,
<FrowningFace className={active ? activeColor : inactiveColor} />,
<ConfusedFace className={active ? activeColor : inactiveColor} />,
<NeutralFace className={active ? activeColor : inactiveColor} />,
<SlightlySmilingFace className={active ? activeColor : inactiveColor} />,
<SmilingFaceWithSmilingEyes className={active ? activeColor : inactiveColor} />,
<GrinningFaceWithSmilingEyes className={active ? activeColor : inactiveColor} />,
<GrinningSquintingFace className={active ? activeColor : inactiveColor} />,
];
if (range == 7) icons = [icons[1], icons[3], icons[4], icons[5], icons[6], icons[8], icons[9]];
else if (range == 5) icons = [icons[3], icons[4], icons[5], icons[6], icons[7]];
else if (range == 4) icons = [icons[4], icons[5], icons[6], icons[7]];
else if (range == 3) icons = [icons[4], icons[5], icons[7]];
return icons[idx];
}

View File

@@ -4,6 +4,18 @@ import { cn } from "../lib/utils";
import type { RatingQuestion } from "../../../types/questions";
import Headline from "./Headline";
import Subheader from "./Subheader";
import {
ConfusedFace,
FrowningFace,
GrinningFaceWithSmilingEyes,
GrinningSquintingFace,
NeutralFace,
PerseveringFace,
SlightlySmilingFace,
SmilingFaceWithSmilingEyes,
TiredFace,
WearyFace,
} from "./Smileys";
interface RatingQuestionProps {
question: RatingQuestion;
@@ -19,6 +31,7 @@ export default function RatingQuestion({
brandColor,
}: RatingQuestionProps) {
const [selectedChoice, setSelectedChoice] = useState<number | null>(null);
const [hoveredNumber, setHoveredNumber] = useState(0);
const handleSelect = (number: number) => {
setSelectedChoice(number);
@@ -30,6 +43,17 @@ export default function RatingQuestion({
}
};
const HiddenRadioInput = ({ number }) => (
<input
type="radio"
name="rating"
value={number}
className="fb-absolute fb-h-full fb-w-full fb-cursor-pointer fb-opacity-0"
onChange={() => handleSelect(number)}
required={question.required}
/>
);
return (
<form
onSubmit={(e) => {
@@ -49,23 +73,71 @@ export default function RatingQuestion({
<fieldset>
<legend className="fb-sr-only">Choices</legend>
<div className="fb-flex">
{Array.from({ length: question.range }, (_, i) => i + 1).map((number) => (
<label
{Array.from({ length: question.range }, (_, i) => i + 1).map((number, i, a) => (
<span
key={number}
className={cn(
selectedChoice === number ? "fb-z-10 fb-border-slate-400 fb-bg-slate-50" : "",
"fb-relative fb-h-10 fb-flex-1 fb-cursor-pointer fb-border fb-bg-white fb-text-center fb-text-sm fb-leading-10 first:fb-rounded-l-md last:fb-rounded-r-md hover:fb-bg-gray-100 focus:fb-outline-none"
)}>
<input
type="radio"
name="nps"
value={number}
className="fb-absolute fb-h-full fb-w-full fb-cursor-pointer fb-opacity-0"
onChange={() => handleSelect(number)}
required={question.required}
/>
{number}
</label>
onMouseOver={() => setHoveredNumber(number)}
onMouseLeave={() => setHoveredNumber(0)}
className="fb-relative fb-max-h-10 fb-flex-1 fb-cursor-pointer fb-bg-white fb-text-center fb-text-sm fb-leading-10">
{question.scale === "number" ? (
<label
className={cn(
selectedChoice === number ? "fb-z-10 fb-border-slate-400 fb-bg-slate-50" : "",
a.length === number ? "fb-rounded-r-md" : "",
number === 1 ? "fb-rounded-l-md" : "",
"fb-block fb-h-full fb-w-full fb-border hover:fb-bg-gray-100 focus:fb-outline-none"
)}>
<HiddenRadioInput number={number} />
{number}
</label>
) : question.scale === "star" ? (
<label
className={cn(
number <= hoveredNumber ? "fb-text-yellow-500" : "",
"fb-flex fb-h-full fb-w-full fb-justify-center"
)}>
<HiddenRadioInput number={number} />
{selectedChoice && selectedChoice >= number ? (
<span className="fb-text-yellow-300">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="fb-max-h-full fb-h-6 fb-w-6 ">
<path
fillRule="evenodd"
d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.007 5.404.433c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.433 2.082-5.006z"
clipRule="evenodd"
/>
</svg>
</span>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
className="fb-h-6 fb-max-h-full fb-w-6">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z"
/>
</svg>
)}
</label>
) : (
<label className="fb-flex fb-h-full fb-w-full fb-justify-center">
<HiddenRadioInput number={number} />
<RatingSmiley
active={selectedChoice == number || hoveredNumber == number}
idx={i}
range={question.range}
/>
</label>
)}
</span>
))}
</div>
<div className="fb-flex fb-justify-between fb-text-slate-500 fb-leading-6 fb-px-1.5 fb-text-xs">
@@ -88,3 +160,32 @@ export default function RatingQuestion({
</form>
);
}
interface RatingSmileyProps {
active: boolean;
idx: number;
range: number;
}
function RatingSmiley({ active, idx, range }: RatingSmileyProps): JSX.Element {
const activeColor = "fb-fill-yellow-500";
const inactiveColor = "fb-fill-none";
let icons = [
<TiredFace className={active ? activeColor : inactiveColor} />,
<WearyFace className={active ? activeColor : inactiveColor} />,
<PerseveringFace className={active ? activeColor : inactiveColor} />,
<FrowningFace className={active ? activeColor : inactiveColor} />,
<ConfusedFace className={active ? activeColor : inactiveColor} />,
<NeutralFace className={active ? activeColor : inactiveColor} />,
<SlightlySmilingFace className={active ? activeColor : inactiveColor} />,
<SmilingFaceWithSmilingEyes className={active ? activeColor : inactiveColor} />,
<GrinningFaceWithSmilingEyes className={active ? activeColor : inactiveColor} />,
<GrinningSquintingFace className={active ? activeColor : inactiveColor} />,
];
if (range == 7) icons = [icons[1], icons[3], icons[4], icons[5], icons[6], icons[8], icons[9]];
else if (range == 5) icons = [icons[3], icons[4], icons[5], icons[6], icons[7]];
else if (range == 4) icons = [icons[4], icons[5], icons[6], icons[7]];
else if (range == 3) icons = [icons[4], icons[5], icons[7]];
return icons[idx];
}

View File

@@ -0,0 +1,471 @@
import { h, FunctionComponent } from "preact";
import type { JSX } from "preact";
export const TiredFace: FunctionComponent<JSX.HTMLAttributes<SVGCircleElement>> = (props) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeMiterlimit="10"
strokeWidth="2"
{...props}
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="m21.88 23.92c5.102-0.06134 7.273-1.882 8.383-3.346"
/>
<path
stroke="currentColor"
strokeMiterlimit="10"
strokeWidth="2"
d="m46.24 47.56c0-2.592-2.867-7.121-10.25-6.93-6.974 0.1812-10.22 4.518-10.22 7.111s4.271-1.611 10.05-1.492c6.317 0.13 10.43 3.903 10.43 1.311z"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="m23.16 28.47c5.215 1.438 5.603 0.9096 8.204 1.207 1.068 0.1221-2.03 2.67-7.282 4.397"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="m50.12 23.92c-5.102-0.06134-7.273-1.882-8.383-3.346"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="m48.84 28.47c-5.215 1.438-5.603 0.9096-8.204 1.207-1.068 0.1221 2.03 2.67 7.282 4.397"
/>
</g>
</svg>
);
};
export const WearyFace: FunctionComponent<JSX.HTMLAttributes<SVGCircleElement>> = (props) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeMiterlimit="10"
strokeWidth="2"
{...props}
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="m22.88 23.92c5.102-0.06134 7.273-1.882 8.383-3.346"
/>
<path
stroke="currentColor"
strokeMiterlimit="10"
strokeWidth="2"
d="m46.24 47.56c0-2.592-2.867-7.121-10.25-6.93-6.974 0.1812-10.22 4.518-10.22 7.111s4.271-1.611 10.05-1.492c6.317 0.13 10.43 3.903 10.43 1.311z"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="m49.12 23.92c-5.102-0.06134-7.273-1.882-8.383-3.346"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="m48.24 30.51c-6.199 1.47-7.079 1.059-8.868-1.961"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="m23.76 30.51c6.199 1.47 7.079 1.059 8.868-1.961"
/>
</g>
</svg>
);
};
export const PerseveringFace: FunctionComponent<JSX.HTMLAttributes<SVGCircleElement>> = (props) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeMiterlimit="10"
strokeWidth="2"
{...props}
/>
<line
x1="44.5361"
x2="50.9214"
y1="21.4389"
y2="24.7158"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
/>
<line
x1="26.9214"
x2="20.5361"
y1="21.4389"
y2="24.7158"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M24,28c2.3334,1.3333,4.6666,2.6667,7,4c-2.3334,1.3333-4.6666,2.6667-7,4"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M48,28c-2.3334,1.3333-4.6666,2.6667-7,4c2.3334,1.3333,4.6666,2.6667,7,4"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M28,51c0.2704-0.3562,1-8,8.4211-8.0038C43,42.9929,43.6499,50.5372,44,51C38.6667,51,33.3333,51,28,51z"
/>
</g>
</svg>
);
};
export const FrowningFace: FunctionComponent<JSX.HTMLAttributes<SVGCircleElement>> = (props) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeMiterlimit="10"
strokeWidth="2"
{...props}
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M26.5,48c1.8768-3.8326,5.8239-6.1965,10-6c3.8343,0.1804,7.2926,2.4926,9,6"
/>
<path d="M30,31c0,1.6568-1.3448,3-3,3c-1.6553,0-3-1.3433-3-3c0-1.6552,1.3447-3,3-3C28.6552,28,30,29.3448,30,31" />
<path d="M48,31c0,1.6568-1.3447,3-3,3s-3-1.3433-3-3c0-1.6552,1.3447-3,3-3S48,29.3448,48,31" />
</g>
</svg>
);
};
export const ConfusedFace: FunctionComponent<JSX.HTMLAttributes<SVGCircleElement>> = (props) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeMiterlimit="10"
strokeWidth="2"
{...props}
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="m44.7 43.92c-6.328-1.736-11.41-0.906-17.4 1.902"
/>
<path d="M30,31c0,1.6568-1.3448,3-3,3c-1.6553,0-3-1.3433-3-3c0-1.6552,1.3447-3,3-3C28.6552,28,30,29.3448,30,31" />
<path d="M48,31c0,1.6568-1.3447,3-3,3s-3-1.3433-3-3c0-1.6552,1.3447-3,3-3S48,29.3448,48,31" />
</g>
</svg>
);
};
export const NeutralFace: FunctionComponent<JSX.HTMLAttributes<SVGCircleElement>> = (props) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeMiterlimit="10"
strokeWidth="2"
{...props}
/>
<line
x1="27"
x2="45"
y1="43"
y2="43"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
/>
<path d="M30,31c0,1.6568-1.3448,3-3,3c-1.6553,0-3-1.3433-3-3c0-1.6552,1.3447-3,3-3C28.6552,28,30,29.3448,30,31" />
<path d="M48,31c0,1.6568-1.3447,3-3,3s-3-1.3433-3-3c0-1.6552,1.3447-3,3-3S48,29.3448,48,31" />
</g>
</svg>
);
};
export const SlightlySmilingFace: FunctionComponent<JSX.HTMLAttributes<SVGCircleElement>> = (props) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
{...props}
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M45.8149,44.9293 c-2.8995,1.6362-6.2482,2.5699-9.8149,2.5699s-6.9153-0.9336-9.8149-2.5699"
/>
<path d="M30,31c0,1.6568-1.3448,3-3,3c-1.6553,0-3-1.3433-3-3c0-1.6552,1.3447-3,3-3C28.6552,28,30,29.3448,30,31" />
<path d="M48,31c0,1.6568-1.3447,3-3,3s-3-1.3433-3-3c0-1.6552,1.3447-3,3-3S48,29.3448,48,31" />
</g>
</svg>
);
};
export const SmilingFaceWithSmilingEyes: FunctionComponent<JSX.HTMLAttributes<SVGCircleElement>> = (
props
) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
{...props}
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M45.8147,45.2268a15.4294,15.4294,0,0,1-19.6294,0"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M31.6941,33.4036a4.7262,4.7262,0,0,0-8.6382,0"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M48.9441,33.4036a4.7262,4.7262,0,0,0-8.6382,0"
/>
</g>
</svg>
);
};
export const GrinningFaceWithSmilingEyes: FunctionComponent<JSX.HTMLAttributes<SVGCircleElement>> = (
props
) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
{...props}
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M50.595,41.64a11.5554,11.5554,0,0,1-.87,4.49c-12.49,3.03-25.43.34-27.49-.13a11.4347,11.4347,0,0,1-.83-4.36h.11s14.8,3.59,28.89.07Z"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M49.7251,46.13c-1.79,4.27-6.35,7.23-13.69,7.23-7.41,0-12.03-3.03-13.8-7.36C24.2951,46.47,37.235,49.16,49.7251,46.13Z"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M31.6941,32.4036a4.7262,4.7262,0,0,0-8.6382,0"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M48.9441,32.4036a4.7262,4.7262,0,0,0-8.6382,0"
/>
</g>
</svg>
);
};
export const GrinningSquintingFace: FunctionComponent<JSX.HTMLAttributes<SVGCircleElement>> = (props) => {
return (
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="line">
<circle
cx="36"
cy="36"
r="23"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
{...props}
/>
<polyline
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
points="25.168 27.413 31.755 31.427 25.168 35.165"
/>
<polyline
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
points="46.832 27.413 40.245 31.427 46.832 35.165"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M50.595,41.64a11.5554,11.5554,0,0,1-.87,4.49c-12.49,3.03-25.43.34-27.49-.13a11.4347,11.4347,0,0,1-.83-4.36h.11s14.8,3.59,28.89.07Z"
/>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M49.7251,46.13c-1.79,4.27-6.35,7.23-13.69,7.23-7.41,0-12.03-3.03-13.8-7.36C24.2951,46.47,37.235,49.16,49.7251,46.13Z"
/>
</g>
</svg>
);
};
export let icons = [<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg"></svg>];

View File

@@ -1,7 +1,7 @@
import { Question } from "./questions";
import type { Question } from "./questions";
export interface QuestionSummary {
question: Question;
export interface QuestionSummary<T extends Question> {
question: T;
responses: {
id: string;
personId: string;