Merge branch 'main' of https://github.com/formbricks/formbricks into feat/3214-unify-menu

This commit is contained in:
Johannes
2024-10-14 17:52:29 -07:00
98 changed files with 1632 additions and 1384 deletions

View File

@@ -21,7 +21,7 @@
"forwardPorts": [3000, 5432, 8025],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "cp .env.example .env && sed -i '/^ENCRYPTION_KEY=/c\\ENCRYPTION_KEY='$(openssl rand -hex 32) .env && sed -i '/^NEXTAUTH_SECRET=/c\\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env && pnpm install && pnpm db:migrate:dev",
"postCreateCommand": "cp .env.example .env && sed -i '/^ENCRYPTION_KEY=/c\\ENCRYPTION_KEY='$(openssl rand -hex 32) .env && sed -i '/^NEXTAUTH_SECRET=/c\\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env && sed -i '/^CRON_SECRET=/c\\CRON_SECRET='$(openssl rand -hex 32) .env && pnpm install && pnpm db:migrate:dev",
"postAttachCommand": "pnpm dev --filter=@formbricks/web... --filter=@formbricks/demo...",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.

2
.gitignore vendored
View File

@@ -58,3 +58,5 @@ packages/lib/uploads
# Vite Timestamps
*vite.config.*.timestamp-*
# js compiled assets
apps/web/public/js

View File

@@ -1,5 +1,7 @@
<div id="top"></div>
<p align="center">Help us grow and star us on Github! ⭐️</p>
<p align="center">
<a href="https://formbricks.com">

View File

@@ -5,7 +5,7 @@
"scripts": {
"clean": "rimraf .turbo node_modules .next",
"dev": "next dev -p 3002 --turbo",
"go": "next dev -p 3002",
"go": "next dev -p 3002 --turbo",
"build": "next build",
"start": "next start",
"lint": "next lint"
@@ -13,8 +13,8 @@
"dependencies": {
"@formbricks/js": "workspace:*",
"@formbricks/ui": "workspace:*",
"lucide-react": "0.418.0",
"next": "14.2.10",
"lucide-react": "0.452.0",
"next": "14.2.15",
"react": "18.3.1",
"react-dom": "18.3.1"
},

View File

@@ -43,7 +43,7 @@ All you need to do is copy a `<script>` tag to your HTML head, and thats abou
var apiHost = "https://app.formbricks.com";
var environmentId = "<your-environment-id>";
var userId = "<your-user-id>"; //optional
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=apiHost+"/api/packages/js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: environmentId, apiHost: apiHost, userId: userId})},500)}();
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=apiHost+"/js/formbricks.umd.cjs";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: environmentId, apiHost: apiHost, userId: userId})},500)}();
</script>
<!-- END Formbricks Surveys -->
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -0,0 +1,169 @@
import { MdxImage } from "@/components/MdxImage";
import AddQuestion from "./images/add-question.webp";
import EmailField from "./images/email-field.webp";
import EmbedImage from "./images/embed.png";
import MessageField from "./images/message-field.webp";
import NameField from "./images/name-field.webp";
import QueryImage from "./images/query-form.webp";
import SingleSelect from "./images/single-select-questionare.webp";
import ToggleButton from "./images/welcome1.webp";
export const metadata = {
title: "Creating a Contact Form with Formbricks: A Step-by-Step Guide",
description:
"Learn how to easily create a professional contact form using Formbricks, perfect for beginners and experts alike.",
};
# Creating a Contact Form with Formbricks: A Step-by-Step Guide
Welcome to this comprehensive guide on creating a contact form using Formbricks. Whether you're just starting out or you're a seasoned developer, this tutorial will walk you through every step of building an engaging and effective contact form.
## What Well Build
By the end of this tutorial, you'll have created a simple contact form featuring:
1. A welcoming introduction.
2. Fields for collecting the user's name and email.
3. A question to find out why theyre contacting you.
4. A message field for users to share their queries.
### Setting Up Your Form
First, let's lay the groundwork for your form:
1. Head to the Surveys page and click on **New Survey**.
2. Select **Start from Scratch** to create a new form.
3. In the form editor, click the three dots next to a question, then select **Change Question Type** and choose **Statement (Call to Action)**.
<MdxImage
src={ToggleButton}
alt="Toggle button for Statement (Call to Action)"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
4. Add a welcoming statement to greet your users and explain the form's purpose.
5. Personalize the greeting to make it inviting and encourage engagement.
<Note>
A warm welcome sets the tone for your form. Make it friendly to encourage users to participate.
</Note>
### Adding the Name Field
Next, let's capture the user's name:
1. Click **Add Question**.
<MdxImage
src={AddQuestion}
alt="Adding a question in Formbricks"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
2. Enter the prompts for the name field.
3. Turn off the **Long Answer** option at the bottom right.
4. Adjust any **settings**, such as making the field required.
<MdxImage
src={NameField}
alt="Name field configuration"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
### Adding the Email Field
Now, lets add a field to collect the user's email address:
1. Click **Add Question** again.
2. Select Email as the input type.
3. Enter a prompt for the email field.
<MdxImage
src={EmailField}
alt="Email field configuration"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
### Adding a Reason for Contact
Lets now understand why the user is contacting you:
1. Click **Add Question** once again.
2. Select **Change Question** Type and choose **Single Select**.
3. Add the question "Why are you contacting us today?"
<MdxImage
src={SingleSelect}
alt="Single Select question configuration"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
<Note>Predefined options help categorize inquiries, making it easier for you to respond appropriately.</Note>
4. Add options like "General Inquiry," "Support," and "Feedback."
<MdxImage
src={QueryImage}
alt="Single Select question configuration"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
### Adding a Message Field
Finally, lets provide a space for the users message:
1. Click **Add Question** for the last time.
2. Add the question: "Your Message."
3. Set the placeholder text to something like "Please write your message here."
<MdxImage
src={MessageField}
alt="Message field configuration"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
4. Consider setting a minimum character count to ensure detailed messages.
### Finalizing Your Form
Once your form is complete, follow these final steps:
1. Review and rearrange the questions if necessary.
2. Test the form by filling it out as a user.
3. Customize the **Thank You** message for submissions.
4. Publish the form to get a shareable link.
5. Enable submission notifications:
- Go to your Formbricks account settings.
- Verify your email address.
- Ensure that **Survey** notifications are enabled.
### Integrating the Contact Form into Your Website
After publishing the form, follow these steps to integrate it into your site:
1. **Copy the Shareable Link**
- Find your form in the Formbricks dashboard, and click Share.
- Select Embed in a Web Page.
<MdxImage
src={EmbedImage}
alt="Embed Image configuration"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
2. **Embed the Code**
- Copy the provided code and paste it into your website where you want the form to appear.
3. **Test the Integration**
- Check if the form displays correctly on your site.
- Submit a test entry to ensure everything works and notifications are received.
## Conclusion
Congratulations! Youve successfully created and integrated a professional contact form using Formbricks. This form will help you collect valuable information from your visitors in an efficient, user-friendly way.
A great contact form strikes the balance between collecting necessary details and being simple enough to encourage submissions. **Youve achieved just that!**

View File

