fix: Tweak survey settings tab and share modal to shorten viral loop (#1882)

Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: ShubhamPalriwala <spalriwalau@gmail.com>
This commit is contained in:
Johannes
2024-01-17 08:29:16 -06:00
committed by GitHub
parent f2800632e3
commit f35b007e98
84 changed files with 852 additions and 851 deletions

View File

@@ -2,7 +2,7 @@ import { Head, Html, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en" className="h-full bg-gray-50">
<Html lang="en" className="h-full bg-slate-50">
<Head />
<body className="h-full">
<Main />

View File

@@ -100,22 +100,22 @@ export default function AppPage({}) {
</div>
<div className="md:grid md:grid-cols-3">
<div className="col-span-3 rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-gray-600 dark:bg-gray-800">
<div className="col-span-3 rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-800">
<h3 className="text-lg font-semibold dark:text-white">
Reset person / pull data from Formbricks app
</h3>
<p className="text-slate-700 dark:text-gray-300">
<p className="text-slate-700 dark:text-slate-300">
On formbricks.reset() a few things happen: <strong>New person is created</strong> and{" "}
<strong>surveys & no-code actions are pulled from Formbricks:</strong>.
</p>
<button
className="my-4 rounded-lg bg-slate-500 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
className="my-4 rounded-lg bg-slate-500 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
onClick={() => {
formbricks.reset();
}}>
Reset
</button>
<p className="text-xs text-slate-700 dark:text-gray-300">
<p className="text-xs text-slate-700 dark:text-slate-300">
If you made a change in Formbricks app and it does not seem to work, hit &apos;Reset&apos; and
try again.
</p>
@@ -124,7 +124,7 @@ export default function AppPage({}) {
<div className="p-6">
<div>
<button
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
onClick={() => {
formbricks.track("Code Action");
}}>
@@ -132,7 +132,7 @@ export default function AppPage({}) {
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-gray-300">
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sends a{" "}
<a href="https://formbricks.com/docs/actions/code" className="underline" target="_blank">
Code Action
@@ -143,12 +143,12 @@ export default function AppPage({}) {
</div>
<div className="p-6">
<div>
<button className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600">
<button className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
No-Code Action
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-gray-300">
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sends a{" "}
<a
href="https://formbricks.com/docs/actions/no-code"
@@ -172,12 +172,12 @@ export default function AppPage({}) {
onClick={() => {
formbricks.setAttribute("Plan", "Free");
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600">
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Set Plan to &apos;Free&apos;
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-gray-300">
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sets the{" "}
<a
href="https://formbricks.com/docs/attributes/custom-attributes"
@@ -195,12 +195,12 @@ export default function AppPage({}) {
onClick={() => {
formbricks.setAttribute("Plan", "Paid");
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600">
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Set Plan to &apos;Paid&apos;
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-gray-300">
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sets the{" "}
<a
href="https://formbricks.com/docs/attributes/custom-attributes"
@@ -218,12 +218,12 @@ export default function AppPage({}) {
onClick={() => {
formbricks.setEmail("test@web.com");
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600">
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Set Email
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-gray-300">
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sets the{" "}
<a
href="https://formbricks.com/docs/attributes/identify-users"
@@ -242,7 +242,7 @@ export default function AppPage({}) {
onClick={() => {
window.location.href = "/app";
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600">
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Deactivate User Identification
</button>
</div>
@@ -252,13 +252,13 @@ export default function AppPage({}) {
onClick={() => {
window.location.href = "/app?userId=true";
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600">
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Activate User Identification
</button>
</div>
)}
<div>
<p className="text-xs text-slate-700 dark:text-gray-300">
<p className="text-xs text-slate-700 dark:text-slate-300">
This button activates/deactivates{" "}
<a
href="https://formbricks.com/docs/attributes/identify-users"

View File

@@ -50,7 +50,7 @@ export default function NPSQuestion({ question, onSubmit, lastQuestion, brandCol
selectedChoice === number
? "z-10 bg-slate-50 dark:bg-slate-500"
: "dark:bg-slate-700 dark:hover:bg-slate-500",
"relative h-10 flex-1 cursor-pointer border bg-white text-center text-sm leading-10 text-slate-900 first:rounded-l-md last:rounded-r-md hover:bg-gray-100 focus:outline-none dark:border-slate-600 dark:text-white "
"relative h-10 flex-1 cursor-pointer border bg-white text-center text-sm leading-10 text-slate-900 first:rounded-l-md last:rounded-r-md hover:bg-slate-100 focus:outline-none dark:border-slate-600 dark:text-white "
)}>
<input
type="radio"

View File

@@ -56,7 +56,7 @@ export default function RatingQuestion({
className={cn(
selectedChoice === number
? "z-10 border-slate-400 bg-slate-50"
: "bg-white hover:bg-gray-100 dark:bg-slate-700 dark:hover:bg-slate-500",
: "bg-white hover:bg-slate-100 dark:bg-slate-700 dark:hover:bg-slate-500",
"relative h-10 flex-1 cursor-pointer border border-slate-100 text-center text-sm leading-10 text-slate-800 first:rounded-l-md last:rounded-r-md focus:outline-none dark:border-slate-500 dark:text-slate-200 "
)}>
<input

View File

@@ -10,7 +10,7 @@ interface LayoutProps {
export default function Layout({ title, description, children }: LayoutProps) {
return (
<div className="mx-auto bg-gradient-to-br from-gray-800 via-gray-900 to-gray-900">
<div className="mx-auto bg-gradient-to-br from-slate-800 via-slate-900 to-slate-900">
<MetaInformation title={title} description={description} />
<HeaderTribe />
<main className="">{children}</main>

View File

@@ -71,7 +71,7 @@ const ConciergePage = () => {
))}
<div className="px-6">
<Button
className="w-full justify-center bg-gray-800 text-gray-300 hover:text-white"
className="w-full justify-center bg-slate-800 text-slate-300 hover:text-white"
href="https://cal.com/johannes/kick-off"
target="_blank">
Schedule free Kick-Off call

View File

@@ -247,7 +247,7 @@ export default function AddNoCodeActionModal({
<AlertTitle>How do Code Actions work?</AlertTitle>
<AlertDescription>
You can track code action anywhere in your app using{" "}
<span className="rounded bg-gray-100 px-2 py-1 text-xs">
<span className="rounded bg-slate-100 px-2 py-1 text-xs">
formbricks.track(&quot;{watch("name")}&quot;)
</span>{" "}
in your code. Read more in our{" "}

View File

@@ -8,7 +8,7 @@ export default function Loading() {
<div className="mb-6 text-right">
<Button
variant="darkCTA"
className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-gray-200">
className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-slate-200">
<CursorArrowRaysIcon className="mr-2 h-5 w-5 text-white" />
Loading
</Button>
@@ -26,19 +26,19 @@ export default function Loading() {
<div key={index} className="m-2 grid h-16 grid-cols-6 content-center rounded-lg hover:bg-slate-100">
<div className="col-span-4 flex items-center pl-6 text-sm">
<div className="flex items-center">
<div className="h-6 w-6 flex-shrink-0 animate-pulse rounded-full bg-gray-200 text-slate-500"></div>
<div className="h-6 w-6 flex-shrink-0 animate-pulse rounded-full bg-slate-200 text-slate-500"></div>
<div className="ml-4 text-left">
<div className="font-medium text-slate-900">
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-gray-200"></div>
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-slate-200"></div>
</div>
<div className="mt-1 text-xs text-slate-400">
<div className="h-2 w-24 animate-pulse rounded-full bg-gray-200"></div>
<div className="h-2 w-24 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
</div>
</div>
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="m-28 h-4 animate-pulse rounded-full bg-gray-200"></div>
<div className="m-28 h-4 animate-pulse rounded-full bg-slate-200"></div>
</div>
<div className="text-center"></div>
</div>

View File

@@ -1,5 +1,4 @@
import { TagIcon } from "@heroicons/react/24/solid";
import { QuestionMarkCircleIcon } from "@heroicons/react/24/solid";
import { QuestionMarkCircleIcon, TagIcon } from "@heroicons/react/24/solid";
import { Button } from "@formbricks/ui/Button";
@@ -34,20 +33,20 @@ export default function Loading() {
</div>
<div className="ml-4 text-left">
<div className="font-medium text-slate-900">
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-gray-200"></div>
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-slate-200"></div>
</div>
<div className="mt-1 text-xs text-slate-400">
<div className="h-2 w-24 animate-pulse rounded-full bg-gray-200"></div>
<div className="h-2 w-24 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
</div>
</div>
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="m-4 h-4 animate-pulse rounded-full bg-gray-200"></div>
<div className="m-4 h-4 animate-pulse rounded-full bg-slate-200"></div>
</div>
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="m-4 h-4 animate-pulse rounded-full bg-gray-200"></div>
<div className="m-4 h-4 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
))}

View File

@@ -3,16 +3,16 @@ export default function NavbarLoading() {
<div>
<div className="flex justify-between space-x-4 px-4 py-2">
<div className="flex">
<div className=" mx-2 h-8 w-8 animate-pulse rounded-md bg-gray-200"></div>
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-gray-200"></div>
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-gray-200"></div>
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-gray-200"></div>
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-gray-200"></div>
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-gray-200"></div>
<div className=" mx-2 h-8 w-8 animate-pulse rounded-md bg-slate-200"></div>
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
</div>
<div className="flex">
<div className=" mx-2 h-8 w-8 animate-pulse rounded-full bg-gray-200"></div>
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-gray-200"></div>
<div className=" mx-2 h-8 w-8 animate-pulse rounded-full bg-slate-200"></div>
<div className=" mx-2 h-8 w-20 animate-pulse rounded-md bg-slate-200"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,120 @@
import clsx from "clsx";
import { useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { createShortUrlAction } from "../actions";
type UrlShortenerFormDataProps = {
url: string;
};
type UrlValidationState = "default" | "valid" | "invalid";
export default function UrlShortenerForm({ webAppUrl }: { webAppUrl: string }) {
const [urlValidationState, setUrlValidationState] = useState<UrlValidationState>("default");
const [shortUrl, setShortUrl] = useState("");
const {
register,
handleSubmit,
watch,
formState: { isSubmitting },
} = useForm<UrlShortenerFormDataProps>({
mode: "onSubmit",
defaultValues: {
url: "",
},
});
const handleUrlValidation = () => {
const value = watch("url").trim();
if (!value) {
setUrlValidationState("default");
return;
}
const regexPattern = new RegExp("^" + webAppUrl);
const isValid = regexPattern.test(value);
if (!isValid) {
setUrlValidationState("invalid");
toast.error("Only formbricks survey links allowed.");
} else {
setUrlValidationState("valid");
}
};
const shortenUrl = async (data: UrlShortenerFormDataProps) => {
if (urlValidationState !== "valid") return;
const shortUrl = await createShortUrlAction(data.url.trim());
setShortUrl(shortUrl);
};
const copyShortUrlToClipboard = () => {
navigator.clipboard.writeText(shortUrl);
toast.success("URL copied to clipboard!");
};
return (
<div className="space-y-2 p-4">
<form onSubmit={handleSubmit(shortenUrl)}>
<div className="w-full space-y-2 rounded-lg">
<Label>Paste Survey Link</Label>
<div className="flex gap-3 ">
<Input
autoFocus
placeholder={`${webAppUrl}...`}
className={clsx(
"",
urlValidationState === "valid"
? "border-green-500 bg-green-50"
: urlValidationState === "invalid"
? "border-red-200 bg-red-50"
: "border-slate-200"
)}
{...register("url", {
required: true,
})}
onBlur={handleUrlValidation}
/>
<Button
disabled={watch("url") === ""}
variant="darkCTA"
size="sm"
type="submit"
loading={isSubmitting}>
Shorten
</Button>
</div>
</div>
</form>
{shortUrl && (
<div className="w-full space-y-2 rounded-lg">
<Label>Short Link</Label>
<div className="flex gap-3 ">
<span
className="h-10 w-full cursor-pointer rounded-md border border-slate-300 bg-slate-100 px-3 py-2 text-sm text-slate-700"
onClick={() => {
if (shortUrl) {
copyShortUrlToClipboard();
}
}}>
{shortUrl}
</span>
<Button
disabled={shortUrl === ""}
variant="secondary"
size="sm"
type="button"
onClick={() => copyShortUrlToClipboard()}>
<span>Copy</span>
</Button>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,86 +1,18 @@
import { LinkIcon } from "@heroicons/react/24/outline";
import clsx from "clsx";
import { useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { Modal } from "@formbricks/ui/Modal";
import { createShortUrlAction } from "../actions";
import UrlShortenerForm from "./UrlShortenerForm";
type UrlShortenerModalProps = {
open: boolean;
setOpen: (v: boolean) => void;
webAppUrl: string;
};
type UrlShortenerFormDataProps = {
url: string;
};
type UrlValidationState = "default" | "valid" | "invalid";
export default function UrlShortenerModal({ open, setOpen, webAppUrl }: UrlShortenerModalProps) {
const [urlValidationState, setUrlValidationState] = useState<UrlValidationState>("default");
const [shortUrl, setShortUrl] = useState("");
const {
register,
handleSubmit,
watch,
reset,
formState: { isSubmitting },
} = useForm<UrlShortenerFormDataProps>({
mode: "onSubmit",
defaultValues: {
url: "",
},
});
const handleUrlValidation = () => {
const value = watch("url").trim();
if (!value) {
setUrlValidationState("default");
return;
}
const regexPattern = new RegExp("^" + webAppUrl);
const isValid = regexPattern.test(value);
if (!isValid) {
setUrlValidationState("invalid");
toast.error("Only formbricks survey links allowed.");
} else {
setUrlValidationState("valid");
}
};
const shortenUrl = async (data: UrlShortenerFormDataProps) => {
if (urlValidationState !== "valid") return;
const shortUrl = await createShortUrlAction(data.url.trim());
setShortUrl(shortUrl);
};
const resetForm = () => {
setUrlValidationState("default");
reset(); // resets the long url field
setShortUrl("");
};
const copyShortUrlToClipboard = () => {
navigator.clipboard.writeText(shortUrl);
toast.success("URL copied to clipboard!");
};
return (
<Modal
open={open}
setOpen={(v) => {
setOpen(v);
resetForm();
}}
noPadding
closeOnOutsideClick={false}>
<Modal open={open} setOpen={setOpen} noPadding closeOnOutsideClick={false}>
<div className="flex h-full flex-col rounded-lg pb-4">
<div className="rounded-t-lg bg-slate-100">
<div className="flex items-center justify-between p-6">
@@ -97,63 +29,7 @@ export default function UrlShortenerModal({ open, setOpen, webAppUrl }: UrlShort
</div>
</div>
</div>
<form onSubmit={handleSubmit(shortenUrl)}>
<div className="grid w-full space-y-2 rounded-lg px-6 py-4">
<Label>Paste URL</Label>
<div className="grid grid-cols-6 gap-3">
<Input
autoFocus
placeholder={`${webAppUrl}...`}
className={clsx(
"col-span-5",
urlValidationState === "valid"
? "border-green-500 bg-green-50"
: urlValidationState === "invalid"
? "border-red-200 bg-red-50"
: urlValidationState === "default"
? "border-slate-200"
: "bg-white"
)}
{...register("url", {
required: true,
})}
onBlur={handleUrlValidation}
/>
<Button
disabled={watch("url") === "" ? true : false}
variant="darkCTA"
size="sm"
className="col-span-1 text-center"
type="submit"
loading={isSubmitting}>
Shorten
</Button>
</div>
</div>
</form>
<div className="grid w-full space-y-2 rounded-lg px-6 py-4">
<Label>Short URL</Label>
<div className="grid grid-cols-6 gap-3">
<span
className="col-span-5 cursor-pointer rounded-md border border-slate-300 bg-slate-100 px-3 py-2 text-sm text-slate-700"
onClick={() => {
if (shortUrl) {
copyShortUrlToClipboard();
}
}}>
{shortUrl}
</span>
<Button
disabled={shortUrl === "" ? true : false}
variant="secondary"
size="sm"
className="col-span-1 justify-center"
type="button"
onClick={() => copyShortUrlToClipboard()}>
<span>Copy</span>
</Button>
</div>
</div>
<UrlShortenerForm webAppUrl={webAppUrl} />
</div>
</Modal>
);

View File

@@ -1,4 +1,4 @@
import { ArrowDownIcon, CheckIcon } from "@heroicons/react/24/solid";
import { CheckIcon, ExclamationTriangleIcon } from "@heroicons/react/24/solid";
import clsx from "clsx";
import Link from "next/link";
@@ -18,16 +18,15 @@ export default async function WidgetStatusIndicator({ environmentId, type }: Wid
const stati = {
notImplemented: {
icon: ArrowDownIcon,
color: "slate",
title: "Connect Formbricks to your app.",
subtitle: "You have not yet connected Formbricks to your app. Follow setup guide.",
icon: ExclamationTriangleIcon,
title: "Connect Formbricks to your app or website.",
subtitle:
"Your app or website is not yet connected with Formbricks. To run in-app surveys follow the setup guide.",
},
running: {
icon: CheckIcon,
color: "green",
title: "Receiving data.",
subtitle: "You have successfully connected Formbricks to your app.",
subtitle: "Your app or website is connected with Formbricks.",
},
};
@@ -58,7 +57,7 @@ export default async function WidgetStatusIndicator({ environmentId, type }: Wid
<currentStatus.icon />
</div>
<p className="text-md font-bold text-slate-800 md:text-xl">{currentStatus.title}</p>
<p className="text-sm text-slate-700">{currentStatus.subtitle}</p>
<p className="w-2/3 text-balance text-sm text-slate-600">{currentStatus.subtitle}</p>
</div>
);
}
@@ -67,11 +66,11 @@ export default async function WidgetStatusIndicator({ environmentId, type }: Wid
<Link href={`/environments/${environment.id}/settings/setup`}>
<div className="group my-4 flex justify-center">
<div className=" flex rounded-full bg-slate-100 px-2 py-1">
<p className="mr-2 text-sm text-slate-400 group-hover:underline">{currentStatus.subtitle}</p>
<p className="mr-2 text-sm text-slate-500 group-hover:underline">{currentStatus.subtitle}</p>
<div
className={clsx(
"h-5 w-5 rounded-full p-0.5",
status === "notImplemented" && "bg-slate-100 text-slate-700",
"h-5 w-5 rounded-full",
status === "notImplemented" && "border border-white bg-white text-amber-600",
status === "running" && "bg-green-100 text-green-700"
)}>
<currentStatus.icon />

View File

@@ -39,7 +39,7 @@ export default function AirtableConnect({ environmentId, enabled, webAppUrl }: A
</div>
<p className="my-8">Sync responses directly with Airtable.</p>
{!enabled && (
<p className="mb-8 rounded border-gray-200 bg-gray-100 p-3 text-sm">
<p className="mb-8 rounded border-slate-200 bg-slate-100 p-3 text-sm">
Airtable Integration is not configured in your instance of Formbricks.
</p>
)}

View File

@@ -187,7 +187,7 @@ export default function AddIntegrationModal({
<span>{selectedItem ? selectedItem.name : `${label}`}</span>
</span>
<span className="flex h-full items-center border-l pl-3">
<ChevronDownIcon className="h-4 w-4 text-gray-500" />
<ChevronDownIcon className="h-4 w-4 text-slate-500" />
</span>
</button>
</DropdownMenu.Trigger>
@@ -201,7 +201,7 @@ export default function AddIntegrationModal({
items.map((item) => (
<DropdownMenu.Item
key={item.id}
className="flex cursor-pointer items-center p-3 hover:bg-gray-100 hover:outline-none data-[disabled]:cursor-default data-[disabled]:opacity-50"
className="flex cursor-pointer items-center p-3 hover:bg-slate-100 hover:outline-none data-[disabled]:cursor-default data-[disabled]:opacity-50"
onSelect={() => setSelectedItem(item)}>
{item.name}
</DropdownMenu.Item>

View File

@@ -40,7 +40,7 @@ export default function Connect({ enabled, environmentId, webAppUrl }: ConnectPr
</div>
<p className="my-8">Sync responses directly with Google Sheets.</p>
{!enabled && (
<p className="mb-8 rounded border-gray-200 bg-gray-100 p-3 text-sm">
<p className="mb-8 rounded border-slate-200 bg-slate-100 p-3 text-sm">
Google Sheets Integration is not configured in your instance of Formbricks.
<br />
Please follow the{" "}

View File

@@ -8,7 +8,7 @@ export default function Loading() {
<div className="mb-6 text-right">
<Button
variant="darkCTA"
className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-gray-200">
className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-slate-200">
Link new sheet
</Button>
</div>
@@ -28,27 +28,27 @@ export default function Loading() {
<div className="col-span-3 flex items-center pl-6 text-sm">
<div className="text-left">
<div className="font-medium text-slate-900">
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-gray-200"></div>
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
</div>
<div className="col-span-1 my-auto flex items-center justify-center text-center text-sm text-slate-500">
<div className="font-medium text-slate-500">
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-gray-200"></div>
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
<div className="col-span-4 my-auto flex items-center justify-center text-center text-sm text-slate-500">
<div className="font-medium text-slate-500">
<div className="mt-0 h-4 w-36 animate-pulse rounded-full bg-gray-200"></div>
<div className="mt-0 h-4 w-36 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
<div className="col-span-2 my-auto flex items-center justify-center text-center text-sm text-slate-500">
<div className="font-medium text-slate-500">
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-gray-200"></div>
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
<div className="col-span-2 my-auto flex items-center justify-center whitespace-nowrap text-center text-sm text-slate-500">
<div className="h-4 w-16 animate-pulse rounded-full bg-gray-200"></div>
<div className="h-4 w-16 animate-pulse rounded-full bg-slate-200"></div>
</div>
<div className="text-center"></div>
</div>

View File

@@ -392,7 +392,7 @@ export default function AddIntegrationModal({
idx === mapping.length - 1 ? "visible" : "invisible"
}`}
onClick={addRow}>
<PlusIcon className="h-5 w-5 font-bold text-gray-500" />
<PlusIcon className="h-5 w-5 font-bold text-slate-500" />
</button>
<button
type="button"
@@ -550,7 +550,7 @@ const DropdownSelector = ({
</span>
</span>
<span className="flex h-full items-center border-l pl-3">
<ChevronDownIcon className="h-4 w-4 text-gray-500" />
<ChevronDownIcon className="h-4 w-4 text-slate-500" />
</span>
</button>
</DropdownMenu.Trigger>
@@ -564,7 +564,7 @@ const DropdownSelector = ({
items.map((item) => (
<DropdownMenu.Item
key={item.id}
className="flex cursor-pointer items-center p-3 hover:bg-gray-100 hover:outline-none data-[disabled]:cursor-default data-[disabled]:opacity-50"
className="flex cursor-pointer items-center p-3 hover:bg-slate-100 hover:outline-none data-[disabled]:cursor-default data-[disabled]:opacity-50"
onSelect={() => setSelectedItem(item)}>
{item.name}
</DropdownMenu.Item>
@@ -580,7 +580,7 @@ const DropdownSelector = ({
onClick={() => {
refetch();
}}>
<ArrowPathIcon className="h-5 w-5 font-bold text-gray-500" />
<ArrowPathIcon className="h-5 w-5 font-bold text-slate-500" />
</button>
)}
</div>

View File

@@ -50,7 +50,7 @@ export default function Connect({ enabled, environmentId, webAppUrl }: ConnectPr
</div>
<p className="my-8">Sync responses directly with your Notion database.</p>
{!enabled && (
<p className="mb-8 rounded border-gray-200 bg-gray-100 p-3 text-sm">
<p className="mb-8 rounded border-slate-200 bg-slate-100 p-3 text-sm">
Notion Integration is not configured in your instance of Formbricks.
<br />
Please follow the{" "}

View File

@@ -8,7 +8,7 @@ export default function Loading() {
<div className="mb-6 text-right">
<Button
variant="darkCTA"
className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-gray-200">
className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-slate-200">
Link new database
</Button>
</div>
@@ -27,27 +27,27 @@ export default function Loading() {
<div className="col-span-3 flex items-center pl-6 text-sm">
<div className="text-left">
<div className="font-medium text-slate-900">
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-gray-200"></div>
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
</div>
<div className="col-span-1 my-auto flex items-center justify-center text-center text-sm text-slate-500">
<div className="font-medium text-slate-500">
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-gray-200"></div>
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
<div className="col-span-4 my-auto flex items-center justify-center text-center text-sm text-slate-500">
<div className="font-medium text-slate-500">
<div className="mt-0 h-4 w-36 animate-pulse rounded-full bg-gray-200"></div>
<div className="mt-0 h-4 w-36 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
<div className="col-span-2 my-auto flex items-center justify-center text-center text-sm text-slate-500">
<div className="font-medium text-slate-500">
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-gray-200"></div>
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
<div className="col-span-2 my-auto flex items-center justify-center whitespace-nowrap text-center text-sm text-slate-500">
<div className="h-4 w-16 animate-pulse rounded-full bg-gray-200"></div>
<div className="h-4 w-16 animate-pulse rounded-full bg-slate-200"></div>
</div>
<div className="text-center"></div>
</div>

View File

@@ -154,7 +154,7 @@ export default function WebhookSettingsTab({
}}
readOnly={webhook.source !== "user"}
className={clsx(
webhook.source === "user" ? null : "cursor-not-allowed bg-gray-100 text-gray-500",
webhook.source === "user" ? null : "cursor-not-allowed bg-slate-100 text-slate-500",
endpointAccessible === true
? "border-green-500 bg-green-50"
: endpointAccessible === false

View File

@@ -12,10 +12,10 @@ function Pagination({ environmentId, currentPage, totalItems, itemsPerPage }) {
<li>
<a
href={previousPageLink}
className={`ml-0 flex h-8 items-center justify-center rounded-l-lg border border-gray-300 bg-white px-3 text-gray-500 ${
className={`ml-0 flex h-8 items-center justify-center rounded-l-lg border border-slate-300 bg-white px-3 text-slate-500 ${
currentPage === 1
? "cursor-not-allowed opacity-50"
: "hover:bg-gray-100 hover:text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
: "hover:bg-slate-100 hover:text-slate-700 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-white"
}`}>
Previous
</a>
@@ -30,8 +30,8 @@ function Pagination({ environmentId, currentPage, totalItems, itemsPerPage }) {
<a
href={pageNum === currentPage ? "#" : pageLink}
className={`flex h-8 items-center justify-center px-3 ${
pageNum === currentPage ? "bg-blue-50 text-green-500" : "bg-white text-gray-500"
} border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white`}>
pageNum === currentPage ? "bg-blue-50 text-green-500" : "bg-white text-slate-500"
} border border-slate-300 hover:bg-slate-100 hover:text-slate-700 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-white`}>
{pageNum}
</a>
</li>
@@ -41,10 +41,10 @@ function Pagination({ environmentId, currentPage, totalItems, itemsPerPage }) {
<li>
<a
href={nextPageLink}
className={`ml-0 flex h-8 items-center justify-center rounded-r-lg border border-gray-300 bg-white px-3 text-gray-500 ${
className={`ml-0 flex h-8 items-center justify-center rounded-r-lg border border-slate-300 bg-white px-3 text-slate-500 ${
currentPage === totalPages
? "cursor-not-allowed opacity-50"
: "hover:bg-gray-100 hover:text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
: "hover:bg-slate-100 hover:text-slate-700 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-white"
}`}>
Next
</a>

View File

@@ -8,7 +8,7 @@ function LoadingCard({ title, description }) {
<div className="w-full">
<div className="rounded-lg px-6 py-5 hover:bg-slate-100">
<div className="flex justify-end">
<div className="mt-4 h-6 w-28 animate-pulse rounded-full bg-gray-200"></div>
<div className="mt-4 h-6 w-28 animate-pulse rounded-full bg-slate-200"></div>
</div>
<div className="mt-6 rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-9 content-center rounded-t-lg bg-slate-100 px-6 text-left text-sm font-semibold text-slate-900">
@@ -18,8 +18,8 @@ function LoadingCard({ title, description }) {
<div className="col-span-2">Created at</div>
</div>
<div className="px-6">
<div className="my-4 h-6 w-full animate-pulse rounded-full bg-gray-200"></div>
<div className="my-4 h-6 w-full animate-pulse rounded-full bg-gray-200"></div>
<div className="my-4 h-6 w-full animate-pulse rounded-full bg-slate-200"></div>
<div className="my-4 h-6 w-full animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
</div>

View File

@@ -182,7 +182,7 @@ export default function PricingTableComponent({
</div>
) : (
<>
{/* <div className="relative isolate mt-8 overflow-hidden rounded-lg bg-gray-900 px-3 pt-8 shadow-2xl sm:px-8 md:pt-12 lg:flex lg:gap-x-10 lg:px-12 lg:pt-0">
{/* <div className="relative isolate mt-8 overflow-hidden rounded-lg bg-slate-900 px-3 pt-8 shadow-2xl sm:px-8 md:pt-12 lg:flex lg:gap-x-10 lg:px-12 lg:pt-0">
<svg
viewBox="0 0 1024 1024"
className="absolute left-1/2 top-1/2 -z-10 h-[64rem] w-[64rem] -translate-y-1/2 [mask-image:radial-gradient(closest-side,white,transparent)] sm:left-full sm:-ml-80 lg:left-1/2 lg:ml-0 lg:-translate-x-1/2 lg:translate-y-0"
@@ -206,11 +206,11 @@ export default function PricingTableComponent({
Launch Special:
<br /> Go Unlimited! Forever!
</h2>
<p className="text-md mt-6 leading-8 text-gray-300">
<p className="text-md mt-6 leading-8 text-slate-300">
Get access to all pro features and unlimited responses + identified users for a flat fee of
only $99/month.
<br /> <br />
<span className="text-gray-400">
<span className="text-slate-400">
This deal ends on 31st of October 2023 at 11:59 PM PST.
</span>
</p>
@@ -218,7 +218,7 @@ export default function PricingTableComponent({
<div className="flex flex-1 flex-col items-center justify-center lg:pr-8">
<Button
variant="minimal"
className="w-full justify-center bg-white py-2 text-gray-800 shadow-sm"
className="w-full justify-center bg-white py-2 text-slate-800 shadow-sm"
loading={upgradingPlan}
onClick={() =>
upgradePlan([
@@ -232,7 +232,7 @@ export default function PricingTableComponent({
</div>
</div> */}
<div className="relative isolate mt-8 overflow-hidden rounded-lg bg-gray-900 px-3 pt-8 shadow-2xl sm:px-8 md:pt-12 lg:flex lg:gap-x-10 lg:px-12 lg:pt-0">
<div className="relative isolate mt-8 overflow-hidden rounded-lg bg-slate-900 px-3 pt-8 shadow-2xl sm:px-8 md:pt-12 lg:flex lg:gap-x-10 lg:px-12 lg:pt-0">
<svg
viewBox="0 0 1024 1024"
className="absolute left-1/2 top-1/2 -z-10 h-[64rem] w-[64rem] -translate-y-1/2 [mask-image:radial-gradient(closest-side,white,transparent)] sm:left-full sm:-ml-80 lg:left-1/2 lg:ml-0 lg:-translate-x-1/2 lg:translate-y-0"
@@ -253,7 +253,7 @@ export default function PricingTableComponent({
</svg>
<div className="mx-auto max-w-md text-center lg:mx-0 lg:flex-auto lg:py-16 lg:text-left">
<h2 className="text-2xl font-bold text-white sm:text-3xl">Get the most out of Formbricks</h2>
<p className="text-md mt-6 leading-8 text-gray-300">
<p className="text-md mt-6 leading-8 text-slate-300">
Get access to all features by upgrading to a paid plan.
<br />
With our metered billing you will not be charged until you exceed the free tier limits.{" "}

View File

@@ -3,9 +3,9 @@ export default function Loading() {
<div>
<h2 className="my-4 text-2xl font-medium leading-6 text-slate-800">Billing & Plan</h2>
<div className="grid grid-cols-2 gap-4 rounded-lg p-8">
<div className=" h-[75vh] animate-pulse rounded-md bg-gray-200 "></div>
<div className=" h-96 animate-pulse rounded-md bg-gray-200"></div>
<div className="col-span-2 h-96 bg-gray-200 p-8"></div>
<div className=" h-[75vh] animate-pulse rounded-md bg-slate-200 "></div>
<div className=" h-96 animate-pulse rounded-md bg-slate-200"></div>
<div className="col-span-2 h-96 bg-slate-200 p-8"></div>
</div>
</div>
);

View File

@@ -29,7 +29,7 @@ export default function Loading() {
</div>
<Button
variant="darkCTA"
className="pointer-events-none mt-4 animate-pulse cursor-not-allowed select-none bg-gray-200">
className="pointer-events-none mt-4 animate-pulse cursor-not-allowed select-none bg-slate-200">
Loading
</Button>
</div>
@@ -64,7 +64,7 @@ export default function Loading() {
</div>
<Button
variant="darkCTA"
className="pointer-events-none mt-4 animate-pulse cursor-not-allowed select-none bg-gray-200">
className="pointer-events-none mt-4 animate-pulse cursor-not-allowed select-none bg-slate-200">
Loading
</Button>
</div>

View File

@@ -37,9 +37,9 @@ export default function ShareInviteModal({ inviteToken, open, setOpen }: ShareIn
<CheckIcon className="h-6 w-6 text-teal-600" aria-hidden="true" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-lg font-semibold leading-6 text-gray-900">Your team invite link is ready!</h3>
<h3 className="text-lg font-semibold leading-6 text-slate-900">Your team invite link is ready!</h3>
<div className="mt-4">
<p className="text-sm text-gray-500">Share this link to let your team member join your team:</p>
<p className="text-sm text-slate-500">Share this link to let your team member join your team:</p>
<p
ref={linkTextRef}
className="relative mt-3 w-full truncate rounded-lg border border-slate-300 bg-slate-50 p-3 text-center text-slate-800"

View File

@@ -0,0 +1,23 @@
import { FaSlack } from "react-icons/fa";
interface IntegrationsTipProps {
environmentId: string;
}
export default async function IntegrationsTip({ environmentId }: IntegrationsTipProps) {
return (
<div>
<div className="flex items-center space-y-3 rounded-lg border border-blue-100 bg-blue-50 p-4 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base">
<FaSlack className="mr-3 h-4 w-4 text-blue-400" />
<p className="text-sm">
Need Slack or Discord notifications?
<a
href={`/environments/${environmentId}/integrations`}
className="ml-1 cursor-pointer text-sm underline">
Use the integration.
</a>
</p>
</div>
</div>
);
}

View File

@@ -8,6 +8,7 @@ import { TUserNotificationSettings } from "@formbricks/types/user";
import SettingsTitle from "../components/SettingsTitle";
import EditAlerts from "./components/EditAlerts";
import EditWeeklySummary from "./components/EditWeeklySummary";
import IntegrationsTip from "./components/IntegrationsTip";
import type { Membership, User } from "./types";
async function getUser(userId: string | undefined): Promise<User> {
@@ -110,6 +111,7 @@ export default async function ProfileSettingsPage({ params }) {
description="Set up an alert to get an email on new responses.">
<EditAlerts memberships={memberships} user={user} environmentId={params.environmentId} />
</SettingsCard>
<IntegrationsTip environmentId={params.environmentId} />
<SettingsCard
beta
title="Weekly summary (Products)"

View File

@@ -9,7 +9,7 @@ function LoadingCard({ title, description, skeletonLines }) {
<div className="rounded-lg px-6 py-5 hover:bg-slate-100">
{skeletonLines.map((line, index) => (
<div key={index} className="mt-4">
<div className={`animate-pulse rounded-full bg-gray-200 ${line.classes}`}></div>
<div className={`animate-pulse rounded-full bg-slate-200 ${line.classes}`}></div>
</div>
))}
</div>

View File

@@ -9,7 +9,7 @@ function LoadingCard({ title, description, skeletonLines }) {
<div className="rounded-lg px-6 py-5 hover:bg-slate-100">
{skeletonLines.map((line, index) => (
<div key={index} className="mt-4">
<div className={`animate-pulse rounded-full bg-gray-200 ${line.classes}`}></div>
<div className={`animate-pulse rounded-full bg-slate-200 ${line.classes}`}></div>
</div>
))}
</div>

View File

@@ -9,7 +9,7 @@ function LoadingCard({ title, description, skeletonLines }) {
<div className="rounded-lg px-6 py-5 hover:bg-slate-100">
{skeletonLines.map((line, index) => (
<div key={index} className="mt-4">
<div className={`animate-pulse bg-gray-200 ${line.classes}`}></div>
<div className={`animate-pulse bg-slate-200 ${line.classes}`}></div>
</div>
))}
</div>

View File

@@ -2,7 +2,7 @@ import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId
import { questionTypes } from "@/app/lib/questions";
import { InboxStackIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
import React, { useState } from "react";
import { useState } from "react";
import { getPersonIdentifier } from "@formbricks/lib/person/util";
import { timeSince } from "@formbricks/lib/time";
@@ -88,7 +88,7 @@ export default function DateQuestionSummary({
<div className="my-1 flex justify-center">
<button
onClick={() => setDisplayCount((prevCount) => prevCount + responsesPerPage)}
className="my-2 flex h-8 items-center justify-center rounded-lg border border-gray-300 bg-white px-3 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-700">
className="my-2 flex h-8 items-center justify-center rounded-lg border border-slate-300 bg-white px-3 text-sm text-slate-500 hover:bg-slate-100 hover:text-slate-700">
Show more
</button>
</div>

View File

@@ -1,139 +0,0 @@
"use client";
import { generateSingleUseIdAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions";
import { ArrowPathIcon } from "@heroicons/react/24/outline";
import { DocumentDuplicateIcon, EyeIcon } from "@heroicons/react/24/solid";
import { useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
import { truncateMiddle } from "@formbricks/lib/strings";
import { TSurvey } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
interface LinkSingleUseSurveyModalProps {
survey: TSurvey;
surveyBaseUrl: string;
}
export default function LinkSingleUseSurveyModal({ survey, surveyBaseUrl }: LinkSingleUseSurveyModalProps) {
const [singleUseIds, setSingleUseIds] = useState<string[] | null>(null);
useEffect(() => {
fetchSingleUseIds();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [survey.singleUse?.isEncrypted]);
const fetchSingleUseIds = async () => {
const ids = await generateSingleUseIds(survey.singleUse?.isEncrypted ?? false);
setSingleUseIds(ids);
};
const generateSingleUseIds = async (isEncrypted: boolean) => {
const promises = Array(7)
.fill(null)
.map(() => generateSingleUseIdAction(survey.id, isEncrypted));
return await Promise.all(promises);
};
const defaultSurveyUrl = `${surveyBaseUrl}/s/${survey.id}`;
const [selectedSingleUseIds, setSelectedSingleIds] = useState<number[]>([]);
const linkTextRef = useRef<HTMLDivElement>(null);
const handleLinkOnClick = (index: number) => {
if (!singleUseIds) return;
setSelectedSingleIds([...selectedSingleUseIds, index]);
const surveyUrl = `${defaultSurveyUrl}?suId=${singleUseIds[index]}`;
navigator.clipboard.writeText(surveyUrl);
toast.success("URL copied to clipboard!");
};
return (
<>
{singleUseIds && (
<div className="w-full">
<div className="flex justify-end">
<Button
variant="darkCTA"
title="Preview survey"
aria-label="Preview survey"
className="flex w-fit justify-center"
href={`${defaultSurveyUrl}?suId=${singleUseIds[0]}&preview=true`}
target="_blank"
EndIcon={EyeIcon}>
Preview Survey
</Button>
</div>
<div className="my-4 border-t border-slate-300 pb-2 pt-4">
<p className="mb-3 font-semibold text-slate-800">Single Use Links</p>
<div ref={linkTextRef} className="min flex flex-col space-y-4">
{singleUseIds &&
singleUseIds.map((singleUseId, index) => {
const isSelected = selectedSingleUseIds.includes(index);
return (
<div className="flex h-fit justify-center p-0">
<div
key={singleUseId}
className={cn(
"row relative flex w-full cursor-pointer items-center justify-between overflow-hidden rounded-lg border border-slate-300 bg-white px-6 py-2 text-left text-slate-800 transition-all duration-200 ease-in-out hover:border-slate-500",
isSelected && "border-slate-200 text-slate-400 hover:border-slate-200"
)}
onClick={() => {
if (!isSelected) {
handleLinkOnClick(index);
}
}}>
<span>{truncateMiddle(`${defaultSurveyUrl}?suId=${singleUseId}`, 48)}</span>
</div>
<div className="ml-2 min-h-full">
<Button
variant="secondary"
className="m-0 my-0 h-full w-full overflow-hidden whitespace-pre text-center"
onClick={() => handleLinkOnClick(index)}>
{isSelected ? "Copied" : " Copy "}
</Button>
</div>
</div>
);
})}
</div>
</div>
<div className="mt-4 flex flex-col justify-center gap-2 sm:flex-row sm:justify-end">
<Button
variant="secondary"
title="Generate new single-use survey link"
aria-label="Generate new single-use survey link"
className="flex justify-center"
onClick={() => {
fetchSingleUseIds();
setSelectedSingleIds([]);
toast.success("New survey links generated!");
}}
EndIcon={ArrowPathIcon}>
Regenerate
</Button>
<Button
variant="secondary"
onClick={() => {
setSelectedSingleIds(Array.from(singleUseIds.keys()));
const allSurveyUrls = singleUseIds
.map((singleUseId) => `${defaultSurveyUrl}?suId=${singleUseId}`)
.join("\n");
navigator.clipboard.writeText(allSurveyUrls);
toast.success("All URLs copied to clipboard!");
}}
title="Copy all survey links to clipboard"
aria-label="Copy all survey links to clipboard"
className="flex justify-center"
EndIcon={DocumentDuplicateIcon}>
Copy all URLs
</Button>
</div>
</div>
)}
</>
);
}

View File

@@ -2,15 +2,14 @@ import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId
import { questionTypes } from "@/app/lib/questions";
import { InboxStackIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
import { useMemo } from "react";
import { useState } from "react";
import { useMemo, useState } from "react";
import { getPersonIdentifier } from "@formbricks/lib/person/util";
import { TSurveyQuestionType } from "@formbricks/types/surveys";
import type { TSurveyQuestionSummary } from "@formbricks/types/surveys";
import {
TSurveyMultipleChoiceMultiQuestion,
TSurveyMultipleChoiceSingleQuestion,
TSurveyQuestionType,
} from "@formbricks/types/surveys";
import { PersonAvatar } from "@formbricks/ui/Avatars";
import { ProgressBar } from "@formbricks/ui/ProgressBar";
@@ -212,7 +211,7 @@ export default function MultipleChoiceSummary({
<div className="flex w-full items-center justify-center">
<button
onClick={() => setOtherDisplayCount(otherDisplayCount + responsesPerPage)}
className="my-2 flex h-8 items-center justify-center rounded-lg border border-gray-300 bg-white px-3 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-700">
className="my-2 flex h-8 items-center justify-center rounded-lg border border-slate-300 bg-white px-3 text-sm text-slate-500 hover:bg-slate-100 hover:text-slate-700">
Show more
</button>
</div>

View File

@@ -2,7 +2,7 @@ import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId
import { questionTypes } from "@/app/lib/questions";
import { InboxStackIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
import React, { useState } from "react";
import { useState } from "react";
import { getPersonIdentifier } from "@formbricks/lib/person/util";
import { timeSince } from "@formbricks/lib/time";
@@ -87,7 +87,7 @@ export default function OpenTextSummary({
<div className="flex justify-center py-1">
<button
onClick={() => setDisplayCount((prevCount) => prevCount + responsesPerPage)}
className="my-2 flex h-8 items-center justify-center rounded-lg border border-gray-300 bg-white px-3 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-700">
className="my-2 flex h-8 items-center justify-center rounded-lg border border-slate-300 bg-white px-3 text-sm text-slate-500 hover:bg-slate-100 hover:text-slate-700">
Show more
</button>
</div>

View File

@@ -1,8 +1,12 @@
"use client";
import LinkSingleUseSurveyModal from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/LinkSingleUseSurveyModal";
import { CodeBracketIcon, EnvelopeIcon, LinkIcon } from "@heroicons/react/24/outline";
import { useMemo, useState } from "react";
import { generateSingleUseIdAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions";
import { ArrowLeftIcon, CodeBracketIcon, EnvelopeIcon, LinkIcon } from "@heroicons/react/24/outline";
import { DocumentDuplicateIcon } from "@heroicons/react/24/solid";
import { BellRing, BlocksIcon, Code2Icon, RefreshCcw } from "lucide-react";
import Link from "next/link";
import { useCallback, useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
import { TProduct } from "@formbricks/types/product";
@@ -23,90 +27,195 @@ interface ShareEmbedSurveyProps {
product: TProduct;
user: TUser;
}
export default function ShareEmbedSurvey({
survey,
open,
setOpen,
webAppUrl,
product,
user,
}: ShareEmbedSurveyProps) {
const surveyUrl = useMemo(() => webAppUrl + "/s/" + survey.id, [survey, webAppUrl]);
const isSingleUseLinkSurvey = survey.singleUse?.enabled;
export default function ShareEmbedSurvey({ survey, open, setOpen, webAppUrl, user }: ShareEmbedSurveyProps) {
const environmentId = survey.environmentId;
const isSingleUseLinkSurvey = survey.singleUse?.enabled ?? false;
const { email } = user;
const { brandColor } = product;
const surveyBrandColor = survey.productOverwrites?.brandColor || brandColor;
const tabs = [
{ id: "link", label: `${isSingleUseLinkSurvey ? "Single Use Links" : "Share the Link"}`, icon: LinkIcon },
{ id: "email", label: "Embed in an Email", icon: EnvelopeIcon },
{ id: "webpage", label: "Embed in a Web Page", icon: CodeBracketIcon },
{ id: "link", label: `${isSingleUseLinkSurvey ? "Single Use Links" : "Share the Link"}`, icon: LinkIcon },
];
const [activeId, setActiveId] = useState(tabs[0].id);
const [showInitialPage, setShowInitialPage] = useState(true);
const linkTextRef = useRef(null);
const [surveyUrl, setSurveyUrl] = useState("");
const getUrl = useCallback(async () => {
let url = webAppUrl + "/s/" + survey.id;
if (survey.singleUse?.enabled) {
const singleUseId = await generateSingleUseIdAction(survey.id, survey.singleUse.isEncrypted);
url += "?suId=" + singleUseId;
}
setSurveyUrl(url);
}, [survey, webAppUrl]);
useEffect(() => {
getUrl();
}, [survey, webAppUrl, getUrl]);
const handleTextSelection = () => {
if (linkTextRef.current) {
const range = document.createRange();
range.selectNodeContents(linkTextRef.current);
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
}
}
};
const handleOpenChange = (open: boolean) => {
setActiveId(tabs[0].id);
setOpen(open);
setShowInitialPage(open); // Reset to initial page when modal opens
};
const handleInitialPageButton = () => {
setShowInitialPage(!showInitialPage);
};
const generateNewSingleUseLink = () => {
getUrl();
toast.success("New single use link generated");
};
return (
<Dialog
open={open}
onOpenChange={(open) => {
setActiveId(tabs[0].id);
setOpen(open);
}}>
<DialogContent className="bottom-0 flex h-[95%] w-full flex-col gap-0 overflow-hidden rounded-2xl bg-white p-0 sm:max-w-none lg:bottom-auto lg:h-auto lg:w-[960px]">
<div className="border-b border-gray-200 px-4 py-3 lg:px-6 lg:py-4 ">Share or embed your survey</div>
<div className="flex grow overflow-x-hidden overflow-y-scroll">
<div className="hidden basis-[326px] border-r border-gray-200 px-6 py-8 lg:block lg:shrink-0">
<div className="flex w-max flex-col gap-3">
{tabs.map((tab) => (
<Button
StartIcon={tab.icon}
startIconClassName={cn("h-4 w-4")}
variant="minimal"
key={tab.id}
onClick={() => setActiveId(tab.id)}
className={cn(
"rounded-[4px] px-4 py-[6px] text-slate-600",
// "focus:ring-0 focus:ring-offset-0", // enable these classes to remove the focus rings on buttons
tab.id === activeId
? " border border-gray-200 bg-slate-100 font-semibold text-slate-900"
: "border-transparent text-slate-500 hover:text-slate-700"
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className=" w-full max-w-xl bg-white p-0 md:max-w-3xl lg:h-[700px] lg:max-w-5xl">
{showInitialPage ? (
<div className="h-full max-w-full overflow-hidden">
<div className="flex h-[200px] w-full flex-col items-center justify-center space-y-6 p-8 text-center lg:h-2/5">
<p className="pt-2 text-xl font-semibold text-slate-800">Your survey is public 🎉</p>
<div className="flex max-w-full flex-col items-center justify-center space-x-2 lg:flex-row">
<div
ref={linkTextRef}
className="mt-2 max-w-[70%] overflow-hidden rounded-lg border border-slate-300 bg-slate-50 px-3 py-2 text-slate-800"
style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}
onClick={() => handleTextSelection()}>
{surveyUrl}
</div>
<div className="mt-2 flex items-center justify-center space-x-2">
<Button
variant="darkCTA"
className="inline"
title="Copy survey link to clipboard"
aria-label="Copy survey link to clipboard"
onClick={() => {
navigator.clipboard.writeText(surveyUrl);
toast.success("URL copied to clipboard!");
}}
EndIcon={DocumentDuplicateIcon}>
Copy Link
</Button>
{survey.singleUse?.enabled && (
<Button
variant="darkCTA"
className="inline"
title="Regenerate single use survey link"
aria-label="Regenerate single use survey link"
onClick={() => generateNewSingleUseLink()}>
<RefreshCcw className="h-5 w-5" />
</Button>
)}
aria-current={tab.id === activeId ? "page" : undefined}>
{tab.label}
</Button>
))}
</div>
</div>
</div>
<div className="flex h-[300px] flex-col items-center justify-center gap-8 rounded-b-lg bg-slate-50 px-8 lg:h-3/5">
<p className="-mt-8 text-sm text-slate-500">What&apos;s next?</p>
<div className="grid grid-cols-3 gap-2">
<button
onClick={handleInitialPageButton}
className="flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-sm text-slate-500 hover:border-slate-200 md:p-8">
<Code2Icon className="h-6 w-6 text-slate-700" />
Embed survey
</button>
<Link
href={`/environments/${environmentId}//settings/notifications`}
className="flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-sm text-slate-500 hover:border-slate-200 md:p-8">
<BellRing className="h-6 w-6 text-slate-700" />
Configure alerts
</Link>
<Link
href={`/environments/${environmentId}/integrations`}
className="flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-sm text-slate-500 hover:border-slate-200 md:p-8">
<BlocksIcon className="h-6 w-6 text-slate-700" />
Setup integrations
</Link>
</div>
</div>
</div>
<div className="flex w-full grow flex-col gap-6 bg-gray-50 px-4 py-6 lg:p-6">
<div className="flex h-full overflow-y-scroll lg:h-[590px] lg:overflow-y-visible">
{isSingleUseLinkSurvey ? (
<LinkSingleUseSurveyModal survey={survey} surveyBaseUrl={webAppUrl} />
) : activeId === "link" ? (
<LinkTab surveyUrl={surveyUrl} survey={survey} brandColor={surveyBrandColor} />
) : activeId === "email" ? (
<EmailTab surveyId={survey.id} email={email} />
) : activeId === "webpage" ? (
<WebpageTab surveyUrl={surveyUrl} />
) : null}
) : (
<div className="h-full overflow-hidden">
<div className="border-b border-slate-200 py-2">
<Button
variant="minimal"
className="focus:ring-0"
onClick={handleInitialPageButton}
StartIcon={ArrowLeftIcon}>
Back
</Button>
</div>
<div className="mx-auto flex max-w-max rounded-md bg-slate-100 p-1 lg:hidden">
{tabs.slice(0, 2).map((tab) => (
<Button
variant="minimal"
key={tab.id}
onClick={() => setActiveId(tab.id)}
className={cn(
"rounded-sm px-3 py-[6px]",
tab.id === activeId
? "bg-white text-slate-900"
: "border-transparent text-slate-700 hover:text-slate-900"
)}>
{tab.label}
</Button>
))}
<div className="grid h-full grid-cols-4">
<div className="col-span-1 hidden flex-col gap-3 border-r border-slate-200 p-4 lg:flex">
{tabs.map((tab) => (
<Button
StartIcon={tab.icon}
startIconClassName="h-4 w-4"
variant="minimal"
key={tab.id}
onClick={() => setActiveId(tab.id)}
className={cn(
"rounded-md border px-4 py-2 text-slate-600",
// "focus:ring-0 focus:ring-offset-0", // enable these classes to remove the focus rings on buttons
tab.id === activeId
? "border-slate-200 bg-slate-100 font-semibold text-slate-900"
: "border-transparent text-slate-500 hover:text-slate-700"
)}
aria-current={tab.id === activeId ? "page" : undefined}>
{tab.label}
</Button>
))}
</div>
<div className="col-span-4 h-full overflow-y-auto bg-slate-50 px-4 py-6 lg:col-span-3 lg:p-6">
<div className="">
{activeId === "email" ? (
<EmailTab surveyId={survey.id} email={email} />
) : activeId === "webpage" ? (
<WebpageTab surveyUrl={surveyUrl} />
) : activeId === "link" ? (
<LinkTab
surveyUrl={surveyUrl}
webAppUrl={webAppUrl}
generateNewSingleUseLink={generateNewSingleUseLink}
isSingleUseLinkSurvey={isSingleUseLinkSurvey}
/>
) : null}
</div>
<div className="mt-2 rounded-md p-3 text-center lg:hidden">
{tabs.slice(0, 2).map((tab) => (
<Button
variant="minimal"
key={tab.id}
onClick={() => setActiveId(tab.id)}
className={cn(
"rounded-md px-4 py-2",
tab.id === activeId
? "bg-white text-slate-900 shadow-sm"
: "border-transparent text-slate-700 hover:text-slate-900"
)}>
{tab.label}
</Button>
))}
</div>
</div>
</div>
</div>
</div>
)}
</DialogContent>
</Dialog>
);

View File

@@ -30,33 +30,27 @@ export default function ShareSurveyResults({
setOpen(open);
}}>
{showPublishModal && surveyUrl ? (
<DialogContent className="bottom-0 flex h-[95%] w-full cursor-pointer flex-col gap-0 overflow-hidden rounded-2xl bg-white p-0 sm:max-w-none lg:bottom-auto lg:h-auto lg:w-[40%]">
<div className="no-scrollbar mt-4 flex grow flex-col items-center justify-center overflow-x-hidden overflow-y-scroll">
<CheckCircleIcon className="mt-4 h-20 w-20 text-slate-300" />
<div className="mt-6 px-4 py-3 text-lg font-medium text-slate-600 lg:px-6 lg:py-3">
Your survey results are public on the web.
</div>
<div className="text-md px-4 py-3 text-slate-500 lg:px-6 lg:py-0">
Your survey results are shared with anyone who has the link.
</div>
<div className="text-md mb-6 px-4 py-3 text-slate-500 lg:px-6 lg:py-0">
The results will not be indexed by search engines.
<DialogContent className="flex flex-col rounded-2xl bg-white px-12 py-6">
<div className="flex flex-col items-center gap-y-6 text-center">
<CheckCircleIcon className="h-20 w-20 text-slate-300" />
<div>
<p className="text-lg font-medium text-slate-600">Your survey results are public!</p>
<p className="text-balanced mt-2 text-sm text-slate-500">
Your survey results are shared with anyone who has the link. The results will not be indexed
by search engines.
</p>
</div>
<div className="flex gap-2">
<div className="relative grow overflow-auto rounded-lg border border-slate-300 bg-white px-3 py-2 text-slate-800">
<span
style={{
wordBreak: "break-all",
}}>
{surveyUrl}
</span>
<div className="whitespace-nowrap rounded-lg border border-slate-300 bg-white px-3 py-2 text-slate-800">
<span>{surveyUrl}</span>
</div>
<Button
variant="secondary"
size="sm"
title="Copy survey link to clipboard"
aria-label="Copy survey link to clipboard"
className="hover:cursor-pointer"
onClick={() => {
navigator.clipboard.writeText(surveyUrl);
toast.success("URL copied to clipboard!");
@@ -65,7 +59,7 @@ export default function ShareSurveyResults({
</Button>
</div>
<div className="my-6 flex gap-2">
<div className="flex gap-2">
<Button
type="submit"
variant="secondary"
@@ -81,22 +75,21 @@ export default function ShareSurveyResults({
</div>
</DialogContent>
) : (
<DialogContent className="bottom-0 flex h-[95%] w-full flex-col gap-0 overflow-hidden rounded-2xl bg-white p-0 sm:max-w-none lg:bottom-auto lg:h-auto lg:w-[40%]">
<div className="no-scrollbar mt-4 flex grow flex-col items-center justify-center overflow-x-hidden overflow-y-scroll">
<GlobeEuropeAfricaIcon className="mt-4 h-20 w-20 text-slate-300" />
<div className=" mt-6 px-4 py-3 text-lg font-medium text-slate-600 lg:px-6 lg:py-3">
Publish Results to web
</div>
<div className="text-md px-4 py-3 text-slate-500 lg:px-6 lg:py-0">
Your survey results are shared with anyone who has the link.
</div>
<div className=" text-md px-4 py-3 text-slate-500 lg:px-6 lg:py-0">
The results will not be indexed by search engines.
<DialogContent className="flex flex-col rounded-2xl bg-white p-8">
<div className="flex flex-col items-center gap-y-6 text-center">
<GlobeEuropeAfricaIcon className="h-20 w-20 text-slate-300" />
<div>
<p className="text-lg font-medium text-slate-600">Publish Results to web</p>
<p className="text-balanced mt-2 text-sm text-slate-500">
Your survey results are shared with anyone who has the link. The results will not be indexed
by search engines.
</p>
</div>
<Button
type="submit"
variant="darkCTA"
className="my-8 h-full text-center"
className="h-full text-center"
onClick={() => handlePublish()}>
Publish to web
</Button>

View File

@@ -5,10 +5,9 @@ import {
generateResultShareUrlAction,
getResultShareUrlAction,
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions";
import { LinkIcon } from "@heroicons/react/24/outline";
import { ArrowUpRightIcon, GlobeAltIcon, LinkIcon } from "@heroicons/react/24/outline";
import { DownloadIcon } from "lucide-react";
import { useState } from "react";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { TProduct } from "@formbricks/types/product";
@@ -80,7 +79,7 @@ export default function SurveyShareButton({ survey, webAppUrl, product, user }:
<DropdownMenu>
<DropdownMenuTrigger
asChild
className="focus:bg-muted cursor-pointer border border-slate-300 outline-none">
className="focus:bg-muted cursor-pointer border border-slate-300 outline-none hover:border-slate-400">
<div className="min-w-auto h-auto rounded-md border bg-white p-3 sm:flex sm:min-w-[7rem] sm:px-6 sm:py-3">
<div className="hidden w-full items-center justify-between sm:flex">
<span className="text-sm text-slate-700"> Share</span>
@@ -96,7 +95,9 @@ export default function SurveyShareButton({ survey, webAppUrl, product, user }:
onClick={() => {
setShowLinkModal(true);
}}>
<p className="text-slate-700">Share Survey</p>
<p className="text-slate-700">
Share Survey <ArrowUpRightIcon className="ml-2 inline h-4 w-4" />
</p>
</DropdownMenuItem>
)}
<DropdownMenuItem
@@ -104,7 +105,9 @@ export default function SurveyShareButton({ survey, webAppUrl, product, user }:
onClick={() => {
setShowResultsLinkModal(true);
}}>
<p className="text-slate-700">Publish Results</p>
<p className="text-slate-700">
Publish Results <GlobeAltIcon className="ml-2 inline h-4 w-4" />
</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -57,11 +57,11 @@ export default function EmailTab({ surveyId, email }: EmailTabProps) {
};
return (
<div className="flex h-full grow flex-col gap-5">
<div className="flex flex-col gap-5 ">
<div className="flex items-center justify-end gap-4">
{showEmbed ? (
<Button
variant="darkCTA"
variant="secondary"
title="Embed survey in your website"
aria-label="Embed survey in your website"
onClick={() => {
@@ -97,37 +97,35 @@ export default function EmailTab({ surveyId, email }: EmailTabProps) {
{showEmbed ? "Hide Embed Code" : "View Embed Code"}
</Button>
</div>
<div className="grow overflow-y-scroll rounded-xl border border-gray-200 bg-white px-4 py-[18px]">
{showEmbed ? (
{showEmbed ? (
<div className="prose prose-slate -mt-4 max-w-full">
<CodeBlock
customCodeClass="!whitespace-normal sm:!whitespace-pre-wrap !break-all sm:!break-normal"
customCodeClass="text-sm h-48 overflow-y-scroll"
language="html"
showCopyToClipboard={false}>
{emailHtml}
</CodeBlock>
) : (
<div>
<div className="mb-6 flex gap-2">
<div className="h-3 w-3 rounded-full bg-red-500"></div>
<div className="h-3 w-3 rounded-full bg-amber-500"></div>
<div className="h-3 w-3 rounded-full bg-emerald-500"></div>
</div>
<div className="">
<div className="mb-2 border-b border-slate-200 pb-2 text-sm">
To : {email || "user@mail.com"}
</div>
<div className="border-b border-slate-200 pb-2 text-sm">Subject : {subject}</div>
<div className="p-4">
{emailHtml ? (
<div dangerouslySetInnerHTML={{ __html: emailHtmlPreview }}></div>
) : (
<LoadingSpinner />
)}
</div>
</div>
) : (
<div className="mb-12 grow overflow-y-auto rounded-xl border border-slate-200 bg-white p-4">
<div className="mb-6 flex gap-2">
<div className="h-3 w-3 rounded-full bg-red-500"></div>
<div className="h-3 w-3 rounded-full bg-amber-500"></div>
<div className="h-3 w-3 rounded-full bg-emerald-500"></div>
</div>
<div className="">
<div className="mb-2 border-b border-slate-200 pb-2 text-sm">To : {email || "user@mail.com"}</div>
<div className="border-b border-slate-200 pb-2 text-sm">Subject : {subject}</div>
<div className="p-4">
{emailHtml ? (
<div dangerouslySetInnerHTML={{ __html: emailHtmlPreview }}></div>
) : (
<LoadingSpinner />
)}
</div>
</div>
)}
</div>
</div>
)}
</div>
);
}

View File

@@ -1,22 +1,25 @@
"use client";
import UrlShortenerForm from "@/app/(app)/environments/[environmentId]/components/UrlShortenerForm";
import { DocumentDuplicateIcon } from "@heroicons/react/24/solid";
import { ArrowUpRightIcon } from "lucide-react";
import { RefreshCcw } from "lucide-react";
import Link from "next/link";
import { useRef } from "react";
import toast from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
import { TSurvey } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
import { SurveyInline } from "@formbricks/ui/Survey";
interface EmailTabProps {
interface LinkTabProps {
surveyUrl: string;
survey: TSurvey;
brandColor: string;
webAppUrl: string;
generateNewSingleUseLink: () => void;
isSingleUseLinkSurvey: boolean;
}
export default function LinkTab({ surveyUrl, survey, brandColor }: EmailTabProps) {
export default function LinkTab({
surveyUrl,
webAppUrl,
generateNewSingleUseLink,
isSingleUseLinkSurvey,
}: LinkTabProps) {
const linkTextRef = useRef(null);
const handleTextSelection = () => {
@@ -32,51 +35,87 @@ export default function LinkTab({ surveyUrl, survey, brandColor }: EmailTabProps
}
};
return (
<div className="flex h-full grow flex-col gap-5">
<div className="flex flex-wrap justify-between gap-2">
<div
ref={linkTextRef}
className="relative grow overflow-auto rounded-lg border border-slate-300 bg-white px-3 py-2 text-slate-800"
onClick={() => handleTextSelection()}>
<span style={{ wordBreak: "break-all" }}>{surveyUrl}</span>
</div>
<Button
variant="darkCTA"
title="Copy survey link to clipboard"
aria-label="Copy survey link to clipboard"
onClick={() => {
navigator.clipboard.writeText(surveyUrl);
toast.success("URL copied to clipboard!");
}}
EndIcon={DocumentDuplicateIcon}>
Copy URL
</Button>
</div>
<div className="relative grow overflow-y-scroll rounded-xl border border-gray-200 bg-white px-4 py-[18px]">
<SurveyInline
brandColor={brandColor}
survey={survey}
isBrandingEnabled={false}
autoFocus={false}
isRedirectDisabled={false}
key={survey.id}
onFileUpload={async () => ""}
/>
const docsLinks = [
{
title: "Identify users",
description: "You have the email address or a userId? Append it to the URL.",
link: "https://formbricks.com/docs/link-surveys/user-identification",
},
{
title: "Data prefilling",
description: "You want to prefill some fields in the survey? Here is how.",
link: "https://formbricks.com/docs/link-surveys/data-prefilling",
},
{
title: "Source tracking",
description: "Run GDPR & CCPA compliant source tracking without extra tools.",
link: "https://formbricks.com/docs/link-surveys/source-tracking",
},
{
title: "Create single-use links",
description: "Accept only one submission per link. Here is how.",
link: "https://formbricks.com/docs/link-surveys/single-use-links",
},
];
<Button
variant="minimal"
className={cn(
"absolute bottom-8 left-1/2 -translate-x-1/2 transform rounded-lg border border-slate-200 bg-white"
)}
EndIcon={ArrowUpRightIcon}
title="Open survey in new tab"
aria-label="Open survey in new tab"
endIconClassName="h-4 w-4 "
href={`${surveyUrl}?preview=true`}
target="_blank">
Open in new tab
</Button>
return (
<div className="flex h-full grow flex-col gap-6">
<div>
<p className="text-lg font-semibold text-slate-800">Share the link to get responses</p>
<div className="mt-2 flex max-w-full flex-col items-center space-x-2 lg:flex-row">
<div
ref={linkTextRef}
className="mt-2 max-w-[65%] overflow-hidden rounded-lg border border-slate-300 bg-white px-3 py-3 text-sm text-slate-800"
style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}
onClick={() => handleTextSelection()}>
{surveyUrl}
</div>
<div className="mt-2 flex items-center justify-center space-x-2">
<Button
variant="darkCTA"
className="inline"
title="Copy survey link to clipboard"
aria-label="Copy survey link to clipboard"
onClick={() => {
navigator.clipboard.writeText(surveyUrl);
toast.success("URL copied to clipboard!");
}}
EndIcon={DocumentDuplicateIcon}>
Copy Link
</Button>
{isSingleUseLinkSurvey && (
<Button
variant="darkCTA"
className="inline"
title="Regenerate single use survey link"
aria-label="Regenerate single use survey link"
onClick={() => generateNewSingleUseLink()}>
<RefreshCcw className="h-5 w-5" />
</Button>
)}
</div>
</div>
</div>
<div className="flex flex-wrap justify-between gap-2">
<p className="pt-2 font-semibold text-slate-700">You can do a lot more with links surveys 💡</p>
<div className="grid grid-cols-2 gap-2">
{docsLinks.map((tip) => (
<Link
key={tip.title}
target="_blank"
href={tip.link}
className="relative w-full rounded-md border border-slate-100 bg-white px-6 py-4 text-sm text-slate-600 hover:bg-slate-50 hover:text-slate-800">
<p className="mb-1 font-semibold">{tip.title}</p>
<p className="text-slate-500 hover:text-slate-700">{tip.description}</p>
</Link>
))}
</div>
</div>
<div className="">
<p className="mb-2 pt-2 font-semibold text-slate-700">Survey link got too long? Shorten it!</p>
<div className="rounded-md border border-slate-200 bg-white">
<UrlShortenerForm webAppUrl={webAppUrl} />
</div>
</div>
</div>
);

View File

@@ -8,14 +8,14 @@ import CodeBlock from "@formbricks/ui/CodeBlock";
export default function WebpageTab({ surveyUrl }) {
const iframeCode = `<div style="position: relative; height:100vh; max-height:100vh; overflow:auto;">
<iframe
<iframe
src="${surveyUrl}"
frameborder="0" style="position: absolute; left:0; top:0; width:100%; height:100%; border:0;">
</iframe>
</div>`;
</iframe>
</div>`;
return (
<div className="flex h-full grow flex-col gap-5">
<div className="flex h-full grow flex-col">
<div className="flex justify-between">
<div className=""></div>
<Button
@@ -30,9 +30,9 @@ export default function WebpageTab({ surveyUrl }) {
Copy code
</Button>
</div>
<div className="grow overflow-y-scroll rounded-xl border border-gray-200 bg-white px-4 py-[18px]">
<div className="prose prose-slate max-w-full">
<CodeBlock
customCodeClass="!whitespace-normal sm:!whitespace-pre-wrap !break-all sm:!break-normal"
customCodeClass="text-sm h-48 overflow-y-scroll text-sm"
language="html"
showCopyToClipboard={false}>
{iframeCode}

View File

@@ -62,7 +62,7 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
{firstQuestion.subheader}
</Text>
<Section className="mt-4 block h-20 w-full rounded-lg border border-solid border-gray-200 bg-slate-50" />
<Section className="mt-4 block h-20 w-full rounded-lg border border-solid border-slate-200 bg-slate-50" />
<EmailFooter />
</EmailTemplateWrapper>
);
@@ -76,7 +76,7 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
<Text className="m-0 p-0" dangerouslySetInnerHTML={{ __html: firstQuestion.html || "" }}></Text>
</Container>
<Container className="m-0 mt-4 block w-full max-w-none rounded-lg border border-solid border-gray-200 bg-slate-50 p-4 font-medium text-slate-800">
<Container className="m-0 mt-4 block w-full max-w-none rounded-lg border border-solid border-slate-200 bg-slate-50 p-4 font-medium text-slate-800">
<Text className="m-0 inline-block">{firstQuestion.label}</Text>
</Container>
<Container className="mx-0 mt-4 flex max-w-none justify-end">
@@ -110,12 +110,12 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
{firstQuestion.subheader}
</Text>
<Container className="mx-0 mt-4 flex w-max flex-col">
<Section className="block overflow-hidden rounded-md border border-gray-200">
<Section className="block overflow-hidden rounded-md border border-slate-200">
{Array.from({ length: 11 }, (_, i) => (
<EmailButton
key={i}
href={`${urlWithPrefilling}${firstQuestion.id}=${i}`}
className="m-0 inline-flex h-10 w-10 items-center justify-center border-gray-200 p-0 text-slate-800">
className="m-0 inline-flex h-10 w-10 items-center justify-center border-slate-200 p-0 text-slate-800">
{i}
</EmailButton>
))}
@@ -229,7 +229,7 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
<Container className="mx-0 max-w-none">
{firstQuestion.choices.map((choice) => (
<Section
className="mt-2 block w-full rounded-lg border border-solid border-gray-200 bg-slate-50 p-4 text-slate-800"
className="mt-2 block w-full rounded-lg border border-solid border-slate-200 bg-slate-50 p-4 text-slate-800"
key={choice.id}>
{choice.label}
</Section>
@@ -253,7 +253,7 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
.map((choice) => (
<Link
key={choice.id}
className="mt-2 block rounded-lg border border-solid border-gray-200 bg-slate-50 p-4 text-slate-800 hover:bg-slate-100"
className="mt-2 block rounded-lg border border-solid border-slate-200 bg-slate-50 p-4 text-slate-800 hover:bg-slate-100"
href={`${urlWithPrefilling}${firstQuestion.id}=${choice.label}`}>
{choice.label}
</Link>
@@ -312,7 +312,7 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
{firstQuestion.subheader}
</Text>
<Section className="mt-4 flex h-12 w-full items-center justify-center rounded-lg border border-solid border-gray-200 bg-white">
<Section className="mt-4 flex h-12 w-full items-center justify-center rounded-lg border border-solid border-slate-200 bg-white">
<CalendarDaysIcon className="mb-1 inline h-4 w-4" />
<Text className="inline text-sm font-medium">Select a date</Text>
</Section>

View File

@@ -362,7 +362,7 @@ const CustomFilter = ({ environmentTags, responses, survey, totalResponses }: Cu
setIsFilterDropDownOpen(value);
}}>
<DropdownMenuTrigger>
<div className="flex h-auto min-w-[8rem] items-center justify-between rounded-md border bg-white p-3 sm:min-w-[11rem] sm:px-6 sm:py-3">
<div className="flex h-auto min-w-[8rem] items-center justify-between rounded-md border border-slate-200 bg-white p-3 hover:border-slate-300 sm:min-w-[11rem] sm:px-6 sm:py-3">
<span className="text-sm text-slate-700">
{filterRange === FilterDropDownLabels.CUSTOM_RANGE
? `${dateRange?.from ? format(dateRange?.from, "dd LLL") : "Select first date"} - ${
@@ -419,7 +419,7 @@ const CustomFilter = ({ environmentTags, responses, survey, totalResponses }: Cu
setIsDownloadDropDownOpen(value);
}}>
<DropdownMenuTrigger asChild className="focus:bg-muted cursor-pointer outline-none">
<div className="min-w-auto h-auto rounded-md border bg-white p-3 sm:flex sm:min-w-[11rem] sm:px-6 sm:py-3">
<div className="min-w-auto h-auto rounded-md border border-slate-200 bg-white p-3 hover:border-slate-300 sm:flex sm:min-w-[11rem] sm:px-6 sm:py-3">
<div className="hidden w-full items-center justify-between sm:flex">
<span className="text-sm text-slate-700">Download</span>
{isDownloadDropDownOpen ? (

View File

@@ -131,7 +131,7 @@ const ResponseFilter = () => {
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger className="flex min-w-[8rem] items-center justify-between rounded border border-slate-200 bg-slate-100 p-3 text-sm text-slate-600 hover:border-slate-300 sm:min-w-[11rem] sm:px-6 sm:py-3">
<PopoverTrigger className="flex min-w-[8rem] items-center justify-between rounded border border-slate-200 bg-white p-3 text-sm text-slate-600 hover:border-slate-300 sm:min-w-[11rem] sm:px-6 sm:py-3">
Filter {selectedFilter.filter.length > 0 && `(${selectedFilter.filter.length})`}
<div className="ml-3">
{isOpen ? (

View File

@@ -1,7 +1,7 @@
"use client";
import SurveyShareButton from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/LinkModalButton";
import SuccessMessage from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage";
import SurveyShareButton from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyShareButton";
import SurveyStatusDropdown from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown";
import { updateSurveyAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
import { EllipsisHorizontalIcon, PencilSquareIcon } from "@heroicons/react/24/solid";
@@ -60,7 +60,7 @@ const SummaryHeader = ({
<div>
<div className="flex gap-4">
<p className="text-3xl font-bold text-slate-800">{survey.name}</p>
{survey.resultShareKey && <Badge text="Public Results" type="success" size="normal"></Badge>}
{survey.resultShareKey && <Badge text="Public Results" type="warning" size="normal"></Badge>}
</div>
<span className="text-base font-extralight text-slate-600">{product.name}</span>
</div>

View File

@@ -3,10 +3,13 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { canUserAccessProduct } from "@formbricks/lib/product/auth";
import { getProduct } from "@formbricks/lib/product/service";
import { canUserAccessSurvey, verifyUserRoleAccess } from "@formbricks/lib/survey/auth";
import { deleteSurvey, getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
import { formatSurveyDateFields } from "@formbricks/lib/survey/util";
import { AuthorizationError } from "@formbricks/types/errors";
import { TProduct } from "@formbricks/types/product";
import { TSurvey } from "@formbricks/types/surveys";
export async function updateSurveyAction(survey: TSurvey): Promise<TSurvey> {
@@ -40,3 +43,14 @@ export const deleteSurveyAction = async (surveyId: string) => {
await deleteSurvey(surveyId);
};
export const refetchProduct = async (productId: string): Promise<TProduct | null> => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const isAuthorized = await canUserAccessProduct(session.user.id, productId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
const product = await getProduct(productId);
return product;
};

View File

@@ -45,12 +45,12 @@ export default function EditThankYouCard({
<div
className={cn(
open ? "scale-100 shadow-lg " : "scale-97 shadow-md",
"z-20 flex flex-row rounded-lg bg-white transition-transform duration-300 ease-in-out"
"group flex flex-row rounded-lg bg-white transition-transform duration-300 ease-in-out"
)}>
<div
className={cn(
open ? "bg-slate-700" : "bg-slate-400",
"flex w-10 items-center justify-center rounded-l-lg hover:bg-slate-600 group-aria-expanded:rounded-bl-none"
open ? "bg-slate-50" : "bg-white group-hover:bg-slate-50",
"flex w-10 items-center justify-center rounded-l-lg border-b border-l border-t group-aria-expanded:rounded-bl-none"
)}>
<p>🙏</p>
</div>

View File

@@ -56,12 +56,12 @@ export default function EditWelcomeCard({
<div
className={cn(
open ? "scale-100 shadow-lg " : "scale-97 shadow-md",
"flex flex-row rounded-lg bg-white transition-transform duration-300 ease-in-out"
"group flex flex-row rounded-lg bg-white transition-transform duration-300 ease-in-out"
)}>
<div
className={cn(
open ? "bg-slate-700" : "bg-slate-400",
"flex w-10 items-center justify-center rounded-l-lg hover:bg-slate-600 group-aria-expanded:rounded-bl-none"
open ? "bg-slate-50" : "bg-white group-hover:bg-slate-50",
"flex w-10 items-center justify-center rounded-l-lg border-b border-l border-t group-aria-expanded:rounded-bl-none"
)}>
<p></p>
</div>
@@ -178,7 +178,7 @@ export default function EditWelcomeCard({
<Label htmlFor="timeToFinish" className="">
Time to Finish
</Label>
<div className="text-sm text-gray-500 dark:text-gray-400">
<div className="text-sm text-slate-500 dark:text-slate-400">
Display an estimate of completion time for survey
</div>
</div>
@@ -199,7 +199,7 @@ export default function EditWelcomeCard({
<Label htmlFor="showResponseCount" className="">
Show Response Count
</Label>
<div className="text-sm text-gray-500 dark:text-gray-400">
<div className="text-sm text-slate-500 dark:text-slate-400">
Display number of responses for survey
</div>
</div>

View File

@@ -6,6 +6,7 @@ import toast from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
import { TSurvey, TSurveyHiddenFields, TSurveyQuestions } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { Switch } from "@formbricks/ui/Switch";
@@ -49,14 +50,14 @@ const HiddenFieldsCard: FC<HiddenFieldsCardProps> = ({
<div
className={cn(
open ? "scale-100 shadow-lg " : "scale-97 shadow-md",
"z-10 flex flex-row rounded-lg bg-white transition-transform duration-300 ease-in-out"
"group flex flex-row rounded-lg bg-white transition-transform duration-300 ease-in-out"
)}>
<div
className={cn(
open ? "bg-slate-700" : "bg-slate-400",
"flex w-10 items-center justify-center rounded-l-lg hover:bg-slate-600 group-aria-expanded:rounded-bl-none"
open ? "bg-slate-50" : "bg-white group-hover:bg-slate-50",
"flex w-10 items-center justify-center rounded-l-lg border-b border-l border-t group-aria-expanded:rounded-bl-none"
)}>
<p>👁</p>
<p>🥷</p>
</div>
<Collapsible.Root
open={open}
@@ -105,7 +106,7 @@ const HiddenFieldsCard: FC<HiddenFieldsCardProps> = ({
);
})
) : (
<p className="text-sm italic text-gray-500">No hidden fields yet. Add the first one below.</p>
<p className="text-sm italic text-slate-500">No hidden fields yet. Add the first one below.</p>
)}
</div>
<form
@@ -113,6 +114,11 @@ const HiddenFieldsCard: FC<HiddenFieldsCardProps> = ({
onSubmit={(e) => {
e.preventDefault();
// Check if the hiddenField is empty or just whitespace
if (!hiddenField.trim()) {
return toast.error("The field cannot be empty.");
}
const errorMessage = validateHiddenField(
// current field
hiddenField,
@@ -131,7 +137,7 @@ const HiddenFieldsCard: FC<HiddenFieldsCardProps> = ({
setHiddenField("");
}}>
<Label htmlFor="headline">Hidden Field</Label>
<div className="mt-2">
<div className="mt-2 flex gap-2">
<Input
autoFocus
id="headline"
@@ -140,6 +146,9 @@ const HiddenFieldsCard: FC<HiddenFieldsCardProps> = ({
onChange={(e) => setHiddenField(e.target.value.trim())}
placeholder="Type field id..."
/>
<Button variant="secondary" type="submit" size="sm" className="whitespace-nowrap">
Add hidden field ID
</Button>
</div>
</form>
</Collapsible.CollapsibleContent>

View File

@@ -25,7 +25,7 @@ interface HowToSendCardProps {
}
export default function HowToSendCard({ localSurvey, setLocalSurvey, environment }: HowToSendCardProps) {
const [open, setOpen] = useState(localSurvey.type === "web" ? false : true);
const [open, setOpen] = useState(false);
const [widgetSetupCompleted, setWidgetSetupCompleted] = useState(false);
useEffect(() => {
@@ -88,8 +88,8 @@ export default function HowToSendCard({ localSurvey, setLocalSurvey, environment
<CheckCircleIcon className="h-8 w-8 text-green-400" />
</div>
<div>
<p className="font-semibold text-slate-800">How to ask</p>
<p className="mt-1 text-sm text-slate-500">In-app survey, link survey or email survey.</p>
<p className="font-semibold text-slate-800">Survey Type</p>
<p className="mt-1 text-sm text-slate-500">Choose between in-app or link survey.</p>
</div>
</div>
</Collapsible.CollapsibleTrigger>
@@ -143,14 +143,13 @@ export default function HowToSendCard({ localSurvey, setLocalSurvey, environment
Your app is not yet connected to Formbricks.
</p>
<p className="text-xs font-normal">
Follow the{" "}
<Link
href={`/environments/${environment.id}/settings/setup`}
className="underline hover:text-amber-900"
target="_blank">
set up guide
Connect Formbricks
</Link>{" "}
to connect Formbricks and launch surveys in your app.
and launch surveys in your app or website.
</p>
</div>
</div>

View File

@@ -21,7 +21,7 @@ export default function ImageSurveyBg({ localSurvey, handleBgChange }: ImageSurv
: "";
return (
<div className="mb-2 mt-4 w-full rounded-lg border bg-slate-50 p-4">
<div className="mt-2 w-full">
<div className="flex w-full items-center justify-center">
<FileInput
id="survey-bg-file-input"

View File

@@ -26,8 +26,7 @@ import { Draggable } from "react-beautiful-dnd";
import { cn } from "@formbricks/lib/cn";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
import { TProduct } from "@formbricks/types/product";
import { TSurveyQuestionType } from "@formbricks/types/surveys";
import { TSurvey } from "@formbricks/types/surveys";
import { TSurvey, TSurveyQuestionType } from "@formbricks/types/surveys";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { Switch } from "@formbricks/ui/Switch";
@@ -142,7 +141,7 @@ export default function QuestionCard({
<div
className={cn(
open ? "bg-slate-700" : "bg-slate-400",
"top-0 w-10 rounded-l-lg p-2 text-center text-sm text-white hover:bg-slate-600",
"top-0 w-10 rounded-l-lg p-2 text-center text-sm text-white hover:cursor-grab hover:bg-slate-600",
isInvalid && "bg-red-400 hover:bg-red-600"
)}>
{questionIdx + 1}

View File

@@ -1,6 +1,6 @@
import { ChevronDownIcon } from "@heroicons/react/24/solid";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
type Option = {
label: string;
@@ -41,7 +41,7 @@ const Dropdown = ({ options, defaultValue, onSelect, disabled = false }: Dropdow
<span>{selectedOption ? selectedOption.label : "Select an option"}</span>
</span>
<span className="flex h-full items-center border-l pl-3">
<ChevronDownIcon className="h-4 w-4 text-gray-500" />
<ChevronDownIcon className="h-4 w-4 text-slate-500" />
</span>
</button>
</DropdownMenu.Trigger>
@@ -53,7 +53,7 @@ const Dropdown = ({ options, defaultValue, onSelect, disabled = false }: Dropdow
{options.map((option) => (
<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"
className="flex cursor-pointer items-center p-3 hover:bg-slate-100 hover:outline-none data-[disabled]:cursor-default data-[disabled]:opacity-50"
disabled={disabled || option.disabled}
onSelect={() => handleSelect(option)}>
{option.icon && <option.icon className="mr-3 h-5 w-5" />}

View File

@@ -5,10 +5,8 @@ import * as Collapsible from "@radix-ui/react-collapsible";
import Link from "next/link";
import { useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { TSurvey } from "@formbricks/types/surveys";
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
import { Badge } from "@formbricks/ui/Badge";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/RadioGroup";
@@ -78,9 +76,9 @@ export default function RecontactOptionsCard({
}
}, [localSurvey.type]);
/* if (localSurvey.type === "link") {
return null;
} */
if (localSurvey.type === "link") {
return null; // Hide card completely
}
return (
<Collapsible.Root
@@ -93,25 +91,15 @@ export default function RecontactOptionsCard({
className="w-full rounded-lg border border-slate-300 bg-white">
<Collapsible.CollapsibleTrigger
asChild
className={cn(
localSurvey.type !== "link" ? "cursor-pointer hover:bg-slate-50" : "cursor-not-allowed bg-slate-50",
"h-full w-full rounded-lg "
)}>
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50">
<div className="inline-flex px-4 py-4">
<div className="flex items-center pl-2 pr-5">
<CheckCircleIcon
className={cn(localSurvey.type !== "link" ? "text-green-400" : "text-slate-300", "h-8 w-8 ")}
/>
<CheckCircleIcon className="h-8 w-8 text-green-400" />
</div>
<div>
<p className="font-semibold text-slate-800">Recontact Options</p>
<p className="mt-1 text-sm text-slate-500">Decide how often people can answer this survey.</p>
</div>
{localSurvey.type === "link" && (
<div className="flex w-full items-center justify-end pr-2">
<Badge size="normal" text="In-app survey settings" type="gray" />
</div>
)}
</div>
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="pb-3">

View File

@@ -5,6 +5,7 @@ import * as Collapsible from "@radix-ui/react-collapsible";
import { KeyboardEventHandler, useEffect, useState } from "react";
import toast from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
import { TSurvey } from "@formbricks/types/surveys";
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
import { DatePicker } from "@formbricks/ui/DatePicker";
@@ -287,7 +288,10 @@ export default function ResponseOptionsCard({
<Collapsible.Root
open={open}
onOpenChange={setOpen}
className="w-full rounded-lg border border-slate-300 bg-white">
className={cn(
open ? "" : "hover:bg-slate-50",
"w-full space-y-2 rounded-lg border border-slate-300 bg-white "
)}>
<Collapsible.CollapsibleTrigger asChild className="h-full w-full cursor-pointer">
<div className="inline-flex px-4 py-4">
<div className="flex items-center pl-2 pr-5">
@@ -295,7 +299,7 @@ export default function ResponseOptionsCard({
</div>
<div>
<p className="font-semibold text-slate-800">Response Options</p>
<p className="mt-1 text-sm text-slate-500">Decide how and how long people can respond.</p>
<p className="mt-1 text-sm text-slate-500">Response limits, redirections and more.</p>
</div>
</div>
</Collapsible.CollapsibleTrigger>

View File

@@ -63,7 +63,12 @@ export default function SettingsView({
environmentId={environment.id}
/>
<StylingCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} colours={colours} />
<StylingCard
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
colours={colours}
environmentId={environment.id}
/>
</div>
);
}

View File

@@ -2,8 +2,10 @@
import { CheckCircleIcon } from "@heroicons/react/24/solid";
import * as Collapsible from "@radix-ui/react-collapsible";
import Link from "next/link";
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { TPlacement } from "@formbricks/types/common";
import { TSurvey, TSurveyBackgroundBgType } from "@formbricks/types/surveys";
import { ColorPicker } from "@formbricks/ui/ColorPicker";
@@ -17,15 +19,21 @@ interface StylingCardProps {
localSurvey: TSurvey;
setLocalSurvey: React.Dispatch<React.SetStateAction<TSurvey>>;
colours: string[];
environmentId: string;
}
export default function StylingCard({ localSurvey, setLocalSurvey, colours }: StylingCardProps) {
const [open, setOpen] = useState(false);
export default function StylingCard({
localSurvey,
setLocalSurvey,
colours,
environmentId,
}: StylingCardProps) {
const [open, setOpen] = useState(localSurvey.type === "link" ? true : false);
const progressBarHidden = localSurvey.styling?.hideProgressBar ?? false;
const { type, productOverwrites, styling } = localSurvey;
const { brandColor, clickOutsideClose, darkOverlay, placement, highlightBorderColor } =
productOverwrites ?? {};
const { bg, bgType, brightness } = styling?.background ?? {};
const { bgType } = styling?.background ?? {};
const [inputValue, setInputValue] = useState(100);
@@ -56,34 +64,6 @@ export default function StylingCard({ localSurvey, setLocalSurvey, colours }: St
});
};
const toggleBackgroundColor = () => {
setLocalSurvey({
...localSurvey,
styling: {
...localSurvey.styling,
background: {
...localSurvey.styling?.background,
bg: !!bg ? undefined : "#ffff",
bgType: !!bg ? undefined : "color",
},
},
});
};
const toggleBrightness = () => {
setLocalSurvey({
...localSurvey,
styling: {
...localSurvey.styling,
background: {
...localSurvey.styling?.background,
brightness: !!brightness ? undefined : 100,
},
},
});
setInputValue(100);
};
const toggleHighlightBorderColor = () => {
setLocalSurvey({
...localSurvey,
@@ -189,7 +169,10 @@ export default function StylingCard({ localSurvey, setLocalSurvey, colours }: St
<Collapsible.Root
open={open}
onOpenChange={setOpen}
className="w-full rounded-lg border border-slate-300 bg-white">
className={cn(
open ? "" : "hover:bg-slate-50",
"w-full space-y-2 rounded-lg border border-slate-300 bg-white "
)}>
<Collapsible.CollapsibleTrigger asChild className="h-full w-full cursor-pointer">
<div className="inline-flex px-4 py-4">
<div className="flex items-center pl-2 pr-5">
@@ -204,6 +187,51 @@ export default function StylingCard({ localSurvey, setLocalSurvey, colours }: St
<Collapsible.CollapsibleContent>
<hr className="py-1 text-slate-600" />
<div className="p-3">
{type == "link" && (
<>
<>
{/* Background */}
<div className="p-3">
<div className="ml-2">
<h3 className="text-sm font-semibold text-slate-700">Change Background</h3>
<p className="text-xs font-normal text-slate-500">
Pick a background from our library or upload your own.
</p>
</div>
<SurveyBgSelectorTab
localSurvey={localSurvey}
handleBgChange={handleBgChange}
colours={colours}
bgType={bgType}
/>
</div>
{/* Overlay */}
<div className="my-3 p-3">
<div className="ml-2">
<h3 className="text-sm font-semibold text-slate-700">Background Overlay</h3>
<p className="text-xs font-normal text-slate-500">
Darken or lighten background of your choice.
</p>
</div>
<div>
<div className="mt-4 flex flex-col justify-center rounded-lg border bg-slate-50 p-6">
<h3 className="mb-4 text-sm font-semibold text-slate-700">Brightness</h3>
<input
id="small-range"
type="range"
min="1"
max="200"
value={inputValue}
onChange={handleInputChange}
className="range-sm mb-6 h-1 w-full cursor-pointer appearance-none rounded-lg bg-slate-200 dark:bg-slate-700"
/>
</div>
</div>
</div>
</>
</>
)}
{/* Brand Color */}
<div className="p-3">
<div className="ml-2 flex items-center space-x-1">
@@ -224,67 +252,7 @@ export default function StylingCard({ localSurvey, setLocalSurvey, colours }: St
</div>
)}
</div>
{type == "link" && (
<>
{/* Background */}
<div className="p-3">
<div className="ml-2 flex items-center space-x-1">
<Switch id="autoCompleteBg" checked={!!bg} onCheckedChange={toggleBackgroundColor} />
<Label htmlFor="autoCompleteBg" className="cursor-pointer">
<div className="ml-2">
<h3 className="text-sm font-semibold text-slate-700">Change Background</h3>
<p className="text-xs font-normal text-slate-500">
Pick a background from our library or upload your own.
</p>
</div>
</Label>
</div>
{bg && (
<SurveyBgSelectorTab
localSurvey={localSurvey}
handleBgChange={handleBgChange}
colours={colours}
bgType={bgType}
/>
)}
</div>
{/* Overlay */}
<div className="p-3">
<div className="ml-2 flex items-center space-x-1">
<Switch
id="autoCompleteOverlay"
checked={!!brightness}
onCheckedChange={toggleBrightness}
/>
<Label htmlFor="autoCompleteOverlay" className="cursor-pointer">
<div className="ml-2">
<h3 className="text-sm font-semibold text-slate-700">Background Overlay</h3>
<p className="text-xs font-normal text-slate-500">
Darken or lighten background of your choice.
</p>
</div>
</Label>
</div>
{brightness && (
<div>
<div className="mt-4 flex flex-col justify-center rounded-lg border bg-slate-50 p-4 px-8">
<h3 className="mb-4 text-sm font-semibold text-slate-700">Transparency</h3>
<input
id="small-range"
type="range"
min="1"
max="200"
value={inputValue}
onChange={handleInputChange}
className="range-sm mb-6 h-1 w-full cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-gray-700"
/>
</div>
</div>
)}
</div>
</>
)}
{/* positioning */}
{/* Positioning */}
{type !== "link" && (
<div className="p-3 ">
<div className="ml-2 flex items-center space-x-1">
@@ -369,6 +337,17 @@ export default function StylingCard({ localSurvey, setLocalSurvey, colours }: St
</Label>
</div>
</div>
<div className="mt-2 flex items-center space-x-3 rounded-lg px-4 py-2 text-slate-500">
<p className="text-xs">
To keep the styling over all surveys consistent, you can{" "}
<Link
href={`/environments/${environmentId}/settings/lookandfeel`}
className="underline hover:text-slate-900"
target="_blank">
set global styles in the Look & Feel settings.
</Link>{" "}
</p>
</div>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.Root>

View File

@@ -15,7 +15,7 @@ interface SurveyBgSelectorTabProps {
const TabButton = ({ isActive, onClick, children }) => (
<button
className={`w-1/4 rounded-md p-2 text-sm font-medium leading-none text-slate-800 ${
className={`w-1/3 rounded-md p-3 text-sm font-medium leading-none text-slate-800 ${
isActive ? "bg-white shadow-sm" : ""
}`}
onClick={onClick}>
@@ -29,7 +29,7 @@ export default function SurveyBgSelectorTab({
colours,
bgType,
}: SurveyBgSelectorTabProps) {
const [tab, setTab] = useState(bgType || "image");
const [tab, setTab] = useState(bgType || "color");
const renderContent = () => {
switch (tab) {
@@ -45,16 +45,16 @@ export default function SurveyBgSelectorTab({
};
return (
<div className="mt-4 flex flex-col items-center justify-center rounded-lg border bg-slate-50 p-4 px-8">
<div className="flex w-full items-center justify-between rounded-lg border border-slate-300 bg-slate-50 px-6 py-1.5">
<TabButton isActive={tab === "image"} onClick={() => setTab("image")}>
Image
<div className="mt-4 flex flex-col items-center justify-center rounded-lg border bg-slate-50 p-4">
<div className="flex w-full items-center justify-between rounded-lg border border-slate-300 bg-slate-100 p-2">
<TabButton isActive={tab === "color"} onClick={() => setTab("color")}>
Color
</TabButton>
<TabButton isActive={tab === "animation"} onClick={() => setTab("animation")}>
Animation
</TabButton>
<TabButton isActive={tab === "color"} onClick={() => setTab("color")}>
Color
<TabButton isActive={tab === "image"} onClick={() => setTab("image")}>
Image
</TabButton>
</div>
{renderContent()}

View File

@@ -1,5 +1,6 @@
"use client";
import { refetchProduct } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
import Loading from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/loading";
import React from "react";
import { useEffect, useState } from "react";
@@ -42,7 +43,7 @@ export default function SurveyEditor({
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
const [localSurvey, setLocalSurvey] = useState<TSurvey | null>();
const [invalidQuestions, setInvalidQuestions] = useState<String[] | null>(null);
const [localProduct, setLocalProduct] = useState<TProduct>(product);
useEffect(() => {
if (survey) {
if (localSurvey) return;
@@ -54,6 +55,24 @@ export default function SurveyEditor({
}
}, [survey, localSurvey]);
useEffect(() => {
const listener = () => {
if (document.visibilityState === "visible") {
const fetchLatestProduct = async () => {
const latestProduct = await refetchProduct(localProduct.id);
if (latestProduct) {
setLocalProduct(latestProduct);
}
};
fetchLatestProduct();
}
};
document.addEventListener("visibilitychange", listener);
return () => {
document.removeEventListener("visibilitychange", listener);
};
}, [localProduct.id]);
// when the survey type changes, we need to reset the active question id to the first question
useEffect(() => {
if (localSurvey?.questions?.length && localSurvey.questions.length > 0) {
@@ -78,7 +97,7 @@ export default function SurveyEditor({
activeId={activeView}
setActiveId={setActiveView}
setInvalidQuestions={setInvalidQuestions}
product={product}
product={localProduct}
responseCount={responseCount}
/>
<div className="relative z-0 flex flex-1 overflow-hidden">
@@ -90,7 +109,7 @@ export default function SurveyEditor({
setLocalSurvey={setLocalSurvey}
activeQuestionId={activeQuestionId}
setActiveQuestionId={setActiveQuestionId}
product={product}
product={localProduct}
invalidQuestions={invalidQuestions}
setInvalidQuestions={setInvalidQuestions}
/>
@@ -112,7 +131,7 @@ export default function SurveyEditor({
survey={localSurvey}
setActiveQuestionId={setActiveQuestionId}
activeQuestionId={activeQuestionId}
product={product}
product={localProduct}
environment={environment}
previewType={localSurvey.type === "web" ? "modal" : "fullwidth"}
onFileUpload={async (file) => file.name}

View File

@@ -5,13 +5,11 @@ import { CheckCircleIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/solid"
import * as Collapsible from "@radix-ui/react-collapsible";
import { useCallback, useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { TActionClass } from "@formbricks/types/actionClasses";
import { TMembershipRole } from "@formbricks/types/memberships";
import { TSurvey } from "@formbricks/types/surveys";
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
import { Badge } from "@formbricks/ui/Badge";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import {
@@ -125,6 +123,10 @@ export default function WhenToSendCard({
}
}, [addTriggerEvent, localSurvey.triggers.length]);
if (localSurvey.type === "link") {
return null; // Hide card completely
}
return (
<>
<Collapsible.Root
@@ -137,30 +139,13 @@ export default function WhenToSendCard({
className="w-full rounded-lg border border-slate-300 bg-white">
<Collapsible.CollapsibleTrigger
asChild
className={cn(
localSurvey.type !== "link"
? "cursor-pointer hover:bg-slate-50"
: "cursor-not-allowed bg-slate-50",
"h-full w-full rounded-lg "
)}>
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50">
<div className="inline-flex px-4 py-4">
<div className="flex items-center pl-2 pr-5">
{!localSurvey.triggers || localSurvey.triggers.length === 0 || !localSurvey.triggers[0] ? (
<div
className={cn(
localSurvey.type !== "link"
? "border-amber-500 bg-amber-50"
: "border-slate-300 bg-slate-100",
"h-7 w-7 rounded-full border "
)}
/>
<div className="h-8 w-8 rounded-full border border-amber-500 bg-amber-50" />
) : (
<CheckCircleIcon
className={cn(
localSurvey.type !== "link" ? "text-green-400" : "text-slate-300",
"h-8 w-8 "
)}
/>
<CheckCircleIcon className="h-8 w-8 text-green-400" />
)}
</div>
@@ -168,11 +153,6 @@ export default function WhenToSendCard({
<p className="font-semibold text-slate-800">Survey Trigger</p>
<p className="mt-1 text-sm text-slate-500">Choose the actions which trigger the survey.</p>
</div>
{localSurvey.type === "link" && (
<div className="flex w-full items-center justify-end pr-2">
<Badge size="normal" text="In-app survey settings" type="gray" />
</div>
)}
</div>
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="">
@@ -228,28 +208,26 @@ export default function WhenToSendCard({
</Button>
</div>
{localSurvey.type !== "link" && (
<div className="ml-2 flex items-center space-x-1 px-4 pb-4">
<label
htmlFor="triggerDelay"
className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
<div className="">
<p className="text-sm font-semibold text-slate-700">
Wait
<Input
type="number"
min="0"
id="triggerDelay"
value={localSurvey.delay.toString()}
onChange={(e) => handleTriggerDelay(e)}
className="ml-2 mr-2 inline w-16 bg-white text-center text-sm"
/>
seconds before showing the survey.
</p>
</div>
</label>
</div>
)}
<div className="ml-2 flex items-center space-x-1 px-4 pb-4">
<label
htmlFor="triggerDelay"
className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
<div className="">
<p className="text-sm font-semibold text-slate-700">
Wait
<Input
type="number"
min="0"
id="triggerDelay"
value={localSurvey.delay.toString()}
onChange={(e) => handleTriggerDelay(e)}
className="ml-2 mr-2 inline w-16 bg-white text-center text-sm"
/>
seconds before showing the survey.
</p>
</div>
</label>
</div>
<AdvancedOptionToggle
htmlId="autoClose"

View File

@@ -9,13 +9,10 @@ import { cn } from "@formbricks/lib/cn";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import { TSurvey } from "@formbricks/types/surveys";
import { Alert, AlertDescription, AlertTitle } from "@formbricks/ui/Alert";
import { Badge } from "@formbricks/ui/Badge";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
/* */
const filterConditions = [
{ id: "equals", name: "equals" },
{ id: "notEquals", name: "not equals" },
@@ -66,39 +63,27 @@ export default function WhoToSendCard({ localSurvey, setLocalSurvey, attributeCl
setLocalSurvey(updatedSurvey);
};
if (localSurvey.type === "link") {
return null; // Hide card completely
}
return (
<>
<Collapsible.Root
open={open}
onOpenChange={(openState) => {
if (localSurvey.type !== "link") {
setOpen(openState);
}
}}
onOpenChange={setOpen}
className="w-full rounded-lg border border-slate-300 bg-white">
<Collapsible.CollapsibleTrigger
asChild
className={cn(
localSurvey.type !== "link"
? "cursor-pointer hover:bg-slate-50"
: "cursor-not-allowed bg-slate-50",
"h-full w-full rounded-lg "
)}>
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50">
<div className="inline-flex px-4 py-6">
<div className="flex items-center pl-2 pr-5">
<CheckCircleIcon
className={cn(localSurvey.type !== "link" ? "text-green-400" : "text-slate-300", "h-8 w-8 ")}
/>
<CheckCircleIcon className="h-8 w-8 text-green-400 " />
</div>
<div>
<p className="font-semibold text-slate-800">Target Audience</p>
<p className="mt-1 text-sm text-slate-500">Pre-segment your users with attributes filters.</p>
</div>
{localSurvey.type === "link" && (
<div className="flex w-full items-center justify-end pr-2">
<Badge size="normal" text="In-app survey settings" type="gray" />
</div>
)}
</div>
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="">

View File

@@ -2,24 +2,24 @@ export default function Loading() {
return (
<div className="flex h-full w-full flex-col items-center justify-between p-6">
{/* Top Part - Loading Navbar */}
<div className="flex h-[10vh] w-full animate-pulse rounded-lg bg-gray-200 font-medium text-slate-900"></div>
<div className="flex h-[10vh] w-full animate-pulse rounded-lg bg-slate-200 font-medium text-slate-900"></div>
{/* Bottom Part - Divided into Left and Right */}
<div className="mt-4 flex h-[85%] w-full flex-row">
{/* Left Part - 7 Horizontal Bars */}
<div className="flex h-full w-1/2 flex-col justify-between space-y-2">
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-gray-200"></div>
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-gray-200"></div>
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-gray-200"></div>
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-gray-200"></div>
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-gray-200"></div>
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-gray-200"></div>
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-gray-200"></div>
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
</div>
{/* Right Part - Simple Box */}
<div className="ml-4 flex h-full w-1/2 flex-col">
<div className="ph-no-capture h-full animate-pulse rounded-lg bg-gray-200"></div>
<div className="ph-no-capture h-full animate-pulse rounded-lg bg-slate-200"></div>
</div>
</div>
</div>

View File

@@ -98,7 +98,7 @@ export default function SurveyDropDownMenu({
};
if (loading) {
return (
<div className="opacity-0.2 absolute left-0 top-0 h-full w-full bg-gray-100">
<div className="opacity-0.2 absolute left-0 top-0 h-full w-full bg-slate-100">
<LoadingSpinner />
</div>
);

View File

@@ -99,7 +99,7 @@ const Objective: React.FC<ObjectiveProps> = ({ next, skip, formbricksResponseId,
className={cn(
selectedChoice === choice.label
? "z-10 border-slate-400 bg-slate-100"
: "border-gray-200",
: "border-slate-200",
"relative flex cursor-pointer flex-col rounded-md border p-4 hover:bg-slate-100 focus:outline-none"
)}>
<span className="flex items-center text-sm">
@@ -108,7 +108,7 @@ const Objective: React.FC<ObjectiveProps> = ({ next, skip, formbricksResponseId,
id={choice.id}
value={choice.label}
checked={choice.label === selectedChoice}
className="checked:text-brand-dark focus:text-brand-dark h-4 w-4 border border-gray-300 focus:ring-0 focus:ring-offset-0"
className="checked:text-brand-dark focus:text-brand-dark h-4 w-4 border border-slate-300 focus:ring-0 focus:ring-offset-0"
aria-labelledby={`${choice.id}-label`}
onChange={(e) => {
setSelectedChoice(e.currentTarget.value);

View File

@@ -135,7 +135,7 @@ const Product: React.FC<Product> = ({ done, isLoading, environmentId, product })
checked
readOnly
type="radio"
className="h-4 w-4 border border-gray-300 focus:ring-0 focus:ring-offset-0"
className="h-4 w-4 border border-slate-300 focus:ring-0 focus:ring-offset-0"
style={{ borderColor: "brandColor", color: "brandColor" }}
/>
<span className="ml-3 font-medium">{choice}</span>

View File

@@ -94,7 +94,7 @@ const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId, sessio
className={cn(
selectedChoice === choice.label
? "z-10 border-slate-400 bg-slate-100"
: "border-gray-200",
: "border-slate-200",
"relative flex cursor-pointer flex-col rounded-md border p-4 hover:bg-slate-100 focus:outline-none"
)}>
<span className="flex items-center text-sm">
@@ -104,7 +104,7 @@ const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId, sessio
value={choice.label}
name="role"
checked={choice.label === selectedChoice}
className="checked:text-brand-dark focus:text-brand-dark h-4 w-4 border border-gray-300 focus:ring-0 focus:ring-offset-0"
className="checked:text-brand-dark focus:text-brand-dark h-4 w-4 border border-slate-300 focus:ring-0 focus:ring-offset-0"
aria-labelledby={`${choice.id}-label`}
onChange={(e) => {
setSelectedChoice(e.currentTarget.value);

View File

@@ -2,12 +2,12 @@ export default function Loading() {
return (
<div className="flex h-[100vh] w-[80vw] animate-pulse flex-col items-center justify-between p-12 text-white">
<div className="flex w-full justify-between">
<div className="h-12 w-1/6 rounded-lg bg-gray-200"></div>
<div className="h-12 w-1/3 rounded-lg bg-gray-200"></div>
<div className="h-12 w-1/6 rounded-lg bg-slate-200"></div>
<div className="h-12 w-1/3 rounded-lg bg-slate-200"></div>
<div className="h-0 w-1/6"></div>
</div>
<div className="h-1/3 w-1/2 rounded-lg bg-gray-200"></div>
<div className="h-10 w-1/2 rounded-lg bg-gray-200"></div>
<div className="h-1/3 w-1/2 rounded-lg bg-slate-200"></div>
<div className="h-10 w-1/2 rounded-lg bg-slate-200"></div>
</div>
);
}

View File

@@ -2,8 +2,8 @@ export default function Loading() {
return (
<div className="flex h-full w-full items-center justify-center">
<div className="flex h-1/2 w-1/4 flex-col">
<div className="ph-no-capture h-16 w-1/3 animate-pulse rounded-lg bg-gray-200 font-medium text-slate-900"></div>
<div className="ph-no-capture mt-4 h-full animate-pulse rounded-lg bg-gray-200 text-slate-900"></div>
<div className="ph-no-capture h-16 w-1/3 animate-pulse rounded-lg bg-slate-200 font-medium text-slate-900"></div>
<div className="ph-no-capture mt-4 h-full animate-pulse rounded-lg bg-slate-200 text-slate-900"></div>
</div>
</div>
);

View File

@@ -34,7 +34,7 @@ const SurveyInactive = ({
<h1 className="text-4xl font-bold text-slate-800">
{status === "completed" && surveyClosedMessage ? surveyClosedMessage.heading : `Survey ${status}.`}
</h1>
<p className="text-lg leading-10 text-gray-500">
<p className="text-lg leading-10 text-slate-500">
{status === "completed" && surveyClosedMessage
? surveyClosedMessage.subheading
: descriptions[status]}

View File

@@ -21,7 +21,7 @@ const SurveyLinkUsed = ({ singleUseMessage }: SurveyLinkUsedProps) => {
<h1 className="text-4xl font-bold text-slate-800">
{!!singleUseMessage?.heading ? singleUseMessage?.heading : defaultHeading}
</h1>
<p className="text-lg leading-10 text-gray-500">
<p className="text-lg leading-10 text-slate-500">
{!!singleUseMessage?.subheading ? singleUseMessage?.subheading : defaultSubheading}
</p>
</div>

View File

@@ -2,8 +2,8 @@ export default function Loading() {
return (
<div className="flex h-full w-full items-center justify-center">
<div className="flex h-1/2 w-1/4 flex-col">
<div className="ph-no-capture h-16 w-1/3 animate-pulse rounded-lg bg-gray-200 font-medium text-slate-900"></div>
<div className="ph-no-capture mt-4 h-full animate-pulse rounded-lg bg-gray-200 text-slate-900"></div>
<div className="ph-no-capture h-16 w-1/3 animate-pulse rounded-lg bg-slate-200 font-medium text-slate-900"></div>
<div className="ph-no-capture mt-4 h-full animate-pulse rounded-lg bg-slate-200 text-slate-900"></div>
</div>
</div>
);

View File

@@ -1,7 +1,6 @@
import { QuestionMarkCircleIcon } from "@heroicons/react/24/solid";
import Image from "next/image";
import Link from "next/link";
import React from "react";
import { Button } from "@formbricks/ui/Button";
@@ -14,7 +13,7 @@ export default function NotFound() {
<div className="flex flex-col items-center space-y-3 text-slate-300">
<QuestionMarkCircleIcon className="h-20 w-20" />,
<h1 className="text-4xl font-bold text-slate-800">Survey not found.</h1>
<p className="text-lg leading-10 text-gray-500">There is no survey with this ID.</p>
<p className="text-lg leading-10 text-slate-500">There is no survey with this ID.</p>
<Button variant="darkCTA" className="mt-2" href="https://formbricks.com">
Create your own
</Button>

View File

@@ -22,6 +22,9 @@ test.describe("JS Package Test", async () => {
.getByText("Product ExperienceProduct Market Fit (Superhuman)Measure PMF by assessing how")
.click();
await page.getByRole("button", { name: "Settings", exact: true }).click();
await page.getByText("Survey Type").click();
await page.locator("label").filter({ hasText: "In-App SurveyEmbed a survey" }).click();
await page
.locator("div")

View File

@@ -17,7 +17,7 @@ export const signUpAndLogin = async (
await page.getByPlaceholder("work@email.com").press("Tab");
await page.getByPlaceholder("*******").fill(password);
await page.press('input[name="password"]', "Enter");
await page.getByRole("link", { name: "Login" }).click();
await page.getByText("Login").click();
await page.getByRole("button", { name: "Login with Email" }).click();
await page.getByPlaceholder("work@email.com").fill(email);
await page.getByPlaceholder("*******").click();
@@ -64,7 +64,7 @@ export const signupUsingInviteToken = async (page: Page, name: string, email: st
await page.getByPlaceholder("work@email.com").press("Tab");
await page.getByPlaceholder("*******").fill(password);
await page.press('input[name="password"]', "Enter");
await page.getByRole("link", { name: "Login" }).click();
await page.getByText("Login").click();
await page.getByRole("button", { name: "Login with Email" }).click();
await page.getByPlaceholder("work@email.com").fill(email);
await page.getByPlaceholder("*******").click();

View File

@@ -41,7 +41,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"bg-background animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-50 grid w-full gap-4 rounded-b-lg border p-6 shadow-lg sm:max-w-lg sm:rounded-lg",
"bg-background animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-50 grid gap-4 rounded-b-lg border p-6 shadow-lg sm:rounded-lg",
className
)}
{...props}>
@@ -105,4 +105,4 @@ const DialogDescription = React.forwardRef<
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription };
export { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger };

View File

@@ -21,7 +21,7 @@ export default async function EnvironmentNotice({ environmentId, subPageUrl }: E
<div>
<div className="flex items-center space-y-3 rounded-lg border border-blue-100 bg-blue-50 p-4 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base">
<LightBulbIcon className="mr-3 h-4 w-4 text-blue-400" />
<p>
<p className="text-sm">
{`You're currently in the ${environment.type} environment.`}
<a
href={`${WEBAPP_URL}/environments/${otherEnvironmentId}${subPageUrl}`}

View File

@@ -29,10 +29,10 @@ function Pagination({
<li>
<a
href={previousPageLink}
className={`ml-0 flex h-8 items-center justify-center rounded-l-lg border border-gray-300 bg-white px-3 text-gray-500 ${
className={`ml-0 flex h-8 items-center justify-center rounded-l-lg border border-slate-300 bg-white px-3 text-slate-500 ${
currentPage === 1
? "cursor-not-allowed opacity-50"
: "hover:bg-gray-100 hover:text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
: "hover:bg-slate-100 hover:text-slate-700 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-white"
}`}>
Previous
</a>
@@ -45,8 +45,8 @@ function Pagination({
<a
href={pageNum === currentPage ? "#" : pageLink}
className={`flex h-8 items-center justify-center px-3 ${
pageNum === currentPage ? "bg-blue-50 text-green-500" : "bg-white text-gray-500"
} border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white`}>
pageNum === currentPage ? "bg-blue-50 text-green-500" : "bg-white text-slate-500"
} border border-slate-300 hover:bg-slate-100 hover:text-slate-700 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-white`}>
{pageNum}
</a>
</li>
@@ -56,10 +56,10 @@ function Pagination({
<li>
<a
href={nextPageLink}
className={`ml-0 flex h-8 items-center justify-center rounded-r-lg border border-gray-300 bg-white px-3 text-gray-500 ${
className={`ml-0 flex h-8 items-center justify-center rounded-r-lg border border-slate-300 bg-white px-3 text-slate-500 ${
currentPage === totalPages
? "cursor-not-allowed opacity-50"
: "hover:bg-gray-100 hover:text-gray-700 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
: "hover:bg-slate-100 hover:text-slate-700 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-white"
}`}>
Next
</a>

View File

@@ -30,7 +30,7 @@ export const RatingResponse: React.FC<RatingResponseProps> = ({ scale, range, an
if (i < parseInt(answer.toString())) {
stars.push(<StarIcon key={i} className="h-7 text-yellow-400" />);
} else {
stars.push(<StarIcon key={i} className="h-7 text-gray-300" />);
stars.push(<StarIcon key={i} className="h-7 text-slate-300" />);
}
}
return <div className="flex">{stars}</div>;

View File

@@ -23,7 +23,7 @@ const SearchBox = React.forwardRef<HTMLInputElement, InputProps>(({ className, .
{...props}
/>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<Search className="h-5 w-5 text-gray-400" />
<Search className="h-5 w-5 text-slate-400" />
</div>
</div>
);

View File

@@ -170,7 +170,7 @@ export default function ResponseNotes({ user, responseId, notes, isOpen, setIsOp
onClick={() => {
handleEditPencil(note);
}}>
<PencilIcon className="h-3 w-3 text-gray-500" />
<PencilIcon className="h-3 w-3 text-slate-500" />
</button>
)}
<TooltipProvider>
@@ -181,7 +181,7 @@ export default function ResponseNotes({ user, responseId, notes, isOpen, setIsOp
onClick={() => {
handleResolveNote(note);
}}>
<CheckIcon className="h-4 w-4 text-gray-500" />
<CheckIcon className="h-4 w-4 text-slate-500" />
</button>
</TooltipTrigger>
<TooltipContent className="max-w-[45rem] break-all" side="left" sideOffset={5}>

View File

@@ -16,8 +16,7 @@ import { timeSince } from "@formbricks/lib/time";
import { formatDateWithOrdinal } from "@formbricks/lib/utils/datetime";
import { TEnvironment } from "@formbricks/types/environment";
import { TResponse } from "@formbricks/types/responses";
import { TSurveyQuestionType } from "@formbricks/types/surveys";
import { TSurvey } from "@formbricks/types/surveys";
import { TSurvey, TSurveyQuestionType } from "@formbricks/types/surveys";
import { TTag } from "@formbricks/types/tags";
import { TUser } from "@formbricks/types/user";
@@ -298,7 +297,7 @@ export default function SingleResponseCard({
className={`h-4 w-4 ${
canResponseBeDeleted
? "text-slate-500 hover:text-red-700"
: "cursor-not-allowed text-gray-400"
: "cursor-not-allowed text-slate-400"
} `}
/>
</TooltipRenderer>

View File

@@ -1,7 +1,7 @@
import { cn } from "@formbricks/lib/cn";
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("animate-pulse rounded-full bg-gray-200", className)} {...props}></div>;
return <div className={cn("animate-pulse rounded-full bg-slate-200", className)} {...props}></div>;
}
export { Skeleton };

View File

@@ -14,9 +14,9 @@ export const UpgradePlanNotice = ({
}) => {
return (
<Alert className="flex items-center">
<LightBulbIcon className="h-5 w-5 text-gray-500" />
<LightBulbIcon className="h-5 w-5 text-slate-500" />
<AlertDescription>
<span className="mr-2 text-sm text-gray-500">{message}</span>
<span className="mr-2 text-sm text-slate-500">{message}</span>
<span className="underline">
<Link href={url}>{textForUrl}</Link>
</span>