Merge branch 'main' of https://github.com/formbricks/formbricks into feat/3214-unify-menu
@@ -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
@@ -58,3 +58,5 @@ packages/lib/uploads
|
||||
# Vite Timestamps
|
||||
*vite.config.*.timestamp-*
|
||||
|
||||
# js compiled assets
|
||||
apps/web/public/js
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -43,7 +43,7 @@ All you need to do is copy a `<script>` tag to your HTML head, and that’s 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 -->
|
||||
```
|
||||
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 44 KiB |
BIN
apps/docs/app/best-practices/contact-form/images/embed.png
Normal file
|
After Width: | Height: | Size: 201 KiB |
|
After Width: | Height: | Size: 48 KiB |
BIN
apps/docs/app/best-practices/contact-form/images/name-field.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
apps/docs/app/best-practices/contact-form/images/query-form.webp
Normal file
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
BIN
apps/docs/app/best-practices/contact-form/images/welcome1.webp
Normal file
|
After Width: | Height: | Size: 69 KiB |
169
apps/docs/app/best-practices/contact-form/page.mdx
Normal 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 We’ll 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 they’re 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, let’s 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
|
||||
|
||||
Let’s 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, let’s provide a space for the user’s 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! You’ve 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. **You’ve achieved just that!**
|
||||
@@ -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>
|
||||
.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 -->
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`}
|
||||
|
||||
@@ -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]) => {
|
||||
|
||||
24
apps/web/app/(app)/components/LoadingCard.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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),
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>{" "}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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 'Powered by Formbricks' 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 'Powered by Formbricks' Signature</Label>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</PageContentWrapper>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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're working on SDKs for Flutter, Swift and Kotlin.</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="mt-2 text-sm italic text-slate-700">
|
||||
We'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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
32
packages/ee/multi-language/components/language-labels.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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") }),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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") }),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -123,6 +123,7 @@ export const TemplateList = ({
|
||||
(template: TTemplate) => {
|
||||
return (
|
||||
<Template
|
||||
key={template.name}
|
||||
template={template}
|
||||
activeTemplate={activeTemplate}
|
||||
setActiveTemplate={setActiveTemplate}
|
||||
|
||||
@@ -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",
|
||||
|
||||
7
packages/vite-plugins/.eslintrc.cjs
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ["@formbricks/eslint-config/library.js"],
|
||||
parserOptions: {
|
||||
project: "tsconfig.json",
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
};
|
||||
61
packages/vite-plugins/copy-compiled-assets/index.ts
Normal 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})`);
|
||||
},
|
||||
};
|
||||
}
|
||||
22
packages/vite-plugins/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
3
packages/vite-plugins/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "@formbricks/config-typescript/js-library.json"
|
||||
}
|
||||