mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-16 19:15:05 -05:00
Compare commits
5 Commits
mattinannt
...
feature/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7165272f1 | ||
|
|
cd523daa73 | ||
|
|
d646f82a4a | ||
|
|
bacabca29f | ||
|
|
646247024a |
Binary file not shown.
|
Before Width: | Height: | Size: 8.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -1,45 +0,0 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import SurveyEmbed from "@/components/SurveyEmbed";
|
||||
import AddImageOrVideoToQuestionImage from "./images/add-image-or-video-to-question-image.webp";
|
||||
import AddImageOrVideoToQuestionVideo from "./images/add-image-or-video-to-question-video.webp";
|
||||
import AddImageOrVideoToQuestion from "./images/add-image-or-video-to-question.webp";
|
||||
|
||||
#### Add Image or Video to a Question
|
||||
|
||||
Enhance your questions by adding images or videos. This makes instructions clearer and the survey more engaging.
|
||||
|
||||
## How to Add Images
|
||||
|
||||
Click the icon on the right side of the question to add an image or video:
|
||||
|
||||
<MdxImage
|
||||
src={AddImageOrVideoToQuestion}
|
||||
alt="Overview of adding image or video to question"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Upload an image by clicking the upload icon or dragging the file:
|
||||
|
||||
<MdxImage
|
||||
src={AddImageOrVideoToQuestionImage}
|
||||
alt="Overview of adding image to question"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
## How to Add Videos
|
||||
Toggle to add a video via link:
|
||||
|
||||
<MdxImage
|
||||
src={AddImageOrVideoToQuestionVideo}
|
||||
alt="Overview of adding video to question"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Supported Video Platforms
|
||||
|
||||
We support YouTube, Vimeo, and Loom URLs.
|
||||
|
||||
<Note>**YouTube Privacy Mode**: This option reduces tracking by converting YouTube URLs to no-cookie URLs. It only works with YouTube.</Note>
|
||||
@@ -160,33 +160,6 @@ const NavigationGroup = ({
|
||||
const pathname = usePathname();
|
||||
const [isActiveGroup, setIsActiveGroup] = useState<boolean>(false);
|
||||
|
||||
// We need to expand the group with the current link so we loop over all links
|
||||
// Until we find the one and then expand the groups
|
||||
useEffect(() => {
|
||||
const findMatchingGroup = () => {
|
||||
for (const group of navigation) {
|
||||
for (const link of group.links) {
|
||||
if (!link.children) continue;
|
||||
|
||||
const matchingChild = link.children.find((child) => pathname && child.href.startsWith(pathname));
|
||||
|
||||
if (matchingChild) {
|
||||
setOpenGroups([`${group.title}-${link.title}`]);
|
||||
setActiveGroup(group);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
findMatchingGroup();
|
||||
|
||||
return () => {
|
||||
setOpenGroups([]);
|
||||
setActiveGroup(null);
|
||||
};
|
||||
}, [pathname, setActiveGroup, setOpenGroups]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsActiveGroup(activeGroup?.title === group.title);
|
||||
}, [activeGroup?.title, group.title]);
|
||||
|
||||
@@ -49,10 +49,6 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Verify Email before Survey", href: "/link-surveys/verify-email-before-survey" },
|
||||
{ title: "PIN Protected Surveys", href: "/link-surveys/pin-protected-surveys" },
|
||||
{ title: "Partial Submissions", href: "/global/partial-submissions" },
|
||||
{
|
||||
title: "Add Image/Video to Question",
|
||||
href: "/global/add-image-or-video-question",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -80,10 +76,6 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Recall Functionality", href: "/global/recall" }, // global
|
||||
{ title: "Partial Submissions", href: "/global/partial-submissions" }, // global
|
||||
{ title: "Shareable Dashboards", href: "/global/shareable-dashboards" },
|
||||
{
|
||||
title: "Add Image/Video to Question",
|
||||
href: "/global/add-image-or-video-question",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -48,9 +48,6 @@ export const AnimatedSurveyBg = ({ handleBgChange, background }: AnimatedSurveyB
|
||||
"/animated-bgs/Thumbnails/36_Thumb.mp4": "/animated-bgs/4K/36_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/37_Thumb.mp4": "/animated-bgs/4K/37_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/38_Thumb.mp4": "/animated-bgs/4K/38_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/39_Thumb.mp4": "/animated-bgs/4K/39_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/40_Thumb.mp4": "/animated-bgs/4K/40_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/41_Thumb.mp4": "/animated-bgs/4K/41_4k.mp4",
|
||||
};
|
||||
|
||||
const togglePlayback = (index: number, type: "play" | "pause") => {
|
||||
|
||||
@@ -107,9 +107,9 @@ export const FormStylingSettings = ({
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
|
||||
<Collapsible.CollapsibleContent className="flex flex-col" ref={parent}>
|
||||
<hr className="py-1 text-slate-600" />
|
||||
<hr className="py-1 text-slate-600" key={"hello"} />
|
||||
|
||||
<div className="flex flex-col gap-6 p-6 pt-2">
|
||||
<div className="flex flex-col gap-6 p-6 pt-2" key={"hjiii"}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TabBar } from "@formbricks/ui/components/TabBar";
|
||||
import { AnimatedSurveyBg } from "./AnimatedSurveyBg";
|
||||
@@ -31,7 +30,7 @@ export const SurveyBgSelectorTab = ({
|
||||
isUnsplashConfigured,
|
||||
}: SurveyBgSelectorTabProps) => {
|
||||
const [activeTab, setActiveTab] = useState(bgType || "color");
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
const [colorBackground, setColorBackground] = useState(bg);
|
||||
const [animationBackground, setAnimationBackground] = useState(bg);
|
||||
const [uploadBackground, setUploadBackground] = useState(bg);
|
||||
@@ -94,7 +93,7 @@ export const SurveyBgSelectorTab = ({
|
||||
tabStyle="button"
|
||||
className="bg-slate-100"
|
||||
/>
|
||||
<div className="w-full rounded-b-lg border-x border-b border-slate-200 px-4 pb-4 pt-2" ref={parent}>
|
||||
<div className="w-full rounded-b-lg border-x border-b border-slate-200 px-4 pb-4 pt-2">
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,7 @@ export const SurveyMenuBar = ({
|
||||
const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
||||
const [isSurveyPublishing, setIsSurveyPublishing] = useState(false);
|
||||
const [isSurveySaving, setIsSurveySaving] = useState(false);
|
||||
const cautionText = "Changes will lead to inconsistencies.";
|
||||
const cautionText = "This survey received responses.";
|
||||
|
||||
useEffect(() => {
|
||||
if (audiencePrompt && activeId === "settings") {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { triggers } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/HardcodedTriggers";
|
||||
import { SurveyCheckboxGroup } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/SurveyCheckboxGroup";
|
||||
import { TriggerCheckboxGroup } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/TriggerCheckboxGroup";
|
||||
import { validWebHookURL } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/lib/utils";
|
||||
import clsx from "clsx";
|
||||
import { Webhook } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -43,11 +42,6 @@ export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWe
|
||||
|
||||
const handleTestEndpoint = async (sendSuccessToast: boolean) => {
|
||||
try {
|
||||
const { valid, error } = validWebHookURL(testEndpointInput);
|
||||
if (!valid) {
|
||||
toast.error(error ?? "Something went wrong please try again!");
|
||||
return;
|
||||
}
|
||||
setHittingEndpoint(true);
|
||||
await testEndpointAction({ url: testEndpointInput });
|
||||
setHittingEndpoint(false);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { triggers } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/HardcodedTriggers";
|
||||
import { SurveyCheckboxGroup } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/SurveyCheckboxGroup";
|
||||
import { TriggerCheckboxGroup } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/TriggerCheckboxGroup";
|
||||
import { validWebHookURL } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/lib/utils";
|
||||
import clsx from "clsx";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -47,11 +46,6 @@ export const WebhookSettingsTab = ({ webhook, surveys, setOpen }: ActionSettings
|
||||
|
||||
const handleTestEndpoint = async (sendSuccessToast: boolean) => {
|
||||
try {
|
||||
const { valid, error } = validWebHookURL(testEndpointInput);
|
||||
if (!valid) {
|
||||
toast.error(error ?? "Something went wrong please try again!");
|
||||
return;
|
||||
}
|
||||
setHittingEndpoint(true);
|
||||
await testEndpointAction({ url: testEndpointInput });
|
||||
setHittingEndpoint(false);
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
export const validWebHookURL = (urlInput: string) => {
|
||||
const trimmedInput = urlInput.trim();
|
||||
if (!trimmedInput) {
|
||||
return { valid: false, error: "Please enter a URL" };
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(trimmedInput);
|
||||
|
||||
if (url.protocol !== "https:") {
|
||||
return { valid: false, error: "URL must start with https://" };
|
||||
}
|
||||
|
||||
const domainError: string =
|
||||
"Please enter a complete URL with a valid domain (e.g., https://formbricks.com)";
|
||||
|
||||
const multipleSlashesPattern = /(?<!:)\/\/+/;
|
||||
if (multipleSlashesPattern.test(trimmedInput)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: domainError,
|
||||
};
|
||||
}
|
||||
|
||||
const validDomainPattern = /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
if (!validDomainPattern.test(url.hostname)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: domainError,
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
} catch (error) {
|
||||
return { valid: false, error: "Invalid URL format. Please enter a complete URL including https://" };
|
||||
}
|
||||
};
|
||||
@@ -44,7 +44,7 @@ export const CTASummary = ({ questionSummary, survey, attributeClasses }: CTASum
|
||||
<p className="font-semibold text-slate-700">CTR</p>
|
||||
<div>
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(questionSummary.ctr.percentage, 2)}%
|
||||
{convertFloatToNDecimal(questionSummary.ctr.percentage, 1)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ export const CalSummary = ({ questionSummary, survey, attributeClasses }: CalSum
|
||||
<p className="font-semibold text-slate-700">Booked</p>
|
||||
<div>
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(questionSummary.booked.percentage, 2)}%
|
||||
{convertFloatToNDecimal(questionSummary.booked.percentage, 1)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@ export const CalSummary = ({ questionSummary, survey, attributeClasses }: CalSum
|
||||
<p className="font-semibold text-slate-700">Dismissed</p>
|
||||
<div>
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(questionSummary.skipped.percentage, 2)}%
|
||||
{convertFloatToNDecimal(questionSummary.skipped.percentage, 1)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -70,7 +70,7 @@ export const ConsentSummary = ({
|
||||
</p>
|
||||
<div>
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(summaryItem.percentage, 2)}%
|
||||
{convertFloatToNDecimal(summaryItem.percentage, 1)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { InboxIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
|
||||
@@ -69,14 +68,6 @@ export const MultipleChoiceSummary = ({
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
attributeClasses={attributeClasses}
|
||||
additionalInfo={
|
||||
questionSummary.type === "multipleChoiceMulti" ? (
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxIcon className="mr-2 h-4 w-4" />
|
||||
{`${questionSummary.selectionCount} Selections`}
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
{results.map((result, resultsIdx) => (
|
||||
@@ -101,12 +92,12 @@ export const MultipleChoiceSummary = ({
|
||||
</p>
|
||||
<div>
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(result.percentage, 2)}%
|
||||
{convertFloatToNDecimal(result.percentage, 1)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="flex w-full pt-1 text-slate-600 sm:items-end sm:justify-end sm:pt-0">
|
||||
{result.count} {result.count === 1 ? "Selection" : "Selections"}
|
||||
{result.count} {result.count === 1 ? "response" : "responses"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="group-hover:opacity-80">
|
||||
|
||||
@@ -76,7 +76,7 @@ export const NPSSummary = ({ questionSummary, survey, attributeClasses, setFilte
|
||||
</p>
|
||||
<div>
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(questionSummary[group]?.percentage, 2)}%
|
||||
{convertFloatToNDecimal(questionSummary[group]?.percentage, 1)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -102,7 +102,7 @@ export const OpenTextSummary = ({
|
||||
<TableBody>
|
||||
{questionSummary.samples.slice(0, visibleResponses).map((response) => (
|
||||
<TableRow key={response.id}>
|
||||
<TableCell width={180}>
|
||||
<TableCell>
|
||||
{response.person ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
@@ -110,7 +110,7 @@ export const OpenTextSummary = ({
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.person.id} />
|
||||
</div>
|
||||
<p className="ph-no-capture break-normal text-slate-600 group-hover:underline md:ml-2">
|
||||
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
|
||||
{getPersonIdentifier(response.person, response.personAttributes)}
|
||||
</p>
|
||||
</Link>
|
||||
@@ -119,12 +119,12 @@ export const OpenTextSummary = ({
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId="anonymous" />
|
||||
</div>
|
||||
<p className="break-normal text-slate-600 md:ml-2">Anonymous</p>
|
||||
<p className="break-all text-slate-600 md:ml-2">Anonymous</p>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{response.value}</TableCell>
|
||||
<TableCell width={120}>{timeSince(new Date(response.updatedAt).toISOString())}</TableCell>
|
||||
<TableCell>{timeSince(new Date(response.updatedAt).toISOString())}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { InboxIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import {
|
||||
@@ -39,14 +38,6 @@ export const PictureChoiceSummary = ({
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
attributeClasses={attributeClasses}
|
||||
additionalInfo={
|
||||
questionSummary.question.allowMulti ? (
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxIcon className="mr-2 h-4 w-4" />
|
||||
{`${questionSummary.selectionCount} Selections`}
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
{results.map((result, index) => (
|
||||
@@ -75,12 +66,12 @@ export const PictureChoiceSummary = ({
|
||||
</div>
|
||||
<div className="self-end">
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(result.percentage, 2)}%
|
||||
{convertFloatToNDecimal(result.percentage, 1)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="flex w-full pt-1 text-slate-600 sm:items-end sm:justify-end sm:pt-0">
|
||||
{result.count} {result.count === 1 ? "Selection" : "Selections"}
|
||||
{result.count} {result.count === 1 ? "response" : "responses"}
|
||||
</p>
|
||||
</div>
|
||||
<ProgressBar barColor="bg-brand-dark" progress={result.percentage / 100 || 0} />
|
||||
|
||||
@@ -38,7 +38,7 @@ export const RankingSummary = ({
|
||||
<div className="rounded bg-gray-100 px-2 py-1">{result.value}</div>
|
||||
<span className="ml-auto flex items-center space-x-1">
|
||||
<span className="font-bold text-slate-600">
|
||||
#{convertFloatToNDecimal(result.avgRanking, 2)}
|
||||
#{convertFloatToNDecimal(result.avgRanking, 1)}
|
||||
</span>
|
||||
<span>average</span>
|
||||
</span>
|
||||
|
||||
@@ -78,7 +78,7 @@ export const RatingSummary = ({
|
||||
</div>
|
||||
<div>
|
||||
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
|
||||
{convertFloatToNDecimal(result.percentage, 2)}%
|
||||
{convertFloatToNDecimal(result.percentage, 1)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { TimerIcon } from "lucide-react";
|
||||
import { getQuestionIcon } from "@formbricks/lib/utils/questions";
|
||||
import { TSurveyQuestionType, TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
import { TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/components/Tooltip";
|
||||
|
||||
interface SummaryDropOffsProps {
|
||||
@@ -8,11 +7,6 @@ interface SummaryDropOffsProps {
|
||||
}
|
||||
|
||||
export const SummaryDropOffs = ({ dropOff }: SummaryDropOffsProps) => {
|
||||
const getIcon = (questionType: TSurveyQuestionType) => {
|
||||
const Icon = getQuestionIcon(questionType);
|
||||
return <Icon className="mt-[3px] h-5 w-5 shrink-0 text-slate-600" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="">
|
||||
@@ -37,10 +31,7 @@ export const SummaryDropOffs = ({ dropOff }: SummaryDropOffsProps) => {
|
||||
<div
|
||||
key={quesDropOff.questionId}
|
||||
className="grid grid-cols-6 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
|
||||
<div className="col-span-3 flex gap-3 pl-4 md:pl-6">
|
||||
{getIcon(quesDropOff.questionType)}
|
||||
<p>{quesDropOff.headline}</p>
|
||||
</div>
|
||||
<div className="col-span-3 pl-4 md:pl-6">{quesDropOff.headline}</div>
|
||||
<div className="whitespace-pre-wrap text-center font-semibold">
|
||||
{quesDropOff.ttc > 0 ? (quesDropOff.ttc / 1000).toFixed(2) + "s" : "N/A"}
|
||||
</div>
|
||||
|
||||
@@ -138,10 +138,7 @@ export const SurveyAnalysisCTA = ({
|
||||
)}
|
||||
|
||||
{!isViewer && (
|
||||
<Button
|
||||
href={`/environments/${environment.id}/surveys/${survey.id}/edit`}
|
||||
EndIcon={SquarePenIcon}
|
||||
size="base">
|
||||
<Button href={`/environments/${environment.id}/surveys/${survey.id}/edit`} EndIcon={SquarePenIcon}>
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -235,7 +235,6 @@ export const getSurveySummaryDropOff = (
|
||||
const dropOff = survey.questions.map((question, index) => {
|
||||
return {
|
||||
questionId: question.id,
|
||||
questionType: question.type,
|
||||
headline: getLocalizedValue(question.headline, "default"),
|
||||
ttc: convertFloatTo2Decimal(totalTtc[question.id]) || 0,
|
||||
impressions: impressionsArr[index] || 0,
|
||||
@@ -319,7 +318,7 @@ export const getQuestionSummary = async (
|
||||
insightsEnabled: question.insightsEnabled,
|
||||
});
|
||||
|
||||
values = [];
|
||||
values;
|
||||
break;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.MultipleChoiceSingle:
|
||||
@@ -340,8 +339,6 @@ export const getQuestionSummary = async (
|
||||
}, {});
|
||||
|
||||
const otherValues: TSurveyQuestionSummaryMultipleChoice["choices"][number]["others"] = [];
|
||||
let totalSelectionCount = 0;
|
||||
let totalResponseCount = 0;
|
||||
responses.forEach((response) => {
|
||||
const responseLanguageCode = getLanguageCode(survey.languages, response.language);
|
||||
|
||||
@@ -350,51 +347,36 @@ export const getQuestionSummary = async (
|
||||
? response.data[question.id]
|
||||
: checkForI18n(response, question.id, survey, responseLanguageCode);
|
||||
|
||||
let hasValidAnswer = false;
|
||||
|
||||
if (Array.isArray(answer)) {
|
||||
answer.forEach((value) => {
|
||||
if (value) {
|
||||
totalSelectionCount++;
|
||||
if (questionChoices.includes(value)) {
|
||||
choiceCountMap[value]++;
|
||||
} else if (isOthersEnabled) {
|
||||
otherValues.push({
|
||||
value,
|
||||
person: response.person,
|
||||
personAttributes: response.personAttributes,
|
||||
});
|
||||
}
|
||||
hasValidAnswer = true;
|
||||
}
|
||||
});
|
||||
} else if (typeof answer === "string") {
|
||||
if (answer) {
|
||||
totalSelectionCount++;
|
||||
if (questionChoices.includes(answer)) {
|
||||
choiceCountMap[answer]++;
|
||||
} else if (isOthersEnabled) {
|
||||
if (questionChoices.includes(value)) {
|
||||
choiceCountMap[value]++;
|
||||
} else {
|
||||
otherValues.push({
|
||||
value: answer,
|
||||
value,
|
||||
person: response.person,
|
||||
personAttributes: response.personAttributes,
|
||||
});
|
||||
}
|
||||
hasValidAnswer = true;
|
||||
});
|
||||
} else if (typeof answer === "string") {
|
||||
if (questionChoices.includes(answer)) {
|
||||
choiceCountMap[answer]++;
|
||||
} else {
|
||||
otherValues.push({
|
||||
value: answer,
|
||||
person: response.person,
|
||||
personAttributes: response.personAttributes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (hasValidAnswer) {
|
||||
totalResponseCount++;
|
||||
}
|
||||
});
|
||||
|
||||
Object.entries(choiceCountMap).map(([label, count]) => {
|
||||
values.push({
|
||||
value: label,
|
||||
count,
|
||||
percentage:
|
||||
totalSelectionCount > 0 ? convertFloatTo2Decimal((count / totalSelectionCount) * 100) : 0,
|
||||
percentage: responses.length > 0 ? convertFloatTo2Decimal((count / responses.length) * 100) : 0,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -402,18 +384,14 @@ export const getQuestionSummary = async (
|
||||
values.push({
|
||||
value: getLocalizedValue(lastChoice.label, "default") || "Other",
|
||||
count: otherValues.length,
|
||||
percentage:
|
||||
totalSelectionCount > 0
|
||||
? convertFloatTo2Decimal((otherValues.length / totalSelectionCount) * 100)
|
||||
: 0,
|
||||
percentage: convertFloatTo2Decimal((otherValues.length / responses.length) * 100),
|
||||
others: otherValues.slice(0, VALUES_LIMIT),
|
||||
});
|
||||
}
|
||||
summary.push({
|
||||
type: question.type,
|
||||
question,
|
||||
responseCount: totalResponseCount,
|
||||
selectionCount: totalSelectionCount,
|
||||
responseCount: responses.length,
|
||||
choices: values,
|
||||
});
|
||||
|
||||
@@ -428,14 +406,12 @@ export const getQuestionSummary = async (
|
||||
choiceCountMap[choice.id] = 0;
|
||||
});
|
||||
let totalResponseCount = 0;
|
||||
let totalSelectionCount = 0;
|
||||
|
||||
responses.forEach((response) => {
|
||||
const answer = response.data[question.id];
|
||||
if (Array.isArray(answer)) {
|
||||
totalResponseCount++;
|
||||
answer.forEach((value) => {
|
||||
totalSelectionCount++;
|
||||
totalResponseCount++;
|
||||
choiceCountMap[value]++;
|
||||
});
|
||||
}
|
||||
@@ -447,8 +423,8 @@ export const getQuestionSummary = async (
|
||||
imageUrl: choice.imageUrl,
|
||||
count: choiceCountMap[choice.id],
|
||||
percentage:
|
||||
totalSelectionCount > 0
|
||||
? convertFloatTo2Decimal((choiceCountMap[choice.id] / totalSelectionCount) * 100)
|
||||
totalResponseCount > 0
|
||||
? convertFloatTo2Decimal((choiceCountMap[choice.id] / totalResponseCount) * 100)
|
||||
: 0,
|
||||
});
|
||||
});
|
||||
@@ -457,7 +433,6 @@ export const getQuestionSummary = async (
|
||||
type: question.type,
|
||||
question,
|
||||
responseCount: totalResponseCount,
|
||||
selectionCount: totalSelectionCount,
|
||||
choices: values,
|
||||
});
|
||||
|
||||
|
||||
@@ -211,7 +211,6 @@ export const SigninForm = ({
|
||||
)}
|
||||
{emailAuthEnabled && (
|
||||
<Button
|
||||
size="base"
|
||||
onClick={() => {
|
||||
if (!showLogin) {
|
||||
setShowLogin(true);
|
||||
@@ -225,7 +224,7 @@ export const SigninForm = ({
|
||||
loading={loggingIn}>
|
||||
{totpLogin ? "Submit" : "Login with Email"}
|
||||
{lastLoggedInWith && lastLoggedInWith === "Email" ? (
|
||||
<span className="absolute right-3 text-xs opacity-50">Last Used</span>
|
||||
<span className="absolute right-3 text-xs">Last Used</span>
|
||||
) : null}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { sendForgotPasswordEmail } from "@formbricks/email";
|
||||
import { loginLimiter } from "@/app/middleware/bucket";
|
||||
|
||||
export const POST = async (request: Request) => {
|
||||
const { email } = await request.json();
|
||||
|
||||
try {
|
||||
await loginLimiter(request.headers.get("x-forwarded-for") || request.connection.remoteAddress);
|
||||
|
||||
const foundUser = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: email.toLowerCase(),
|
||||
|
||||
@@ -29,8 +29,3 @@ export const syncUserIdentificationLimiter = rateLimit({
|
||||
interval: SYNC_USER_IDENTIFICATION_RATE_LIMIT.interval,
|
||||
allowedPerInterval: SYNC_USER_IDENTIFICATION_RATE_LIMIT.allowedPerInterval,
|
||||
});
|
||||
|
||||
export const forgotPasswordLimiter = rateLimit({
|
||||
interval: LOGIN_RATE_LIMIT.interval,
|
||||
allowedPerInterval: LOGIN_RATE_LIMIT.allowedPerInterval,
|
||||
});
|
||||
|
||||
@@ -28,5 +28,3 @@ export const isSyncWithUserIdentificationEndpoint = (
|
||||
const match = url.match(regex);
|
||||
return match ? { environmentId: match[1], userId: match[2] } : false;
|
||||
};
|
||||
|
||||
export const forgotPasswordRoute = (url: string) => url === "/api/v1/users/forgot-password";
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
shareUrlLimiter,
|
||||
signUpLimiter,
|
||||
syncUserIdentificationLimiter,
|
||||
forgotPasswordLimiter,
|
||||
} from "@/app/middleware/bucket";
|
||||
import {
|
||||
clientSideApiRoute,
|
||||
@@ -13,7 +12,6 @@ import {
|
||||
loginRoute,
|
||||
shareUrlRoute,
|
||||
signupRoute,
|
||||
forgotPasswordRoute,
|
||||
} from "@/app/middleware/endpointValidator";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
import { NextResponse } from "next/server";
|
||||
@@ -62,8 +60,6 @@ export const middleware = async (request: NextRequest) => {
|
||||
}
|
||||
} else if (shareUrlRoute(request.nextUrl.pathname)) {
|
||||
await shareUrlLimiter(`share-${ip}`);
|
||||
} else if (forgotPasswordRoute(request.nextUrl.pathname)) {
|
||||
await forgotPasswordLimiter(`forgot-password-${ip}`);
|
||||
}
|
||||
return NextResponse.next();
|
||||
} catch (e) {
|
||||
@@ -87,6 +83,5 @@ export const config = {
|
||||
"/api/auth/signout",
|
||||
"/auth/login",
|
||||
"/api/packages/:path*",
|
||||
"/api/v1/users/forgot-password",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -162,11 +162,6 @@ const nextConfig = {
|
||||
key: "X-Content-Type-Options",
|
||||
value: "nosniff",
|
||||
},
|
||||
{
|
||||
key: "Content-Security-Policy",
|
||||
value:
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self' https:; frame-src 'self'; media-src 'self' https:; object-src 'none'; base-uri 'self'; form-action 'self';",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -91,6 +91,19 @@ export const updateAttributes = async (
|
||||
// clean attributes and remove existing attributes if config already exists
|
||||
const updatedAttributes = { ...attributes };
|
||||
|
||||
try {
|
||||
const existingAttributes = config.get().attributes;
|
||||
if (existingAttributes) {
|
||||
for (const [key, value] of Object.entries(existingAttributes)) {
|
||||
if (updatedAttributes[key] === value) {
|
||||
delete updatedAttributes[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.debug("config not set; sending all attributes to backend");
|
||||
}
|
||||
|
||||
// send to backend if updatedAttributes is not empty
|
||||
if (Object.keys(updatedAttributes).length === 0) {
|
||||
logger.debug("No attributes to update. Skipping update.");
|
||||
@@ -125,6 +138,14 @@ export const updateAttributes = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const isExistingAttribute = (key: string, value: string): boolean => {
|
||||
if (config.get().attributes[key] === value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const setAttributeInApp = async (
|
||||
key: string,
|
||||
value: any
|
||||
@@ -137,6 +158,11 @@ export const setAttributeInApp = async (
|
||||
const userId = config.get().personState.data.userId;
|
||||
|
||||
logger.debug("Setting attribute: " + key + " to value: " + value);
|
||||
// check if attribute already exists with this value
|
||||
if (isExistingAttribute(key, value.toString())) {
|
||||
logger.debug("Attribute already set to this value. Skipping update.");
|
||||
return okVoid();
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
logger.error(
|
||||
@@ -164,10 +190,6 @@ export const setAttributeInApp = async (
|
||||
...config.get(),
|
||||
personState,
|
||||
filteredSurveys,
|
||||
attributes: {
|
||||
...config.get().attributes,
|
||||
[key]: value.toString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -162,6 +162,24 @@ export const initialize = async (
|
||||
logger.debug("Adding widget container to DOM");
|
||||
addWidgetContainer();
|
||||
|
||||
let updatedAttributes: TAttributes | null = null;
|
||||
if (configInput.attributes) {
|
||||
if (configInput.userId) {
|
||||
const res = await updateAttributes(
|
||||
configInput.apiHost,
|
||||
configInput.environmentId,
|
||||
configInput.userId,
|
||||
configInput.attributes
|
||||
);
|
||||
if (res.ok !== true) {
|
||||
return err(res.error);
|
||||
}
|
||||
updatedAttributes = res.value;
|
||||
} else {
|
||||
updatedAttributes = { ...configInput.attributes };
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
existingConfig &&
|
||||
existingConfig.environmentState &&
|
||||
@@ -177,12 +195,13 @@ export const initialize = async (
|
||||
isEnvironmentStateExpired = true;
|
||||
}
|
||||
|
||||
// if the config has a userId and the person state has expired, we need to sync the person state
|
||||
if (
|
||||
configInput.userId &&
|
||||
(existingConfig.personState === null ||
|
||||
(existingConfig.personState.expiresAt && new Date(existingConfig.personState.expiresAt) < new Date()))
|
||||
existingConfig.personState.expiresAt &&
|
||||
new Date(existingConfig.personState.expiresAt) < new Date()
|
||||
) {
|
||||
logger.debug("Person state needs syncing - either null or expired");
|
||||
logger.debug("Person state expired. Syncing.");
|
||||
isPersonStateExpired = true;
|
||||
}
|
||||
|
||||
@@ -220,7 +239,7 @@ export const initialize = async (
|
||||
environmentState,
|
||||
personState,
|
||||
filteredSurveys,
|
||||
attributes: configInput.attributes ?? {},
|
||||
attributes: configInput.attributes || {},
|
||||
});
|
||||
|
||||
const surveyNames = filteredSurveys.map((s) => s.name);
|
||||
@@ -229,30 +248,12 @@ export const initialize = async (
|
||||
putFormbricksInErrorState(config);
|
||||
}
|
||||
} else {
|
||||
logger.debug("No valid configuration found. Resetting config and creating new one.");
|
||||
logger.debug(
|
||||
"No valid configuration found or it has been expired. Resetting config and creating new one."
|
||||
);
|
||||
config.resetConfig();
|
||||
logger.debug("Syncing.");
|
||||
|
||||
let updatedAttributes: TAttributes | null = null;
|
||||
if (configInput.attributes) {
|
||||
if (configInput.userId) {
|
||||
const res = await updateAttributes(
|
||||
configInput.apiHost,
|
||||
configInput.environmentId,
|
||||
configInput.userId,
|
||||
configInput.attributes
|
||||
);
|
||||
|
||||
if (res.ok !== true) {
|
||||
return err(res.error);
|
||||
}
|
||||
|
||||
updatedAttributes = res.value;
|
||||
} else {
|
||||
updatedAttributes = { ...configInput.attributes };
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const environmentState = await fetchEnvironmentState(
|
||||
{
|
||||
@@ -280,7 +281,7 @@ export const initialize = async (
|
||||
personState,
|
||||
environmentState,
|
||||
filteredSurveys,
|
||||
attributes: updatedAttributes ?? {},
|
||||
attributes: configInput.attributes || {},
|
||||
});
|
||||
} catch (e) {
|
||||
handleErrorOnFirstInit();
|
||||
@@ -290,6 +291,17 @@ export const initialize = async (
|
||||
await trackNoCodeAction("New Session");
|
||||
}
|
||||
|
||||
// update attributes in config
|
||||
if (updatedAttributes && Object.keys(updatedAttributes).length > 0) {
|
||||
config.update({
|
||||
...config.get(),
|
||||
attributes: {
|
||||
...config.get().attributes,
|
||||
...updatedAttributes,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug("Adding event listeners");
|
||||
addEventListeners();
|
||||
addCleanupEventListeners();
|
||||
|
||||
@@ -367,7 +367,6 @@ export const mockSurveySummaryOutput = {
|
||||
dropOffCount: 0,
|
||||
dropOffPercentage: 0,
|
||||
headline: "Question Text",
|
||||
questionType: "openText",
|
||||
questionId: "ars2tjk8hsi8oqk1uac00mo8",
|
||||
ttc: 0,
|
||||
impressions: 0,
|
||||
|
||||
@@ -5323,129 +5323,6 @@ const employeeWellBeing = (): TTemplate => {
|
||||
};
|
||||
};
|
||||
|
||||
const longTermRetentionCheckIn = (): TTemplate => {
|
||||
return {
|
||||
name: "Long-Term Retention Check-In",
|
||||
role: "productManager",
|
||||
industries: ["saas", "other"],
|
||||
channels: ["app", "link"],
|
||||
description:
|
||||
"Gauge long-term user satisfaction, loyalty, and areas for improvement to retain loyal users.",
|
||||
preset: {
|
||||
...surveyDefault,
|
||||
name: "Long-Term Retention Check-In",
|
||||
questions: [
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
range: 5,
|
||||
scale: "star",
|
||||
headline: { default: "How satisfied are you with {{productName}} overall?" },
|
||||
required: true,
|
||||
lowerLabel: { default: "Not satisfied" },
|
||||
upperLabel: { default: "Very satisfied" },
|
||||
isColorCodingEnabled: true,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "What do you find most valuable about {{productName}}?" },
|
||||
required: false,
|
||||
placeholder: { default: "Describe the feature or benefit you value most..." },
|
||||
inputType: "text",
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
shuffleOption: "none",
|
||||
choices: [
|
||||
{ id: createId(), label: { default: "Features" } },
|
||||
{ id: createId(), label: { default: "Customer support" } },
|
||||
{ id: createId(), label: { default: "User experience" } },
|
||||
{ id: createId(), label: { default: "Pricing" } },
|
||||
{ id: createId(), label: { default: "Reliability and uptime" } },
|
||||
],
|
||||
headline: {
|
||||
default: "Which aspect of {{productName}} do you find most essential to your experience?",
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
range: 5,
|
||||
scale: "number",
|
||||
headline: { default: "How well does {{productName}} meet your expectations?" },
|
||||
required: true,
|
||||
lowerLabel: { default: "Falls short" },
|
||||
upperLabel: { default: "Exceeds expectations" },
|
||||
isColorCodingEnabled: true,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: {
|
||||
default: "What challenges or frustrations have you faced while using {{productName}}?",
|
||||
},
|
||||
required: false,
|
||||
placeholder: { default: "Describe any challenges or improvements you’d like to see..." },
|
||||
inputType: "text",
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.NPS,
|
||||
headline: { default: "How likely are you to recommend {{productName}} to a friend or colleague?" },
|
||||
required: false,
|
||||
lowerLabel: { default: "Not likely" },
|
||||
upperLabel: { default: "Very likely" },
|
||||
isColorCodingEnabled: false,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
|
||||
shuffleOption: "none",
|
||||
choices: [
|
||||
{ id: createId(), label: { default: "New features and improvements" } },
|
||||
{ id: createId(), label: { default: "Enhanced customer support" } },
|
||||
{ id: createId(), label: { default: "Better pricing options" } },
|
||||
{ id: createId(), label: { default: "More integrations" } },
|
||||
{ id: createId(), label: { default: "User experience refinements" } },
|
||||
],
|
||||
headline: { default: "What would make you more likely to remain a long-term user?" },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "If you could change one thing about {{productName}}, what would it be?" },
|
||||
required: false,
|
||||
placeholder: { default: "Share any changes or features you wish we’d consider..." },
|
||||
inputType: "text",
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
range: 5,
|
||||
scale: "smiley",
|
||||
headline: { default: "How happy are you with our product updates and frequency?" },
|
||||
required: true,
|
||||
lowerLabel: { default: "Not happy" },
|
||||
upperLabel: { default: "Very happy" },
|
||||
isColorCodingEnabled: true,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Any additional feedback or comments?" },
|
||||
required: false,
|
||||
placeholder: { default: "Share any thoughts or feedback that might help us improve..." },
|
||||
inputType: "text",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const templates: TTemplate[] = [
|
||||
cartAbandonmentSurvey(),
|
||||
siteAbandonmentSurvey(),
|
||||
@@ -5488,7 +5365,6 @@ export const templates: TTemplate[] = [
|
||||
understandLowEngagement(),
|
||||
employeeSatisfaction(),
|
||||
employeeWellBeing(),
|
||||
longTermRetentionCheckIn(),
|
||||
];
|
||||
|
||||
export const customSurvey = {
|
||||
|
||||
@@ -267,10 +267,6 @@ export const QUESTIONS_ICON_MAP: Record<TSurveyQuestionTypeEnum, JSX.Element> =
|
||||
{} as Record<TSurveyQuestionTypeEnum, JSX.Element>
|
||||
);
|
||||
|
||||
export const getQuestionIcon = (type: TSurveyQuestionTypeEnum) => {
|
||||
return questionTypes.find((questionType) => questionType.id === type)?.icon;
|
||||
};
|
||||
|
||||
export const QUESTIONS_NAME_MAP = questionTypes.reduce(
|
||||
(prev, curr) => ({
|
||||
...prev,
|
||||
|
||||
@@ -2106,7 +2106,6 @@ export const ZSurveyQuestionSummaryMultipleChoice = z.object({
|
||||
type: z.union([z.literal("multipleChoiceMulti"), z.literal("multipleChoiceSingle")]),
|
||||
question: ZSurveyMultipleChoiceQuestion,
|
||||
responseCount: z.number(),
|
||||
selectionCount: z.number(),
|
||||
choices: z.array(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
@@ -2136,7 +2135,6 @@ export const ZSurveyQuestionSummaryPictureSelection = z.object({
|
||||
type: z.literal("pictureSelection"),
|
||||
question: ZSurveyPictureSelectionQuestion,
|
||||
responseCount: z.number(),
|
||||
selectionCount: z.number(),
|
||||
choices: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
@@ -2426,7 +2424,6 @@ export const ZSurveySummary = z.object({
|
||||
dropOff: z.array(
|
||||
z.object({
|
||||
questionId: z.string().cuid2(),
|
||||
questionType: ZSurveyQuestionType,
|
||||
headline: z.string(),
|
||||
ttc: z.number(),
|
||||
impressions: z.number(),
|
||||
|
||||
@@ -34,7 +34,6 @@ export const AzureButton = ({
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="base"
|
||||
type="button"
|
||||
EndIcon={MicrosoftIcon}
|
||||
startIconClassName="ml-2"
|
||||
@@ -42,7 +41,7 @@ export const AzureButton = ({
|
||||
variant="secondary"
|
||||
className="relative w-full justify-center">
|
||||
{text}
|
||||
{lastUsed && <span className="absolute right-3 text-xs opacity-50">Last Used</span>}
|
||||
{lastUsed && <span className="absolute right-3 text-xs">Last Used</span>}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -26,7 +26,6 @@ export const GithubButton = ({
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="base"
|
||||
type="button"
|
||||
EndIcon={GithubIcon}
|
||||
startIconClassName="ml-2"
|
||||
@@ -34,7 +33,7 @@ export const GithubButton = ({
|
||||
variant="secondary"
|
||||
className="relative w-full justify-center">
|
||||
{text}
|
||||
{lastUsed && <span className="absolute right-3 text-xs opacity-50">Last Used</span>}
|
||||
{lastUsed && <span className="absolute right-3 text-xs">Last Used</span>}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -26,7 +26,6 @@ export const GoogleButton = ({
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="base"
|
||||
type="button"
|
||||
EndIcon={GoogleIcon}
|
||||
startIconClassName="ml-3"
|
||||
@@ -34,7 +33,7 @@ export const GoogleButton = ({
|
||||
variant="secondary"
|
||||
className="relative w-full justify-center">
|
||||
{text}
|
||||
{lastUsed && <span className="absolute right-3 text-xs opacity-50">Last Used</span>}
|
||||
{lastUsed && <span className="absolute right-3 text-xs">Last Used</span>}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,14 +32,13 @@ export const OpenIdButton = ({
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="base"
|
||||
type="button"
|
||||
startIconClassName="ml-2"
|
||||
onClick={handleLogin}
|
||||
variant="secondary"
|
||||
className="relative w-full justify-center">
|
||||
{text}
|
||||
{lastUsed && <span className="absolute right-3 text-xs opacity-50">Last Used</span>}
|
||||
{lastUsed && <span className="absolute right-3 text-xs">Last Used</span>}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -157,7 +157,6 @@ export const SignupOptions = ({
|
||||
)}
|
||||
{showLogin && (
|
||||
<Button
|
||||
size="base"
|
||||
type="submit"
|
||||
className="w-full justify-center"
|
||||
loading={signingUp}
|
||||
@@ -168,7 +167,6 @@ export const SignupOptions = ({
|
||||
|
||||
{!showLogin && (
|
||||
<Button
|
||||
size="base"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowLogin(true);
|
||||
|
||||
Reference in New Issue
Block a user