mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-20 17:31:27 -05:00
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:
@@ -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;
|
||||
};
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)}%
|
||||
|
||||
@@ -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;
|
||||
})}
|
||||
|
||||
464
apps/web/components/Smileys.tsx
Normal file
464
apps/web/components/Smileys.tsx
Normal 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>];
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
471
packages/js/src/components/Smileys.tsx
Normal file
471
packages/js/src/components/Smileys.tsx
Normal 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>];
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user