Compare commits

..

22 Commits

Author SHA1 Message Date
pandeymangg
474ae4478f formssssS 2024-05-24 23:04:05 +05:30
pandeymangg
a01107521e fix: FormProvider 2024-05-24 15:05:48 +05:30
pandeymangg
2e17e70c00 fix: isDirty changes with react hook form 2024-05-24 13:49:00 +05:30
pandeymangg
641b597ddc isDirty 2024-05-24 12:11:30 +05:30
pandeymangg
b6986e5470 Merge branch 'main' into fix/product-settings-form 2024-05-23 11:30:45 +05:30
pandeymangg
82a5eed6e5 fix: Form element 2024-05-23 11:28:16 +05:30
pandeymangg
0f20a4460f fix: placement form 2024-05-23 10:41:33 +05:30
pandeymangg
95ae35a3b5 fix: adds form component and refactors the current approach 2024-05-23 10:19:05 +05:30
Matti Nannt
27b37a5f27 fix: convert number attributes to strings automatically (#2679) 2024-05-22 17:48:02 +02:00
Dhruwang Jariwala
c37d3ae0f9 fix: ui issues (#2665) 2024-05-22 14:03:17 +00:00
Matti Nannt
e69bb3501d docs: update advanced docker update steps (#2678) 2024-05-22 15:22:30 +02:00
pandeymangg
ddd91607b1 Merge branch 'main' into fix/product-settings-form 2024-05-22 18:46:32 +05:30
pandeymangg
325eeb10ef fix: look and feel settings react hook form 2024-05-22 18:34:44 +05:30
Matti Nannt
eda9c00548 fix: migration instructions to 2.0 (#2676) 2024-05-22 14:35:11 +02:00
Jonas Höbenreich
6a7e0d3ecb fix: set "desktop" as default device type (#2675) 2024-05-22 12:11:25 +00:00
Anshuman Pandey
48859facf4 fix: formbricks init error (#2633)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2024-05-22 12:07:17 +00:00
Matti Nannt
732b8b599f fix: docker file shouldn't check npm registry on runtime (#2663) 2024-05-21 13:39:51 +02:00
Johannes
00ad4c3895 fix: fix links to integrations on docs (#2664) 2024-05-21 11:57:24 +02:00
Dhruwang Jariwala
4858bdd838 fix: rating question and updated attribute labels on person page (#2657)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-05-20 15:02:28 +00:00
Shubham Palriwala
eee78a79d9 fix: navbar on mobile docs for grouped features (#2658)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-05-20 14:57:18 +00:00
Johannes
aa890affc9 fix: update links for integrations (#2659) 2024-05-20 16:45:41 +02:00
Anshuman Pandey
10aed2d9d8 fix: UI fixes (#2653)
Co-authored-by: Johannes <johannes@formbricks.com>
2024-05-20 09:11:18 +00:00
83 changed files with 988 additions and 2562 deletions

View File

@@ -1,26 +0,0 @@
name: Build formbricks-com
on:
workflow_call:
jobs:
build:
name: Build Formbricks-com
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Setup Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 20.x
- name: Install pnpm
uses: pnpm/action-setup@v2
- name: Install dependencies
run: pnpm install --config.platform=linux --config.architecture=x64
- name: Build Formbricks-com
run: pnpm build --filter=formbricks-com...

View File

@@ -1,131 +0,0 @@
name: Kamal Deploy
concurrency:
group: deploy-to-kamal
cancel-in-progress: false
on:
workflow_dispatch:
push:
branches:
- main
jobs:
Deploy:
runs-on: ubuntu-latest
environment: production
env:
DOCKER_BUILDKIT: 1
IS_FORMBRICKS_CLOUD: ${{ vars.IS_FORMBRICKS_CLOUD }}
WEBAPP_URL: ${{ vars.WEBAPP_URL }}
MIGRATE_DATABASE_URL: ${{ secrets.MIGRATE_DATABASE_URL }}
NEXTAUTH_URL: ${{ vars.NEXTAUTH_URL }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
SHORT_URL_BASE: ${{ vars.SHORT_URL_BASE }}
MAIL_FROM: ${{ secrets.MAIL_FROM }}
SMTP_HOST: ${{ secrets.SMTP_HOST }}
SMTP_PORT: ${{ secrets.SMTP_PORT }}
SMTP_USER: ${{ secrets.SMTP_USER }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
PRIVACY_URL: ${{ vars.PRIVACY_URL }}
TERMS_URL: ${{ vars.TERMS_URL }}
IMPRINT_URL: ${{ vars.IMPRINT_URL }}
GITHUB_ID: ${{ secrets.FB_GITHUB_ID }}
GITHUB_SECRET: ${{ secrets.FB_GITHUB_SECRET }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
AZUREAD_CLIENT_ID: ${{ secrets.AZUREAD_CLIENT_ID }}
AZUREAD_CLIENT_SECRET: ${{ secrets.AZUREAD_CLIENT_SECRET }}
AZUREAD_TENANT_ID: ${{ secrets.AZUREAD_TENANT_ID }}
OIDC_CLIENT_ID: ${{ secrets.OIDC_CLIENT_ID }}
OIDC_CLIENT_SECRET: ${{ secrets.OIDC_CLIENT_SECRET }}
OIDC_ISSUER: ${{ secrets.OIDC_ISSUER }}
OIDC_DISPLAY_NAME: ${{ secrets.OIDC_DISPLAY_NAME }}
OIDC_SIGNING_ALGORITHM: ${{ secrets.OIDC_SIGNING_ALGORITHM }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
ASSET_PREFIX_URL: ${{ vars.ASSET_PREFIX_URL }}
NOTION_OAUTH_CLIENT_ID: ${{ secrets.NOTION_OAUTH_CLIENT_ID }}
NOTION_OAUTH_CLIENT_SECRET: ${{ secrets.NOTION_OAUTH_CLIENT_SECRET }}
SLACK_CLIENT_ID: ${{ secrets.SLACK_CLIENT_ID }}
SLACK_CLIENT_SECRET: ${{ secrets.SLACK_CLIENT_SECRET }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
GOOGLE_SHEETS_CLIENT_ID: ${{ secrets.GOOGLE_SHEETS_CLIENT_ID }}
GOOGLE_SHEETS_CLIENT_SECRET: ${{ secrets.GOOGLE_SHEETS_CLIENT_SECRET }}
GOOGLE_SHEETS_REDIRECT_URL: ${{ secrets.GOOGLE_SHEETS_REDIRECT_URL }}
AIRTABLE_CLIENT_ID: ${{ secrets.AIRTABLE_CLIENT_ID }}
ENTERPRISE_LICENSE_KEY: ${{ secrets.ENTERPRISE_LICENSE_KEY }}
DEFAULT_TEAM_ID: ${{ vars.DEFAULT_TEAM_ID }}
ONBOARDING_DISABLED: ${{ vars.ONBOARDING_DISABLED }}
CUSTOMER_IO_API_KEY: ${{ secrets.CUSTOMER_IO_API_KEY }}
CUSTOMER_IO_SITE_ID: ${{ secrets.CUSTOMER_IO_SITE_ID }}
NEXT_PUBLIC_POSTHOG_API_KEY: ${{ vars.NEXT_PUBLIC_POSTHOG_API_KEY }}
NEXT_PUBLIC_POSTHOG_API_HOST: ${{ vars.NEXT_PUBLIC_POSTHOG_API_HOST }}
NEXT_PUBLIC_FORMBRICKS_API_HOST: ${{ vars.NEXT_PUBLIC_FORMBRICKS_API_HOST }}
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID }}
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID }}
NEXT_PUBLIC_SENTRY_DSN: ${{ vars.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
NODE_ENV: production
CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}
CLOUDFLARE_DNS_API_TOKEN: ${{ secrets.CLOUDFLARE_DNS_API_TOKEN }}
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
S3_REGION: ${{ vars.S3_REGION }}
S3_BUCKET_NAME: ${{ vars.S3_BUCKET_NAME }}
OPENTELEMETRY_LISTENER_URL: ${{ vars.OPENTELEMETRY_LISTENER_URL }}
RATE_LIMITING_DISABLED: ${{ vars.RATE_LIMITING_DISABLED }}
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
DB_HOST: ${{ secrets.DB_HOST }}
DB_USER: ${{ secrets.DB_USER }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
DB_NAME: ${{ secrets.DB_NAME }}
REDIS_URL: ${{ secrets.REDIS_URL }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3.0
bundler-cache: true
- name: Install dependencies
run: |
gem install kamal
- uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
- name: Create builder
run: docker buildx create --use --name formbricks-gh-actions-builder
if: steps.buildx.outputs.should_create_builder == 'true'
- name: Push env variables to Kamal
run: |
kamal() { command kamal "$@" -c kamal/deploy.yml; }
kamal env push
- name: Run deploy command
run: |
kamal() { command kamal "$@" -c kamal/deploy.yml; }
set +e
DEPLOY_OUTPUT=$(kamal deploy 2>&1)
DEPLOY_EXIT_CODE=$?
echo "$DEPLOY_OUTPUT"
if [[ "$DEPLOY_OUTPUT" == *"container not unhealthy (healthy)"* ]]; then
echo "Deployment reported healthy container. Considering as success."
kamal lock release
exit 0
else
exit $DEPLOY_EXIT_CODE
fi
shell: bash

View File

@@ -1,128 +0,0 @@
name: Kamal Setup
concurrency:
group: setup-kamal
cancel-in-progress: false
on:
workflow_dispatch: # Only to be triggered when accessories are updated
jobs:
Setup:
runs-on: ubuntu-latest
environment: production
env:
DOCKER_BUILDKIT: 1
IS_FORMBRICKS_CLOUD: ${{ vars.IS_FORMBRICKS_CLOUD }}
WEBAPP_URL: ${{ vars.WEBAPP_URL }}
NEXTAUTH_URL: ${{ vars.NEXTAUTH_URL }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
MIGRATE_DATABASE_URL: ${{ secrets.MIGRATE_DATABASE_URL }}
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
SHORT_URL_BASE: ${{ vars.SHORT_URL_BASE }}
MAIL_FROM: ${{ secrets.MAIL_FROM }}
SMTP_HOST: ${{ secrets.SMTP_HOST }}
SMTP_PORT: ${{ secrets.SMTP_PORT }}
SMTP_USER: ${{ secrets.SMTP_USER }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
PRIVACY_URL: ${{ vars.PRIVACY_URL }}
TERMS_URL: ${{ vars.TERMS_URL }}
IMPRINT_URL: ${{ vars.IMPRINT_URL }}
GITHUB_ID: ${{ secrets.FB_GITHUB_ID }}
GITHUB_SECRET: ${{ secrets.FB_GITHUB_SECRET }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
AZUREAD_CLIENT_ID: ${{ secrets.AZUREAD_CLIENT_ID }}
AZUREAD_CLIENT_SECRET: ${{ secrets.AZUREAD_CLIENT_SECRET }}
AZUREAD_TENANT_ID: ${{ secrets.AZUREAD_TENANT_ID }}
OIDC_CLIENT_ID: ${{ secrets.OIDC_CLIENT_ID }}
OIDC_CLIENT_SECRET: ${{ secrets.OIDC_CLIENT_SECRET }}
OIDC_ISSUER: ${{ secrets.OIDC_ISSUER }}
OIDC_DISPLAY_NAME: ${{ secrets.OIDC_DISPLAY_NAME }}
OIDC_SIGNING_ALGORITHM: ${{ secrets.OIDC_SIGNING_ALGORITHM }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
ASSET_PREFIX_URL: ${{ vars.ASSET_PREFIX_URL }}
NOTION_OAUTH_CLIENT_ID: ${{ secrets.NOTION_OAUTH_CLIENT_ID }}
NOTION_OAUTH_CLIENT_SECRET: ${{ secrets.NOTION_OAUTH_CLIENT_SECRET }}
SLACK_CLIENT_ID: ${{ secrets.SLACK_CLIENT_ID }}
SLACK_CLIENT_SECRET: ${{ secrets.SLACK_CLIENT_SECRET }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
GOOGLE_SHEETS_CLIENT_ID: ${{ secrets.GOOGLE_SHEETS_CLIENT_ID }}
GOOGLE_SHEETS_CLIENT_SECRET: ${{ secrets.GOOGLE_SHEETS_CLIENT_SECRET }}
GOOGLE_SHEETS_REDIRECT_URL: ${{ secrets.GOOGLE_SHEETS_REDIRECT_URL }}
AIRTABLE_CLIENT_ID: ${{ secrets.AIRTABLE_CLIENT_ID }}
ENTERPRISE_LICENSE_KEY: ${{ secrets.ENTERPRISE_LICENSE_KEY }}
DEFAULT_TEAM_ID: ${{ vars.DEFAULT_TEAM_ID }}
ONBOARDING_DISABLED: ${{ vars.ONBOARDING_DISABLED }}
CUSTOMER_IO_API_KEY: ${{ secrets.CUSTOMER_IO_API_KEY }}
CUSTOMER_IO_SITE_ID: ${{ secrets.CUSTOMER_IO_SITE_ID }}
NEXT_PUBLIC_POSTHOG_API_KEY: ${{ vars.NEXT_PUBLIC_POSTHOG_API_KEY }}
NEXT_PUBLIC_POSTHOG_API_HOST: ${{ vars.NEXT_PUBLIC_POSTHOG_API_HOST }}
NEXT_PUBLIC_FORMBRICKS_API_HOST: ${{ vars.NEXT_PUBLIC_FORMBRICKS_API_HOST }}
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID }}
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID }}
NEXT_PUBLIC_SENTRY_DSN: ${{ vars.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
NODE_ENV: production
CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}
CLOUDFLARE_DNS_API_TOKEN: ${{ secrets.CLOUDFLARE_DNS_API_TOKEN }}
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
S3_REGION: ${{ vars.S3_REGION }}
S3_BUCKET_NAME: ${{ vars.S3_BUCKET_NAME }}
OPENTELEMETRY_LISTENER_URL: ${{ vars.OPENTELEMETRY_LISTENER_URL }}
RATE_LIMITING_DISABLED: ${{ vars.RATE_LIMITING_DISABLED }}
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
DB_HOST: ${{ secrets.DB_HOST }}
DB_USER: ${{ secrets.DB_USER }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
DB_NAME: ${{ secrets.DB_NAME }}
REDIS_URL: ${{ secrets.REDIS_URL }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3.0
bundler-cache: true
- name: Install dependencies
run: |
gem install kamal
- uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
- name: Create builder
run: docker buildx create --use --name formbricks-gh-actions-builder
if: steps.buildx.outputs.should_create_builder == 'true'
- name: Push env variables to Kamal
run: |
kamal() { command kamal "$@" -c kamal/deploy.yml; }
kamal env push
- name: Run setup command
run: |
kamal() { command kamal "$@" -c kamal/deploy.yml; }
set +e
DEPLOY_OUTPUT=$(kamal setup 2>&1)
DEPLOY_EXIT_CODE=$?
echo "$DEPLOY_OUTPUT"
if [[ "$DEPLOY_OUTPUT" == *"container not unhealthy (healthy)"* ]]; then
echo "Deployment reported healthy container. Considering as success."
kamal lock release
exit 0
else
exit $DEPLOY_EXIT_CODE
fi
shell: bash

View File

@@ -1,9 +1,4 @@
name: Docker
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Docker Release to GitHub
on:
workflow_dispatch:
@@ -53,6 +48,17 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set tags based on event type
id: set-tags
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
echo "::set-output name=tags::latest,${{ github.ref }}"
fi
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "::set-output name=tags::experimental"
fi
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
@@ -60,6 +66,7 @@ jobs:
uses: docker/metadata-action@v5 # v5.0.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: ${{ steps.set-tags.outputs.tags }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action

View File

@@ -1,44 +0,0 @@
name: Release on Dockerhub
on:
push:
tags:
- "v*"
jobs:
release-image-on-dockerhub:
name: Release on Dockerhub
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
steps:
- name: Checkout Repo
uses: actions/checkout@v2
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Get Release Tag
id: extract_release_tag
run: |
TAG=${{ github.ref }}
TAG=${TAG#refs/tags/v}
echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./apps/web/Dockerfile
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/formbricks:${{ env.RELEASE_TAG }}
${{ secrets.DOCKER_USERNAME }}/formbricks:latest

View File

@@ -1,31 +0,0 @@
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
import { classNames } from "../../lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={classNames(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };

View File

@@ -1,91 +0,0 @@
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import * as React from "react";
import { classNames } from "../../lib/utils";
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
({ className, ...props }, ref) => (
<ol
ref={ref}
className={classNames(
"text-muted-foreground flex flex-wrap items-center gap-1.5 break-words text-sm sm:gap-2.5",
className
)}
{...props}
/>
)
);
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
({ className, ...props }, ref) => (
<li ref={ref} className={classNames("inline-flex items-center gap-1.5", className)} {...props} />
)
);
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a";
return (
<Comp ref={ref} className={classNames("hover:text-foreground transition-colors", className)} {...props} />
);
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={classNames("text-foreground font-normal", className)}
{...props}
/>
)
);
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
<li role="presentation" aria-hidden="true" className={classNames("[&>svg]:size-3.5", className)} {...props}>
{children ?? <ChevronRight />}
</li>
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={classNames("flex h-9 w-9 items-center justify-center", className)}
{...props}>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
};

View File

@@ -1,47 +0,0 @@
import { Slot } from "@radix-ui/react-slot";
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
import { classNames } from "../../lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return <Comp className={classNames(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
}
);
Button.displayName = "Button";
export { Button, buttonVariants };

View File

@@ -1,53 +0,0 @@
import * as React from "react";
import { classNames } from "../../lib/utils";
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={classNames("bg-card text-card-foreground rounded-lg border shadow-sm", className)}
{...props}
/>
)
);
Card.displayName = "Card";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={classNames("flex flex-col space-y-1.5 p-6", className)} {...props} />
)
);
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3
ref={ref}
className={classNames("text-2xl font-semibold leading-none tracking-tight", className)}
{...props}
/>
)
);
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={classNames("text-muted-foreground text-sm", className)} {...props} />
)
);
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => <div ref={ref} className={classNames("p-6 pt-0", className)} {...props} />
);
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={classNames("flex items-center p-6 pt-0", className)} {...props} />
)
);
CardFooter.displayName = "CardFooter";
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };

View File

@@ -1,182 +0,0 @@
"use client";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import * as React from "react";
import { classNames } from "../../lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={classNames(
"focus:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
inset && "pl-8",
className
)}
{...props}>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={classNames(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
));
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={classNames(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={classNames(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={classNames(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={classNames(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={classNames("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={classNames("bg-muted -mx-1 my-1 h-px", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={classNames("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />;
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
};

View File

@@ -1,22 +0,0 @@
import * as React from "react";
import { classNames } from "../../lib/utils";
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={classNames(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
);
});
Input.displayName = "Input";
export { Input };

View File

@@ -1,89 +0,0 @@
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
import * as React from "react";
import { classNames } from "../../lib/utils";
import { ButtonProps, buttonVariants } from "./button";
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={classNames("mx-auto flex w-full justify-center", className)}
{...props}
/>
);
Pagination.displayName = "Pagination";
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
({ className, ...props }, ref) => (
<ul ref={ref} className={classNames("flex flex-row items-center gap-1", className)} {...props} />
)
);
PaginationContent.displayName = "PaginationContent";
const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(
({ className, ...props }, ref) => <li ref={ref} className={classNames("", className)} {...props} />
);
PaginationItem.displayName = "PaginationItem";
type PaginationLinkProps = {
isActive?: boolean;
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">;
const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={classNames(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
);
PaginationLink.displayName = "PaginationLink";
const PaginationPrevious = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={classNames("gap-1 pl-2.5", className)}
{...props}>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
);
PaginationPrevious.displayName = "PaginationPrevious";
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={classNames("gap-1 pr-2.5", className)}
{...props}>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
);
PaginationNext.displayName = "PaginationNext";
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span aria-hidden className={classNames("flex h-9 w-9 items-center justify-center", className)} {...props}>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
);
PaginationEllipsis.displayName = "PaginationEllipsis";
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
};

View File

@@ -1,24 +0,0 @@
"use client";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import * as React from "react";
import { classNames } from "../../lib/utils";
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={classNames("bg-secondary relative h-4 w-full overflow-hidden rounded-full", className)}
{...props}>
<ProgressPrimitive.Indicator
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress };

View File

@@ -1,26 +0,0 @@
"use client";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import * as React from "react";
import { classNames } from "../../lib/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={classNames(
"bg-border shrink-0",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
));
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };

View File

@@ -1,120 +0,0 @@
"use client";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { type VariantProps, cva } from "class-variance-authority";
import { X } from "lucide-react";
import * as React from "react";
import { classNames } from "../../lib/utils";
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={classNames(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
className
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
);
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>(
({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content ref={ref} className={classNames(sheetVariants({ side }), className)} {...props}>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
)
);
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={classNames("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={classNames("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props}
/>
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={classNames("text-foreground text-lg font-semibold", className)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={classNames("text-muted-foreground text-sm", className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetOverlay,
SheetPortal,
SheetTitle,
SheetTrigger,
};

View File

@@ -1,85 +0,0 @@
import * as React from "react";
import { classNames } from "../../lib/utils";
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table ref={ref} className={classNames("w-full caption-bottom text-sm", className)} {...props} />
</div>
)
);
Table.displayName = "Table";
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<thead ref={ref} className={classNames("[&_tr]:border-b", className)} {...props} />
)
);
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tbody ref={ref} className={classNames("[&_tr:last-child]:border-0", className)} {...props} />
)
);
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={classNames("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)}
{...props}
/>
)
);
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
className={classNames(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
);
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<th
ref={ref}
className={classNames(
"text-muted-foreground h-12 px-4 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
)
);
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<td
ref={ref}
className={classNames("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
)
);
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
({ className, ...props }, ref) => (
<caption ref={ref} className={classNames("text-muted-foreground mt-4 text-sm", className)} {...props} />
)
);
TableCaption.displayName = "TableCaption";
export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow };

View File

@@ -1,55 +0,0 @@
"use client";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import * as React from "react";
import { classNames } from "../../lib/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={classNames(
"bg-muted text-muted-foreground inline-flex h-10 items-center justify-center rounded-md p-1",
className
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={classNames(
"ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm",
className
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={classNames(
"ring-offset-background focus-visible:ring-ring mt-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
className
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsContent, TabsList, TabsTrigger };

View File

@@ -1,30 +0,0 @@
"use client";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import * as React from "react";
import { classNames } from "../../lib/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={classNames(
"bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md",
className
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };

View File

@@ -13,21 +13,13 @@
"dependencies": {
"@formbricks/js": "workspace:*",
"@formbricks/ui": "workspace:*",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"classnames": "^2.5.1",
"lucide-react": "^0.378.0",
"next": "14.2.3",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@formbricks/tsconfig": "workspace:*",
"eslint-config-formbricks": "workspace:*"
"eslint-config-formbricks": "workspace:*",
"@formbricks/tsconfig": "workspace:*"
}
}

View File

@@ -1,663 +0,0 @@
import {
ChevronLeft,
ChevronRight,
Copy,
CreditCard,
File,
Home,
LineChart,
ListFilter,
MoreVertical,
Package,
Package2,
PanelLeft,
Search,
Settings,
ShoppingCart,
Truck,
Users2,
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
import formbricks from "@formbricks/js/app";
import { Badge } from "../../components/ui/badge";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "../../components/ui/breadcrumb";
import { Button } from "../../components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "../../components/ui/card";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../../components/ui/dropdown-menu";
import { Input } from "../../components/ui/input";
import { Pagination, PaginationContent, PaginationItem } from "../../components/ui/pagination";
import { Progress } from "../../components/ui/progress";
import { Separator } from "../../components/ui/separator";
import { Sheet, SheetContent, SheetTrigger } from "../../components/ui/sheet";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../components/ui/tabs";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../components/ui/tooltip";
declare const window: any;
const SaaSPage = ({}) => {
const router = useRouter();
useEffect(() => {
// enable Formbricks debug mode by adding formbricksDebug=true GET parameter
const addFormbricksDebugParam = () => {
const urlParams = new URLSearchParams(window.location.search);
if (!urlParams.has("formbricksDebug")) {
urlParams.set("formbricksDebug", "true");
const newUrl = `${window.location.pathname}?${urlParams.toString()}`;
window.history.replaceState({}, "", newUrl);
}
};
addFormbricksDebugParam();
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
const userId = "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING";
const userInitAttributes = {
language: "de",
"Init Attribute 1": "eight",
"Init Attribute 2": "two",
};
formbricks.init({
environmentId: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
apiHost: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST,
userId,
attributes: userInitAttributes,
});
}
// Connect next.js router to Formbricks
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
const handleRouteChange = formbricks?.registerRouteChange;
router.events.on("routeChangeComplete", handleRouteChange);
return () => {
router.events.off("routeChangeComplete", handleRouteChange);
};
}
});
return (
<div className="bg-muted/40 flex min-h-screen w-full flex-col">
<TooltipProvider delayDuration={10}>
<aside className="bg-background fixed inset-y-0 left-0 z-10 hidden w-14 flex-col border-r sm:flex">
<nav className="flex flex-col items-center gap-4 px-2 sm:py-5">
<Link
href="#"
className="bg-primary text-primary-foreground group flex h-9 w-9 shrink-0 items-center justify-center gap-2 rounded-full text-lg font-semibold md:h-8 md:w-8 md:text-base">
<Package2 className="h-4 w-4 transition-all group-hover:scale-110" />
<span className="sr-only">Acme Inc</span>
</Link>
<Tooltip>
<TooltipTrigger asChild>
<Link
href="#"
className="text-muted-foreground hover:text-foreground flex h-9 w-9 items-center justify-center rounded-lg transition-colors md:h-8 md:w-8">
<Home className="h-5 w-5" />
<span className="sr-only">Dashboard</span>
</Link>
</TooltipTrigger>
<TooltipContent side="right">Dashboard</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Link
href="#"
className="bg-accent text-accent-foreground hover:text-foreground flex h-9 w-9 items-center justify-center rounded-lg transition-colors md:h-8 md:w-8">
<ShoppingCart className="h-5 w-5" />
<span className="sr-only">Orders</span>
</Link>
</TooltipTrigger>
<TooltipContent side="right">Orders</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Link
href="#"
className="text-muted-foreground hover:text-foreground flex h-9 w-9 items-center justify-center rounded-lg transition-colors md:h-8 md:w-8">
<Package className="h-5 w-5" />
<span className="sr-only">Products</span>
</Link>
</TooltipTrigger>
<TooltipContent side="right">Products</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Link
href="#"
className="text-muted-foreground hover:text-foreground flex h-9 w-9 items-center justify-center rounded-lg transition-colors md:h-8 md:w-8">
<Users2 className="h-5 w-5" />
<span className="sr-only">Customers</span>
</Link>
</TooltipTrigger>
<TooltipContent side="right">Customers</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Link
href="#"
className="text-muted-foreground hover:text-foreground flex h-9 w-9 items-center justify-center rounded-lg transition-colors md:h-8 md:w-8">
<LineChart className="h-5 w-5" />
<span className="sr-only">Analytics</span>
</Link>
</TooltipTrigger>
<TooltipContent side="right">Analytics</TooltipContent>
</Tooltip>
</nav>
<nav className="mt-auto flex flex-col items-center gap-4 px-2 sm:py-5">
<Tooltip>
<TooltipTrigger asChild>
<Link
href="#"
className="text-muted-foreground hover:text-foreground flex h-9 w-9 items-center justify-center rounded-lg transition-colors md:h-8 md:w-8">
<Settings className="h-5 w-5" />
<span className="sr-only">Settings</span>
</Link>
</TooltipTrigger>
<TooltipContent side="right">Settings</TooltipContent>
</Tooltip>
</nav>
</aside>
<div className="flex flex-col sm:gap-4 sm:py-4 sm:pl-14">
<header className="bg-background sticky top-0 z-30 flex h-14 items-center gap-4 border-b px-4 sm:static sm:h-auto sm:border-0 sm:bg-transparent sm:px-6">
<Sheet>
<SheetTrigger asChild>
<Button size="icon" variant="outline" className="sm:hidden">
<PanelLeft className="h-5 w-5" />
<span className="sr-only">Toggle Menu</span>
</Button>
</SheetTrigger>
<SheetContent side="left" className="sm:max-w-xs">
<nav className="grid gap-6 text-lg font-medium">
<Link
href="#"
className="bg-primary text-primary-foreground group flex h-10 w-10 shrink-0 items-center justify-center gap-2 rounded-full text-lg font-semibold md:text-base">
<Package2 className="h-5 w-5 transition-all group-hover:scale-110" />
<span className="sr-only">Acme Inc</span>
</Link>
<Link
href="#"
className="text-muted-foreground hover:text-foreground flex items-center gap-4 px-2.5">
<Home className="h-5 w-5" />
Dashboard
</Link>
<Link href="#" className="text-foreground flex items-center gap-4 px-2.5">
<ShoppingCart className="h-5 w-5" />
Orders
</Link>
<Link
href="#"
className="text-muted-foreground hover:text-foreground flex items-center gap-4 px-2.5">
<Package className="h-5 w-5" />
Products
</Link>
<Link
href="#"
className="text-muted-foreground hover:text-foreground flex items-center gap-4 px-2.5">
<Users2 className="h-5 w-5" />
Customers
</Link>
<Link
href="#"
className="text-muted-foreground hover:text-foreground flex items-center gap-4 px-2.5">
<LineChart className="h-5 w-5" />
Settings
</Link>
</nav>
</SheetContent>
</Sheet>
<Breadcrumb className="hidden md:flex">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="#">Dashboard</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="#">Orders</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Recent Orders</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="relative ml-auto flex-1 md:grow-0">
<Search className="text-muted-foreground absolute left-2.5 top-2.5 h-4 w-4" />
<Input
type="search"
placeholder="Search..."
className="bg-background w-full rounded-lg pl-8 md:w-[200px] lg:w-[336px]"
/>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="overflow-hidden rounded-full">
<Image
src="/user-avatar.png"
width={36}
height={36}
alt="Avatar"
className="overflow-hidden rounded-full"
/>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem>Support</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Logout</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</header>
<main className="grid flex-1 items-start gap-4 p-4 sm:px-6 sm:py-0 md:gap-8 lg:grid-cols-3 xl:grid-cols-3">
<div className="grid auto-rows-max items-start gap-4 md:gap-8 lg:col-span-2">
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-2 xl:grid-cols-4">
<Card className="sm:col-span-2" x-chunk="dashboard-05-chunk-0">
<CardHeader className="pb-3">
<CardTitle>Your Orders</CardTitle>
<CardDescription className="max-w-lg text-balance leading-relaxed">
Introducing Our Dynamic Orders Dashboard for Seamless Management and Insightful
Analysis.
</CardDescription>
</CardHeader>
<CardFooter>
<Button>Create New Order</Button>
</CardFooter>
</Card>
<Card x-chunk="dashboard-05-chunk-1">
<CardHeader className="pb-2">
<CardDescription>This Week</CardDescription>
<CardTitle className="text-4xl">$1,329</CardTitle>
</CardHeader>
<CardContent>
<div className="text-muted-foreground text-xs">+25% from last week</div>
</CardContent>
<CardFooter>
<Progress value={25} aria-label="25% increase" />
</CardFooter>
</Card>
<Card x-chunk="dashboard-05-chunk-2">
<CardHeader className="pb-2">
<CardDescription>This Month</CardDescription>
<CardTitle className="text-4xl">$5,329</CardTitle>
</CardHeader>
<CardContent>
<div className="text-muted-foreground text-xs">+10% from last month</div>
</CardContent>
<CardFooter>
<Progress value={12} aria-label="12% increase" />
</CardFooter>
</Card>
</div>
<Tabs defaultValue="week">
<div className="flex items-center">
<TabsList>
<TabsTrigger value="week">Week</TabsTrigger>
<TabsTrigger value="month">Month</TabsTrigger>
<TabsTrigger value="year">Year</TabsTrigger>
</TabsList>
<div className="ml-auto flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-7 gap-1 text-sm">
<ListFilter className="h-3.5 w-3.5" />
<span className="sr-only sm:not-sr-only">Filter</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Filter by</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked>Fulfilled</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Declined</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Refunded</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
<Button size="sm" variant="outline" className="h-7 gap-1 text-sm">
<File className="h-3.5 w-3.5" />
<span className="sr-only sm:not-sr-only">Export</span>
</Button>
</div>
</div>
<TabsContent value="week">
<Card x-chunk="dashboard-05-chunk-3">
<CardHeader className="px-7">
<CardTitle>Orders</CardTitle>
<CardDescription>Recent orders from your store.</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Customer</TableHead>
<TableHead className="hidden sm:table-cell">Type</TableHead>
<TableHead className="hidden sm:table-cell">Status</TableHead>
<TableHead className="hidden md:table-cell">Date</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow className="bg-accent">
<TableCell>
<div className="font-medium">Liam Johnson</div>
<div className="text-muted-foreground hidden text-sm md:inline">
liam@example.com
</div>
</TableCell>
<TableCell className="hidden sm:table-cell">Sale</TableCell>
<TableCell className="hidden sm:table-cell">
<Badge className="text-xs" variant="secondary">
Fulfilled
</Badge>
</TableCell>
<TableCell className="hidden md:table-cell">2023-06-23</TableCell>
<TableCell className="text-right">$250.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className="font-medium">Olivia Smith</div>
<div className="text-muted-foreground hidden text-sm md:inline">
olivia@example.com
</div>
</TableCell>
<TableCell className="hidden sm:table-cell">Refund</TableCell>
<TableCell className="hidden sm:table-cell">
<Badge className="text-xs" variant="outline">
Declined
</Badge>
</TableCell>
<TableCell className="hidden md:table-cell">2023-06-24</TableCell>
<TableCell className="text-right">$150.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className="font-medium">Noah Williams</div>
<div className="text-muted-foreground hidden text-sm md:inline">
noah@example.com
</div>
</TableCell>
<TableCell className="hidden sm:table-cell">Subscription</TableCell>
<TableCell className="hidden sm:table-cell">
<Badge className="text-xs" variant="secondary">
Fulfilled
</Badge>
</TableCell>
<TableCell className="hidden md:table-cell">2023-06-25</TableCell>
<TableCell className="text-right">$350.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className="font-medium">Emma Brown</div>
<div className="text-muted-foreground hidden text-sm md:inline">
emma@example.com
</div>
</TableCell>
<TableCell className="hidden sm:table-cell">Sale</TableCell>
<TableCell className="hidden sm:table-cell">
<Badge className="text-xs" variant="secondary">
Fulfilled
</Badge>
</TableCell>
<TableCell className="hidden md:table-cell">2023-06-26</TableCell>
<TableCell className="text-right">$450.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className="font-medium">Liam Johnson</div>
<div className="text-muted-foreground hidden text-sm md:inline">
liam@example.com
</div>
</TableCell>
<TableCell className="hidden sm:table-cell">Sale</TableCell>
<TableCell className="hidden sm:table-cell">
<Badge className="text-xs" variant="secondary">
Fulfilled
</Badge>
</TableCell>
<TableCell className="hidden md:table-cell">2023-06-23</TableCell>
<TableCell className="text-right">$250.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className="font-medium">Liam Johnson</div>
<div className="text-muted-foreground hidden text-sm md:inline">
liam@example.com
</div>
</TableCell>
<TableCell className="hidden sm:table-cell">Sale</TableCell>
<TableCell className="hidden sm:table-cell">
<Badge className="text-xs" variant="secondary">
Fulfilled
</Badge>
</TableCell>
<TableCell className="hidden md:table-cell">2023-06-23</TableCell>
<TableCell className="text-right">$250.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className="font-medium">Olivia Smith</div>
<div className="text-muted-foreground hidden text-sm md:inline">
olivia@example.com
</div>
</TableCell>
<TableCell className="hidden sm:table-cell">Refund</TableCell>
<TableCell className="hidden sm:table-cell">
<Badge className="text-xs" variant="outline">
Declined
</Badge>
</TableCell>
<TableCell className="hidden md:table-cell">2023-06-24</TableCell>
<TableCell className="text-right">$150.00</TableCell>
</TableRow>
<TableRow>
<TableCell>
<div className="font-medium">Emma Brown</div>
<div className="text-muted-foreground hidden text-sm md:inline">
emma@example.com
</div>
</TableCell>
<TableCell className="hidden sm:table-cell">Sale</TableCell>
<TableCell className="hidden sm:table-cell">
<Badge className="text-xs" variant="secondary">
Fulfilled
</Badge>
</TableCell>
<TableCell className="hidden md:table-cell">2023-06-26</TableCell>
<TableCell className="text-right">$450.00</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
<div>
<Card className="overflow-hidden" x-chunk="dashboard-05-chunk-4">
<CardHeader className="bg-muted/50 flex flex-row items-start">
<div className="grid gap-0.5">
<CardTitle className="group flex items-center gap-2 text-lg">
Order Oe31b70H
<Button
size="icon"
variant="outline"
className="h-6 w-6 opacity-0 transition-opacity group-hover:opacity-100">
<Copy className="h-3 w-3" />
<span className="sr-only">Copy Order ID</span>
</Button>
</CardTitle>
<CardDescription>Date: November 23, 2023</CardDescription>
</div>
<div className="ml-auto flex items-center gap-1">
<Button size="sm" variant="outline" className="h-8 gap-1">
<Truck className="h-3.5 w-3.5" />
<span className="lg:sr-only xl:not-sr-only xl:whitespace-nowrap">Track Order</span>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="icon" variant="outline" className="h-8 w-8">
<MoreVertical className="h-3.5 w-3.5" />
<span className="sr-only">More</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Export</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Trash</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
<CardContent className="p-6 text-sm">
<div className="grid gap-3">
<div className="font-semibold">Order Details</div>
<ul className="grid gap-3">
<li className="flex items-center justify-between">
<span className="text-muted-foreground">
Glimmer Lamps x <span>2</span>
</span>
<span>$250.00</span>
</li>
<li className="flex items-center justify-between">
<span className="text-muted-foreground">
Aqua Filters x <span>1</span>
</span>
<span>$49.00</span>
</li>
</ul>
<Separator className="my-2" />
<ul className="grid gap-3">
<li className="flex items-center justify-between">
<span className="text-muted-foreground">Subtotal</span>
<span>$299.00</span>
</li>
<li className="flex items-center justify-between">
<span className="text-muted-foreground">Shipping</span>
<span>$5.00</span>
</li>
<li className="flex items-center justify-between">
<span className="text-muted-foreground">Tax</span>
<span>$25.00</span>
</li>
<li className="flex items-center justify-between font-semibold">
<span className="text-muted-foreground">Total</span>
<span>$329.00</span>
</li>
</ul>
</div>
<Separator className="my-4" />
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-3">
<div className="font-semibold">Shipping Information</div>
<address className="text-muted-foreground grid gap-0.5 not-italic">
<span>Liam Johnson</span>
<span>1234 Main St.</span>
<span>Anytown, CA 12345</span>
</address>
</div>
<div className="grid auto-rows-max gap-3">
<div className="font-semibold">Billing Information</div>
<div className="text-muted-foreground">Same as shipping address</div>
</div>
</div>
<Separator className="my-4" />
<div className="grid gap-3">
<div className="font-semibold">Customer Information</div>
<dl className="grid gap-3">
<div className="flex items-center justify-between">
<dt className="text-muted-foreground">Customer</dt>
<dd>Liam Johnson</dd>
</div>
<div className="flex items-center justify-between">
<dt className="text-muted-foreground">Email</dt>
<dd>
<a href="mailto:">liam@acme.com</a>
</dd>
</div>
<div className="flex items-center justify-between">
<dt className="text-muted-foreground">Phone</dt>
<dd>
<a href="tel:">+1 234 567 890</a>
</dd>
</div>
</dl>
</div>
<Separator className="my-4" />
<div className="grid gap-3">
<div className="font-semibold">Payment Information</div>
<dl className="grid gap-3">
<div className="flex items-center justify-between">
<dt className="text-muted-foreground flex items-center gap-1">
<CreditCard className="h-4 w-4" />
Visa
</dt>
<dd>**** **** **** 4532</dd>
</div>
</dl>
</div>
</CardContent>
<CardFooter className="bg-muted/50 flex flex-row items-center border-t px-6 py-3">
<div className="text-muted-foreground text-xs">
Updated <time dateTime="2023-11-23">November 23, 2023</time>
</div>
<Pagination className="ml-auto mr-0 w-auto">
<PaginationContent>
<PaginationItem>
<Button size="icon" variant="outline" className="h-6 w-6">
<ChevronLeft className="h-3.5 w-3.5" />
<span className="sr-only">Previous Order</span>
</Button>
</PaginationItem>
<PaginationItem>
<Button size="icon" variant="outline" className="h-6 w-6">
<ChevronRight className="h-3.5 w-3.5" />
<span className="sr-only">Next Order</span>
</Button>
</PaginationItem>
</PaginationContent>
</Pagination>
</CardFooter>
</Card>
</div>
</main>
</div>
</TooltipProvider>
</div>
);
};
export default SaaSPage;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -24,8 +24,7 @@ export const metadata = {
The Airtable integration allows you to automatically send responses to an Airtable of your choice.
<Note>
This feature is enabled by default in Formbricks Cloud but needs to be self-configured when running a
self-hosted version of Formbricks.
If you are on a self-hosted instance, you will need to configure this integration separately. Please follow the guides [here](/self-hosting/integrations) to configure integrations on your self-hosted instance.
</Note>
## Formbricks Cloud

View File

@@ -21,9 +21,7 @@ export const metadata = {
The Google Sheets integration allows you to automatically send responses to a Google Sheet of your choice.
<Note>
This feature is enabled by default in Formbricks Cloud but needs to be self-configured when running a
self-hosted version of Formbricks. For self-configuration, see additional setup
[below](#setup-in-self-hosted-formbricks).
If you are on a self-hosted instance, you will need to configure this integration separately. Please follow the guides [here](/self-hosting/integrations) to configure integrations on your self-hosted instance.
</Note>
## Connect Google Sheets

View File

@@ -27,7 +27,7 @@ export const metadata = {
Make is a powerful tool to send information between Formbricks and thousands of apps. Here's how to set it up.
<Note>
### Nail down your survey first ? Any changes in the survey cause additional work in the _Scenario_. It
Nailed down your survey?? Any changes in the survey cause additional work in the _Scenario_. It
makes sense to first settle on the survey you want to run and then get to setting up Make.
</Note>

View File

@@ -29,7 +29,7 @@ export const metadata = {
n8n allows you to build flexible workflows focused on deep data integration. And with sharable templates and a user-friendly UI, the less technical people on your team can collaborate on them too. Unlike other tools, complexity is not a limitation. So you can build whatever you want — without stressing over budget. Hook up Formbricks with n8n and you can send your data to 350+ other apps. Here is how to do it.
<Note>
### Nail down your survey first Any changes in the survey cause additional work in the n8n node. It makes
Nail down your survey? Any changes in the survey cause additional work in the n8n node. It makes
sense to first settle on the survey you want to run and then get to setting up n8n.
</Note>

View File

@@ -21,8 +21,7 @@ export const metadata = {
The notion integration allows you to automatically send responses to a Notion database of your choice.
<Note>
This feature is enabled by default in Formbricks Cloud but needs to be self-configured when running a
self-hosted version of Formbricks.
If you are on a self-hosted instance, you will need to configure this integration separately. Please follow the guides [here](/self-hosting/integrations) to configure integrations on your self-hosted instance.
</Note>
## Formbricks Cloud

View File

@@ -10,7 +10,7 @@ export const metadata = {
At Formbricks, we understand the importance of integrating with third-party applications. We have step-by-step guides to configure our third-party integrations with a your Formbricks instance. We currently support the below integrations, click on them to see their individual guides:
<Note>
If you are on a self-hosted instance, you will need to configure these integrations manually. Please follow the guides [here](/self-hosting/integrations) to configure them.
If you are on a self-hosted instance, you will need to configure these integrations manually. Please follow the guides [here](/self-hosting/integrations) to configure integrations on your self-hosted instance.
</Note>
- [Airtable](/developer-docs/integrations/airtable): Automatically send responses to an Airtable of your choice.

View File

@@ -22,8 +22,7 @@ export const metadata = {
The slack integration allows you to automatically send responses to a Slack channel of your choice.
<Note>
This feature is enabled by default in Formbricks Cloud but needs to be self-configured when running a
self-hosted version of Formbricks.
If you are on a self-hosted instance, you will need to configure this integration separately. Please follow the guides [here](/self-hosting/integrations) to configure integrations on your self-hosted instance.
</Note>
## Formbricks Cloud

View File

@@ -8,7 +8,7 @@ export const metadata = {
# Advanced Setup
Quickly set up and start using Formbricks with our [official Docker image](https://github.com/formbricks/formbricks/pkgs/container/formbricks) that we've already built for you.
Quickly set up and start using Formbricks with our [official Docker image](https://github.com/formbricks/formbricks/pkgs/container/formbricks) that we've already built for you.
The pre-built image is ready-to-run, and it only requires minimal configuration on your part. It's as easy as downloading the Docker image and firing up the container.
@@ -104,20 +104,12 @@ You're now ready to start the Formbricks Docker setup. The following command wil
## Update
1. Stop the Formbricks stack
<Note>
Please take a look at our [migration guide](/self-hosting/migration-guide) for version specific steps to
update Formbricks.
</Note>
<Col>
<CodeGroup title="Stop the docker instance">
```bash
docker compose down
```
</CodeGroup>
</Col>
2. Pull the latest changes
1. Pull the latest Formbricks image
<Col>
<CodeGroup title="Pull the changes into docker">
@@ -130,8 +122,20 @@ You're now ready to start the Formbricks Docker setup. The following command wil
</Col>
3. Update env vars as necessary in the docker-compose file.
4. Re-start the Formbricks stack
2. Stop the Formbricks stack
<Col>
<CodeGroup title="Stop the docker instance">
```bash
docker compose down
```
</CodeGroup>
</Col>
3. Re-start the Formbricks stack with the updated image
<Col>
<CodeGroup title="Relaunch the Docker Instance">

View File

@@ -39,10 +39,10 @@ We have step-by-step guides to configure our third-party integrations with a sel
- [Airtable](#airtable)
- [Google Sheets](#google-sheets)
- [Notion](#notion)
- Make: We do not support [Make.com](http://Make.com) for Self-hosted instances yet! Please follow our Cloud guide [here](/integrations#make)
- Make: We do not support for self-hosted instances yet.
- [n8n](#n8n)
- [Slack](#slack)
- [Wordpress]: Wordpress setup is similar to steps mentioned in Cloud [here](/integrations#wordpress), just change the API Host to your self-hosted URL.
- Wordpress: Wordpress setup is similar to the [Cloud setup](/developer-docs/integrations/wordpress), just change the API Host to your self-hosted URL.
- [Zapier](#zapier)
<Note>

View File

@@ -20,7 +20,9 @@ Formbricks v2.0 comes with huge features such as Multi-Language Surveys and Adva
and
<Note>
If you've used the Formbricks Enterprise Edition with a free beta license key, your instance will be downgraded to the Community Edition 2.0. You find all license details on the [license page.](/self-hosting/license/)
If you've used the Formbricks Enterprise Edition with a free beta license key, your instance will be
downgraded to the Community Edition 2.0. You find all license details on the [license
page.](/self-hosting/license/)
</Note>
### Steps to Migrate
@@ -35,7 +37,7 @@ To run all these steps, please navigate to the `formbricks` folder where your `d
<CodeGroup title="Backup Postgres">
```bash
docker exec formbricks-quickstart-postgres-1 pg_dump -Fc -U postgres -d formbricks > formbricks_pre_v2.0_$(date +%Y%m%d_%H%M%S).dump
docker exec formbricks-postgres-1 pg_dump -Fc -U postgres -d formbricks > formbricks_pre_v2.0_$(date +%Y%m%d_%H%M%S).dump
```
</CodeGroup>
@@ -51,7 +53,19 @@ docker exec formbricks-quickstart-postgres-1 pg_dump -Fc -U postgres -d formbric
restore scenario you will need to use `psql` then with an empty `formbricks` database.
</Note>
2. Stop the running Formbricks instance & remove the related containers:
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">
@@ -63,7 +77,7 @@ docker-compose down
</CodeGroup>
</Col>
3. Restarting the containers will automatically pull the latest version of Formbricks:
4. Restarting the containers with the latest version of Formbricks:
<Col>
<CodeGroup title="Restart the containers">
@@ -75,7 +89,7 @@ docker-compose up -d
</CodeGroup>
</Col>
4. Now let's migrate the data to the latest schema:
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>
@@ -83,6 +97,7 @@ docker-compose up -d
<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" \
@@ -95,7 +110,7 @@ docker run --rm \
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.
5. That's it! Once the migration is complete, you can **now access your Formbricks instance** at the same URL as before.
6. That's it! Once the migration is complete, you can **now access your Formbricks instance** at the same URL as before.
### App Surveys with @formbricks/js

View File

@@ -32,7 +32,7 @@ export const Layout = ({
</Link>
</div>
<Header />
<Navigation className="hidden lg:mt-10 lg:block" />
<Navigation className="hidden lg:mt-10 lg:block" isMobile={false} />
</div>
</motion.header>
<div className="relative flex h-full flex-col px-4 pt-14 sm:px-6 lg:px-8">

View File

@@ -90,7 +90,7 @@ const MobileNavigationDialog = ({ isOpen, close }: { isOpen: boolean; close: ()
<motion.div
layoutScroll
className="ring-zinc-900/7.5 fixed bottom-0 left-0 top-14 w-full overflow-y-auto bg-white px-4 pb-4 pt-6 shadow-lg shadow-zinc-900/10 ring-1 min-[416px]:max-w-sm sm:px-6 sm:pb-10 dark:bg-zinc-900 dark:ring-zinc-800">
<Navigation />
<Navigation isMobile={true} />
</motion.div>
</Transition.Child>
</Dialog.Panel>

View File

@@ -46,25 +46,41 @@ const NavLink = ({
active = false,
isAnchorLink = false,
}: {
href: string;
href?: string;
children: React.ReactNode;
active: boolean;
isAnchorLink?: boolean;
}) => {
return (
<Link
href={href}
aria-current={active ? "page" : undefined}
className={clsx(
"flex justify-between gap-2 py-1 pr-3 text-sm transition",
isAnchorLink ? "pl-7" : "pl-4",
active
? "rounded-r-md bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-white"
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white"
)}>
<span className="flex w-full truncate">{children}</span>
</Link>
const commonClasses = clsx(
"flex justify-between gap-2 py-1 pr-3 text-sm transition",
isAnchorLink ? "pl-7" : "pl-4",
active
? "rounded-r-md bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-white"
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white"
);
if (href) {
return (
<Link
href={href}
aria-current={active ? "page" : undefined}
className={clsx(
"flex justify-between gap-2 py-1 pr-3 text-sm transition",
isAnchorLink ? "pl-7" : "pl-4",
active
? "rounded-r-md bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-white"
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white"
)}>
<span className="flex w-full truncate">{children}</span>
</Link>
);
} else {
return (
<div aria-current={active ? "page" : undefined} className={commonClasses}>
<span className="flex w-full truncate">{children}</span>
</div>
);
}
};
const VisibleSectionHighlight = ({ group, pathname }: { group: NavGroup; pathname: string }) => {
@@ -131,6 +147,7 @@ const NavigationGroup = ({
setActiveGroup,
openGroups,
setOpenGroups,
isMobile,
}: {
group: NavGroup;
className?: string;
@@ -138,6 +155,7 @@ const NavigationGroup = ({
setActiveGroup: (group: NavGroup | null) => void;
openGroups: string[];
setOpenGroups: (groups: string[]) => void;
isMobile: boolean;
}) => {
const isInsideMobileNavigation = useIsInsideMobileNavigation();
const pathname = usePathname();
@@ -171,13 +189,15 @@ const NavigationGroup = ({
{group.links.map((link) => (
<motion.li key={link.title} layout="position" className="relative">
{link.href ? (
<NavLink href={link.href} active={!!pathname?.startsWith(link.href)}>
<NavLink
href={isMobile && link.children ? "" : link.href}
active={!!pathname?.startsWith(link.href)}>
{link.title}
</NavLink>
) : (
<div onClick={() => toggleParentTitle(link.title)}>
<NavLink
href={link.children?.[0]?.href || ""}
href={!isMobile ? link.children?.[0]?.href || "" : undefined}
active={
!!(
isParentOpen(link.title) &&
@@ -197,7 +217,7 @@ const NavigationGroup = ({
</div>
)}
<AnimatePresence mode="popLayout" initial={false}>
{link.children && isParentOpen(link.title) && (
{isActiveGroup && link.children && isParentOpen(link.title) && (
<motion.ul
role="list"
initial={{ opacity: 0 }}
@@ -221,7 +241,11 @@ const NavigationGroup = ({
);
};
export const Navigation = (props: React.ComponentPropsWithoutRef<"nav">) => {
interface NavigationProps extends React.ComponentPropsWithoutRef<"nav"> {
isMobile: boolean;
}
export const Navigation = ({ isMobile, ...props }: NavigationProps) => {
const [activeGroup, setActiveGroup] = useState<NavGroup | null>(navigation[0]);
const [openGroups, setOpenGroups] = useState<string[]>([]);
@@ -237,6 +261,7 @@ export const Navigation = (props: React.ComponentPropsWithoutRef<"nav">) => {
setActiveGroup={setActiveGroup}
openGroups={openGroups}
setOpenGroups={setOpenGroups}
isMobile={isMobile}
/>
))}
<li className="sticky bottom-0 z-10 mt-6 min-[416px]:hidden">

View File

@@ -18,12 +18,11 @@ RUN turbo prune @formbricks/web --docker
FROM base AS installer
# Enable corepack and prepare pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
RUN corepack enable
# Install necessary build tools and compilers
RUN apk update && apk add --no-cache g++ cmake make gcc python3 openssl-dev jq
# Set hardcoded environment variables
ENV DATABASE_URL="postgresql://placeholder:for@build:5432/gets_overwritten_at_runtime?schema=public"
ENV NEXTAUTH_SECRET="placeholder_for_next_auth_of_64_chars_get_overwritten_at_runtime"
@@ -59,7 +58,7 @@ RUN jq -r '.devDependencies.prisma' packages/database/package.json > /prisma_ver
## step 3: setup production runner
#
FROM base AS runner
RUN corepack enable && corepack prepare pnpm@latest --activate
RUN corepack enable
RUN apk add --no-cache curl \
&& apk add --no-cache supercronic \
@@ -92,5 +91,5 @@ RUN mkdir -p /home/nextjs/apps/web/uploads/
VOLUME /home/nextjs/apps/web/uploads/
CMD supercronic -quiet /app/docker/cronjobs & \
(cd packages/database && pnpm db:migrate:deploy) && \
(cd packages/database && npm run db:migrate:deploy) && \
exec node apps/web/server.js

View File

@@ -2,7 +2,7 @@
import { formbricksEnabled } from "@/app/lib/formbricks";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { useCallback, useEffect } from "react";
import formbricks from "@formbricks/js/app";
import { env } from "@formbricks/lib/env";
@@ -15,22 +15,23 @@ export const FormbricksClient = ({ session }) => {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
if (formbricksEnabled && session?.user && formbricks) {
formbricks.init({
environmentId: env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID || "",
apiHost: env.NEXT_PUBLIC_FORMBRICKS_API_HOST || "",
userId: session.user.id,
});
formbricks.setEmail(session.user.email);
}
}, [session]);
const initializeFormbricksAndSetupRouteChanges = useCallback(async () => {
formbricks.init({
environmentId: env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID || "",
apiHost: env.NEXT_PUBLIC_FORMBRICKS_API_HOST || "",
userId: session.user.id,
});
formbricks.setEmail(session.user.email);
formbricks.registerRouteChange();
}, [session.user.email, session.user.id]);
useEffect(() => {
if (formbricksEnabled && formbricks) {
formbricks?.registerRouteChange();
if (formbricksEnabled && session?.user && formbricks) {
initializeFormbricksAndSetupRouteChanges();
}
}, [pathname, searchParams]);
}, [session, pathname, searchParams, initializeFormbricksAndSetupRouteChanges]);
return null;
};

View File

@@ -200,7 +200,7 @@ export const ActionSettingsTab = ({
This is a code action. Please make changes in your code base.
</p>
) : actionClass.type === "noCode" ? (
<>
<div className="max-h-60 overflow-auto">
<div>
<Label>Select By</Label>
</div>
@@ -225,7 +225,7 @@ export const ActionSettingsTab = ({
setIsInnerHtml={setIsInnerHtml}
register={register}
/>
</>
</div>
) : actionClass.type === "automatic" ? (
<p className="text-sm text-slate-600">
This action was created automatically. You cannot make changes to it.

View File

@@ -205,7 +205,7 @@ export const MainNavigation = ({
{product && (
<aside
className={cn(
"z-20 flex flex-col justify-between rounded-r-xl border-r border-slate-200 bg-white pt-3 shadow-md transition-all duration-100",
"z-40 flex flex-col justify-between rounded-r-xl border-r border-slate-200 bg-white pt-3 shadow-md transition-all duration-100",
!isCollapsed ? "w-sidebar-collapsed" : "w-sidebar-expanded",
environment.type === "development" ? `h-[calc(100vh-1.25rem)]` : "h-screen"
)}>

View File

@@ -11,7 +11,7 @@ interface SideBarProps {
export const TopControlBar = ({ environment, environments }: SideBarProps) => {
return (
<div className="fixed inset-0 top-0 z-10 flex h-14 w-full items-center justify-end bg-slate-50 px-6">
<div className="fixed inset-0 top-0 z-30 flex h-14 w-full items-center justify-end bg-slate-50 px-6">
<div className="shadow-xs z-10">
<div className="flex w-fit space-x-2 py-2">
<WidgetStatusIndicator environment={environment} type="mini" />

View File

@@ -3,6 +3,7 @@
import { authorize } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
import FormbricksLogo from "@/images/logo.svg";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { Button } from "@formbricks/ui/Button";
@@ -41,6 +42,12 @@ export const AirtableConnect = ({ environmentId, enabled, webAppUrl }: AirtableC
{!enabled && (
<p className="mb-8 rounded border-slate-200 bg-slate-100 p-3 text-sm">
Airtable Integration is not configured in your instance of Formbricks.
<br />
Please follow the{" "}
<Link href="https://formbricks.com/docs/self-hosting/integrations#airtable" className="underline">
docs
</Link>{" "}
to configure it.
</p>
)}
<Button variant="darkCTA" loading={isConnecting} onClick={handleGoogleLogin} disabled={!enabled}>

View File

@@ -44,7 +44,9 @@ export const Connect = ({ enabled, environmentId, webAppUrl }: ConnectProps) =>
Google Sheets Integration is not configured in your instance of Formbricks.
<br />
Please follow the{" "}
<Link href="https://formbricks.com/docs/integrations/google-sheets" className="underline">
<Link
href="https://formbricks.com/docs/self-hosting/integrations#google-sheets"
className="underline">
docs
</Link>{" "}
to configure it.

View File

@@ -54,7 +54,7 @@ export const Connect = ({ enabled, environmentId, webAppUrl }: ConnectProps) =>
Notion Integration is not configured in your instance of Formbricks.
<br />
Please follow the{" "}
<Link href="https://formbricks.com/docs/integrations/notion" className="underline">
<Link href="https://formbricks.com/docs/self-hosting/integrations#notion" className="underline">
docs
</Link>{" "}
to configure it.

View File

@@ -56,7 +56,7 @@ export const Connect = ({ isEnabled, environmentId, webAppUrl }: ConnectProps) =
Slack Integration is not configured in your instance of Formbricks.
<br />
Please follow the{" "}
<Link href="https://formbricks.com/docs/integrations/slack" className="underline">
<Link href="https://formbricks.com/docs/self-hosting/integrations#slack" className="underline">
docs
</Link>{" "}
to configure it.

View File

@@ -9,9 +9,30 @@ import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service"
import { deleteProduct, getProducts, updateProduct } from "@formbricks/lib/product/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { TEnvironment } from "@formbricks/types/environment";
import { Result, ok } from "@formbricks/types/errorHandlers";
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TProduct, TProductUpdateInput } from "@formbricks/types/product";
interface State {
params: { environmentId: string; productId: string };
response?: Result<TProduct>;
}
export const updateProductFormAction = async (state: State, data: FormData): Promise<State> => {
console.log({ state });
const formData = Object.fromEntries(data);
console.log({ formData });
const updatedProduct = await updateProductAction(state.params.environmentId, state.params.productId, {
name: formData.name as string,
});
return {
...state,
response: ok(updatedProduct),
};
};
export const updateProductAction = async (
environmentId: string,
productId: string,

View File

@@ -1,91 +0,0 @@
"use client";
import { useRouter } from "next/navigation";
import { SubmitHandler, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { TProduct } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { updateProductAction } from "../actions";
type TEditProductName = {
name: string;
};
type EditProductNameProps = {
product: TProduct;
environmentId: string;
isProductNameEditDisabled: boolean;
};
export const EditProductName: React.FC<EditProductNameProps> = ({
product,
environmentId,
isProductNameEditDisabled,
}) => {
const router = useRouter();
const {
register,
handleSubmit,
formState: { isSubmitting },
watch,
} = useForm<TEditProductName>({
defaultValues: {
name: product.name,
},
});
const productNameValue = watch("name", product.name || "");
const isNotEmptySpaces = (value: string) => value.trim() !== "";
const updateProduct: SubmitHandler<TEditProductName> = async (data) => {
data.name = data.name.trim();
try {
if (!isNotEmptySpaces(data.name)) {
toast.error("Please enter at least one character");
return;
}
if (data.name === product.name) {
toast.success("This is already your product name");
return;
}
const updatedProduct = await updateProductAction(environmentId, product.id, { name: data.name });
if (isProductNameEditDisabled) {
toast.error("Only Owners, Admins and Editors can perform this action.");
throw new Error();
}
if (!!updatedProduct?.id) {
toast.success("Product name updated successfully.");
router.refresh();
}
} catch (err) {
console.error(err);
toast.error(`Error: Unable to save product information`);
}
};
return !isProductNameEditDisabled ? (
<form className="w-full max-w-sm items-center space-y-2" onSubmit={handleSubmit(updateProduct)}>
<Label htmlFor="fullname">What&apos;s your product called?</Label>
<Input
type="text"
id="fullname"
defaultValue={product.name}
{...register("name", { required: true })}
/>
<Button
type="submit"
variant="darkCTA"
size="sm"
loading={isSubmitting}
disabled={!isNotEmptySpaces(productNameValue) || isSubmitting}>
Update
</Button>
</form>
) : (
<p className="text-sm text-red-700">Only Owners, Admins and Editors can perform this action.</p>
);
};

View File

@@ -0,0 +1,116 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRef } from "react";
import { useFormState } from "react-dom";
import { SubmitHandler, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { z } from "zod";
import { TProduct, ZProduct } from "@formbricks/types/product";
import { FormControl, FormField, FormItem, FormLabel, FormMessage, FormProvider } from "@formbricks/ui/Form";
import { Input } from "@formbricks/ui/Input";
import { updateProductAction, updateProductFormAction } from "../actions";
import { SubmitButton } from "./SubmitBtn";
type EditProductNameProps = {
product: TProduct;
environmentId: string;
isProductNameEditDisabled: boolean;
};
const ZProductNameInput = ZProduct.pick({ name: true });
type TEditProductName = z.infer<typeof ZProductNameInput>;
export const EditProductNameForm: React.FC<EditProductNameProps> = ({
product,
environmentId,
isProductNameEditDisabled,
}) => {
const form = useForm<TEditProductName>({
defaultValues: {
name: product.name,
},
resolver: zodResolver(ZProductNameInput),
mode: "onChange",
});
const formRef = useRef<HTMLFormElement>(null);
const [serverState, formAction] = useFormState(updateProductFormAction, {
params: { environmentId, productId: product.id },
});
const { errors, isDirty } = form.formState;
const nameError = errors.name?.message;
// const isSubmitting = form.formState.isSubmitting;
// const updateProduct: SubmitHandler<TEditProductName> = async (data) => {
// const name = data.name.trim();
// try {
// if (nameError) {
// toast.error(nameError);
// return;
// }
// const updatedProduct = await updateProductAction(environmentId, product.id, { name });
// if (isProductNameEditDisabled) {
// toast.error("Only Owners, Admins and Editors can perform this action.");
// return;
// }
// if (!!updatedProduct?.id) {
// toast.success("Product name updated successfully.");
// form.resetField("name", { defaultValue: updatedProduct.name });
// }
// } catch (err) {
// console.error(err);
// toast.error(`Error: Unable to save product information`);
// }
// };
return !isProductNameEditDisabled ? (
<FormProvider {...form}>
<form
ref={formRef}
className="w-full max-w-sm items-center space-y-2"
action={formAction}
onSubmit={(e) =>
form.handleSubmit(() => {
e.preventDefault();
formRef.current?.submit();
})
}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel htmlFor="name">What&apos;s your product called?</FormLabel>
<FormControl>
<Input
type="text"
id="name"
{...field}
placeholder="Product Name"
autoComplete="off"
required
isInvalid={!!nameError}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<SubmitButton />
</form>
</FormProvider>
) : (
<p className="text-sm text-red-700">Only Owners, Admins and Editors can perform this action.</p>
);
};

View File

@@ -1,76 +0,0 @@
"use client";
import { useRouter } from "next/navigation";
import { SubmitHandler, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { TProduct } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { updateProductAction } from "../actions";
type EditWaitingTimeFormValues = {
recontactDays: number;
};
type EditWaitingTimeProps = {
environmentId: string;
product: TProduct;
};
export const EditWaitingTime: React.FC<EditWaitingTimeProps> = ({ product, environmentId }) => {
const router = useRouter();
const {
register,
handleSubmit,
formState: { errors },
} = useForm<EditWaitingTimeFormValues>({
defaultValues: {
recontactDays: product.recontactDays,
},
});
const updateWaitingTime: SubmitHandler<EditWaitingTimeFormValues> = async (data) => {
try {
const updatedProduct = await updateProductAction(environmentId, product.id, data);
if (!!updatedProduct?.id) {
toast.success("Waiting period updated successfully.");
router.refresh();
}
} catch (err) {
toast.error(`Error: ${err.message}`);
}
};
return (
<form className="w-full max-w-sm items-center space-y-2" onSubmit={handleSubmit(updateWaitingTime)}>
<Label htmlFor="recontactDays">Wait X days before showing next survey:</Label>
<Input
type="number"
id="recontactDays"
defaultValue={product.recontactDays}
{...register("recontactDays", {
min: { value: 0, message: "Must be a positive number" },
max: { value: 365, message: "Must be less than 365" },
valueAsNumber: true,
required: {
value: true,
message: "Required",
},
})}
/>
{errors?.recontactDays ? (
<div className="my-2">
<p className="text-xs text-red-500">{errors?.recontactDays?.message}</p>
</div>
) : null}
<Button type="submit" variant="darkCTA" size="sm">
Update
</Button>
</form>
);
};

View File

@@ -0,0 +1,90 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { SubmitHandler, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { z } from "zod";
import { TProduct, ZProduct } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/Button";
import { FormControl, FormField, FormItem, FormLabel, FormMessage, FormProvider } from "@formbricks/ui/Form";
import { Input } from "@formbricks/ui/Input";
import { updateProductAction } from "../actions";
type EditWaitingTimeProps = {
environmentId: string;
product: TProduct;
};
const ZProductRecontactDaysInput = ZProduct.pick({ recontactDays: true });
type EditWaitingTimeFormValues = z.infer<typeof ZProductRecontactDaysInput>;
export const EditWaitingTimeForm: React.FC<EditWaitingTimeProps> = ({ product, environmentId }) => {
const form = useForm<EditWaitingTimeFormValues>({
defaultValues: {
recontactDays: product.recontactDays,
},
resolver: zodResolver(ZProductRecontactDaysInput),
mode: "onChange",
});
const { isDirty, isSubmitting } = form.formState;
const updateWaitingTime: SubmitHandler<EditWaitingTimeFormValues> = async (data) => {
try {
const updatedProduct = await updateProductAction(environmentId, product.id, data);
if (!!updatedProduct?.id) {
toast.success("Waiting period updated successfully.");
form.resetField("recontactDays", { defaultValue: updatedProduct.recontactDays });
}
} catch (err) {
toast.error(`Error: ${err.message}`);
}
};
return (
<FormProvider {...form}>
<form
className="flex w-full max-w-sm flex-col space-y-4"
onSubmit={form.handleSubmit(updateWaitingTime)}>
<FormField
control={form.control}
name="recontactDays"
render={({ field }) => (
<FormItem>
<FormLabel htmlFor="recontactDays">Wait X days before showing next survey:</FormLabel>
<FormControl>
<Input
type="number"
id="recontactDays"
{...field}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange("");
}
field.onChange(parseInt(value, 10));
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
variant="darkCTA"
size="sm"
className="w-fit"
loading={isSubmitting}
disabled={isSubmitting || !isDirty}>
Update
</Button>
</form>
</FormProvider>
);
};

View File

@@ -0,0 +1,15 @@
"use client";
import { useFormStatus } from "react-dom";
import { Button } from "@formbricks/ui/Button";
export const SubmitButton = () => {
const formStatus = useFormStatus();
return (
<Button type="submit" variant="darkCTA" size="sm" loading={formStatus.pending}>
Update
</Button>
);
};

View File

@@ -15,8 +15,8 @@ import { SettingsId } from "@formbricks/ui/SettingsId";
import { SettingsCard } from "../../settings/components/SettingsCard";
import { DeleteProduct } from "./components/DeleteProduct";
import { EditProductName } from "./components/EditProductName";
import { EditWaitingTime } from "./components/EditWaitingTime";
import { EditProductNameForm } from "./components/EditProductNameForm";
import { EditWaitingTimeForm } from "./components/EditWaitingTimeForm";
const Page = async ({ params }: { params: { environmentId: string } }) => {
const [, product, session, team] = await Promise.all([
@@ -57,7 +57,7 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
</PageHeader>
<SettingsCard title="Product Name" description="Change your products name.">
<EditProductName
<EditProductNameForm
environmentId={params.environmentId}
product={product}
isProductNameEditDisabled={isProductNameEditDisabled}
@@ -66,7 +66,7 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
<SettingsCard
title="Recontact Waiting Time"
description="Control how frequently users can be surveyed across all surveys.">
<EditWaitingTime environmentId={params.environmentId} product={product} />
<EditWaitingTimeForm environmentId={params.environmentId} product={product} />
</SettingsCard>
<SettingsCard
title="Delete Product"

View File

@@ -1,134 +0,0 @@
"use client";
import { useState } from "react";
import toast from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
import { TPlacement } from "@formbricks/types/common";
import { TProduct, TProductUpdateInput } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/Button";
import { Label } from "@formbricks/ui/Label";
import { getPlacementStyle } from "@formbricks/ui/PreviewSurvey/lib/utils";
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/RadioGroup";
import { updateProductAction } from "../actions";
const placements = [
{ name: "Bottom Right", value: "bottomRight", disabled: false },
{ name: "Top Right", value: "topRight", disabled: false },
{ name: "Top Left", value: "topLeft", disabled: false },
{ name: "Bottom Left", value: "bottomLeft", disabled: false },
{ name: "Centered Modal", value: "center", disabled: false },
];
interface EditPlacementProps {
product: TProduct;
environmentId: string;
}
export const EditPlacement = ({ product }: EditPlacementProps) => {
const [currentPlacement, setCurrentPlacement] = useState<TPlacement>(product.placement);
const [overlay, setOverlay] = useState(product.darkOverlay ? "darkOverlay" : "lightOverlay");
const [clickOutside, setClickOutside] = useState(product.clickOutsideClose ? "allow" : "disallow");
const [updatingPlacement, setUpdatingPlacement] = useState(false);
const overlayStyle =
currentPlacement === "center" && overlay === "darkOverlay" ? "bg-gray-700/80" : "bg-slate-200";
const handleUpdatePlacement = async () => {
try {
setUpdatingPlacement(true);
let inputProduct: Partial<TProductUpdateInput> = {
placement: currentPlacement,
darkOverlay: overlay === "darkOverlay",
clickOutsideClose: clickOutside === "allow",
};
await updateProductAction(product.id, inputProduct);
toast.success("Placement updated successfully.");
} catch (error) {
toast.error(`Error: ${error.message}`);
} finally {
setUpdatingPlacement(false);
}
};
return (
<div className="w-full items-center">
<div className="flex">
<RadioGroup onValueChange={(e) => setCurrentPlacement(e as TPlacement)} value={currentPlacement}>
{placements.map((placement) => (
<div key={placement.value} className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id={placement.value} value={placement.value} disabled={placement.disabled} />
<Label htmlFor={placement.value} className="text-slate-900">
{placement.name}
</Label>
</div>
))}
</RadioGroup>
<div
className={cn(
clickOutside === "disallow" ? "cursor-not-allowed" : "",
"relative ml-8 h-40 w-full rounded",
overlayStyle
)}>
<div
className={cn(
"absolute h-16 w-16 cursor-default rounded bg-slate-700",
getPlacementStyle(currentPlacement)
)}></div>
</div>
</div>
{currentPlacement === "center" && (
<>
<div className="mt-6 space-y-2">
<Label className="font-semibold">Centered modal overlay color</Label>
<RadioGroup onValueChange={(e) => setOverlay(e)} value={overlay} className="flex space-x-4">
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="lightOverlay" value="lightOverlay" />
<Label htmlFor="lightOverlay" className="text-slate-900">
Light Overlay
</Label>
</div>
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="darkOverlay" value="darkOverlay" />
<Label htmlFor="darkOverlay" className="text-slate-900">
Dark Overlay
</Label>
</div>
</RadioGroup>
</div>
<div className="mt-6 space-y-2">
<Label className="font-semibold">Allow users to exit by clicking outside the study</Label>
<RadioGroup
onValueChange={(e) => setClickOutside(e)}
value={clickOutside}
className="flex space-x-4">
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="disallow" value="disallow" />
<Label htmlFor="disallow" className="text-slate-900">
Don&apos;t Allow
</Label>
</div>
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="allow" value="allow" />
<Label htmlFor="allow" className="text-slate-900">
Allow
</Label>
</div>
</RadioGroup>
</div>
</>
)}
<Button
variant="darkCTA"
className="mt-4"
size="sm"
loading={updatingPlacement}
onClick={handleUpdatePlacement}>
Save
</Button>
</div>
);
};

View File

@@ -0,0 +1,193 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { SubmitHandler, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { z } from "zod";
import { cn } from "@formbricks/lib/cn";
import { TProduct } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/Button";
import { FormControl, FormField, FormItem, FormLabel, FormProvider } from "@formbricks/ui/Form";
import { Label } from "@formbricks/ui/Label";
import { getPlacementStyle } from "@formbricks/ui/PreviewSurvey/lib/utils";
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/RadioGroup";
import { updateProductAction } from "../actions";
const placements = [
{ name: "Bottom Right", value: "bottomRight", disabled: false },
{ name: "Top Right", value: "topRight", disabled: false },
{ name: "Top Left", value: "topLeft", disabled: false },
{ name: "Bottom Left", value: "bottomLeft", disabled: false },
{ name: "Centered Modal", value: "center", disabled: false },
];
interface EditPlacementProps {
product: TProduct;
environmentId: string;
}
const ZProductPlacementInput = z.object({
placement: z.enum(["bottomRight", "topRight", "topLeft", "bottomLeft", "center"]),
darkOverlay: z.boolean(),
clickOutsideClose: z.boolean(),
});
type EditPlacementFormValues = z.infer<typeof ZProductPlacementInput>;
export const EditPlacementForm = ({ product }: EditPlacementProps) => {
const form = useForm<EditPlacementFormValues>({
defaultValues: {
placement: product.placement,
darkOverlay: product.darkOverlay ?? false,
clickOutsideClose: product.clickOutsideClose ?? false,
},
resolver: zodResolver(ZProductPlacementInput),
});
const currentPlacement = form.watch("placement");
const darkOverlay = form.watch("darkOverlay");
const clickOutsideClose = form.watch("clickOutsideClose");
const isSubmitting = form.formState.isSubmitting;
const overlayStyle = currentPlacement === "center" && darkOverlay ? "bg-gray-700/80" : "bg-slate-200";
const onSubmit: SubmitHandler<EditPlacementFormValues> = async (data) => {
try {
await updateProductAction(product.id, {
placement: data.placement,
darkOverlay: data.darkOverlay,
clickOutsideClose: data.clickOutsideClose,
});
toast.success("Placement updated successfully.");
} catch (error) {
toast.error(`Error: ${error.message}`);
}
};
return (
<FormProvider {...form}>
<form className="w-full items-center" onSubmit={form.handleSubmit(onSubmit)}>
<div className="flex">
<FormField
control={form.control}
name="placement"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
{...field}
onValueChange={(value) => {
field.onChange(value);
}}
className="h-full">
{placements.map((placement) => (
<div key={placement.value} className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem
id={placement.value}
value={placement.value}
disabled={placement.disabled}
checked={field.value === placement.value}
/>
<Label htmlFor={placement.value} className="text-slate-900">
{placement.name}
</Label>
</div>
))}
</RadioGroup>
</FormControl>
</FormItem>
)}
/>
<div
className={cn(
clickOutsideClose ? "" : "cursor-not-allowed",
"relative ml-8 h-40 w-full rounded",
overlayStyle
)}>
<div
className={cn(
"absolute h-16 w-16 cursor-default rounded bg-slate-700",
getPlacementStyle(currentPlacement)
)}></div>
</div>
</div>
{currentPlacement === "center" && (
<>
<div className="mt-6 space-y-2">
<FormField
control={form.control}
name="darkOverlay"
render={({ field }) => (
<FormItem>
<FormLabel className="font-semibold">Centered modal overlay color</FormLabel>
<FormControl>
<RadioGroup
onValueChange={(value) => {
field.onChange(value === "darkOverlay");
}}
className="flex space-x-4">
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="lightOverlay" value="lightOverlay" checked={!field.value} />
<Label htmlFor="lightOverlay" className="text-slate-900">
Light Overlay
</Label>
</div>
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="darkOverlay" value="darkOverlay" checked={field.value} />
<Label htmlFor="darkOverlay" className="text-slate-900">
Dark Overlay
</Label>
</div>
</RadioGroup>
</FormControl>
</FormItem>
)}
/>
</div>
<div className="mt-6 space-y-2">
<FormField
control={form.control}
name="clickOutsideClose"
render={({ field }) => (
<FormItem>
<FormLabel className="font-semibold">
Allow users to exit by clicking outside the study
</FormLabel>
<FormControl>
<RadioGroup
onValueChange={(value) => {
field.onChange(value === "allow");
}}
className="flex space-x-4">
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="disallow" value="disallow" checked={!field.value} />
<Label htmlFor="disallow" className="text-slate-900">
Don&apos;t Allow
</Label>
</div>
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="allow" value="allow" checked={field.value} />
<Label htmlFor="allow" className="text-slate-900">
Allow
</Label>
</div>
</RadioGroup>
</FormControl>
</FormItem>
)}
/>
</div>
</>
)}
<Button variant="darkCTA" className="mt-4 w-fit" size="sm" loading={isSubmitting}>
Save
</Button>
</form>
</FormProvider>
);
};

View File

@@ -19,7 +19,7 @@ import { PageHeader } from "@formbricks/ui/PageHeader";
import { SettingsCard } from "../../settings/components/SettingsCard";
import { EditFormbricksBranding } from "./components/EditBranding";
import { EditPlacement } from "./components/EditPlacement";
import { EditPlacementForm } from "./components/EditPlacementForm";
import { ThemeStyling } from "./components/ThemeStyling";
const Page = async ({ params }: { params: { environmentId: string } }) => {
@@ -77,7 +77,7 @@ const Page = async ({ params }: { params: { environmentId: string } }) => {
<SettingsCard
title="In-app Survey Placement"
description="Change where surveys will be shown in your web app.">
<EditPlacement product={product} environmentId={params.environmentId} />
<EditPlacementForm product={product} environmentId={params.environmentId} />
</SettingsCard>
<SettingsCard
title="Formbricks Branding"

View File

@@ -71,7 +71,7 @@ export const BulkInviteTab = ({ setOpen, onSubmit, canDoRoleManagement }: BulkIn
return (
<div className="space-y-4">
<div
className="relative flex h-52 cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border border-gray-300"
className="relative flex h-52 cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border border-slate-300 bg-slate-50 transition-colors hover:bg-slate-100"
onClick={() => fileInputRef.current?.click()}>
{csvFile ? (
<XIcon
@@ -97,16 +97,18 @@ export const BulkInviteTab = ({ setOpen, onSubmit, canDoRoleManagement }: BulkIn
</Alert>
)}
</div>
<div className="flex justify-end pt-6">
<div className="flex justify-end">
<div className="flex space-x-2">
<Link
download
href="/sample-csv/formbricks-team-members-template.csv"
target="_blank"
rel="noopener noreferrer">
<Button variant="minimal">Download CSV template</Button>
<Button variant="minimal" size="sm">
Download CSV template
</Button>
</Link>
<Button onClick={onImport} variant="darkCTA" disabled={!csvFile}>
<Button onClick={onImport} size="sm" variant="darkCTA" disabled={!csvFile}>
Import
</Button>
</div>

View File

@@ -77,7 +77,7 @@ export const IndividualInviteTab = ({
</div>
</div>
</div>
<div className="flex justify-end p-6">
<div className="flex justify-end pt-4">
<div className="flex space-x-2">
<Button
size="sm"

View File

@@ -24,6 +24,13 @@ export const POST = async (req: Request, context: Context): Promise<Response> =>
const { userId, environmentId } = context.params;
const jsonInput = await req.json();
// transform all attributes to string if attributes are present
if (jsonInput.attributes) {
for (const key in jsonInput.attributes) {
jsonInput.attributes[key] = String(jsonInput.attributes[key]);
}
}
// validate using zod
const inputValidation = z.object({ attributes: ZAttributes }).safeParse(jsonInput);

View File

@@ -59,7 +59,7 @@ export const POST = async (request: Request): Promise<Response> => {
url: responseInput?.meta?.url,
userAgent: {
browser: agent?.browser.name,
device: agent?.device.type,
device: agent?.device.type || "desktop",
os: agent?.os.name,
},
country: country,

View File

@@ -82,7 +82,7 @@ export const POST = async (request: Request, context: Context): Promise<Response
url: responseInput?.meta?.url,
userAgent: {
browser: agent?.browser.name,
device: agent?.device.type,
device: agent?.device.type || "desktop",
os: agent?.os.name,
},
country: country,

View File

@@ -114,7 +114,7 @@ export const GET = async (
const noCodeActionClasses = actionClasses.filter((actionClass) => actionClass.type === "noCode");
// Define 'transformedSurveys' which can be an array of either TLegacySurvey or TSurvey.
let transformedSurveys: TLegacySurvey[] | TSurvey[] = surveys;
let transformedSurveys: TLegacySurvey[] | TSurvey[] = filteredSurveys;
let state: TJsWebsiteStateSync | TJsWebsiteLegacyStateSync = {
surveys: !isInAppSurveyLimitReached ? transformedSurveys : [],
actionClasses,

View File

@@ -25,6 +25,7 @@
"@formbricks/tailwind-config": "workspace:*",
"@formbricks/types": "workspace:*",
"@formbricks/ui": "workspace:*",
"@hookform/resolvers": "^3.4.2",
"@json2csv/node": "^7.0.6",
"@opentelemetry/auto-instrumentations-node": "^0.46.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.51.1",

View File

@@ -1,5 +1,3 @@
version: "3.3"
# This should be the same as below if you are running via docker compose up
x-webapp-url: &webapp_url http://localhost:3000

View File

@@ -4,6 +4,7 @@ x-environment: &environment
######################################################## REQUIRED ########################################################
# The url of your Formbricks instance used in the admin panel
# Set this to your public-facing URL, e.g., https://example.com
WEBAPP_URL:
# PostgreSQL DB for Formbricks to connect to
@@ -16,7 +17,7 @@ x-environment: &environment
# Set this to your public-facing URL, e.g., https://example.com
# You do not need the NEXTAUTH_URL environment variable in Vercel.
NEXTAUTH_URL: http://localhost:3000
NEXTAUTH_URL:
# Encryption Key is used for 2FA & Single use URLs for Link Surveys
# You can use: $(openssl rand -hex 32) to generate one

View File

@@ -16,11 +16,17 @@ export class AttributeAPI {
async update(
attributeUpdateInput: Omit<TAttributeUpdateInput, "environmentId">
): Promise<Result<{ changed: boolean; message: string }, NetworkError | Error>> {
// transform all attributes to string if attributes are present into a new attributes copy
const attributes: { [key: string]: string } = {};
for (const key in attributeUpdateInput.attributes) {
attributes[key] = String(attributeUpdateInput.attributes[key]);
}
return makeRequest(
this.apiHost,
`/api/v1/client/${this.environmentId}/people/${attributeUpdateInput.userId}/attributes`,
"PUT",
{ attributes: attributeUpdateInput.attributes }
{ attributes }
);
}
}

View File

@@ -8,7 +8,10 @@ import { AppConfig } from "./config";
const appConfig = AppConfig.getInstance();
const logger = Logger.getInstance();
export const updateAttribute = async (key: string, value: string): Promise<Result<void, NetworkError>> => {
export const updateAttribute = async (
key: string,
value: string | number
): Promise<Result<void, NetworkError>> => {
const { apiHost, environmentId, userId } = appConfig.get();
const api = new FormbricksAPI({
@@ -121,7 +124,7 @@ export const setAttributeInApp = async (
return okVoid();
}
const result = await updateAttribute(key, value.toString());
const result = await updateAttribute(key, value);
if (result.ok) {
// udpdate attribute in config

View File

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

View File

@@ -1,7 +1,7 @@
import { TFormbricksApp } from "@formbricks/js-core/app";
import { TFormbricksWebsite } from "@formbricks/js-core/website";
import { Result, wrapThrowsAsync } from "../../types/errorHandlers";
import { loadFormbricksToProxy } from "./shared/loadFormbricks";
declare global {
interface Window {
@@ -9,97 +9,9 @@ declare global {
}
}
// load the sdk, return the result
const loadFormbricksAppSDK = async (apiHost: string): Promise<Result<void>> => {
if (!window.formbricks) {
const res = await fetch(`${apiHost}/api/packages/app`);
// failed to fetch the app package
if (!res.ok) {
return { ok: false, error: new Error("Failed to load Formbricks App SDK") };
}
const sdkScript = await res.text();
const scriptTag = document.createElement("script");
scriptTag.innerHTML = sdkScript;
document.head.appendChild(scriptTag);
const getFormbricks = async () =>
new Promise<void>((resolve, reject) => {
const checkInterval = setInterval(() => {
if (window.formbricks) {
clearInterval(checkInterval);
resolve();
}
}, 100);
setTimeout(() => {
clearInterval(checkInterval);
reject(new Error("Formbricks App SDK loading timed out"));
}, 10000);
});
try {
await getFormbricks();
return { ok: true, data: undefined };
} catch (error: any) {
// formbricks loading failed, return the error
return {
ok: false,
error: new Error(error.message ?? "Failed to load Formbricks App SDK"),
};
}
}
return { ok: true, data: undefined };
};
type FormbricksAppMethods = {
[K in keyof TFormbricksApp]: TFormbricksApp[K] extends Function ? K : never;
}[keyof TFormbricksApp];
const formbricksProxyHandler: ProxyHandler<TFormbricksApp> = {
get(_target, prop, _receiver) {
return async (...args: any[]) => {
if (!window.formbricks) {
if (prop !== "init") {
console.error(
"🧱 Formbricks - Global error: You need to call formbricks.init before calling any other method"
);
return;
}
// still need to check if the apiHost is passed
if (!args[0]) {
console.error("🧱 Formbricks - Global error: You need to pass the apiHost as the first argument");
return;
}
const { apiHost } = args[0];
const loadSDKResult = await wrapThrowsAsync(loadFormbricksAppSDK)(apiHost);
if (!loadSDKResult.ok) {
console.error(`🧱 Formbricks - Global error: ${loadSDKResult.error.message}`);
return;
}
}
// @ts-expect-error
if (window.formbricks && typeof window.formbricks[prop as FormbricksAppMethods] !== "function") {
console.error(
`🧱 Formbricks - Global error: Formbricks App SDK does not support method ${String(prop)}`
);
return;
}
try {
// @ts-expect-error
return (window.formbricks[prop as FormbricksAppMethods] as Function)(...args);
} catch (error) {
console.error(`Something went wrong: ${error}`);
return;
}
};
return (...args: any[]) => loadFormbricksToProxy(prop as string, "app", ...args);
},
};

View File

@@ -1,2 +0,0 @@
export { default as formbricksApp } from "./app";
export { default as formbricksWebsite } from "./website";

View File

@@ -0,0 +1,38 @@
// Simple queue for formbricks methods
export class MethodQueue {
private queue: (() => Promise<void>)[] = [];
private isExecuting = false;
add = (method: () => Promise<void>) => {
this.queue.push(method);
this.run();
};
private runNext = async () => {
if (this.isExecuting) return;
const method = this.queue.shift();
if (method) {
this.isExecuting = true;
try {
await method();
} finally {
this.isExecuting = false;
if (this.queue.length > 0) {
this.runNext();
}
}
}
};
run = async () => {
if (!this.isExecuting && this.queue.length > 0) {
await this.runNext();
}
};
clear = () => {
this.queue = [];
};
}

View File

@@ -0,0 +1,122 @@
import { Result, wrapThrowsAsync } from "../../../types/errorHandlers";
import { MethodQueue } from "../methodQueue";
let isInitializing = false;
let isInitialized = false;
const methodQueue = new MethodQueue();
// Load the SDK, return the result
const loadFormbricksSDK = async (apiHost: string, sdkType: "app" | "website"): Promise<Result<void>> => {
if (!window.formbricks) {
const res = await fetch(`${apiHost}/api/packages/${sdkType}`);
// Failed to fetch the app package
if (!res.ok) {
return { ok: false, error: new Error(`Failed to load Formbricks ${sdkType} SDK`) };
}
const sdkScript = await res.text();
const scriptTag = document.createElement("script");
scriptTag.innerHTML = sdkScript;
document.head.appendChild(scriptTag);
const getFormbricks = async () =>
new Promise<void>((resolve, reject) => {
const checkInterval = setInterval(() => {
if (window.formbricks) {
clearInterval(checkInterval);
resolve();
}
}, 100);
setTimeout(() => {
clearInterval(checkInterval);
reject(new Error(`Formbricks ${sdkType} SDK loading timed out`));
}, 10000);
});
try {
await getFormbricks();
return { ok: true, data: undefined };
} catch (error: any) {
return {
ok: false,
error: new Error(error.message ?? `Failed to load Formbricks ${sdkType} SDK`),
};
}
}
return { ok: true, data: undefined };
};
// TODO: @pandeymangg - Fix these types
// type FormbricksAppMethods = {
// [K in keyof TFormbricksApp]: TFormbricksApp[K] extends Function ? K : never;
// }[keyof TFormbricksApp];
// type FormbricksWebsiteMethods = {
// [K in keyof TFormbricksWebsite]: TFormbricksWebsite[K] extends Function ? K : never;
// }[keyof TFormbricksWebsite];
export const loadFormbricksToProxy = async (prop: string, sdkType: "app" | "website", ...args: any[]) => {
const executeMethod = async () => {
try {
// @ts-expect-error
return await (window.formbricks[prop] as Function)(...args);
} catch (error) {
console.error(`🧱 Formbricks - Global error: ${error}`);
throw error;
}
};
if (!isInitialized) {
if (isInitializing) {
methodQueue.add(executeMethod);
} else {
if (prop === "init") {
isInitializing = true;
const initialize = async () => {
const { apiHost } = args[0];
const loadSDKResult = await wrapThrowsAsync(loadFormbricksSDK)(apiHost, sdkType);
if (!loadSDKResult.ok) {
isInitializing = false;
console.error(`🧱 Formbricks - Global error: ${loadSDKResult.error.message}`);
return;
}
try {
const result = await (window.formbricks[prop] as Function)(...args);
isInitialized = true;
isInitializing = false;
return result;
} catch (error) {
isInitializing = false;
console.error(`🧱 Formbricks - Global error: ${error}`);
throw error;
}
};
methodQueue.add(initialize);
} else {
console.error(
"🧱 Formbricks - Global error: You need to call formbricks.init before calling any other method"
);
return;
}
}
} else {
// @ts-expect-error
if (window.formbricks && typeof window.formbricks[prop] !== "function") {
console.error(
`🧱 Formbricks - Global error: Formbricks ${sdkType} SDK does not support method ${String(prop)}`
);
return;
}
methodQueue.add(executeMethod);
return;
}
};

View File

@@ -1,7 +1,7 @@
import { TFormbricksApp } from "@formbricks/js-core/app";
import { TFormbricksWebsite } from "@formbricks/js-core/website";
import { Result, wrapThrowsAsync } from "../../types/errorHandlers";
import { loadFormbricksToProxy } from "./shared/loadFormbricks";
declare global {
interface Window {
@@ -9,97 +9,11 @@ declare global {
}
}
// load the sdk, return the result
const loadFormbricksWebsiteSDK = async (apiHost: string): Promise<Result<void>> => {
if (!window.formbricks) {
const res = await fetch(`${apiHost}/api/packages/website`);
// failed to fetch the app package
if (!res.ok) {
return { ok: false, error: new Error("Failed to load Formbricks Website SDK") };
}
const sdkScript = await res.text();
const scriptTag = document.createElement("script");
scriptTag.innerHTML = sdkScript;
document.head.appendChild(scriptTag);
const getFormbricks = async () =>
new Promise<void>((resolve, reject) => {
const checkInterval = setInterval(() => {
if (window.formbricks) {
clearInterval(checkInterval);
resolve();
}
}, 100);
setTimeout(() => {
clearInterval(checkInterval);
reject(new Error("Formbricks Website SDK loading timed out"));
}, 10000);
});
try {
await getFormbricks();
return { ok: true, data: undefined };
} catch (error: any) {
// formbricks loading failed, return the error
return {
ok: false,
error: new Error(error.message ?? "Failed to load Formbricks Website SDK"),
};
}
}
return { ok: true, data: undefined };
};
type FormbricksWebsiteMethods = {
[K in keyof TFormbricksWebsite]: TFormbricksWebsite[K] extends Function ? K : never;
}[keyof TFormbricksWebsite];
const formbricksProxyHandler: ProxyHandler<TFormbricksWebsite> = {
get(_target, prop, _receiver) {
return async (...args: any[]) => {
if (!window.formbricks) {
if (prop !== "init") {
console.error(
"🧱 Formbricks - Global error: You need to call formbricks.init before calling any other method"
);
return;
}
// still need to check if the apiHost is passed
if (!args[0]) {
console.error("🧱 Formbricks - Global error: You need to pass the apiHost as the first argument");
return;
}
const { apiHost } = args[0];
const loadSDKResult = await wrapThrowsAsync(loadFormbricksWebsiteSDK)(apiHost);
if (!loadSDKResult.ok) {
console.error(`🧱 Formbricks - Global error: ${loadSDKResult.error.message}`);
return;
}
}
if (window.formbricks && typeof window.formbricks[prop as FormbricksWebsiteMethods] !== "function") {
console.error(
`🧱 Formbricks - Global error: Formbricks Website SDK does not support method ${String(prop)}`
);
return;
}
try {
return (window.formbricks[prop as FormbricksWebsiteMethods] as Function)(...args);
} catch (error) {
console.error(`🧱 Formbricks - Global error: Something went wrong: ${error}`);
return;
}
};
return (...args: any[]) => loadFormbricksToProxy(prop as string, "website", ...args);
},
};
const formbricksApp: TFormbricksWebsite = new Proxy({} as TFormbricksWebsite, formbricksProxyHandler);
export default formbricksApp;
const formbricksWebsite: TFormbricksWebsite = new Proxy({} as TFormbricksWebsite, formbricksProxyHandler);
export default formbricksWebsite;

View File

@@ -208,8 +208,8 @@ export const RatingQuestion = ({
))}
</div>
<div className="text-subheading mt-4 flex justify-between px-1.5 text-xs leading-6">
<p className="w-1/2 text-left">{getLocalizedValue(question.lowerLabel, "default")}</p>
<p className="w-1/2 text-right">{getLocalizedValue(question.upperLabel, "default")}</p>
<p className="w-1/2 text-left">{getLocalizedValue(question.lowerLabel, languageCode)}</p>
<p className="w-1/2 text-right">{getLocalizedValue(question.upperLabel, languageCode)}</p>
</div>
</fieldset>
</div>

View File

@@ -1,3 +1,4 @@
import { cn } from "@/lib/utils";
import { useEffect, useRef, useState } from "preact/hooks";
interface ScrollableContainerProps {
@@ -58,10 +59,8 @@ export const ScrollableContainer = ({ children }: ScrollableContainerProps) => {
scrollbarGutter: "stable both-edges",
maxHeight: isSurveyPreview ? "40dvh" : "60dvh",
}}
className={`overflow-${isOverflowHidden ? "hidden" : "auto"} px-4 pb-1`}
className={cn("overflow-auto px-4 pb-1", isOverflowHidden ? "no-scrollbar" : "bg-survey-bg")}
onMouseEnter={() => toggleOverflow(false)}
onTouchStart={() => toggleOverflow(false)}
onTouchEnd={() => toggleOverflow(true)}
onMouseLeave={() => toggleOverflow(true)}>
{children}
</div>

View File

@@ -130,6 +130,7 @@ export const StackedCardsContainer = ({
<div style={{ height: cardHeight }}></div>
{cardArrangement === "simple" ? (
<div
className="w-full"
style={{
...borderStyles,
}}>

View File

@@ -104,3 +104,18 @@ p.fb-editor-paragraph {
width: 0%;
}
}
.no-scrollbar {
-ms-overflow-style: none !important; /* Internet Explorer 10+ */
scrollbar-width: thin !important; /* Firefox */
scrollbar-color: transparent transparent !important; /* Firefox */
/* Chrome, Edge, and Safari */
&::-webkit-scrollbar {
width: 0 !important;
background: transparent !important;
}
&::-webkit-scrollbar-thumb {
background: transparent !important;
}
}

View File

@@ -3,7 +3,7 @@ import { z } from "zod";
export const ZAttributeUpdateInput = z.object({
environmentId: z.string().cuid2(),
userId: z.string(),
attributes: z.record(z.string()),
attributes: z.record(z.union([z.string(), z.number()])),
});
export type TAttributeUpdateInput = z.infer<typeof ZAttributeUpdateInput>;

View File

@@ -41,10 +41,14 @@ export const ZProduct = z.object({
id: z.string().cuid2(),
createdAt: z.date(),
updatedAt: z.date(),
name: z.string(),
name: z.string().trim().min(1, { message: "Product name cannot be empty" }),
teamId: z.string(),
styling: ZProductStyling,
recontactDays: z.number().int(),
recontactDays: z
.number({ message: "Recontact days is required" })
.int()
.min(0, { message: "Must be a positive number" })
.max(365, { message: "Must be less than 365" }),
inAppSurveyBranding: z.boolean(),
linkSurveyBranding: z.boolean(),
placement: ZPlacement,

View File

@@ -1,7 +1,7 @@
import { Label } from "@radix-ui/react-dropdown-menu";
import clsx from "clsx";
import { Control, Controller, UseFormRegister } from "react-hook-form";
import { cn } from "@formbricks/lib/cn";
import { TActionClass } from "@formbricks/types/actionClasses";
import { AdvancedOptionToggle } from "../../AdvancedOptionToggle";
@@ -42,9 +42,9 @@ export const PageUrlSelector = ({
title="Page URL"
description="If a user visits a specific URL"
childBorder={true}>
<div className="col-span-1 space-y-3 p-4">
<div className="grid grid-cols-3 gap-x-8">
<div className="col-span-1">
<div className="col-span-1 w-full space-y-3 p-4">
<div className="flex w-full items-end gap-2">
<div>
<Label>URL</Label>
<Controller
name="noCodeConfig.pageUrl.rule"
@@ -66,7 +66,7 @@ export const PageUrlSelector = ({
)}
/>
</div>
<div className="col-span-2 flex items-end">
<div className="flex flex-1 items-end">
<Input
type="text"
className="bg-white"
@@ -81,7 +81,7 @@ export const PageUrlSelector = ({
Enter a URL to see if a user visiting it would be tracked.
</div>
<div className=" rounded bg-slate-50">
<div className="mt-1 flex">
<div className="mt-1 flex items-end">
<Input
type="text"
value={testUrl}
@@ -90,7 +90,7 @@ export const PageUrlSelector = ({
setTestUrl(e.target.value);
setIsMatch("default");
}}
className={clsx(
className={cn(
isMatch === "yes"
? "border-green-500 bg-green-50"
: isMatch === "no"

154
packages/ui/Form/index.tsx Normal file
View File

@@ -0,0 +1,154 @@
"use client";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form";
import { cn } from "@formbricks/lib/cn";
import { Label } from "../Label";
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
}
);
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return <Label ref={ref} className={cn(error && "text-error", className)} htmlFor={formItemId} {...props} />;
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
aria-invalid={!!error}
{...props}
/>
);
});
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
);
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p ref={ref} id={formMessageId} className={cn("text-error text-sm", className)} {...props}>
{body}
</p>
);
}
);
FormMessage.displayName = "FormMessage";
export {
useFormField,
FormProvider,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};

View File

@@ -17,7 +17,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, isInv
className={cn(
"focus:border-brand-dark flex h-10 w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-500 dark:text-slate-300",
className,
isInvalid && "border border-red-600 focus:border-red-600"
isInvalid && "border-error focus:border-error border"
)}
ref={ref}
{...props}

View File

@@ -50,8 +50,7 @@ const DialogContent = React.forwardRef<
`${noPadding ? "" : "px-4 pb-4 pt-5 sm:p-6"}`,
"data-[state='closed']:animate-fadeOut data-[state='open']:animate-fadeIn",
size && sizeClassName && sizeClassName[size],
className,
"max-h-screen overflow-y-auto"
className
)}
{...props}
onPointerDownOutside={(e) => {

View File

@@ -301,7 +301,7 @@ export const SingleResponseCard = ({
<div className={clsx("group relative", isOpen && "min-h-[300px]")}>
<div
className={clsx(
"relative z-30 my-6 rounded-xl border border-slate-200 bg-white shadow-sm transition-all",
"relative z-20 my-6 rounded-xl border border-slate-200 bg-white shadow-sm transition-all",
pageType === "response" &&
(isOpen
? "w-3/4"

View File

@@ -128,8 +128,8 @@ export const SurveyCard = ({
key={survey.id}
className="relative grid w-full grid-cols-8 place-items-center gap-3 rounded-xl border border-slate-200 bg-white p-4
shadow-sm transition-all ease-in-out hover:scale-[101%]">
<div className="col-span-2 flex max-w-full items-center justify-self-start truncate whitespace-nowrap text-sm font-medium text-slate-900">
{survey.name}
<div className="col-span-2 flex max-w-full items-center justify-self-start text-sm font-medium text-slate-900">
<div className="w-full truncate">{survey.name}</div>
</div>
<div
className={cn(

104
pnpm-lock.yaml generated
View File

@@ -45,30 +45,6 @@ importers:
'@formbricks/ui':
specifier: workspace:*
version: link:../../packages/ui
'@radix-ui/react-dialog':
specifier: ^1.0.5
version: 1.0.5(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-dropdown-menu':
specifier: ^2.0.6
version: 2.0.6(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-progress':
specifier: ^1.0.3
version: 1.0.3(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-separator':
specifier: ^1.0.3
version: 1.0.3(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-slot':
specifier: ^1.0.2
version: 1.0.2(@types/react@18.3.1)(react@18.3.1)
'@radix-ui/react-tabs':
specifier: ^1.0.4
version: 1.0.4(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-tooltip':
specifier: ^1.0.7
version: 1.0.7(react-dom@18.3.1)(react@18.3.1)
classnames:
specifier: ^2.5.1
version: 2.5.1
lucide-react:
specifier: ^0.378.0
version: 0.378.0(react@18.3.1)
@@ -357,6 +333,9 @@ importers:
'@formbricks/ui':
specifier: workspace:*
version: link:../../packages/ui
'@hookform/resolvers':
specifier: ^3.4.2
version: 3.4.2(react-hook-form@7.51.4)
'@json2csv/node':
specifier: ^7.0.6
version: 7.0.6
@@ -3453,6 +3432,14 @@ packages:
tailwindcss: 3.4.3
dev: false
/@hookform/resolvers@3.4.2(react-hook-form@7.51.4):
resolution: {integrity: sha512-1m9uAVIO8wVf7VCDAGsuGA0t6Z3m6jVGAN50HkV9vYLl0yixKK/Z1lr01vaRvYCkIKGoy1noVRxMzQYb4y/j1Q==}
peerDependencies:
react-hook-form: ^7.0.0
dependencies:
react-hook-form: 7.51.4(react@18.3.1)
dev: false
/@httptoolkit/websocket-stream@6.0.1:
resolution: {integrity: sha512-A0NOZI+Glp3Xgcz6Na7i7o09+/+xm2m0UCU8gdtM2nIv6/cjLmhMZMqehSpTlgbx9omtLmV8LVqOskPEyWnmZQ==}
dependencies:
@@ -6438,26 +6425,6 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-progress@1.0.3(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-5G6Om/tYSxjSeEdrb1VfKkfZfn/1IlPWd731h2RfPuSbIfNUgfqAwbKfJCg/PP6nuUCTrYzalwHSpSinoWoCag==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.24.5
'@radix-ui/react-context': 1.0.1(@types/react@18.3.1)(react@18.3.1)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-radio-group@1.1.3(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==}
peerDependencies:
@@ -6554,25 +6521,6 @@ packages:
react-remove-scroll: 2.5.5(@types/react@18.3.1)(react@18.3.1)
dev: false
/@radix-ui/react-separator@1.0.3(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.24.5
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-slider@1.1.2(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-NKs15MJylfzVsCagVSWKhGGLNR1W9qWs+HtgbmjjVUB3B9+lb3PYoXxVju3kOrpf0VKyVCtZp+iTwVoqpa1Chw==}
peerDependencies:
@@ -6657,32 +6605,6 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-tabs@1.0.4(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.24.5
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-context': 1.0.1(@types/react@18.3.1)(react@18.3.1)
'@radix-ui/react-direction': 1.0.1(@types/react@18.3.1)(react@18.3.1)
'@radix-ui/react-id': 1.0.1(@types/react@18.3.1)(react@18.3.1)
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.1)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-toggle-group@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==}
peerDependencies:
@@ -11293,10 +11215,6 @@ packages:
clsx: 2.0.0
dev: false
/classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
dev: false
/clean-stack@2.2.0:
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
engines: {node: '>=6'}