mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-21 18:18:48 -06:00
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:
@@ -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 />
|
||||
|
||||
@@ -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 'Reset' 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 'Free'
|
||||
</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 'Paid'
|
||||
</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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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("{watch("name")}")
|
||||
</span>{" "}
|
||||
in your code. Read more in our{" "}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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{" "}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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{" "}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.{" "}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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)"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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" />}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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="">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user