@@ -43,7 +43,7 @@ const FAQ_DATA = [
Absolutely! We provide an option for users to host Formbricks on their own server, ensuring even more
control over data and compliance. And the best part? Self-hosting is available for free, always. For
documentation on self hosting, click{" "}
<a href="/docs/self-hosting/deployment" className="text-brand-dark dark:text-brand-light">
<a href="/self-hosting/deployment" className="text-brand-dark dark:text-brand-light">
here
</a>
.

View File

@@ -34,8 +34,8 @@ Once you open a PR, you will get a message from the CLA bot to fill out the form
We currently officially support the below methods to set up your development environment for Formbricks:
- [Gitpod](/docs/developer-docs/contributing/gitpod)
- [GitHub Codespaces](/docs/developer-docs/contributing/codespaces)
- [Gitpod](/developer-docs/contributing/gitpod)
- [GitHub Codespaces](/developer-docs/contributing/codespaces)
- [Local Machine Setup](#local-machine-setup)
Both Gitpod and GitHub Codespaces have a **generous free tier** to explore and develop. For junior developers we suggest using either of these, because you can dive into coding within minutes, not hours.

View File

@@ -34,7 +34,7 @@ Explore the Formbricks REST API documentation with Postman, providing detailed i
Learn about Formbricks Webhooks and how to set up event-driven notifications in your applications using our UI as well as API. Stay updated with real-time data and trigger actions based on specific response events within your Formbricks environment.
### [Contributing to Formbricks](/developer-docs/contributing)
### [Contributing to Formbricks](/developer-docs/contributing/get-started)
Interested in contributing to the Formbricks ecosystem? This page provides guidance on how to run Formbricks locally, report issues, contribute through code, and collaborate with the Formbricks community to enhance the platform for everyone.

View File

@@ -8,6 +8,104 @@ export const metadata = {
# Migration Guide
## v2.6
Formbricks v2.6 introduces advanced logic jumps for surveys, allowing you to add more advanced branching logic to your surveys including variables, and/or conditions and many more. This release also includes a lot of bug fixes, big performance improvements to website and app surveys and a lot of stability improvements.
<Note>
This release includes the last step of the server-side action tracking deprecation (previously used for
segment filtering by performed actions). The migrations included in this release will delete all tracked
actions from the database. If you still need these action records, please make sure to export them before
upgrading.
</Note>
### Steps to Migrate
This guide is for users who are self-hosting Formbricks using our one-click setup. If you are using a different setup, you might adjust the commands accordingly.
To run all these steps, please navigate to the `formbricks` folder where your `docker-compose.yml` file is located.
1. **Backup your Database**: This is a crucial step. Please make sure to backup your database before proceeding with the upgrade. You can use the following command to backup your database:
<Col>
<CodeGroup title="Backup Postgres">
```bash
docker exec formbricks-postgres-1 pg_dump -Fc -U postgres -d formbricks > formbricks_pre_v2.6_$(date +%Y%m%d_%H%M%S).dump
```
</CodeGroup>
</Col>
<Note>
If you run into “No such container”, use `docker ps` to find your container name, e.g.
`formbricks_postgres_1`.
</Note>
<Note>
If you prefer storing the backup as an `*.sql` file remove the `-Fc` (custom format) option. In case of a
restore scenario you will need to use `psql` then with an empty `formbricks` database.
</Note>
2. Pull the latest version of Formbricks:
<Col>
<CodeGroup title="Stop the containers">
```bash
docker compose pull
```
</CodeGroup>
</Col>
3. Stop the running Formbricks instance & remove the related containers:
<Col>
<CodeGroup title="Stop the containers">
```bash
docker compose down
```
</CodeGroup>
</Col>
4. Restarting the containers with the latest version of Formbricks:
<Col>
<CodeGroup title="Restart the containers">
```bash
docker compose up -d
```
</CodeGroup>
</Col>
5. Now let's migrate the data to the latest schema:
<Note>To find your Docker Network name for your Postgres Database, find it using `docker network ls`</Note>
<Col>
<CodeGroup title="Migrate the data">
```bash
docker pull ghcr.io/formbricks/data-migrations:latest && \
docker run --rm \
--network=formbricks_default \
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
-e UPGRADE_TO_VERSION="v2.6" \
ghcr.io/formbricks/data-migrations:v2.6.0
```
</CodeGroup>
</Col>
The above command will migrate your data to the latest schema. This is a crucial step to migrate your existing data to the new structure. Only if the script runs successful, changes are made to the database. The script can safely run multiple times.
6. That's it! Once the migration is complete, you can **now access your Formbricks instance** at the same URL as before.
## v2.5
Formbricks v2.5 allows you to visualize responses in a data table format. This release also includes a few bug fixes and performance improvements.
@@ -95,7 +193,7 @@ docker run --rm \
--network=formbricks_default \
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
-e UPGRADE_TO_VERSION="v2.5" \
ghcr.io/formbricks/data-migrations:latest
ghcr.io/formbricks/data-migrations:v2.5.3
```
</CodeGroup>

View File

@@ -164,20 +164,13 @@ const NavigationGroup = ({
if (openGroups.includes(title)) {
setOpenGroups(openGroups.filter((t) => t !== title));
} else {
setOpenGroups([...openGroups, title]);
setOpenGroups([title]);
}
setActiveGroup(group);
};
const isParentOpen = (title: string) => openGroups.includes(title);
const sortedLinks = group.links.map((link) => {
if (link.children) {
link.children.sort((a, b) => a.title.localeCompare(b.title));
}
return link;
});
return (
<li className={clsx("relative mt-6", className)}>
<motion.h2 layout="position" className="font-semibold text-slate-900 dark:text-white">
@@ -192,8 +185,8 @@ const NavigationGroup = ({
{isActiveGroup && <ActivePageMarker group={group} pathname={pathname || "/docs"} />}
</AnimatePresence>
<ul role="list" className="border-l border-transparent">
{sortedLinks.map((link) => (
<motion.li key={link.title} layout="position" className="relative">
{group.links.map((link) => (
<motion.li key={`${group.title}-${link.title}`} layout="position" className="relative">
{link.href ? (
<NavLink
href={isMobile && link.children ? "" : link.href}
@@ -201,19 +194,19 @@ const NavigationGroup = ({
{link.title}
</NavLink>
) : (
<div onClick={() => toggleParentTitle(link.title)}>
<div onClick={() => toggleParentTitle(`${group.title}-${link.title}`)}>
<NavLink
href={!isMobile ? link.children?.[0]?.href || "" : undefined}
active={
!!(
isParentOpen(link.title) &&
isParentOpen(`${group.title}-${link.title}`) &&
link.children &&
link.children.some((child) => pathname.startsWith(child.href))
)
}>
<span className="flex w-full justify-between">
{link.title}
{isParentOpen(link.title) ? (
{isParentOpen(`${group.title}-${link.title}`) ? (
<ChevronUpIcon className="my-1 h-4" />
) : (
<ChevronDownIcon className="my-1 h-4" />
@@ -223,7 +216,7 @@ const NavigationGroup = ({
</div>
)}
<AnimatePresence mode="popLayout" initial={false}>
{isActiveGroup && link.children && isParentOpen(link.title) && (
{isActiveGroup && link.children && isParentOpen(`${group.title}-${link.title}`) && (
<motion.ul
role="list"
initial={{ opacity: 0 }}

View File

@@ -18,6 +18,7 @@ export const navigation: Array<NavGroup> = [
{ title: "Feedback Box", href: "/best-practices/feedback-box" },
{ title: "Docs Feedback", href: "/best-practices/docs-feedback" },
{ title: "Improve Email Content", href: "/best-practices/improve-email-content" },
{ title: "Contact Form", href: "/best-practices/contact-form" },
],
},
],
@@ -42,7 +43,7 @@ export const navigation: Array<NavGroup> = [
{ title: "Custom Styling", href: "/global/overwrite-styling" }, // global
{ title: "Conditional Logic", href: "/global/conditional-logic" },
{ title: "Shareable Dashboards", href: "/global/shareable-dashboards" },
{ title: "Start & End Dates", href: "/global/custom-start-end-conditions" },
{ title: "Start & End Dates", href: "/global/schedule-start-end-dates" },
{ title: "Limit submissions", href: "/global/limit-submissions" }, // global
{ title: "Recall Functionality", href: "/global/recall" },
{ title: "Verify Email before Survey", href: "/link-surveys/verify-email-before-survey" },
@@ -70,7 +71,7 @@ export const navigation: Array<NavGroup> = [
{ title: "User Metadata", href: "/global/metadata" }, // global
{ title: "Custom Styling", href: "/global/overwrite-styling" }, // global
{ title: "Conditional Logic", href: "/global/conditional-logic" }, // global
{ title: "Start & End Dates", href: "/global/custom-start-end-conditions" }, // global
{ title: "Start & End Dates", href: "/global/schedule-start-end-dates" }, // global
{ title: "Limit submissions", href: "/global/limit-submissions" }, // global
{ title: "Recall Functionality", href: "/global/recall" }, // global
{ title: "Partial Submissions", href: "/global/partial-submissions" }, // global
@@ -82,6 +83,20 @@ export const navigation: Array<NavGroup> = [
{
title: "Core Features",
links: [
{
title: "Integrations",
children: [
{ title: "Overview", href: "/developer-docs/integrations/overview" },
{ title: "Airtable", href: "/developer-docs/integrations/airtable" },
{ title: "Google Sheets", href: "/developer-docs/integrations/google-sheets" },
{ title: "Make", href: "/developer-docs/integrations/make" },
{ title: "n8n", href: "/developer-docs/integrations/n8n" },
{ title: "Notion", href: "/developer-docs/integrations/notion" },
{ title: "Slack", href: "/developer-docs/integrations/slack" },
{ title: "Wordpress", href: "/developer-docs/integrations/wordpress" },
{ title: "Zapier", href: "/developer-docs/integrations/zapier" },
],
},
{ title: "Access Roles", href: "/global/access-roles" },
{ title: "Styling Theme", href: "/global/styling-theme" },
],
@@ -103,20 +118,6 @@ export const navigation: Array<NavGroup> = [
title: "Developer Docs",
links: [
{ title: "Overview", href: "/developer-docs/overview" },
{
title: "Integrations",
children: [
{ title: "Overview", href: "/developer-docs/integrations/overview" },
{ title: "Airtable", href: "/developer-docs/integrations/airtable" },
{ title: "Google Sheets", href: "/developer-docs/integrations/google-sheets" },
{ title: "Make", href: "/developer-docs/integrations/make" },
{ title: "n8n", href: "/developer-docs/integrations/n8n" },
{ title: "Notion", href: "/developer-docs/integrations/notion" },
{ title: "Slack", href: "/developer-docs/integrations/slack" },
{ title: "Wordpress", href: "/developer-docs/integrations/wordpress" },
{ title: "Zapier", href: "/developer-docs/integrations/zapier" },
],
},
{ title: "SDK: Formbricks JS", href: "/developer-docs/js-sdk" },
{ title: "SDK: React Native", href: "/developer-docs/react-native-in-app-surveys" },
{ title: "SDK: Formbricks API", href: "/developer-docs/api-sdk" },

View File

@@ -36,10 +36,10 @@
"framer-motion": "11.11.4",
"lottie-web": "5.12.2",
"lucide": "0.451.0",
"lucide-react": "0.451.0",
"lucide-react": "0.452.0",
"mdast-util-to-string": "4.0.0",
"mdx-annotations": "0.1.4",
"next": "14.2.10",
"next": "14.2.15",
"next-plausible": "3.12.2",
"next-seo": "6.6.0",
"next-sitemap": "4.2.3",

View File

@@ -34,7 +34,7 @@ export const OnboardingSetupInstructions = ({
var apiHost = "${webAppUrl}";
var environmentId = "${environmentId}";
var userId = "testUser";
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=apiHost+"/api/packages/js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: environmentId, apiHost: apiHost, userId: userId})},500)}();
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=apiHost+"/js/formbricks.umd.cjs";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: environmentId, apiHost: apiHost, userId: userId})},500)}();
</script>
<!-- END Formbricks Surveys -->
`;
@@ -44,7 +44,7 @@ export const OnboardingSetupInstructions = ({
!function(){
var apiHost = "${webAppUrl}";
var environmentId = "${environmentId}";
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=apiHost+"/api/packages/js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: environmentId, apiHost: apiHost})},500)}();
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=apiHost+"/js/formbricks.umd.cjs";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: environmentId, apiHost: apiHost})},500)}();
</script>
<!-- END Formbricks Surveys -->
`;

View File

@@ -21,6 +21,7 @@ interface AddQuestionButtonProps {
export const AddQuestionButton = ({ addQuestion, product, isCxMode }: AddQuestionButtonProps) => {
const [open, setOpen] = useState(false);
const [hoveredQuestionId, setHoveredQuestionId] = useState<string | null>(null);
const availableQuestionTypes = isCxMode ? CXQuestionTypes : questionTypes;
@@ -30,7 +31,7 @@ export const AddQuestionButton = ({ addQuestion, product, isCxMode }: AddQuestio
onOpenChange={setOpen}
className={cn(
open ? "shadow-lg" : "shadow-md",
"group w-full space-y-2 rounded-lg border border-slate-300 bg-white duration-300 hover:cursor-pointer hover:bg-slate-50"
"group w-full space-y-2 rounded-lg border border-slate-300 bg-white duration-200 hover:cursor-pointer hover:bg-slate-50"
)}>
<Collapsible.CollapsibleTrigger asChild className="group h-full w-full">
<div className="inline-flex">
@@ -49,7 +50,7 @@ export const AddQuestionButton = ({ addQuestion, product, isCxMode }: AddQuestio
<button
type="button"
key={questionType.id}
className="mx-2 inline-flex items-center rounded p-0.5 px-4 py-2 text-sm font-medium text-slate-700 last:mb-2 hover:bg-slate-100 hover:text-slate-800"
className="group relative mx-2 inline-flex items-center justify-between rounded p-0.5 px-4 py-2 text-sm font-medium text-slate-700 last:mb-2 hover:bg-slate-100 hover:text-slate-800"
onClick={() => {
addQuestion({
...universalQuestionPresets,
@@ -58,9 +59,19 @@ export const AddQuestionButton = ({ addQuestion, product, isCxMode }: AddQuestio
type: questionType.id,
});
setOpen(false);
}}>
<questionType.icon className="text-brand-dark -ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
{questionType.label}
}}
onMouseEnter={() => setHoveredQuestionId(questionType.id)}
onMouseLeave={() => setHoveredQuestionId(null)}>
<div className="flex items-center">
<questionType.icon className="text-brand-dark -ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
{questionType.label}
</div>
<div
className={`absolute right-4 text-xs font-light text-slate-500 transition-opacity duration-200 ${
hoveredQuestionId === questionType.id ? "opacity-100" : "opacity-0"
}`}>
{questionType.description}
</div>
</button>
))}
</Collapsible.CollapsibleContent>

View File

@@ -171,7 +171,7 @@ export const CreateNewActionTab = ({
<div>
<FormProvider {...form}>
<form onSubmit={handleSubmit(submitHandler)}>
<div className="max-h-[500px] w-full space-y-4 overflow-y-auto pr-4">
<div className="max-h-[400px] w-full space-y-4 overflow-y-auto pr-4">
<div className="w-3/5">
<FormField
name={`type`}

View File

@@ -166,7 +166,9 @@ export const EditorCardMenu = ({
<div className="flex flex-col">
{cardType === "question" && (
<DropdownMenuSub>
<DropdownMenuSubTrigger className="cursor-pointer text-sm text-slate-600 hover:text-slate-700">
<DropdownMenuSubTrigger
className="cursor-pointer text-sm text-slate-600 hover:text-slate-700"
onClick={(e) => e.preventDefault()}>
Change question type
</DropdownMenuSubTrigger>
@@ -206,7 +208,9 @@ export const EditorCardMenu = ({
{cardType === "question" && (
<DropdownMenuSub>
<DropdownMenuSubTrigger className="cursor-pointer">Add question below</DropdownMenuSubTrigger>
<DropdownMenuSubTrigger className="cursor-pointer" onClick={(e) => e.preventDefault()}>
Add question below
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="ml-2">
{Object.entries(availableQuestionTypes).map(([type, name]) => {

View File

@@ -0,0 +1,24 @@
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { cn } from "@formbricks/lib/cn";
export const LoadingCard = ({
title,
description,
skeletonLines,
}: {
title: string;
description: string;
skeletonLines: Array<{ classes: string }>;
}) => {
return (
<SettingsCard title={title} description={description}>
<div className="w-full space-y-4">
{skeletonLines.map((line, index) => (
<div key={index}>
<div className={cn("animate-pulse rounded-full bg-slate-200", line.classes)}></div>
</div>
))}
</div>
</SettingsCard>
);
};

View File

@@ -3,10 +3,7 @@
import { z } from "zod";
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
import {
getOrganizationIdFromAttributeClassId,
getOrganizationIdFromEnvironmentId,
} from "@formbricks/lib/organization/utils";
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
import { getSegmentsByAttributeClassName } from "@formbricks/lib/segment/service";
import { ZAttributeClass } from "@formbricks/types/attribute-classes";
import { ZId } from "@formbricks/types/common";
@@ -19,12 +16,6 @@ const ZGetSegmentsByAttributeClassAction = z.object({
export const getSegmentsByAttributeClassAction = authenticatedActionClient
.schema(ZGetSegmentsByAttributeClassAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromAttributeClassId(parsedInput.attributeClass.id),
rules: ["attributeClass", "read"],
});
await checkAuthorization({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),

View File

@@ -193,7 +193,7 @@ const Page = async ({ params }) => {
];
integrationCards.unshift({
docsHref: "https://formbricks.com/docs/getting-started/framework-guides#next-js",
docsHref: "https://formbricks.com/docs/app-surveys/quickstart",
docsText: "Docs",
docsNewTab: true,
connectHref: `/environments/${environmentId}/product/app-connection`,

View File

@@ -1,138 +1,46 @@
"use client";
import { BrushIcon, KeyIcon, LanguagesIcon, ListChecksIcon, TagIcon, UsersIcon } from "lucide-react";
import { usePathname } from "next/navigation";
import { cn } from "@formbricks/lib/cn";
import { LoadingCard } from "@/app/(app)/components/LoadingCard";
import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const LoadingCard = ({ title, description, skeletonLines }) => {
return (
<div className="w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 text-left shadow-sm">
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
<h3 className="text-lg font-medium leading-6">{title}</h3>
<p className="mt-1 text-sm text-slate-500">{description}</p>
</div>
<div className="w-full">
<div className="rounded-lg px-4">
{skeletonLines.map((line, index) => (
<div key={index} className="mt-4">
<div
className={`flex animate-pulse flex-col items-center justify-center space-y-2 rounded-lg bg-slate-200 py-6 text-center ${line.classes}`}></div>
</div>
))}
</div>
</div>
</div>
);
};
const Loading = () => {
const pathname = usePathname();
const cards = [
{
title: "Widget Status",
description: "Check if the Formbricks widget is alive and kicking.",
skeletonLines: [{ classes: " h-44 max-w-full rounded-md" }],
title: "Website & App Connection Status",
description: "Check if your app is successfully connected with Formbricks. Reload page to recheck.",
skeletonLines: [{ classes: " h-44 max-w-full rounded-lg" }],
},
{
title: "How to setup",
description: "Follow these steps to setup the Formbricks widget within your app.",
skeletonLines: [
{ classes: "h-12 w-24 rounded-lg" },
{ classes: "h-10 w-60 rounded-lg" },
{ classes: "h-10 w-60 rounded-lg" },
{ classes: "h-12 w-24 rounded-lg" },
{ classes: "h-10 w-60 rounded-lg" },
{ classes: "h-10 w-60 rounded-lg" },
],
},
{
title: "Your EnvironmentId",
description: "This id uniquely identifies this Formbricks environment.",
skeletonLines: [{ classes: "h-6 w-4/6 rounded-full" }],
},
{
title: "How To Setup",
description: "Follow these steps to setup the Formbricks widget within your app",
skeletonLines: [
{ classes: "h-6 w-24 rounded-full" },
{ classes: "h-4 w-60 rounded-full" },
{ classes: "h-4 w-60 rounded-full" },
{ classes: "h-6 w-24 rounded-full" },
{ classes: "h-4 w-60 rounded-full" },
{ classes: "h-4 w-60 rounded-full" },
],
},
];
let navigation = [
{
id: "general",
label: "General",
icon: <UsersIcon className="h-5 w-5" />,
current: pathname?.includes("/general"),
},
{
id: "look",
label: "Look & Feel",
icon: <BrushIcon className="h-5 w-5" />,
current: pathname?.includes("/look"),
},
{
id: "languages",
label: "Survey Languages",
icon: <LanguagesIcon className="h-5 w-5" />,
hidden: true,
current: pathname?.includes("/languages"),
},
{
id: "tags",
label: "Tags",
icon: <TagIcon className="h-5 w-5" />,
current: pathname?.includes("/tags"),
},
{
id: "api-keys",
label: "API Keys",
icon: <KeyIcon className="h-5 w-5" />,
current: pathname?.includes("/api-keys"),
},
{
id: "website-connection",
label: "Website Connection",
icon: <ListChecksIcon className="h-5 w-5" />,
current: pathname?.includes("/website-connection"),
hidden: true,
},
{
id: "app-connection",
label: "App Connection",
icon: <ListChecksIcon className="h-5 w-5" />,
current: pathname?.includes("/app-connection"),
hidden: true,
skeletonLines: [{ classes: "h-12 w-4/6 rounded-lg" }],
},
];
return (
<div>
<PageContentWrapper>
<PageHeader pageTitle="Configuration">
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
{navigation.map((navElem) => (
<div
key={navElem.id}
className={cn(
navElem.id === "app-connection"
? "border-brand-dark border-b-2 font-semibold text-slate-900"
: "border-transparent text-slate-500 transition-all duration-150 ease-in-out hover:border-slate-300 hover:text-slate-700",
"flex h-full items-center border-b-2 px-3 text-sm font-medium",
navElem.hidden && "hidden"
)}
aria-current={navElem.id === "app-connection" ? "page" : undefined}>
{navElem.label}
</div>
))}
</nav>
<div className="justify-self-end"></div>
</div>
</PageHeader>
<div className="mt-4 flex max-w-4xl animate-pulse items-center space-y-4 rounded-lg border bg-blue-50 p-6 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base"></div>
{cards.map((card, index) => (
<LoadingCard key={index} {...card} />
))}
</PageContentWrapper>
</div>
<PageContentWrapper>
<PageHeader pageTitle="Configuration">
<ProductConfigNavigation activeId="app-connection" loading />
</PageHeader>
<div className="mt-4 flex max-w-4xl animate-pulse items-center space-y-4 rounded-lg border bg-blue-50 p-6 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base"></div>
{cards.map((card, index) => (
<LoadingCard key={index} {...card} />
))}
</PageContentWrapper>
);
};

View File

@@ -43,17 +43,17 @@ const Page = async ({ params }) => {
description="Check if your app is successfully connected with Formbricks. Reload page to recheck.">
{environment && <WidgetStatusIndicator environment={environment} />}
</SettingsCard>
<SettingsCard
title="How to setup"
description="Follow these steps to setup the Formbricks widget within your app."
noPadding>
<SetupInstructions environmentId={params.environmentId} webAppUrl={WEBAPP_URL} />
</SettingsCard>
<SettingsCard
title="Your EnvironmentId"
description="This id uniquely identifies this Formbricks environment.">
<EnvironmentIdField environmentId={params.environmentId} />
</SettingsCard>
<SettingsCard
title="How To Setup"
description="Follow these steps to setup the Formbricks widget within your app"
noPadding>
<SetupInstructions environmentId={params.environmentId} webAppUrl={WEBAPP_URL} />
</SettingsCard>
</div>
</PageContentWrapper>
);

View File

@@ -71,7 +71,7 @@ if (typeof window !== "undefined") {
Formbricks SDK. Open the browser console to see the logs.{" "}
<Link
className="decoration-brand-dark"
href="https://formbricks.com/docs/developer-docs/app-survey-sdk#debug-mode"
href="https://formbricks.com/docs/developer-docs/js-sdk#debug-mode"
target="_blank">
Read docs.
</Link>{" "}
@@ -122,7 +122,7 @@ if (typeof window !== "undefined") {
</p>
<CodeBlock language="js">{`<!-- START Formbricks Surveys -->
<script type="text/javascript">
!function(){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="${webAppUrl}/api/packages/js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: "${environmentId}", apiHost: "${window.location.protocol}//${window.location.host}"})},500)}();
!function(){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="${webAppUrl}/js/formbricks.umd.cjs";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: "${environmentId}", apiHost: "${window.location.protocol}//${window.location.host}"})},500)}();
</script>
<!-- END Formbricks Surveys -->`}</CodeBlock>
<h4>Step 2: Debug mode</h4>
@@ -131,7 +131,7 @@ if (typeof window !== "undefined") {
Formbricks SDK. Open the browser console to see the logs.{" "}
<Link
className="decoration-brand-dark"
href="https://formbricks.com/docs/developer-docs/app-survey-sdk#debug-mode"
href="https://formbricks.com/docs/developer-docs/js-sdk#debug-mode"
target="_blank">
Read docs.
</Link>{" "}

View File

@@ -1,8 +1,6 @@
"use client";
import { BrushIcon, KeyIcon, LanguagesIcon, ListChecksIcon, TagIcon, UsersIcon } from "lucide-react";
import { usePathname } from "next/navigation";
import { cn } from "@formbricks/lib/cn";
import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
@@ -26,7 +24,7 @@ const LoadingCard = () => {
</div>
</div>
<div className="flex justify-start">
<div className="mt-4 flex h-7 w-44 animate-pulse flex-col items-center justify-center rounded-md bg-black text-sm text-white">
<div className="mt-4 flex h-8 w-44 animate-pulse flex-col items-center justify-center rounded-md bg-black text-sm text-white">
Loading
</div>
</div>
@@ -37,85 +35,14 @@ const LoadingCard = () => {
};
const Loading = () => {
const pathname = usePathname();
let navigation = [
{
id: "general",
label: "General",
icon: <UsersIcon className="h-5 w-5" />,
current: pathname?.includes("/general"),
},
{
id: "look",
label: "Look & Feel",
icon: <BrushIcon className="h-5 w-5" />,
current: pathname?.includes("/look"),
},
{
id: "languages",
label: "Survey Languages",
icon: <LanguagesIcon className="h-5 w-5" />,
hidden: true,
current: pathname?.includes("/languages"),
},
{
id: "tags",
label: "Tags",
icon: <TagIcon className="h-5 w-5" />,
current: pathname?.includes("/tags"),
},
{
id: "api-keys",
label: "API Keys",
icon: <KeyIcon className="h-5 w-5" />,
current: pathname?.includes("/api-keys"),
},
{
id: "website-connection",
label: "Website Connection",
icon: <ListChecksIcon className="h-5 w-5" />,
current: pathname?.includes("/website-connection"),
hidden: true,
},
{
id: "app-connection",
label: "App Connection",
icon: <ListChecksIcon className="h-5 w-5" />,
current: pathname?.includes("/app-connection"),
hidden: true,
},
];
return (
<div>
<PageContentWrapper>
<PageHeader pageTitle="Configuration">
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
{navigation.map((navElem) => (
<div
key={navElem.id}
className={cn(
navElem.id === "api-keys"
? "border-brand-dark border-b-2 font-semibold text-slate-900"
: "border-transparent text-slate-500 transition-all duration-150 ease-in-out hover:border-slate-300 hover:text-slate-700",
"flex h-full items-center border-b-2 px-3 text-sm font-medium",
navElem.hidden && "hidden"
)}
aria-current={navElem.id === "api-keys" ? "page" : undefined}>
{navElem.label}
</div>
))}
</nav>
<div className="justify-self-end"></div>
</div>
</PageHeader>
<div className="mt-4 flex max-w-4xl animate-pulse items-center space-y-4 rounded-lg border bg-blue-50 p-6 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base"></div>
<LoadingCard />
</PageContentWrapper>
</div>
<PageContentWrapper>
<PageHeader pageTitle="Configuration">
<ProductConfigNavigation activeId="api-keys" loading />
</PageHeader>
<div className="mt-4 flex max-w-4xl animate-pulse items-center space-y-4 rounded-lg border bg-blue-50 p-6 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base"></div>
<LoadingCard />
</PageContentWrapper>
);
};

View File

@@ -5,15 +5,17 @@ import { usePathname } from "next/navigation";
import { SecondaryNavigation } from "@formbricks/ui/components/SecondaryNavigation";
interface ProductConfigNavigationProps {
environmentId: string;
activeId: string;
isMultiLanguageAllowed: boolean;
environmentId?: string;
isMultiLanguageAllowed?: boolean;
loading?: boolean;
}
export const ProductConfigNavigation = ({
environmentId,
activeId,
environmentId,
isMultiLanguageAllowed,
loading,
}: ProductConfigNavigationProps) => {
const pathname = usePathname();
let navigation = [
@@ -62,5 +64,5 @@ export const ProductConfigNavigation = ({
},
];
return <SecondaryNavigation navigation={navigation} activeId={activeId} />;
return <SecondaryNavigation navigation={navigation} activeId={activeId} loading={loading} />;
};

View File

@@ -1,34 +1,11 @@
"use client";
import { BrushIcon, KeyIcon, LanguagesIcon, ListChecksIcon, TagIcon, UsersIcon } from "lucide-react";
import { usePathname } from "next/navigation";
import { cn } from "@formbricks/lib/cn";
import { LoadingCard } from "@/app/(app)/components/LoadingCard";
import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const LoadingCard = ({ title, description, skeletonLines }) => {
return (
<div className="w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 shadow-sm">
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
<h3 className="text-lg font-medium leading-6">{title}</h3>
<p className="mt-1 text-sm text-slate-500">{description}</p>
</div>
<div className="w-full">
<div className="rounded-lg px-4 py-4 pb-0 pt-2">
{skeletonLines.map((line, index) => (
<div key={index} className="mt-4">
<div className={`animate-pulse rounded-full bg-slate-200 ${line.classes}`}></div>
</div>
))}
</div>
</div>
</div>
);
};
const Loading = () => {
const pathname = usePathname();
const cards = [
{
title: "Product Name",
@@ -48,83 +25,15 @@ const Loading = () => {
},
];
let navigation = [
{
id: "general",
label: "General",
icon: <UsersIcon className="h-5 w-5" />,
current: pathname?.includes("/general"),
},
{
id: "look",
label: "Look & Feel",
icon: <BrushIcon className="h-5 w-5" />,
current: pathname?.includes("/look"),
},
{
id: "languages",
label: "Survey Languages",
icon: <LanguagesIcon className="h-5 w-5" />,
hidden: true,
current: pathname?.includes("/languages"),
},
{
id: "tags",
label: "Tags",
icon: <TagIcon className="h-5 w-5" />,
current: pathname?.includes("/tags"),
},
{
id: "api-keys",
label: "API Keys",
icon: <KeyIcon className="h-5 w-5" />,
current: pathname?.includes("/api-keys"),
},
{
id: "website-connection",
label: "Website Connection",
icon: <ListChecksIcon className="h-5 w-5" />,
current: pathname?.includes("/website-connection"),
hidden: true,
},
{
id: "app-connection",
label: "App Connection",
icon: <ListChecksIcon className="h-5 w-5" />,
current: pathname?.includes("/app-connection"),
hidden: true,
},
];
return (
<div>
<PageContentWrapper>
<PageHeader pageTitle="Configuration">
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
{navigation.map((navElem) => (
<div
key={navElem.id}
className={cn(
navElem.id === "general"
? "border-brand-dark border-b-2 font-semibold text-slate-900"
: "border-transparent text-slate-500 transition-all duration-150 ease-in-out hover:border-slate-300 hover:text-slate-700",
"flex h-full items-center border-b-2 px-3 text-sm font-medium",
navElem.hidden && "hidden"
)}
aria-current={navElem.id === "general" ? "page" : undefined}>
{navElem.label}
</div>
))}
</nav>
<div className="justify-self-end"></div>
</div>
</PageHeader>
{cards.map((card, index) => (
<LoadingCard key={index} {...card} />
))}
</PageContentWrapper>
</div>
<PageContentWrapper>
<PageHeader pageTitle="Configuration">
<ProductConfigNavigation activeId="general" loading />
</PageHeader>
{cards.map((card, index) => (
<LoadingCard key={index} {...card} />
))}
</PageContentWrapper>
);
};

View File

@@ -0,0 +1,33 @@
"use client";
import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation";
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { LanguageLabels } from "@formbricks/ee/multi-language/components/language-labels";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Loading = () => {
return (
<PageContentWrapper>
<PageHeader pageTitle="Configuration">
<ProductConfigNavigation activeId="languages" loading />
</PageHeader>
<SettingsCard
title="Multi-language surveys"
description="Add languages to create multi-language surveys.">
<div className="flex flex-col space-y-4">
<LanguageLabels />
{[...Array(3)].map((_, idx) => (
<div key={idx} className="my-3 grid h-10 grid-cols-4 gap-4">
<div className="h-full animate-pulse rounded-md bg-slate-200" />
<div className="h-full animate-pulse rounded-md bg-slate-200" />
<div className="h-full animate-pulse rounded-md bg-slate-200" />
</div>
))}
</div>
</SettingsCard>
</PageContentWrapper>
);
};
export default Loading;

View File

@@ -1,8 +1,7 @@
"use client";
import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation";
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { BrushIcon, KeyIcon, LanguagesIcon, ListChecksIcon, TagIcon, UsersIcon } from "lucide-react";
import { usePathname } from "next/navigation";
import { cn } from "@formbricks/lib/cn";
import { Badge } from "@formbricks/ui/components/Badge";
import { Button } from "@formbricks/ui/components/Button";
@@ -21,202 +20,140 @@ const placements = [
];
const Loading = () => {
const pathname = usePathname();
let navigation = [
{
id: "general",
label: "General",
icon: <UsersIcon className="h-5 w-5" />,
current: pathname?.includes("/general"),
},
{
id: "look",
label: "Look & Feel",
icon: <BrushIcon className="h-5 w-5" />,
current: pathname?.includes("/look"),
},
{
id: "languages",
label: "Survey Languages",
icon: <LanguagesIcon className="h-5 w-5" />,
hidden: true,
current: pathname?.includes("/languages"),
},
{
id: "tags",
label: "Tags",
icon: <TagIcon className="h-5 w-5" />,
current: pathname?.includes("/tags"),
},
{
id: "api-keys",
label: "API Keys",
icon: <KeyIcon className="h-5 w-5" />,
current: pathname?.includes("/api-keys"),
},
{
id: "website-connection",
label: "Website Connection",
icon: <ListChecksIcon className="h-5 w-5" />,
current: pathname?.includes("/website-connection"),
hidden: true,
},
{
id: "app-connection",
label: "App Connection",
icon: <ListChecksIcon className="h-5 w-5" />,
current: pathname?.includes("/app-connection"),
hidden: true,
},
];
return (
<div>
<PageContentWrapper>
<PageHeader pageTitle="Configuration">
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
{navigation.map((navElem) => (
<div
key={navElem.id}
className={cn(
navElem.id === "look"
? "border-brand-dark border-b-2 font-semibold text-slate-900"
: "border-transparent text-slate-500 transition-all duration-150 ease-in-out hover:border-slate-300 hover:text-slate-700",
"flex h-full items-center border-b-2 px-3 text-sm font-medium",
navElem.hidden && "hidden"
)}
aria-current={navElem.id === "look" ? "page" : undefined}>
{navElem.label}
<PageContentWrapper>
<PageHeader pageTitle="Configuration">
<ProductConfigNavigation activeId="look" loading />
</PageHeader>
<SettingsCard
title="Theme"
className="max-w-7xl"
description="Create a style theme for all surveys. You can enable custom styling for each survey.">
<div className="flex animate-pulse">
<div className="w-1/2">
<div className="flex flex-col gap-4 pr-6">
<div className="flex flex-col gap-4 rounded-lg bg-slate-50 p-4">
<div className="flex items-center gap-6">
<Switch />
<div className="flex flex-col">
<h3 className="text-sm font-semibold text-slate-700">Enable custom styling</h3>
<p className="text-xs text-slate-500">
Allow users to override this theme in the editor.
</p>
</div>
</div>
</div>
<div className="flex flex-col gap-3 bg-slate-50 p-4">
<div className="w-full rounded-lg border border-slate-300 bg-white">
<div className="flex flex-col p-4">
<h2 className="text-sm font-semibold text-slate-700">Form Styling</h2>
<p className="mt-1 text-xs text-slate-500">
Style the question texts, descriptions and input fields.
</p>
</div>
</div>
<div className="w-full rounded-lg border border-slate-300 bg-white">
<div className="flex flex-col p-4">
<h2 className="text-sm font-semibold text-slate-700">Card Styling</h2>
<p className="mt-1 text-xs text-slate-500">Style the survey card.</p>
</div>
</div>
<div className="w-full rounded-lg border border-slate-300 bg-white">
<div className="flex flex-col p-4">
<div className="flex items-center gap-2">
<h2 className="text-sm font-semibold text-slate-700">Background Styling</h2>
<Badge text="Link Surveys" type="gray" size="normal" />
</div>
<p className="mt-1 text-xs text-slate-500">
Change the background to a color, image or animation.
</p>
</div>
</div>
</div>
</div>
</div>
<div className="relative flex w-1/2 flex-row items-center justify-center rounded-lg bg-slate-100 pt-4">
<div className="relative mb-3 flex h-fit w-5/6 items-center justify-center rounded-lg border border-slate-300 bg-slate-200">
<div className="flex h-[90%] max-h-[90%] w-4/6 flex-1 flex-col">
<div className="flex h-8 w-full items-center rounded-t-lg bg-slate-100">
<div className="ml-6 flex space-x-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="ml-4 flex w-full justify-between font-mono text-sm text-slate-400">
<p>Preview</p>
<div className="flex items-center pr-6">Restart</div>
</div>
</div>
<div className="grid h-[500px] place-items-center bg-white">
<h1 className="text-xl font-semibold text-slate-700">Loading preview...</h1>
</div>
</div>
</div>
</div>
</div>
</SettingsCard>
<SettingsCard title="Logo" description="Upload your company logo to brand surveys and link previews.">
<div className="w-full animate-pulse items-center">
<div className="relative flex h-52 w-full cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-slate-300 bg-slate-50 hover:bg-slate-100 dark:border-slate-600 dark:bg-slate-700 dark:hover:border-slate-500 dark:hover:bg-slate-800">
<p className="text-xl font-semibold text-slate-700">Loading...</p>
</div>
</div>
</SettingsCard>
<SettingsCard
title="In-app Survey Placement"
description="Change where surveys will be shown in your web app.">
<div className="w-full items-center">
<div className="flex cursor-not-allowed select-none">
<RadioGroup>
{placements.map((placement) => (
<div key={placement.value} className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem
className="cursor-not-allowed select-none"
id={placement.value}
value={placement.value}
disabled={placement.disabled}
/>
<Label
htmlFor={placement.value}
className={cn(
placement.disabled ? "cursor-not-allowed text-slate-500" : "text-slate-900"
)}>
{placement.name}
</Label>
</div>
))}
</nav>
<div className="justify-self-end"></div>
</div>
</PageHeader>
<SettingsCard
title="Theme"
className="max-w-7xl"
description="Create a style theme for all surveys. You can enable custom styling for each survey.">
<div className="flex animate-pulse">
<div className="w-1/2">
<div className="flex flex-col gap-4 pr-6">
<div className="flex flex-col gap-4 rounded-lg bg-slate-50 p-4">
<div className="flex items-center gap-6">
<Switch />
<div className="flex flex-col">
<h3 className="text-sm font-semibold text-slate-700">Enable custom styling</h3>
<p className="text-xs text-slate-500">
Allow users to override this theme in the editor.
</p>
</div>
</div>
</div>
<div className="flex flex-col gap-3 bg-slate-50 p-4">
<div className="w-full rounded-lg border border-slate-300 bg-white">
<div className="flex flex-col p-4">
<h2 className="text-sm font-semibold text-slate-700">Form Styling</h2>
<p className="mt-1 text-xs text-slate-500">
Style the question texts, descriptions and input fields.
</p>
</div>
</div>
<div className="w-full rounded-lg border border-slate-300 bg-white">
<div className="flex flex-col p-4">
<h2 className="text-sm font-semibold text-slate-700">Card Styling</h2>
<p className="mt-1 text-xs text-slate-500">Style the survey card.</p>
</div>
</div>
<div className="w-full rounded-lg border border-slate-300 bg-white">
<div className="flex flex-col p-4">
<div className="flex items-center gap-2">
<h2 className="text-sm font-semibold text-slate-700">Background Styling</h2>
<Badge text="Link Surveys" type="gray" size="normal" />
</div>
<p className="mt-1 text-xs text-slate-500">
Change the background to a color, image or animation.
</p>
</div>
</div>
</div>
</div>
</div>
<div className="relative flex w-1/2 flex-row items-center justify-center rounded-lg bg-slate-100 pt-4">
<div className="relative mb-3 flex h-fit w-5/6 items-center justify-center rounded-lg border border-slate-300 bg-slate-200">
<div className="flex h-[95] max-h-[90%] w-4/6 flex-1 flex-col">
<div className="flex h-8 w-full items-center rounded-t-lg bg-slate-100">
<div className="ml-6 flex space-x-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="ml-4 flex w-full justify-between font-mono text-sm text-slate-400">
<p>Preview</p>
<div className="flex items-center pr-6">Restart</div>
</div>
</div>
<div className="grid h-[500px] place-items-center bg-white">
<h1 className="text-xl font-semibold text-slate-700">Loading preview...</h1>
</div>
</div>
</div>
</RadioGroup>
<div className="relative ml-8 h-40 w-full rounded bg-slate-200">
<div className={cn("absolute bottom-3 h-16 w-16 rounded bg-slate-700 sm:right-3")}></div>
</div>
</div>
</SettingsCard>
<Button className="pointer-events-none mt-4 animate-pulse cursor-not-allowed select-none bg-slate-200">
Loading
</Button>
</div>
</SettingsCard>
<SettingsCard
title="In-app Survey Placement"
description="Change where surveys will be shown in your web app.">
<div className="w-full items-center">
<div className="flex cursor-not-allowed select-none">
<RadioGroup>
{placements.map((placement) => (
<div key={placement.value} className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem
className="cursor-not-allowed select-none"
id={placement.value}
value={placement.value}
disabled={placement.disabled}
/>
<Label
htmlFor={placement.value}
className={cn(
placement.disabled ? "cursor-not-allowed text-slate-500" : "text-slate-900"
)}>
{placement.name}
</Label>
</div>
))}
</RadioGroup>
<div className="relative ml-8 h-40 w-full rounded bg-slate-200">
<div className={cn("absolute bottom-3 h-16 w-16 rounded bg-slate-700 sm:right-3")}></div>
</div>
</div>
<Button className="pointer-events-none mt-4 animate-pulse cursor-not-allowed select-none bg-slate-200">
Loading
</Button>
<SettingsCard
title="Formbricks Signature"
description="We love your support but understand if you toggle it off.">
<div className="w-full items-center">
<div className="pointer-events-none flex cursor-not-allowed select-none items-center space-x-2">
<Switch id="signature" checked={false} />
<Label htmlFor="signature">Show &apos;Powered by Formbricks&apos; Signature</Label>
</div>
</SettingsCard>
<SettingsCard
title="Formbricks Signature"
description="We love your support but understand if you toggle it off.">
<div className="w-full items-center">
<div className="pointer-events-none flex cursor-not-allowed select-none items-center space-x-2">
<Switch id="signature" checked={false} />
<Label htmlFor="signature">Show &apos;Powered by Formbricks&apos; Signature</Label>
</div>
</div>
</SettingsCard>
</PageContentWrapper>
</div>
</div>
</SettingsCard>
</PageContentWrapper>
);
};

View File

@@ -0,0 +1,42 @@
"use client";
import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation";
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Loading = () => {
return (
<PageContentWrapper>
<PageHeader pageTitle="Configuration">
<ProductConfigNavigation activeId="tags" />
</PageHeader>
<SettingsCard title="Manage Tags" description="Merge and remove response tags.">
<div className="w-full">
<div className="grid grid-cols-4 content-center rounded-lg bg-white text-left text-sm font-semibold text-slate-900">
<div className="col-span-2">Tag</div>
<div className="col-span-1 text-center">Count</div>
<div className="col-span-1 flex justify-center text-center">Actions</div>
</div>
<div className="w-full">
{[...Array(3)].map((_, idx) => (
<div key={idx} className="grid h-16 w-full grid-cols-4 content-center">
<div className="col-span-2 h-10 animate-pulse rounded-md bg-slate-200" />
<div className="flex items-center justify-center">
<div className="h-5 w-5 animate-pulse rounded-md bg-slate-200" />
</div>
<div className="flex items-center gap-2">
<div className="h-8 w-1/2 animate-pulse rounded-md bg-slate-200" />
<div className="h-8 w-1/2 animate-pulse rounded-md bg-slate-200" />
</div>
</div>
))}
</div>
</div>
</SettingsCard>
</PageContentWrapper>
);
};
export default Loading;

View File

@@ -7,9 +7,11 @@ import { SecondaryNavigation } from "@formbricks/ui/components/SecondaryNavigati
export const AccountSettingsNavbar = ({
environmentId,
activeId,
loading,
}: {
environmentId: string;
activeId: string;
environmentId?: string;
loading?: boolean;
}) => {
const pathname = usePathname();
const navigation = [
@@ -29,5 +31,5 @@ export const AccountSettingsNavbar = ({
},
];
return <SecondaryNavigation navigation={navigation} activeId={activeId} />;
return <SecondaryNavigation navigation={navigation} activeId={activeId} loading={loading} />;
};

View File

@@ -1,22 +1,7 @@
const LoadingCard = ({ title, description, skeletonLines }) => {
return (
<div className="my-4 w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 shadow-sm">
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
<h3 className="text-lg font-medium leading-6">{title}</h3>
<p className="mt-1 text-sm text-slate-500">{description}</p>
</div>
<div className="w-full">
<div className="rounded-lg px-6 py-5">
{skeletonLines.map((line, index) => (
<div key={index} className="mt-4">
<div className={`animate-pulse rounded-full bg-slate-200 ${line.classes}`}></div>
</div>
))}
</div>
</div>
</div>
);
};
import { LoadingCard } from "@/app/(app)/components/LoadingCard";
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Loading = () => {
const cards = [
@@ -32,33 +17,15 @@ const Loading = () => {
},
];
const pages = ["Profile", "Notifications"];
return (
<div className="p-6">
<div>
<div className="flex items-center justify-between space-x-4 pb-4">
<h1 className="text-3xl font-bold text-slate-800">Account Settings</h1>
</div>
</div>
<div className="mb-6 border-b border-slate-200">
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
{pages.map((navElem) => (
<div
key={navElem}
className="flex h-full items-center border-b-2 border-transparent px-3 text-sm font-medium text-slate-500 transition-all duration-150 ease-in-out hover:border-slate-300 hover:text-slate-700">
{navElem}
</div>
))}
</nav>
<div className="justify-self-end"></div>
</div>
</div>
<PageContentWrapper>
<PageHeader pageTitle="Account Settings">
<AccountSettingsNavbar activeId="notifications" loading />
</PageHeader>
{cards.map((card, index) => (
<LoadingCard key={index} {...card} />
))}
</div>
</PageContentWrapper>
);
};

View File

@@ -1,22 +1,7 @@
const LoadingCard = ({ title, description, skeletonLines }) => {
return (
<div className="my-4 w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 shadow-sm">
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
<h3 className="text-lg font-medium leading-6">{title}</h3>
<p className="mt-1 text-sm text-slate-500">{description}</p>
</div>
<div className="w-full">
<div className="rounded-lg px-6 py-5">
{skeletonLines.map((line, index) => (
<div key={index} className="mt-4">
<div className={`animate-pulse rounded-full bg-slate-200 ${line.classes}`}></div>
</div>
))}
</div>
</div>
</div>
);
};
import { LoadingCard } from "@/app/(app)/components/LoadingCard";
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Loading = () => {
const cards = [
@@ -47,33 +32,15 @@ const Loading = () => {
},
];
const pages = ["Profile", "Notifications"];
return (
<div className="p-6">
<div>
<div className="flex items-center justify-between space-x-4 pb-4">
<h1 className="text-3xl font-bold text-slate-800">Account Settings</h1>
</div>
</div>
<div className="mb-6 border-b border-slate-200">
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
{pages.map((navElem) => (
<div
key={navElem}
className="flex h-full items-center border-b-2 border-transparent px-3 text-sm font-medium text-slate-500 transition-all duration-150 ease-in-out hover:border-slate-300 hover:text-slate-700">
{navElem}
</div>
))}
</nav>
<div className="justify-self-end"></div>
</div>
</div>
<PageContentWrapper>
<PageHeader pageTitle="Account Settings">
<AccountSettingsNavbar activeId="profile" loading />
</PageHeader>
{cards.map((card, index) => (
<LoadingCard key={index} {...card} />
))}
</div>
</PageContentWrapper>
);
};

View File

@@ -11,11 +11,13 @@ export const OrganizationSettingsNavbar = ({
isFormbricksCloud,
membershipRole,
activeId,
loading,
}: {
environmentId: string;
environmentId?: string;
isFormbricksCloud: boolean;
membershipRole?: TMembershipRole;
activeId: string;
loading?: boolean;
}) => {
const pathname = usePathname();
const { isAdmin, isOwner } = getAccessFlags(membershipRole);
@@ -48,5 +50,5 @@ export const OrganizationSettingsNavbar = ({
},
];
return <SecondaryNavigation navigation={navigation} activeId={activeId} />;
return <SecondaryNavigation navigation={navigation} activeId={activeId} loading={loading} />;
};

View File

@@ -1,30 +1,17 @@
const pages = ["Members", "Enterprise License"];
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Loading = () => {
return (
<div className="p-6">
<div>
<div className="flex items-center justify-between space-x-4 pb-4">
<h1 className="text-3xl font-bold text-slate-800">Organization Settings</h1>
</div>
</div>
<div className="mb-6 border-b border-slate-200">
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
{pages.map((navElem) => (
<div
key={navElem}
className="flex h-full items-center border-b-2 border-transparent px-3 text-sm font-medium text-slate-500 transition-all duration-150 ease-in-out hover:border-slate-300 hover:text-slate-700">
{navElem}
</div>
))}
</nav>
<div className="justify-self-end"></div>
</div>
</div>
<PageContentWrapper>
<PageHeader pageTitle="Organization Settings">
<OrganizationSettingsNavbar isFormbricksCloud={IS_FORMBRICKS_CLOUD} activeId="enterprise" loading />
</PageHeader>
<div className="my-8 h-64 animate-pulse rounded-xl bg-slate-200"></div>
<div className="my-8 h-96 animate-pulse rounded-md bg-slate-200"></div>
</div>
</PageContentWrapper>
);
};

View File

@@ -1,26 +1,8 @@
import { LoadingCard } from "@/app/(app)/components/LoadingCard";
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
const LoadingCard = ({ title, description, skeletonLines }) => {
return (
<div
data-testid="members-loading-card"
className="my-4 w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 shadow-sm">
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
<h3 className="text-lg font-medium leading-6">{title}</h3>
<p className="mt-1 text-sm text-slate-500">{description}</p>
</div>
<div className="w-full">
<div className="rounded-lg px-6">
{skeletonLines.map((line, index) => (
<div key={index} className="mt-4">
<div className={`animate-pulse rounded-full bg-slate-200 ${line.classes}`}></div>
</div>
))}
</div>
</div>
</div>
);
};
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const cards = [
{
@@ -41,34 +23,16 @@ const cards = [
},
];
const pages = ["Members", IS_FORMBRICKS_CLOUD ? "Billing & Plan" : "Enterprise License"];
const Loading = () => {
return (
<div className="p-6">
<div>
<div className="flex items-center justify-between space-x-4 pb-4">
<h1 className="text-3xl font-bold text-slate-800">Organization Settings</h1>
</div>
</div>
<div className="mb-6 border-b border-slate-200">
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
{pages.map((navElem) => (
<div
key={navElem}
className="flex h-full items-center border-b-2 border-transparent px-3 text-sm font-medium text-slate-500 transition-all duration-150 ease-in-out hover:border-slate-300 hover:text-slate-700">
{navElem}
</div>
))}
</nav>
<div className="justify-self-end"></div>
</div>
</div>
<PageContentWrapper>
<PageHeader pageTitle="Organization Settings">
<OrganizationSettingsNavbar isFormbricksCloud={IS_FORMBRICKS_CLOUD} activeId="members" loading />
</PageHeader>
{cards.map((card, index) => (
<LoadingCard key={index} {...card} />
))}
</div>
</PageContentWrapper>
);
};

View File

@@ -0,0 +1,54 @@
"use client";
import { useEffect, useState } from "react";
interface ScrollToTopProps {
containerId: string; // ID of the scrollable container
}
const ScrollToTop: React.FC<ScrollToTopProps> = ({ containerId }) => {
const [showButton, setShowButton] = useState<boolean>(false);
// Show the button when scrolling down
useEffect(() => {
const container = document.getElementById(containerId);
if (!container) return;
const handleScroll = () => {
if (container.scrollTop > 300) {
setShowButton(true);
} else {
setShowButton(false);
}
};
container.addEventListener("scroll", handleScroll);
return () => {
container.removeEventListener("scroll", handleScroll);
};
}, [containerId]);
// Scroll to top function
const scrollToTop = (): void => {
const container = document.getElementById(containerId);
if (container) {
container.scrollTo({
top: 0,
behavior: "smooth",
});
}
};
return (
<button
onClick={scrollToTop}
className={`fixed bottom-4 right-4 flex h-10 w-10 justify-center rounded-md bg-slate-500 p-2 text-white transition-opacity ${
showButton ? "opacity-80" : "opacity-0"
}`}>
</button>
);
};
export default ScrollToTop;

View File

@@ -11,7 +11,7 @@ import {
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useEffect, useState } from "react";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUser } from "@formbricks/types/user";
import { Badge } from "@formbricks/ui/components/Badge";
@@ -23,12 +23,20 @@ import { PanelInfoView } from "./shareEmbedModal/PanelInfoView";
interface ShareEmbedSurveyProps {
survey: TSurvey;
open: boolean;
modalView: "start" | "embed" | "panel";
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
webAppUrl: string;
user: TUser;
}
export const ShareEmbedSurvey = ({ survey, open, setOpen, webAppUrl, user }: ShareEmbedSurveyProps) => {
export const ShareEmbedSurvey = ({
survey,
open,
modalView,
setOpen,
webAppUrl,
user,
}: ShareEmbedSurveyProps) => {
const router = useRouter();
const environmentId = survey.environmentId;
const isSingleUseLinkSurvey = survey.singleUse?.enabled ?? false;
@@ -39,23 +47,37 @@ export const ShareEmbedSurvey = ({ survey, open, setOpen, webAppUrl, user }: Sha
{ id: "webpage", label: "Embed on website", icon: Code2Icon },
{ id: "link", label: `${isSingleUseLinkSurvey ? "Single use links" : "Share the link"}`, icon: LinkIcon },
{ id: "app", label: "Embed in app", icon: SmartphoneIcon },
];
].filter((tab) => !(survey.type === "link" && tab.id === "app"));
const [activeId, setActiveId] = useState(tabs[0].id);
const [showView, setShowView] = useState("start");
const [activeId, setActiveId] = useState(survey.type === "link" ? tabs[0].id : tabs[3].id);
const [showView, setShowView] = useState<"start" | "embed" | "panel">("start");
const [surveyUrl, setSurveyUrl] = useState("");
const handleOpenChange = (open: boolean) => {
setActiveId(tabs[0].id);
setOpen(open);
setShowView(open ? "start" : ""); // Reset to initial page when modal opens or closes
useEffect(() => {
if (survey.type !== "link") {
setActiveId(tabs[3].id);
}
}, [survey.type]);
// fetch latest responses
useEffect(() => {
if (open) {
setShowView(modalView);
} else {
setShowView("start");
}
}, [open, modalView]);
const handleOpenChange = (open: boolean) => {
setActiveId(survey.type === "link" ? tabs[0].id : tabs[3].id);
setOpen(open);
if (!open) {
setShowView("start");
}
router.refresh();
};
const handleInitialPageButton = () => {
setShowView("start");
setOpen(false);
};
return (
@@ -111,7 +133,8 @@ export const ShareEmbedSurvey = ({ survey, open, setOpen, webAppUrl, user }: Sha
) : showView === "embed" ? (
<EmbedView
handleInitialPageButton={handleInitialPageButton}
tabs={tabs}
tabs={survey.type === "link" ? tabs : [tabs[3]]}
disableBack={false}
activeId={activeId}
environmentId={environmentId}
setActiveId={setActiveId}
@@ -122,7 +145,7 @@ export const ShareEmbedSurvey = ({ survey, open, setOpen, webAppUrl, user }: Sha
webAppUrl={webAppUrl}
/>
) : showView === "panel" ? (
<PanelInfoView handleInitialPageButton={handleInitialPageButton} />
<PanelInfoView handleInitialPageButton={handleInitialPageButton} disableBack={false} />
) : null}
</DialogContent>
</Dialog>

View File

@@ -5,6 +5,7 @@ import {
getResponseCountAction,
getSurveySummaryAction,
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
import ScrollToTop from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ScrollToTop";
import { SummaryDropOffs } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs";
import { CustomFilter } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter";
import { ResultsShareButton } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResultsShareButton";
@@ -167,6 +168,7 @@ export const SummaryPage = ({
<CustomFilter survey={surveyMemoized} />
{!isSharingPage && <ResultsShareButton survey={surveyMemoized} webAppUrl={webAppUrl} />}
</div>
<ScrollToTop containerId="mainContent" />
<SummaryList
summary={surveySummary.summary}
responseCount={responseCount}

View File

@@ -3,14 +3,38 @@
import { ShareEmbedSurvey } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ShareEmbedSurvey";
import { SuccessMessage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage";
import { SurveyStatusDropdown } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown";
import { ArrowUpRightFromSquareIcon, SquarePenIcon } from "lucide-react";
import { BellRing, Code2Icon, Eye, LinkIcon, MoreVertical, SquarePenIcon, UsersRound } from "lucide-react";
import Link from "next/link";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import toast from "react-hot-toast";
import { TEnvironment } from "@formbricks/types/environment";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUser } from "@formbricks/types/user";
import { Badge } from "@formbricks/ui/components/Badge";
import { Button } from "@formbricks/ui/components/Button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@formbricks/ui/components/DropdownMenu";
interface SurveyAnalysisCTAProps {
survey: TSurvey;
environment: TEnvironment;
isViewer: boolean;
webAppUrl: string;
user: TUser;
}
interface ModalState {
share: boolean;
embed: boolean;
panel: boolean;
dropdown: boolean;
}
export const SurveyAnalysisCTA = ({
survey,
@@ -18,82 +42,170 @@ export const SurveyAnalysisCTA = ({
isViewer,
webAppUrl,
user,
}: {
survey: TSurvey;
environment: TEnvironment;
isViewer: boolean;
webAppUrl: string;
user: TUser;
}) => {
}: SurveyAnalysisCTAProps) => {
const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter();
const [showShareSurveyModal, setShowShareSurveyModal] = useState(searchParams.get("share") === "true");
const widgetSetupCompleted = environment.appSetupCompleted;
const [modalState, setModalState] = useState<ModalState>({
share: searchParams.get("share") === "true",
embed: false,
panel: false,
dropdown: false,
});
const surveyUrl = useMemo(() => `${webAppUrl}/s/${survey.id}`, [survey.id, webAppUrl]);
const widgetSetupCompleted = survey.type === "app" && environment.appSetupCompleted;
useEffect(() => {
if (searchParams.get("share") === "true") {
setShowShareSurveyModal(true);
} else {
setShowShareSurveyModal(false);
}
setModalState((prev) => ({
...prev,
share: searchParams.get("share") === "true",
}));
}, [searchParams]);
const setOpenShareSurveyModal = (open: boolean) => {
const searchParams = new URLSearchParams(window.location.search);
const handleShareModalToggle = (open: boolean) => {
const params = new URLSearchParams(window.location.search);
if (open) {
searchParams.set("share", "true");
setShowShareSurveyModal(true);
params.set("share", "true");
} else {
searchParams.delete("share");
setShowShareSurveyModal(false);
params.delete("share");
}
router.push(`${pathname}?${searchParams.toString()}`);
router.push(`${pathname}?${params.toString()}`);
setModalState((prev) => ({ ...prev, share: open }));
};
const handleCopyLink = () => {
navigator.clipboard
.writeText(surveyUrl)
.then(() => {
toast.success("Copied link to clipboard");
})
.catch((err) => {
toast.error("Failed to copy link");
console.error(err);
});
setModalState((prev) => ({ ...prev, dropdown: false }));
};
const getPreviewUrl = () => {
const separator = surveyUrl.includes("?") ? "&" : "?";
return `${surveyUrl}${separator}preview=true`;
};
const handleModalState = (modalView: keyof Omit<ModalState, "dropdown">) => {
return (open: boolean | ((prevState: boolean) => boolean)) => {
const newValue = typeof open === "function" ? open(modalState[modalView]) : open;
setModalState((prev) => ({ ...prev, [modalView]: newValue }));
};
};
const shareEmbedViews = [
{ key: "share", modalView: "start" as const, setOpen: handleShareModalToggle },
{ key: "embed", modalView: "embed" as const, setOpen: handleModalState("embed") },
{ key: "panel", modalView: "panel" as const, setOpen: handleModalState("panel") },
];
return (
<div className="hidden justify-end gap-x-1.5 sm:flex">
{survey.resultShareKey && (
<Badge text="Results are public" type="warning" size="normal" className="rounded-lg"></Badge>
<Badge text="Results are public" type="warning" size="normal" className="rounded-lg" />
)}
{(widgetSetupCompleted || survey.type === "link") && survey.status !== "draft" ? (
{(widgetSetupCompleted || survey.type === "link") && survey.status !== "draft" && (
<SurveyStatusDropdown environment={environment} survey={survey} />
) : null}
{survey.type === "link" && (
<>
<Button
variant="secondary"
size="sm"
onClick={() => {
setOpenShareSurveyModal(true);
}}
EndIcon={ArrowUpRightFromSquareIcon}>
Preview
</Button>
</>
)}
{!isViewer && (
<Button
variant="secondary"
size="sm"
className="h-full"
href={`/environments/${environment.id}/surveys/${survey.id}/edit`}
EndIcon={SquarePenIcon}>
onClick={() => handleModalState("embed")(true)}
EndIcon={Code2Icon}>
Embed
</Button>
)}
{survey.type === "link" && (
<Button
variant="secondary"
size="sm"
onClick={() => window.open(getPreviewUrl(), "_blank")}
EndIcon={Eye}>
Preview
</Button>
)}
{!isViewer && (
<Button href={`/environments/${environment.id}/surveys/${survey.id}/edit`} EndIcon={SquarePenIcon}>
Edit
</Button>
)}
{showShareSurveyModal && user && (
<ShareEmbedSurvey
survey={survey}
open={showShareSurveyModal}
setOpen={setOpenShareSurveyModal}
webAppUrl={webAppUrl}
user={user}
/>
{!isViewer && (
<div id={`${survey.name.toLowerCase().replace(/\s+/g, "-")}-survey-actions`}>
<DropdownMenu
open={modalState.dropdown}
onOpenChange={(open) => setModalState((prev) => ({ ...prev, dropdown: open }))}>
<DropdownMenuTrigger className="z-10 cursor-pointer" asChild>
<Button variant="secondary" className="p-2">
<MoreVertical className="h-7 w-4" />
<span className="sr-only">Open options</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="mr-8 w-40">
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<button onClick={handleCopyLink} className="flex w-full items-center">
<LinkIcon className="mr-2 h-4 w-4" />
Copy Link
</button>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<button
onClick={() => {
handleModalState("panel")(true);
setModalState((prev) => ({ ...prev, dropdown: false }));
}}
className="flex w-full items-center">
<UsersRound className="mr-2 h-4 w-4" />
Send to panel
</button>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link
href={`/environments/${survey.environmentId}/settings/notifications`}
className="flex w-full items-center"
onClick={() => setModalState((prev) => ({ ...prev, dropdown: false }))}>
<BellRing className="mr-2 h-4 w-4" />
Configure alerts
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
{user && <SuccessMessage environment={environment} survey={survey} />}
{user && (
<>
{shareEmbedViews.map(({ key, modalView, setOpen }) => (
<ShareEmbedSurvey
key={key}
survey={survey}
open={modalState[key as keyof ModalState]}
setOpen={setOpen}
webAppUrl={webAppUrl}
user={user}
modalView={modalView}
/>
))}
<SuccessMessage environment={environment} survey={survey} />
</>
)}
</div>
);
};

View File

@@ -1,10 +1,7 @@
"use client";
import ChangeSurveyTypeTip from "@/images/tooltips/change-survey-type-app.mp4";
import { CogIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { Alert, AlertDescription } from "@formbricks/ui/components/Alert";
import { OptionsSwitch } from "@formbricks/ui/components/OptionsSwitch";
export const AppTab = ({ environmentId }) => {
@@ -44,12 +41,9 @@ const MobileAppTab = () => {
to connect your app with Formbricks
</li>
</ol>
<Alert variant="default" className="mt-4">
<AlertDescription className="flex gap-x-2">
<CogIcon className="h-5 w-5 animate-spin" />
<div>We&apos;re working on SDKs for Flutter, Swift and Kotlin.</div>
</AlertDescription>
</Alert>
<div className="mt-2 text-sm italic text-slate-700">
We&apos;re working on SDKs for Flutter, Swift and Kotlin.
</div>
</div>
);
};
@@ -86,7 +80,7 @@ const WebAppTab = ({ environmentId }) => {
</ol>
<div className="mt-4">
<video autoPlay loop muted className="w-full rounded-xl border border-slate-200">
<source src={ChangeSurveyTypeTip} type="video/mp4" />
<source src="/video/tooltips/change-survey-type-app.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>

View File

@@ -14,6 +14,7 @@ interface EmbedViewProps {
activeId: string;
setActiveId: React.Dispatch<React.SetStateAction<string>>;
environmentId: string;
disableBack: boolean;
survey: any;
email: string;
surveyUrl: string;
@@ -24,6 +25,7 @@ interface EmbedViewProps {
export const EmbedView = ({
handleInitialPageButton,
tabs,
disableBack,
activeId,
setActiveId,
environmentId,
@@ -35,53 +37,56 @@ export const EmbedView = ({
}: EmbedViewProps) => {
return (
<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="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>
))}
{!disableBack && (
<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="col-span-4 h-full overflow-y-auto bg-slate-50 px-4 py-6 lg:col-span-3 lg:p-6">
<div>
{activeId === "email" ? (
<EmailTab surveyId={survey.id} email={email} />
) : activeId === "webpage" ? (
<WebsiteTab surveyUrl={surveyUrl} environmentId={environmentId} />
) : activeId === "link" ? (
<LinkTab
survey={survey}
webAppUrl={webAppUrl}
surveyUrl={surveyUrl}
setSurveyUrl={setSurveyUrl}
/>
) : activeId === "app" ? (
<AppTab environmentId={environmentId} />
) : null}
)}
<div className="grid h-full grid-cols-4">
{survey.type === "link" && (
<div className={cn("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 ${survey.type === "link" ? "lg:col-span-3" : ""} lg:p-6`}>
{activeId === "email" ? (
<EmailTab surveyId={survey.id} email={email} />
) : activeId === "webpage" ? (
<WebsiteTab surveyUrl={surveyUrl} environmentId={environmentId} />
) : activeId === "link" ? (
<LinkTab
survey={survey}
webAppUrl={webAppUrl}
surveyUrl={surveyUrl}
setSurveyUrl={setSurveyUrl}
/>
) : activeId === "app" ? (
<AppTab environmentId={environmentId} />
) : null}
<div className="mt-2 rounded-md p-3 text-center lg:hidden">
{tabs.slice(0, 2).map((tab) => (
<Button

View File

@@ -7,21 +7,24 @@ import Image from "next/image";
import { Button } from "@formbricks/ui/components/Button";
interface PanelInfoViewProps {
disableBack: boolean;
handleInitialPageButton: () => void;
}
export const PanelInfoView = ({ handleInitialPageButton }: PanelInfoViewProps) => {
export const PanelInfoView = ({ disableBack, handleInitialPageButton }: PanelInfoViewProps) => {
return (
<div className="h-full overflow-hidden text-slate-900">
<div className="border-b border-slate-200 py-2">
<Button
variant="minimal"
className="focus:ring-0"
onClick={handleInitialPageButton}
StartIcon={ArrowLeftIcon}>
Back
</Button>
</div>
{!disableBack && (
<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="grid h-full grid-cols-2">
<div className="flex flex-col gap-y-6 border-r border-slate-200 p-8">
<Image src={ProlificUI} alt="Prolific panel selection UI" className="rounded-lg shadow-lg" />

View File

@@ -1,6 +1,5 @@
"use client";
import ChangeSurveyTypeTip from "@/images/tooltips/change-survey-type.mp4";
import { CopyIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
@@ -103,7 +102,7 @@ const PopupTab = ({ environmentId }) => {
</ol>
<div className="mt-4">
<video autoPlay loop muted className="w-full rounded-xl border border-slate-200">
<source src={ChangeSurveyTypeTip} type="video/mp4" />
<source src="/video/tooltips/change-survey-type.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>

View File

@@ -1,8 +1,8 @@
"use client";
import { copySurveyToOtherEnvironmentAction } from "@/app/(app)/environments/[environmentId]/surveys/actions";
import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
import {
TSurvey,
TSurveyCopyFormData,
ZSurveyCopyFormValidation,
} from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
@@ -14,7 +14,6 @@ import { Button } from "@formbricks/ui/components/Button";
import { Checkbox } from "@formbricks/ui/components/Checkbox";
import { FormControl, FormField, FormItem, FormProvider } from "@formbricks/ui/components/Form";
import { Label } from "@formbricks/ui/components/Label";
import { TooltipRenderer } from "@formbricks/ui/components/Tooltip";
export const CopySurveyForm = ({
defaultProducts,
@@ -76,17 +75,9 @@ export const CopySurveyForm = ({
return (
<div key={product?.id}>
<div className="flex flex-col gap-4">
<TooltipRenderer
tooltipContent={
<span>
This product is not compatible with the survey type. Please select a different
product.
</span>
}>
<div className="w-fit">
<p className="text-base font-semibold text-slate-900">{product?.name}</p>
</div>
</TooltipRenderer>
<div className="w-fit">
<p className="text-base font-semibold text-slate-900">{product?.name}</p>
</div>
<div className="flex flex-col gap-4">
{product?.environments.map((environment) => {

View File

@@ -80,7 +80,7 @@ export const SurveyCard = ({
</div>
<div
className={cn(
"col-span-1 flex w-fit items-center gap-2 rounded-full py-1 pl-1 pr-2 text-sm text-slate-800",
"col-span-1 flex w-fit items-center gap-2 whitespace-nowrap rounded-full py-1 pl-1 pr-2 text-sm text-slate-800",
surveyStatusLabel === "Scheduled" && "bg-slate-200",
surveyStatusLabel === "In Progress" && "bg-emerald-50",
surveyStatusLabel === "Completed" && "bg-slate-200",
@@ -89,20 +89,20 @@ export const SurveyCard = ({
)}>
<SurveyStatusIndicator status={survey.status} /> {surveyStatusLabel}{" "}
</div>
<div className="col-span-1 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
<div className="col-span-1 max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
{survey.responseCount}
</div>
<div className="col-span-1 flex justify-between">
<SurveyTypeIndicator type={survey.type} />
</div>
<div className="col-span-1 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
<div className="col-span-1 max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
{convertDateString(survey.createdAt.toString())}
</div>
<div className="col-span-1 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
<div className="col-span-1 max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
{timeSince(survey.updatedAt.toString())}
</div>
<div className="col-span-1 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
<div className="col-span-1 max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
{survey.creator ? survey.creator.name : "-"}
</div>
<div className="col-span-1 place-self-end">

View File

@@ -9,6 +9,7 @@ import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useRef, useState } from "react";
import { Controller, FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { cn } from "@formbricks/lib/cn";
import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage";
import { Button } from "@formbricks/ui/components/Button";
import { PasswordInput } from "@formbricks/ui/components/PasswordInput";
import { AzureButton } from "@formbricks/ui/components/SignupOptions/components/AzureButton";
@@ -53,7 +54,9 @@ export const SigninForm = ({
const callbackUrl = searchParams?.get("callbackUrl");
const onSubmit: SubmitHandler<TSigninFormState> = async (data) => {
setLoggingIn(true);
if (typeof window !== "undefined") {
localStorage.setItem(FORMBRICKS_LOGGED_IN_WITH_LS, "Email");
}
try {
const signInResponse = await signIn("credentials", {
callbackUrl: callbackUrl ?? "/",
@@ -104,6 +107,13 @@ export const SigninForm = ({
const formRef = useRef<HTMLFormElement>(null);
const error = searchParams?.get("error");
const inviteToken = callbackUrl ? new URL(callbackUrl).searchParams.get("token") : null;
const [lastLoggedInWith, setLastLoginWith] = useState("");
useEffect(() => {
if (typeof window !== "undefined") {
setLastLoginWith(localStorage.getItem(FORMBRICKS_LOGGED_IN_WITH_LS) || "");
}
}, []);
useEffect(() => {
if (error) {
@@ -139,6 +149,7 @@ export const SigninForm = ({
<FormProvider {...formMethods}>
<div className="text-center">
<h1 className="mb-4 text-slate-700">{formLabel}</h1>
<div className="space-y-2">
<form onSubmit={formMethods.handleSubmit(onSubmit)} className="space-y-2">
{TwoFactorComponent}
@@ -209,34 +220,41 @@ export const SigninForm = ({
formRef.current.requestSubmit();
}
}}
className="w-full justify-center"
className="relative w-full justify-center"
loading={loggingIn}>
{totpLogin ? "Submit" : "Login with Email"}
{lastLoggedInWith && lastLoggedInWith === "Email" ? (
<span className="absolute right-3 text-xs">Last Used</span>
) : null}
</Button>
)}
</form>
{googleOAuthEnabled && !totpLogin && (
<>
<GoogleButton inviteUrl={callbackUrl} />
<GoogleButton inviteUrl={callbackUrl} lastUsed={lastLoggedInWith === "Google"} />
</>
)}
{githubOAuthEnabled && !totpLogin && (
<>
<GithubButton inviteUrl={callbackUrl} />
<GithubButton inviteUrl={callbackUrl} lastUsed={lastLoggedInWith === "Github"} />
</>
)}
{azureOAuthEnabled && !totpLogin && (
<>
<AzureButton inviteUrl={callbackUrl} />
<AzureButton inviteUrl={callbackUrl} lastUsed={lastLoggedInWith === "Azure"} />
</>
)}
{oidcOAuthEnabled && !totpLogin && (
<>
<OpenIdButton inviteUrl={callbackUrl} text={`Continue with ${oidcDisplayName}`} />
<OpenIdButton
inviteUrl={callbackUrl}
text={`Continue with ${oidcDisplayName}`}
lastUsed={lastLoggedInWith === "OpenID"}
/>
</>
)}
</div>

View File

@@ -1,30 +1,17 @@
const pages = ["Members", "Billing & Plan"];
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
const Loading = () => {
return (
<div className="p-6">
<div>
<div className="flex items-center justify-between space-x-4 pb-4">
<h1 className="text-3xl font-bold text-slate-800">Organization Settings</h1>
</div>
</div>
<div className="mb-6 border-b border-slate-200">
<div className="grid h-10 w-full grid-cols-[auto,1fr]">
<nav className="flex h-full min-w-full items-center space-x-4" aria-label="Tabs">
{pages.map((navElem) => (
<div
key={navElem}
className="flex h-full items-center border-b-2 border-transparent px-3 text-sm font-medium text-slate-500 transition-all duration-150 ease-in-out hover:border-slate-300 hover:text-slate-700">
{navElem}
</div>
))}
</nav>
<div className="justify-self-end"></div>
</div>
</div>
<PageContentWrapper>
<PageHeader pageTitle="Organization Settings">
<OrganizationSettingsNavbar isFormbricksCloud={IS_FORMBRICKS_CLOUD} activeId="billing" loading />
</PageHeader>
<div className="my-8 h-64 animate-pulse rounded-xl bg-slate-200"></div>
<div className="my-8 h-96 animate-pulse rounded-md bg-slate-200"></div>
</div>
</PageContentWrapper>
);
};

View File

@@ -1,21 +0,0 @@
// DEPRECATED - This file is deprecated and will be removed in the future
// Deprecated since 22-03-2024
// This endpoint has been deprecated. Please use the new endpoint /api/packages/js instead.
import { responses } from "@/app/lib/api/response";
import { WEBAPP_URL } from "@formbricks/lib/constants";
export const GET = async () => {
try {
return responses.goneResponse(
"This endpoint has been deprecated. Please use the new endpoint /api/packages/<package-name>",
{
"x-deprecated": "true",
"x-deprecated-date": "22-03-2024",
"x-deprecated-redirect": `${WEBAPP_URL}/api/packages/js`,
},
true
);
} catch (error) {
return responses.internalServerErrorResponse("this endpoint is not available");
}
};

View File

@@ -1,52 +0,0 @@
import { responses } from "@/app/lib/api/response";
import fs from "fs/promises";
import { NextRequest } from "next/server";
export const GET = async (_: NextRequest, { params }: { params: { package: string } }) => {
let path: string;
const packageRequested = params.package;
switch (packageRequested) {
case "js":
path = `../../packages/js-core/dist/index.umd.cjs`;
break;
case "surveys":
path = `../../packages/surveys/dist/index.umd.cjs`;
break;
default:
return responses.notFoundResponse(
"package",
packageRequested,
true,
"public, max-age=600, s-maxage=600, stale-while-revalidate=600, stale-if-error=600" // 10 minutes cache for not found
);
}
try {
const packageSrcCode = await fs.readFile(path, "utf-8");
return new Response(packageSrcCode, {
headers: {
"Content-Type": "application/javascript",
"Cache-Control":
"public, max-age=3600, s-maxage=604800, stale-while-revalidate=3600, stale-if-error=3600",
"Access-Control-Allow-Origin": "*",
},
});
} catch (error: any) {
if (error.code === "ENOENT") {
return responses.notFoundResponse(
"package",
packageRequested,
true,
"public, max-age=600, s-maxage=600, stale-while-revalidate=600, stale-if-error=600" // 10 minutes cache for file not found errors
);
} else {
console.error("Error reading file:", error);
return responses.internalServerErrorResponse(
"internal server error",
true,
"public, max-age=600, s-maxage=600, stale-while-revalidate=600, stale-if-error=600" // 10 minutes cache for internal errors
);
}
}
};

View File

@@ -1,10 +1,15 @@
import formbricks from "@formbricks/js";
import { env } from "@formbricks/lib/env";
import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage";
export const formbricksEnabled =
typeof env.NEXT_PUBLIC_FORMBRICKS_API_HOST && env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID;
export const formbricksLogout = async () => {
const loggedInWith = localStorage.getItem(FORMBRICKS_LOGGED_IN_WITH_LS);
localStorage.clear();
if (loggedInWith) {
localStorage.setItem(FORMBRICKS_LOGGED_IN_WITH_LS, loggedInWith);
}
return await formbricks.logout();
};

View File

@@ -16,8 +16,8 @@ export const LegalFooter = ({
if (!IMPRINT_URL && !PRIVACY_URL && !IS_FORMBRICKS_CLOUD) return null;
return (
<div className="absolute bottom-0 z-[1500] h-10 w-full">
<div className="mx-auto flex h-full max-w-lg items-center justify-center p-2 text-center text-xs text-slate-400 text-opacity-50">
<div className="absolute bottom-0 z-[1500] h-10 w-full" role="contentinfo">
<div className="mx-auto flex h-full max-w-lg items-center justify-center p-2 text-center text-xs text-slate-500">
{IMPRINT_URL && (
<Link href={IMPRINT_URL} target="_blank" className="hover:underline" tabIndex={-1}>
Imprint

View File

@@ -163,17 +163,61 @@ const nextConfig = {
},
],
},
{
source: "/js/(.*)",
headers: [
{
key: "Cache-Control",
value: "public, max-age=3600, s-maxage=604800, stale-while-revalidate=3600, stale-if-error=3600",
},
{
key: "Content-Type",
value: "application/javascript; charset=UTF-8",
},
{
key: "Access-Control-Allow-Origin",
value: "*",
},
],
},
// headers for /api/packages/(.*) -- the api route does not exist, but we still need the headers for the rewrites to work correctly!
{
source: "/api/packages/(.*)",
headers: [
{
key: "Cache-Control",
value: "public, max-age=3600, s-maxage=604800, stale-while-revalidate=3600, stale-if-error=3600",
},
{
key: "Content-Type",
value: "application/javascript; charset=UTF-8",
},
{
key: "Access-Control-Allow-Origin",
value: "*",
},
],
},
];
},
async rewrites() {
return [
{
source: "/api/packages/website",
destination: "/api/packages/js",
destination: "/js/formbricks.umd.cjs",
},
{
source: "/api/packages/app",
destination: "/api/packages/js",
destination: "/js/formbricks.umd.cjs",
},
{
source: "/api/packages/js",
destination: "/js/formbricks.umd.cjs",
},
{
source: "/api/packages/surveys",
destination: "/js/surveys.umd.cjs",
},
{
source: "/api/v1/client/:environmentId/website/environment",

View File

@@ -1,11 +1,11 @@
{
"name": "@formbricks/web",
"version": "2.5.3",
"version": "2.6.0",
"private": true,
"scripts": {
"clean": "rimraf .turbo node_modules .next",
"dev": "next dev -p 3000",
"go": "next dev -p 3000",
"dev": "next dev -p 3000 --turbo",
"go": "next dev -p 3000 --turbo",
"build": "next build",
"build:dev": "next build",
"start": "next start",
@@ -31,7 +31,7 @@
"@paralleldrive/cuid2": "2.2.2",
"@radix-ui/react-collapsible": "1.1.1",
"@react-email/components": "0.0.25",
"@sentry/nextjs": "8.33.1",
"@sentry/nextjs": "8.34.0",
"@tanstack/react-table": "8.20.5",
"@vercel/og": "0.6.3",
"@vercel/speed-insights": "1.0.12",
@@ -39,15 +39,15 @@
"dotenv": "16.4.5",
"encoding": "0.1.13",
"file-loader": "6.2.0",
"framer-motion": "11.11.4",
"framer-motion": "11.11.8",
"googleapis": "144.0.0",
"jiti": "2.3.3",
"jsonwebtoken": "9.0.2",
"lodash": "4.17.21",
"lru-cache": "11.0.1",
"lucide-react": "0.451.0",
"lucide-react": "0.452.0",
"mime": "4.0.4",
"next": "14.2.10",
"next": "14.2.15",
"next-safe-action": "7.9.3",
"optional": "0.1.4",
"otplib": "12.0.1",
@@ -68,7 +68,7 @@
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*",
"@neshca/cache-handler": "1.7.3",
"@neshca/cache-handler": "1.7.4",
"@types/bcryptjs": "2.4.6",
"@types/lodash": "4.17.10",
"@types/markdown-it": "14.1.2",

View File

@@ -52,7 +52,8 @@
"data-migration:address-question": "ts-node ./data-migrations/20240924123456_migrate_address_question/data-migration.ts",
"data-migration:advanced-logic": "ts-node ./data-migrations/20240828122408_advanced_logic_editor/data-migration.ts",
"data-migration:segments-actions-cleanup": "ts-node ./data-migrations/20240904091113_removed_actions_table/data-migration.ts",
"data-migration:migrate-survey-types": "ts-node ./data-migrations/20241002123456_migrate_survey_types/data-migration.ts"
"data-migration:migrate-survey-types": "ts-node ./data-migrations/20241002123456_migrate_survey_types/data-migration.ts",
"data-migration:v2.6": "pnpm data-migration:add-display-id-to-response && pnpm data-migration:address-question && pnpm data-migration:advanced-logic && pnpm data-migration:segments-actions-cleanup && pnpm data-migration:migrate-survey-types"
},
"dependencies": {
"@prisma/client": "5.20.0",

View File

@@ -1,6 +1,6 @@
"use client";
import { InfoIcon, PlusIcon } from "lucide-react";
import { PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
@@ -8,14 +8,13 @@ import { iso639Languages } from "@formbricks/lib/i18n/utils";
import type { TLanguage, TProduct } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/components/Button";
import { ConfirmationModal } from "@formbricks/ui/components/ConfirmationModal";
import { Label } from "@formbricks/ui/components/Label";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/components/Tooltip";
import {
createLanguageAction,
deleteLanguageAction,
getSurveysUsingGivenLanguageAction,
updateLanguageAction,
} from "../lib/actions";
import { LanguageLabels } from "./language-labels";
import { LanguageRow } from "./language-row";
interface EditLanguageProps {
@@ -214,35 +213,6 @@ export function EditLanguage({ product }: EditLanguageProps) {
);
}
function AliasTooltip() {
return (
<TooltipProvider delayDuration={80}>
<Tooltip>
<TooltipTrigger tabIndex={-1}>
<div>
<InfoIcon className="h-4 w-4 text-slate-400" />
</div>
</TooltipTrigger>
<TooltipContent>
The alias is an alternate name to identify the language in link surveys and the SDK (optional)
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
function LanguageLabels() {
return (
<div className="mb-2 grid w-full grid-cols-4 gap-4">
<Label htmlFor="languagesId">Language</Label>
<Label htmlFor="languagesId">Identifier (ISO)</Label>
<Label className="flex items-center space-x-2" htmlFor="Alias">
<span>Alias</span> <AliasTooltip />
</Label>
</div>
);
}
const EditSaveButtons: React.FC<{
isEditing: boolean;
onSave: () => void;

View File

@@ -0,0 +1,32 @@
import { InfoIcon } from "lucide-react";
import { Label } from "@formbricks/ui/components/Label";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/components/Tooltip";
export function LanguageLabels() {
return (
<div className="mb-2 grid w-full grid-cols-4 gap-4">
<Label htmlFor="languagesId">Language</Label>
<Label htmlFor="languagesId">Identifier (ISO)</Label>
<Label className="flex items-center space-x-2" htmlFor="Alias">
<span>Alias</span> <AliasTooltip />
</Label>
</div>
);
}
function AliasTooltip() {
return (
<TooltipProvider delayDuration={80}>
<Tooltip>
<TooltipTrigger tabIndex={-1}>
<div>
<InfoIcon className="h-4 w-4 text-slate-400" />
</div>
</TooltipTrigger>
<TooltipContent>
The alias is an alternate name to identify the language in link surveys and the SDK (optional)
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

View File

@@ -25,8 +25,8 @@
"@paralleldrive/cuid2": "2.2.2",
"@radix-ui/react-collapsible": "1.1.0",
"https-proxy-agent": "7.0.5",
"lucide-react": "0.427.0",
"next": "14.2.10",
"lucide-react": "0.452.0",
"next": "14.2.15",
"next-auth": "4.24.7",
"node-fetch": "3.3.2",
"react-hook-form": "7.53.0",

View File

@@ -15,7 +15,7 @@
"@formbricks/ui": "workspace:*",
"@react-email/components": "0.0.25",
"@react-email/render": "1.0.1",
"lucide-react": "0.451.0",
"lucide-react": "0.452.0",
"nodemailer": "6.9.15"
},
"devDependencies": {

View File

@@ -61,8 +61,6 @@ export const fetchPersonState = async (
const data = await response.json();
const { data: state } = data;
console.log("Person state fetched", state);
const defaultPersonState: TJsPersonState = {
expiresAt: new Date(new Date().getTime() + 1000 * 60 * 30), // 30 minutes
data: {

View File

@@ -268,7 +268,7 @@ const loadFormbricksSurveysExternally = (): Promise<typeof window.formbricksSurv
resolve(window.formbricksSurveys);
} else {
const script = document.createElement("script");
script.src = `${config.get().apiHost}/api/packages/surveys`;
script.src = `${config.get().apiHost}/js/surveys.umd.cjs`;
script.async = true;
script.onload = () => resolve(window.formbricksSurveys);
script.onerror = (error) => {

View File

@@ -2,6 +2,7 @@ import { resolve } from "path";
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
import webPackageJson from "../../apps/web/package.json";
import { copyCompiledAssetsPlugin } from "../vite-plugins/copy-compiled-assets";
const config = () => {
return defineConfig({
@@ -27,6 +28,7 @@ const config = () => {
rollupTypes: true,
bundledPackages: ["@formbricks/api", "@formbricks/types"],
}),
copyCompiledAssetsPlugin({ filename: "formbricks", distDir: resolve(__dirname, "dist") }),
],
});
};

View File

@@ -2,7 +2,7 @@
<script type="text/javascript">
!(function () {
var t = document.createElement("script");
(t.type = "text/javascript"), (t.async = !0), (t.src = "http://localhost:3000/api/packages/js");
(t.type = "text/javascript"), (t.async = !0), (t.src = "http://localhost:3000/js/formbricks.umd.cjs");
var e = document.getElementsByTagName("script")[0];
e.parentNode.insertBefore(t, e),
setTimeout(function () {

View File

@@ -1,7 +1,7 @@
{
"name": "@formbricks/js",
"license": "MIT",
"version": "3.0.0",
"version": "3.0.1",
"description": "Formbricks-js allows you to connect your index to Formbricks, display surveys and trigger events.",
"homepage": "https://formbricks.com",
"repository": {

View File

@@ -10,33 +10,28 @@ let isInitialized = false;
// Load the SDK, return the result
const loadFormbricksSDK = async (apiHostParam: string): Promise<Result<void>> => {
if (!window.formbricks) {
const res = await fetch(`${apiHostParam}/api/packages/js`);
// Failed to fetch the app package
if (!res.ok) {
return { ok: false, error: new Error(`Failed to load Formbricks SDK`) };
}
const sdkScript = await res.text();
const scriptTag = document.createElement("script");
scriptTag.innerHTML = sdkScript;
document.head.appendChild(scriptTag);
scriptTag.type = "text/javascript";
scriptTag.src = `${apiHostParam}/js/formbricks.umd.cjs`;
scriptTag.async = true;
const getFormbricks = async (): Promise<void> =>
new Promise<void>((resolve, reject) => {
const checkInterval = setInterval(() => {
if (window.formbricks) {
clearInterval(checkInterval);
resolve();
}
}, 100);
setTimeout(() => {
clearInterval(checkInterval);
const timeoutId = setTimeout(() => {
reject(new Error(`Formbricks SDK loading timed out`));
}, 10000);
scriptTag.onload = () => {
clearTimeout(timeoutId);
resolve();
};
scriptTag.onerror = () => {
clearTimeout(timeoutId);
reject(new Error(`Failed to load Formbricks SDK`));
};
});
document.head.appendChild(scriptTag);
try {
await getFormbricks();
return { ok: true, data: undefined };
@@ -56,7 +51,6 @@ const loadFormbricksSDK = async (apiHostParam: string): Promise<Result<void>> =>
const functionsToProcess: { prop: string; args: unknown[] }[] = [];
export const loadFormbricksToProxy = async (prop: string, ...args: unknown[]): Promise<void> => {
console.log(args);
// all of this should happen when not initialized:
if (!isInitialized) {
if (prop === "init") {

View File

@@ -11,6 +11,7 @@ export const actionClient = createSafeActionClient({
} else if (e instanceof AuthorizationError) {
return e.message;
}
console.error("SERVER ERROR: ", e);
return DEFAULT_SERVER_ERROR_MESSAGE;
},

View File

@@ -66,16 +66,16 @@ export const authOptions: NextAuthOptions = {
}
if (!user || !credentials) {
throw new Error("No user matches the provided credentials");
throw new Error("Invalid credentials");
}
if (!user.password) {
throw new Error("No user matches the provided credentials");
throw new Error("Invalid credentials");
}
const isValid = await verifyPassword(credentials.password, user.password);
if (!isValid) {
throw new Error("No user matches the provided credentials");
throw new Error("Invalid credentials");
}
return {
@@ -190,6 +190,7 @@ export const authOptions: NextAuthOptions = {
},
async signIn({ user, account }: any) {
if (account.provider === "credentials" || account.provider === "token") {
// check if user's email is verified or not
if (!user.emailVerified && !EMAIL_VERIFICATION_DISABLED) {
throw new Error("Email Verification is Pending");
}
@@ -245,7 +246,8 @@ export const authOptions: NextAuthOptions = {
const existingUserWithEmail = await getUserByEmail(user.email);
if (existingUserWithEmail) {
throw new Error("A user with this email exists already.");
// Sign in the user with the existing account
return true;
}
const userProfile = await createUser({

View File

@@ -1,2 +1,3 @@
export const FORMBRICKS_SURVEYS_FILTERS_KEY_LS = "formbricks-surveys-filters";
export const FORMBRICKS_ENVIRONMENT_ID_LS = "formbricks-environment-id";
export const FORMBRICKS_LOGGED_IN_WITH_LS = "formbricks-logged-in-with";

View File

@@ -126,7 +126,8 @@ export const getS3File = async (fileKey: string): Promise<string> => {
export const getLocalFile = async (filePath: string): Promise<TGetFileResponse> => {
try {
const file = await readFile(filePath);
const safeFilePath = path.resolve(process.cwd(), filePath);
const file = await readFile(safeFilePath);
let contentType = "";
try {

View File

@@ -50,7 +50,7 @@ export const questionTypes: TQuestion[] = [
{
id: QuestionId.OpenText,
label: "Free text",
description: "Ask for a text-based answer",
description: "Collect open-ended feedback",
icon: MessageSquareTextIcon,
preset: {
headline: { default: "Who let the dogs out?" },
@@ -62,7 +62,7 @@ export const questionTypes: TQuestion[] = [
{
id: QuestionId.MultipleChoiceSingle,
label: "Single-Select",
description: "A single choice from a list of options (radio buttons)",
description: "Offer a list of options (choose one)",
icon: Rows3Icon,
preset: {
headline: { default: "What do you do?" },
@@ -76,7 +76,7 @@ export const questionTypes: TQuestion[] = [
{
id: QuestionId.MultipleChoiceMulti,
label: "Multi-Select",
description: "Number of choices from a list of options (checkboxes)",
description: "Offer a list of options (choose multiple)",
icon: ListIcon,
preset: {
headline: { default: "What's important on vacay?" },
@@ -91,7 +91,7 @@ export const questionTypes: TQuestion[] = [
{
id: QuestionId.PictureSelection,
label: "Picture Selection",
description: "Ask respondents to select one or more pictures",
description: "Ask respondents to choose one or more images",
icon: ImageIcon,
preset: {
headline: { default: "Which is the cutest puppy?" },
@@ -111,7 +111,7 @@ export const questionTypes: TQuestion[] = [
{
id: QuestionId.Rating,
label: "Rating",
description: "Ask respondents for a rating",
description: "Ask respondents for a rating (stars, smileys, numbers)",
icon: StarIcon,
preset: {
headline: { default: "How would you rate {{productName}}" },
@@ -124,7 +124,7 @@ export const questionTypes: TQuestion[] = [
{
id: QuestionId.NPS,
label: "Net Promoter Score (NPS)",
description: "Rate satisfaction on a 0-10 scale",
description: "Measure Net-Promoter-Score (0-10)",
icon: PresentationIcon,
preset: {
headline: { default: "How likely are you to recommend {{productName}} to a friend or colleague?" },
@@ -135,7 +135,7 @@ export const questionTypes: TQuestion[] = [
{
id: QuestionId.Ranking,
label: "Ranking",
description: "Allow respondents to rank items",
description: "Ask respondents to order items by preference or importance",
icon: ListOrderedIcon,
preset: {
headline: { default: "What is most important for you in life?" },
@@ -151,7 +151,7 @@ export const questionTypes: TQuestion[] = [
{
id: QuestionId.Matrix,
label: "Matrix",
description: "This is a matrix question",
description: "Create a grid to rate multiple items on the same set of criteria",
icon: Grid3X3Icon,
preset: {
headline: { default: "How much do you love these flowers?" },
@@ -162,7 +162,7 @@ export const questionTypes: TQuestion[] = [
{
id: QuestionId.CTA,
label: "Statement (Call to Action)",
description: "Prompt respondents to perform an action",
description: "Display information and prompt users to take a specific action",
icon: MousePointerClickIcon,
preset: {
headline: { default: "You are one of our power users!" },
@@ -178,7 +178,7 @@ export const questionTypes: TQuestion[] = [
{
id: QuestionId.Consent,
label: "Consent",
description: "Ask respondents for consent",
description: "Ask to agree to terms, conditions, or data usage",
icon: CheckIcon,
preset: {
headline: { default: "Terms and Conditions" },
@@ -189,7 +189,7 @@ export const questionTypes: TQuestion[] = [
{
id: QuestionId.FileUpload,
label: "File Upload",
description: "Allow respondents to upload a file",
description: "Enable respondents to upload documents, images, or other files",
icon: ArrowUpFromLineIcon,
preset: {
headline: { default: "File Upload" },
@@ -199,7 +199,7 @@ export const questionTypes: TQuestion[] = [
{
id: QuestionId.Date,
label: "Date",
description: "Ask your users to select a date",
description: "Ask for a date selection",
icon: CalendarDaysIcon,
preset: {
headline: { default: "When is your birthday?" },
@@ -209,7 +209,7 @@ export const questionTypes: TQuestion[] = [
{
id: QuestionId.Cal,
label: "Schedule a meeting",
description: "Allow respondents to schedule a meet",
description: "Ask respondents to book a time slot for meetings or calls",
icon: PhoneIcon,
preset: {
headline: { default: "Schedule a call with me" },
@@ -219,7 +219,7 @@ export const questionTypes: TQuestion[] = [
{
id: QuestionId.Address,
label: "Address",
description: "Allow respondents to provide their address",
description: "Ask for a mailing address",
icon: HomeIcon,
preset: {
headline: { default: "Where do you live?" },
@@ -234,7 +234,7 @@ export const questionTypes: TQuestion[] = [
{
id: QuestionId.ContactInfo,
label: "Contact Info",
description: "Allow respondents to provide their contact info",
description: "Ask for name, surname, email, phone number and company jointly",
icon: ContactIcon,
preset: {
headline: { default: "Contact Info" },

View File

@@ -35,7 +35,7 @@
"build": "tsc && vite build",
"build:dev": "tsc && vite build --mode dev",
"lint": "eslint src --ext .ts,.js,.tsx,.jsx",
"go": "vite build --watch --mode dev",
"dev": "vite build --watch --mode dev",
"clean": "rimraf .turbo node_modules dist .turbo"
},
"devDependencies": {

View File

@@ -373,7 +373,7 @@ const renderHtml = (options: Partial<SurveyInlineProps> & { apiHost?: string }):
}
const script = document.createElement("script");
script.src = "${options.apiHost ?? "http://localhost:3000"}/api/packages/surveys";
script.src = "${options.apiHost ?? "http://localhost:3000"}/js/surveys.umd.cjs";
script.async = true;
script.onload = () => loadSurvey();
script.onerror = (error) => {

View File

@@ -11,7 +11,7 @@ type ResponseErrorComponentProps = {
export const ResponseErrorComponent = ({ questions, responseData, onRetry }: ResponseErrorComponentProps) => {
return (
<div className={"fb-flex fb-flex-col fb-bg-white fb-p-4s"}>
<div className={"fb-flex fb-flex-col fb-bg-white fb-p-4"}>
<span className={"fb-mb-1.5 fb-text-base fb-font-bold fb-leading-6 fb-text-slate-900"}>
{"Your feedback is stuck :("}
</span>

View File

@@ -75,7 +75,7 @@ p.fb-editor-paragraph {
--fb-subheading-color: var(--slate-700);
--fb-placeholder-color: var(--slate-300);
--fb-info-text-color: var(--slate-500);
--fb-signature-text-color: var(--slate-400);
--fb-signature-text-color: var(--slate-500);
--fb-branding-text-color: var(--slate-500);
--fb-survey-background-color: white;
--fb-survey-border-color: var(--slate-50);
@@ -101,22 +101,27 @@ p.fb-editor-paragraph {
from {
width: 100%;
}
to {
width: 0%;
}
}
.fb-no-scrollbar {
-ms-overflow-style: none !important; /* Internet Explorer 10+ */
scrollbar-width: thin !important; /* Firefox */
scrollbar-color: transparent transparent !important; /* Firefox */
-ms-overflow-style: none !important;
/* Internet Explorer 10+ */
scrollbar-width: thin !important;
/* Firefox */
scrollbar-color: transparent transparent !important;
/* Firefox */
/* Chrome, Edge, and Safari */
&::-webkit-scrollbar {
width: 0 !important;
background: transparent !important;
}
&::-webkit-scrollbar-thumb {
background: transparent !important;
}
}
}

View File

@@ -3,6 +3,7 @@ import { resolve } from "path";
import { defineConfig, loadEnv } from "vite";
import dts from "vite-plugin-dts";
import tsconfigPaths from "vite-tsconfig-paths";
import { copyCompiledAssetsPlugin } from "../vite-plugins/copy-compiled-assets";
const config = ({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
@@ -26,7 +27,12 @@ const config = ({ mode }) => {
fileName: "index",
},
},
plugins: [preact(), dts({ rollupTypes: true }), tsconfigPaths()],
plugins: [
preact(),
dts({ rollupTypes: true }),
tsconfigPaths(),
copyCompiledAssetsPlugin({ filename: "surveys", distDir: resolve(__dirname, "dist") }),
],
});
};

View File

@@ -58,7 +58,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%] transform rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-xl",
"fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%] transform rounded-lg bg-white text-left shadow-xl transition-all sm:my-2 sm:w-full sm:max-w-xl",
`${noPadding ? "" : "px-4 pb-4 pt-5 sm:p-6"}`,
"data-[state='closed']:animate-fadeOut data-[state='open']:animate-fadeIn",
size && sizeClassName && sizeClassName[size],

View File

@@ -1,5 +1,6 @@
import { signIn } from "next-auth/react";
import { useCallback, useEffect } from "react";
import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage";
import { Button } from "../../Button";
import { MicrosoftIcon } from "../../icons";
@@ -7,12 +8,18 @@ export const AzureButton = ({
text = "Continue with Azure",
inviteUrl,
directRedirect = false,
lastUsed,
}: {
text?: string;
inviteUrl?: string | null;
directRedirect?: boolean;
lastUsed?: boolean;
}) => {
const handleLogin = useCallback(async () => {
if (typeof window !== "undefined") {
localStorage.setItem(FORMBRICKS_LOGGED_IN_WITH_LS, "Azure");
}
await signIn("azure-ad", {
redirect: true,
callbackUrl: inviteUrl ? inviteUrl : "/",
@@ -32,8 +39,9 @@ export const AzureButton = ({
startIconClassName="ml-2"
onClick={handleLogin}
variant="secondary"
className="w-full justify-center">
className="relative w-full justify-center">
{text}
{lastUsed && <span className="absolute right-3 text-xs">Last Used</span>}
</Button>
);
};

View File

@@ -1,17 +1,23 @@
"use client";
import { signIn } from "next-auth/react";
import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage";
import { Button } from "../../Button";
import { GithubIcon } from "../../icons";
export const GithubButton = ({
text = "Continue with Github",
inviteUrl,
lastUsed,
}: {
text?: string;
inviteUrl?: string | null;
lastUsed?: boolean;
}) => {
const handleLogin = async () => {
if (typeof window !== "undefined") {
localStorage.setItem(FORMBRICKS_LOGGED_IN_WITH_LS, "Github");
}
await signIn("github", {
redirect: true,
callbackUrl: inviteUrl ? inviteUrl : "/", // redirect after login to /
@@ -25,8 +31,9 @@ export const GithubButton = ({
startIconClassName="ml-2"
onClick={handleLogin}
variant="secondary"
className="w-full justify-center">
className="relative w-full justify-center">
{text}
{lastUsed && <span className="absolute right-3 text-xs">Last Used</span>}
</Button>
);
};

View File

@@ -1,17 +1,23 @@
"use client";
import { signIn } from "next-auth/react";
import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage";
import { Button } from "../../Button";
import { GoogleIcon } from "../../icons";
export const GoogleButton = ({
text = "Continue with Google",
inviteUrl,
lastUsed,
}: {
text?: string;
inviteUrl?: string | null;
lastUsed?: boolean;
}) => {
const handleLogin = async () => {
if (typeof window !== "undefined") {
localStorage.setItem(FORMBRICKS_LOGGED_IN_WITH_LS, "Google");
}
await signIn("google", {
redirect: true,
callbackUrl: inviteUrl ? inviteUrl : "/", // redirect after login to /
@@ -25,8 +31,9 @@ export const GoogleButton = ({
startIconClassName="ml-3"
onClick={handleLogin}
variant="secondary"
className="w-full justify-center">
className="relative w-full justify-center">
{text}
{lastUsed && <span className="absolute right-3 text-xs">Last Used</span>}
</Button>
);
};

View File

@@ -1,17 +1,23 @@
import { signIn } from "next-auth/react";
import { useCallback, useEffect } from "react";
import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage";
import { Button } from "../../Button";
export const OpenIdButton = ({
text = "Continue with OpenId Connect",
inviteUrl,
directRedirect = false,
lastUsed,
}: {
text?: string;
inviteUrl?: string | null;
directRedirect?: boolean;
lastUsed?: boolean;
}) => {
const handleLogin = useCallback(async () => {
if (typeof window !== "undefined") {
localStorage.setItem(FORMBRICKS_LOGGED_IN_WITH_LS, "OpenID");
}
await signIn("openid", {
redirect: true,
callbackUrl: inviteUrl ? inviteUrl : "/",
@@ -30,8 +36,9 @@ export const OpenIdButton = ({
startIconClassName="ml-2"
onClick={handleLogin}
variant="secondary"
className="w-full justify-center">
className="relative w-full justify-center">
{text}
{lastUsed && <span className="absolute right-3 text-xs">Last Used</span>}
</Button>
);
};

View File

@@ -21,7 +21,7 @@ export const SurveyInline = (props: Omit<SurveyInlineProps, "containerId">) => {
const loadSurveyScript: () => Promise<void> = async () => {
try {
const response = await fetch("/api/packages/surveys");
const response = await fetch("/js/surveys.umd.cjs");
if (!response.ok) {
throw new Error("Failed to load the surveys package");

View File

@@ -29,9 +29,8 @@ export const TemplateFilters = ({
{allFilters.map((filters, index) => {
if (prefilledFilters[index] !== null) return;
return (
<div className="mt-2 flex flex-wrap gap-1 last:border-r-0">
<div key={filters[0]?.value || index} className="mt-2 flex flex-wrap gap-1 last:border-r-0">
<button
key={index}
type="button"
onClick={() => handleFilterSelect(null, index)}
disabled={templateSearch && templateSearch.length > 0 ? true : false}

View File

@@ -123,6 +123,7 @@ export const TemplateList = ({
(template: TTemplate) => {
return (
<Template
key={template.name}
template={template}
activeTemplate={activeTemplate}
setActiveTemplate={setActiveTemplate}

View File

@@ -47,7 +47,7 @@
"clsx": "2.1.1",
"cmdk": "1.0.0",
"lexical": "0.17.0",
"lucide-react": "0.427.0",
"lucide-react": "0.452.0",
"mime": "4.0.4",
"react-colorful": "5.6.1",
"react-confetti": "6.1.0",

View File

@@ -0,0 +1,7 @@
module.exports = {
extends: ["@formbricks/eslint-config/library.js"],
parserOptions: {
project: "tsconfig.json",
tsconfigRootDir: __dirname,
},
};

View File

@@ -0,0 +1,61 @@
/* eslint-disable no-console -- Console logs are allowed for plguins*/
import { access, copyFile, mkdir, readdir, stat } from "node:fs/promises";
import path from "node:path";
import { type Plugin, type ResolvedConfig } from "vite";
interface CopyCompiledAssetsPluginOptions {
filename: string;
distDir: string;
}
const ensureDirectoryExists = async (dirPath: string): Promise<void> => {
try {
await access(dirPath);
} catch (error) {
if ((error as { code: string }).code === "ENOENT") {
await mkdir(dirPath, { recursive: true });
} else {
throw error;
}
}
};
export function copyCompiledAssetsPlugin(options: CopyCompiledAssetsPluginOptions): Plugin {
let config: ResolvedConfig;
return {
name: "copy-compiled-assets",
apply: "build",
configResolved(_config) {
config = _config;
},
async writeBundle() {
const outputDir = path.resolve(config.root, "../../apps/web/public/js");
const distDir = path.resolve(config.root, options.distDir);
// Create the output directory if it doesn't exist
// fs.ensureDirSync(outputDir);
await ensureDirectoryExists(outputDir);
console.log(`Ensured directory exists: ${outputDir}`);
// Copy files from distDir to outputDir
const filesToCopy = await readdir(distDir);
for (const file of filesToCopy) {
const srcFile = path.resolve(distDir, file);
const destFile = path.resolve(outputDir, file.replace("index", options.filename));
// Check if the srcFile is a regular file before copying
const fileStat = await stat(srcFile);
if (!fileStat.isFile()) {
continue; // Skip directories, or other non-regular files
}
await copyFile(srcFile, destFile);
}
console.log(`Copied ${filesToCopy.length.toString()} files to ${outputDir} (${options.filename})`);
},
};
}

View File

@@ -0,0 +1,22 @@
{
"name": "@formbricks/vite-plugins",
"license": "MIT",
"version": "1.0.0",
"private": true,
"type": "module",
"homepage": "https://formbricks.com",
"repository": {
"type": "git",
"url": "https://github.com/formbricks/formbricks"
},
"sideEffects": false,
"scripts": {
"clean": "rimraf .turbo node_modules dist",
"lint": "eslint . --ext .ts,.js,.tsx,.jsx"
},
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*",
"vite": "5.4.8"
}
}

View File

@@ -0,0 +1,3 @@
{
"extends": "@formbricks/config-typescript/js-library.json"
}

603
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff