Compare commits
2 Commits
v2.0.3
...
add-saas-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3f3846f40 | ||
|
|
c7741cea16 |
26
.github/workflows/build-formbricks-com.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
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...
|
||||
131
.github/workflows/kamal-deploy.yml
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
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
|
||||
128
.github/workflows/kamal-setup.yml
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
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
|
||||
2
.github/workflows/release-docker-github.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Docker Release to Github
|
||||
name: Docker
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
|
||||
31
apps/demo/components/ui/badge.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
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 };
|
||||
91
apps/demo/components/ui/breadcrumb.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
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,
|
||||
};
|
||||
47
apps/demo/components/ui/button.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
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 };
|
||||
53
apps/demo/components/ui/card.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
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 };
|
||||
182
apps/demo/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
"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,
|
||||
};
|
||||
22
apps/demo/components/ui/input.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
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 };
|
||||
89
apps/demo/components/ui/pagination.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
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,
|
||||
};
|
||||
24
apps/demo/components/ui/progress.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"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 };
|
||||
26
apps/demo/components/ui/separator.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
"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 };
|
||||
120
apps/demo/components/ui/sheet.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"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,
|
||||
};
|
||||
85
apps/demo/components/ui/table.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
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 };
|
||||
55
apps/demo/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"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 };
|
||||
30
apps/demo/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
"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 };
|
||||
@@ -13,13 +13,21 @@
|
||||
"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": {
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
"@formbricks/tsconfig": "workspace:*"
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"eslint-config-formbricks": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
663
apps/demo/pages/saas/index.tsx
Normal file
@@ -0,0 +1,663 @@
|
||||
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;
|
||||
BIN
apps/demo/public/user-avatar.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
@@ -24,7 +24,8 @@ export const metadata = {
|
||||
The Airtable integration allows you to automatically send responses to an Airtable of your choice.
|
||||
|
||||
<Note>
|
||||
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.
|
||||
This feature is enabled by default in Formbricks Cloud but needs to be self-configured when running a
|
||||
self-hosted version of Formbricks.
|
||||
</Note>
|
||||
|
||||
## Formbricks Cloud
|
||||
|
||||
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 24 KiB |
@@ -21,8 +21,9 @@ export const metadata = {
|
||||
The Google Sheets integration allows you to automatically send responses to a Google Sheet of your choice.
|
||||
|
||||
<Note>
|
||||
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.
|
||||
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).
|
||||
</Note>
|
||||
|
||||
## Connect Google Sheets
|
||||
@@ -71,7 +72,7 @@ Before the next step, make sure that you have a Formbricks Survey with at least
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
6. Enter the spreadsheet URL for the Google Sheet you want to link with Formbricks and the Survey. On doing so, you will be asked with what questions' responses you want to feed in the Google Sheet. Select the questions and click on the "Link Sheet" button.
|
||||
6. Select the Google Sheet you want to link with Formbricks and the Survey. On doing so, you will be asked with what questions' responses you want to feed in the Google Sheet. Select the questions and click on the "Link Sheet" button.
|
||||
|
||||
<MdxImage
|
||||
src={LinkWithQuestions}
|
||||
@@ -116,6 +117,7 @@ To remove the integration with Google Account,
|
||||
For the above, we ask for:
|
||||
|
||||
1. **User Email**: To identify you (that's it, nothing else, we're opensource, see this in our codebase [here](https://github.com/formbricks/formbricks/blob/main/apps/web/app/api/google-sheet/callback/route.ts#L47C17-L47C25))
|
||||
1. **Google Drive API**: To list all your google sheets (that's it, nothing else, we're opensource, see this method in our codebase [here](https://github.com/formbricks/formbricks/blob/main/packages/lib/googleSheet/service.ts#L13))
|
||||
1. **Google Spreadsheet API**: To write to the spreadsheet you select (that's it, nothing else, we're opensource, see this method in our codebase [here](https://github.com/formbricks/formbricks/blob/main/packages/lib/googleSheet/service.ts#L70))
|
||||
|
||||
<Note>We store as little personal information as possible.</Note>
|
||||
|
||||
@@ -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>
|
||||
Nailed down your survey?? Any changes in the survey cause additional work in the _Scenario_. It
|
||||
### Nail down your survey first ? 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>
|
||||
|
||||
|
||||
@@ -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? Any changes in the survey cause additional work in the n8n node. It makes
|
||||
### Nail down your survey first 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>
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ export const metadata = {
|
||||
The notion integration allows you to automatically send responses to a Notion database of your choice.
|
||||
|
||||
<Note>
|
||||
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.
|
||||
This feature is enabled by default in Formbricks Cloud but needs to be self-configured when running a
|
||||
self-hosted version of Formbricks.
|
||||
</Note>
|
||||
|
||||
## Formbricks Cloud
|
||||
|
||||
@@ -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 integrations on your self-hosted instance.
|
||||
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.
|
||||
</Note>
|
||||
|
||||
- [Airtable](/developer-docs/integrations/airtable): Automatically send responses to an Airtable of your choice.
|
||||
|
||||
@@ -22,7 +22,8 @@ export const metadata = {
|
||||
The slack integration allows you to automatically send responses to a Slack channel of your choice.
|
||||
|
||||
<Note>
|
||||
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.
|
||||
This feature is enabled by default in Formbricks Cloud but needs to be self-configured when running a
|
||||
self-hosted version of Formbricks.
|
||||
</Note>
|
||||
|
||||
## Formbricks Cloud
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 19 KiB |
@@ -1,100 +0,0 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import AddMember from "./images/add-member.webp";
|
||||
import BulkInvite from "./images/bulk-invite.webp";
|
||||
import IndvInvite from "./images/individual-invite.webp";
|
||||
import MenuItem from "./images/team-settings-menu.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Team Access Roles",
|
||||
description:
|
||||
"Assign different roles to team members to grant them specific rights like creating surveys, viewing responses, or managing team members.",
|
||||
};
|
||||
|
||||
# Team Access Roles
|
||||
|
||||
Assign different roles to team members to grant them specific rights like creating surveys, viewing responses, or managing team members.
|
||||
|
||||
<Note>Access Roles is a feature of the **Enterprise Edition**. In the **Community Edition** and on the **Free** and **Startup** plan in the Cloud you can invite unlimited team members as `Admins`.</Note>
|
||||
|
||||
|
||||
| Role | Rights |
|
||||
| --- | --- |
|
||||
| Owner | Full rights; there can only one owner per team. Ownership can be transferred. |
|
||||
| Admin | Full access rights incl. managing team members |
|
||||
| Developer | Full product access to setup and run surveys incl. global styling, actions and attribute management, etc |
|
||||
| Editor | Create and edit surveys. No access to features related to setting up or maintaining Formbricks. |
|
||||
| Viewer | View survey results only. |
|
||||
|
||||
## Inviting team members
|
||||
|
||||
There are two ways to invite team members: One by one or in bulk.
|
||||
|
||||
### Invite team members one by one
|
||||
|
||||
1. Go to the `Team Settings` page via the menu in the lower right corner:
|
||||
|
||||
<MdxImage
|
||||
src={MenuItem}
|
||||
alt="Where to find the Menu Item for Team Settings"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
2. Click on the `Add Member` button:
|
||||
|
||||
<MdxImage
|
||||
src={AddMember}
|
||||
alt="Add Member Button Position"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
3. In the modal, add the Name, Email and Role of the team member you want to invite:
|
||||
|
||||
<MdxImage
|
||||
src={IndvInvite}
|
||||
alt="Individual Invite Modal Tab"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
<Note>Access Roles is a feature of the **Enterprise Edition**. In the **Community Edition** and on the **Free** and **Startup** plan in the Cloud you can invite unlimited team members as `Admins`.</Note>
|
||||
|
||||
|
||||
Formbricks sends an email to the team member with an invitation link. The team member can accept the invitation or create a new account by clicking on the link.
|
||||
|
||||
|
||||
### Invite team members in bulk
|
||||
|
||||
1. Go to the `Team Settings` page via the menu in the lower right corner:
|
||||
|
||||
<MdxImage
|
||||
src={MenuItem}
|
||||
alt="Where to find the Menu Item for Team Settings"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
2. Click on the `Add Member` button:
|
||||
|
||||
<MdxImage
|
||||
src={AddMember}
|
||||
alt="Add Member Button Position"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
3. In the modal, switch to `Bulk Invite`. You can download an example .CSV file to fill in the Name, Email and Role of the team members you want to invite:
|
||||
|
||||
<MdxImage
|
||||
src={BulkInvite}
|
||||
alt="Individual Invite Modal Tab"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
4. Upload the filled .CSV file and invite the team members in bulk ✅
|
||||
|
||||
Formbricks sends an email to each team member in the CSV. The member can accept the invitation or create a new account by clicking on the link.
|
||||
|
||||
---
|
||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
260
apps/docs/app/global/custom-styling/page.mdx
Normal file
@@ -0,0 +1,260 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
|
||||
import PreRequisiteImage from "./images/Pre-requisite.webp";
|
||||
import StepEight from "./images/StepEight.webp";
|
||||
import StepEleven from "./images/StepEleven.webp";
|
||||
import StepFive from "./images/StepFive.webp";
|
||||
import StepFour from "./images/StepFour.webp";
|
||||
import StepNine from "./images/StepNine.webp";
|
||||
import StepOne from "./images/StepOne.webp";
|
||||
import StepSeven from "./images/StepSeven.webp";
|
||||
import StepSix from "./images/StepSix.webp";
|
||||
import StepTen from "./images/StepTen.webp";
|
||||
import StepThree from "./images/StepThree.webp";
|
||||
import StepTwo from "./images/StepTwo.webp";
|
||||
|
||||
import Doggo from "./images/Doggo.jpg";
|
||||
import HipsterLiving from "./images/HipsterLiving.jpg";
|
||||
import Mario from "./images/Mario.webp";
|
||||
import WindowsXp from "./images/WindowsXp.jpg";
|
||||
|
||||
export const metadata = {
|
||||
title: "Custom Styling in Formbricks Surveys",
|
||||
description:
|
||||
"Style your surveys effortlessly with Formbricks to match your brand's unique aesthetic, from Super Mario themes to the minimalist Windows XP style. Customize everything from color schemes to adding your logo, all without needing any coding skills.",
|
||||
};
|
||||
|
||||
# Custom Styling
|
||||
|
||||
Style your surveys effortlessly with Formbricks to match your brand's unique aesthetic, from Super Mario themes to the minimalist Windows XP style. Customize everything from color schemes to adding your logo, all without needing any coding skills. Don’t miss out some of our standout design surveys at the end!
|
||||
|
||||
### Product Styling
|
||||
|
||||
Easily apply a consistent style across all your current & future surveys:
|
||||
|
||||
**Pre-requisites:**
|
||||
|
||||
1. Navigate to the **Settings** tab on the Formbricks Dashboard.
|
||||
2. Select **Look & Feel** under the **Product** section in the left navbar
|
||||
|
||||
<MdxImage
|
||||
src={PreRequisiteImage}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
**Switch between Link Survey & App Survey** to preview different styles
|
||||
|
||||
### **Styling Options**
|
||||
|
||||
1. **Form Styling:** Customize the survey card using the following settings
|
||||
|
||||
<MdxImage
|
||||
src={StepOne}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
- **Brand Color**: Sets the primary color tone of the survey.
|
||||
- **Text Color**: This is a single color scheme that will be used across to display all the text on your survey. Ensures all text is readable against the background.
|
||||
- **Input Color:** Alters the border color of input fields.
|
||||
- **Input Border Color**: This is the color of the border of the form input field.
|
||||
|
||||
2. **Card Styling:** Adjust the look of the survey card
|
||||
|
||||
<MdxImage
|
||||
src={StepTwo}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
- **Roundness**: Adjusts the corner roundness of the survey card and its components (including input boxes, buttons).
|
||||
- **Card Background Color**: Sets the card's main background color.
|
||||
- **Card Border Color**: Changes the border color of the card
|
||||
- **Card Shadow Color**: Adds a shadow effect for depth
|
||||
- **Hide Progress Bar**: Optionally remove the progress bar to simplify the survey experience
|
||||
- **Add Highlight Border**: Adds a distinct border for emphasis.
|
||||
|
||||
3. **Background Styling**: Customize the survey background with static colors, animations, or images (upload your own or get from Unsplash)
|
||||
|
||||
<Note>This is only available for Link Surveys</Note>
|
||||
|
||||
<MdxImage
|
||||
src={StepThree}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
- **Color**: Pick any color for the background
|
||||
- **Animation**: Add dynamic animations to enhance user experience..
|
||||
- **Upload**: Use a custom uploaded image for a personalized touch
|
||||
- Image: Choose from Unsplash's extensive gallery. Note that these images will have a link and mention of the author & Unsplash on the bottom right to give them the credit for their awesome work!
|
||||
- **Background Overlay**: Adjust the background's opacity
|
||||
|
||||
### Add Brand Logo:
|
||||
|
||||
Customize your survey with your brand's logo.
|
||||
|
||||
<Note>only available for link survyes</Note>
|
||||
|
||||
1. In the Look & Feel page itself in Product settings, scroll down to see the Logo Upload box.
|
||||
|
||||
<MdxImage
|
||||
src={StepFour}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
2. Upload your logo
|
||||
|
||||
<MdxImage
|
||||
src={StepFive}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
3. Add a background color: If you’ve uploaded a transparent image and want to add background to it, enable this toggle and select the color of your choice.
|
||||
|
||||
<MdxImage
|
||||
src={StepSix}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
4. Remember to save your changes!
|
||||
|
||||
<MdxImage
|
||||
src={StepSeven}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
<Note>The logo setting applies across all link surveys</Note>
|
||||
|
||||
### Overwrite Product Styling
|
||||
|
||||
Create surveys with specific styles that differ from your general product style.
|
||||
|
||||
**Pre-requisites**:
|
||||
|
||||
- Enable the **Custom Styling** toggle in the **Look & Feel** settings
|
||||
|
||||
<MdxImage
|
||||
src={StepEight}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Open the survey editor you want to change the styling of and switch to the Styling tab.
|
||||
|
||||
<MdxImage
|
||||
src={StepNine}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
2. Activate the **Add Custom Styles** toggle to override the default product styling.
|
||||
|
||||
<MdxImage
|
||||
src={StepTen}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
3. Customize your survey's style as needed!
|
||||
|
||||
<MdxImage
|
||||
src={StepEleven}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
Voila! just hit the save button to apply your changes. Your survey is now ready to impress with its unique look!
|
||||
|
||||
## Overwrite CSS Styles for App & Website Surveys
|
||||
|
||||
You can overwrite the default CSS styles for the app & website surveys by adding the following CSS to your global CSS file (eg. `globals.css`):
|
||||
|
||||
Make sure that you do not change the CSS variable names as they are used by Formbricks to identify the CSS variables. You can change the values to your liking. We have filled in some sample values for you to change according to your desired appearance.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Overwrite Formbricks CSS">
|
||||
|
||||
```css
|
||||
/* Formbricks CSS */
|
||||
--fb-brand-color: red;
|
||||
--fb-brand-text-color: white;
|
||||
--fb-border-color: green;
|
||||
--fb-border-color-highlight: rgb(13, 13, 12);
|
||||
--fb-focus-color: red;
|
||||
--fb-heading-color: yellow;
|
||||
--fb-subheading-color: green;
|
||||
--fb-info-text-color: orange;
|
||||
--fb-signature-text-color: blue;
|
||||
--fb-survey-background-color: black;
|
||||
--fb-accent-background-color: rgb(13, 13, 12);
|
||||
--fb-accent-background-color-selected: red;
|
||||
--fb-placeholder-color: white;
|
||||
--fb-shadow-color: var(--fb-brand-color);
|
||||
--fb-rating-fill: rgb(13, 13, 12);
|
||||
--fb-rating-hover: green;
|
||||
--fb-back-btn-border: blue;
|
||||
--fb-submit-btn-border: transparent;
|
||||
--fb-rating-selected: black;
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
We have an example of this in our [Demo project](https://github.com/formbricks/formbricks/blob/main/apps/demo/styles/globals.css) here.
|
||||
|
||||
### Funky Survey Examples
|
||||
|
||||
- **Super Mario:** I guess he won't let himself be limited by radio buttons and do all three things
|
||||
|
||||
<MdxImage
|
||||
src={Mario}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
- **Hipster Living**: Does your monstera get enough water?
|
||||
|
||||
<MdxImage
|
||||
src={HipsterLiving}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
- **Windows XP**: Hach, nostalgia. Made us wanna play Mafia.
|
||||
<MdxImage
|
||||
src={WindowsXp}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
- **Whosagooooodbooooy**: Things you've likely said to your dog.
|
||||
<MdxImage
|
||||
src={Doggo}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
---
|
||||
@@ -1,131 +0,0 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
|
||||
import StepEleven from "./images/StepEleven.webp";
|
||||
import StepNine from "./images/StepNine.webp";
|
||||
import StepTen from "./images/StepTen.webp";
|
||||
|
||||
import Doggo from "./images/Doggo.jpg";
|
||||
import HipsterLiving from "./images/HipsterLiving.jpg";
|
||||
import Mario from "./images/Mario.webp";
|
||||
import WindowsXp from "./images/WindowsXp.jpg";
|
||||
|
||||
export const metadata = {
|
||||
title: "Overwrite Styling Theme on Individual Surveys",
|
||||
description:
|
||||
"Overwrite the global styling theme for individual surveys to create unique styles for each survey.",
|
||||
};
|
||||
|
||||
# Overwrite Styling Theme on Individual Surveys
|
||||
|
||||
Overwrite the global styling theme for individual surveys to create unique styles for each survey.
|
||||
|
||||
<Note>To set a styling theme for all surveys, please see the [Styling Theme](/global/styling-theme) manual. </Note>
|
||||
|
||||
|
||||
### Overwrite Styling Theme
|
||||
|
||||
1. In the Survey Editor of the survey you want to style, navigate to the **Styling** tab:
|
||||
|
||||
<MdxImage
|
||||
src={StepNine}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
2. Activate the **Add Custom Styles** toggle to override the default product styling:
|
||||
|
||||
<MdxImage
|
||||
src={StepTen}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
3. Customize your survey's style as needed:
|
||||
|
||||
<MdxImage
|
||||
src={StepEleven}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
Voila! just hit the save button to apply your changes. Your survey is now ready to impress with its unique look!
|
||||
|
||||
## Overwrite CSS Styles for App & Website Surveys
|
||||
|
||||
You can overwrite the default CSS styles for the app & website surveys by adding the following CSS to your global CSS file (eg. `globals.css`):
|
||||
|
||||
Make sure that you do not change the CSS variable names as they are used by Formbricks to identify the CSS variables. You can change the values to your liking. We have filled in some sample values for you to change according to your desired appearance.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Overwrite Formbricks CSS">
|
||||
|
||||
```css
|
||||
/* Formbricks CSS */
|
||||
--fb-brand-color: red;
|
||||
--fb-brand-text-color: white;
|
||||
--fb-border-color: green;
|
||||
--fb-border-color-highlight: rgb(13, 13, 12);
|
||||
--fb-focus-color: red;
|
||||
--fb-heading-color: yellow;
|
||||
--fb-subheading-color: green;
|
||||
--fb-info-text-color: orange;
|
||||
--fb-signature-text-color: blue;
|
||||
--fb-survey-background-color: black;
|
||||
--fb-accent-background-color: rgb(13, 13, 12);
|
||||
--fb-accent-background-color-selected: red;
|
||||
--fb-placeholder-color: white;
|
||||
--fb-shadow-color: var(--fb-brand-color);
|
||||
--fb-rating-fill: rgb(13, 13, 12);
|
||||
--fb-rating-hover: green;
|
||||
--fb-back-btn-border: blue;
|
||||
--fb-submit-btn-border: transparent;
|
||||
--fb-rating-selected: black;
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
We have an example of this in our [Demo project](https://github.com/formbricks/formbricks/blob/main/apps/demo/styles/globals.css) here.
|
||||
|
||||
## Funky Survey Examples
|
||||
|
||||
- **Super Mario:** I guess he won't let himself be limited by radio buttons and do all three things
|
||||
|
||||
<MdxImage
|
||||
src={Mario}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
- **Hipster Living**: Does your monstera get enough water?
|
||||
|
||||
<MdxImage
|
||||
src={HipsterLiving}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
- **Windows XP**: Hach, nostalgia. Made us wanna play Mafia.
|
||||
|
||||
<MdxImage
|
||||
src={WindowsXp}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
- **Whosagooooodbooooy**: Things you've likely said to your dog.
|
||||
|
||||
<MdxImage
|
||||
src={Doggo}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
---
|
||||
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 80 KiB |
@@ -1,149 +0,0 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
|
||||
import StepEight from "./images/StepEight.webp";
|
||||
import StepEleven from "./images/StepEleven.webp";
|
||||
import StepFive from "./images/StepFive.webp";
|
||||
import StepFour from "./images/StepFour.webp";
|
||||
import StepNine from "./images/StepNine.webp";
|
||||
import StepSeven from "./images/StepSeven.webp";
|
||||
import StepSix from "./images/StepSix.webp";
|
||||
import StepTen from "./images/StepTen.webp";
|
||||
import AllowOverwrite from "./images/allow-overwrite.webp";
|
||||
import BackgroundSettings from "./images/background-settings.webp";
|
||||
import CardSettings from "./images/card-settings.webp";
|
||||
import FormSettings from "./images/form-settings.webp";
|
||||
import LookAndFeelSettings from "./images/look-and-feel.webp";
|
||||
|
||||
import Doggo from "./images/Doggo.jpg";
|
||||
import HipsterLiving from "./images/HipsterLiving.jpg";
|
||||
import Mario from "./images/Mario.webp";
|
||||
import WindowsXp from "./images/WindowsXp.jpg";
|
||||
|
||||
export const metadata = {
|
||||
title: "Styling Theme",
|
||||
description:
|
||||
"Keep the survey styling consistent over all surveys with a Styling Theme. Customize the colors, fonts, and other styling options to match your brand's aesthetic.",
|
||||
};
|
||||
|
||||
# Styling Theme
|
||||
|
||||
Keep the survey styling consistent over all surveys with a Styling Theme. Customize the colors, fonts, and other styling options to match your brand's aesthetic.
|
||||
|
||||
## Configuration
|
||||
|
||||
In the left side bar, you find the `Configuration` page. On this page you find the `Look & Feel` section:
|
||||
|
||||
<MdxImage
|
||||
src={LookAndFeelSettings}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
## **Styling Options**
|
||||
|
||||
1. **Form Styling:** Customize the survey card using the following settings
|
||||
|
||||
<MdxImage
|
||||
src={FormSettings}
|
||||
alt="Form styling options UI"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
- **Brand Color**: Sets the primary color tone of the survey.
|
||||
- **Text Color**: This is a single color scheme that will be used across to display all the text on your survey. Ensures all text is readable against the background.
|
||||
- **Input Color:** Alters the border color of input fields.
|
||||
- **Input Border Color**: This is the color of the border of the form input field.
|
||||
|
||||
2. **Card Styling:** Adjust the look of the survey card
|
||||
|
||||
<MdxImage
|
||||
src={CardSettings}
|
||||
alt="Card styling options UI"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
- **Roundness**: Adjusts the corner roundness of the survey card and its components (including input boxes, buttons).
|
||||
- **Card Background Color**: Sets the card's main background color.
|
||||
- **Card Border Color**: Changes the border color of the card
|
||||
- **Card Shadow Color**: Adds a shadow effect for depth
|
||||
- **Hide Progress Bar**: Optionally remove the progress bar to simplify the survey experience
|
||||
- **Add Highlight Border**: Adds a distinct border for emphasis.
|
||||
|
||||
3. **Background Styling**: Customize the survey background with static colors, animations, or images (upload your own or get from Unsplash)
|
||||
|
||||
<Note>This is only available for Link Surveys</Note>
|
||||
|
||||
<MdxImage
|
||||
src={BackgroundSettings}
|
||||
alt="Background styling options UI"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
- **Color**: Pick any color for the background
|
||||
- **Animation**: Add dynamic animations to enhance user experience..
|
||||
- **Upload**: Use a custom uploaded image for a personalized touch
|
||||
- Image: Choose from Unsplash's extensive gallery. Note that these images will have a link and mention of the author & Unsplash on the bottom right to give them the credit for their awesome work!
|
||||
- **Background Overlay**: Adjust the background's opacity
|
||||
|
||||
## Add Brand Logo
|
||||
|
||||
Customize your survey with your brand's logo.
|
||||
|
||||
<Note>Brand logos are only visible on Link Survey pages.</Note>
|
||||
|
||||
1. In the Look & Feel page itself in Product settings, scroll down to see the Logo Upload box.
|
||||
|
||||
<MdxImage
|
||||
src={StepFour}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
2. Upload your logo
|
||||
|
||||
<MdxImage
|
||||
src={StepFive}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
3. Add a background color: If you’ve uploaded a transparent image and want to add background to it, enable this toggle and select the color of your choice.
|
||||
|
||||
<MdxImage
|
||||
src={StepSix}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
4. Remember to save your changes!
|
||||
|
||||
<MdxImage
|
||||
src={StepSeven}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
/>
|
||||
|
||||
<Note>The logo settings apply across all Link Surveys pages.</Note>
|
||||
|
||||
## Overwrite Styling Theme
|
||||
|
||||
You can allow to overwrite the styling theme for individual surveys to create unique styles for each survey:
|
||||
|
||||
<MdxImage
|
||||
src={AllowOverwrite}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
In the survey editor, a tab called `Styling` will appear. Here you can overwrite the default styling theme.
|
||||
|
||||
---
|
||||
@@ -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,25 +104,7 @@ You're now ready to start the Formbricks Docker setup. The following command wil
|
||||
|
||||
## Update
|
||||
|
||||
<Note>
|
||||
Please take a look at our [migration guide](/self-hosting/migration-guide) for version specific steps to
|
||||
update Formbricks.
|
||||
</Note>
|
||||
|
||||
1. Pull the latest Formbricks image
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Pull the changes into docker">
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
|
||||
2. Stop the Formbricks stack
|
||||
1. Stop the Formbricks stack
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Stop the docker instance">
|
||||
@@ -135,7 +117,21 @@ You're now ready to start the Formbricks Docker setup. The following command wil
|
||||
|
||||
</Col>
|
||||
|
||||
3. Re-start the Formbricks stack with the updated image
|
||||
2. Pull the latest changes
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Pull the changes into docker">
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
|
||||
3. Update env vars as necessary in the docker-compose file.
|
||||
4. Re-start the Formbricks stack
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Relaunch the Docker Instance">
|
||||
|
||||
@@ -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 for self-hosted instances yet.
|
||||
- Make: We do not support [Make.com](http://Make.com) for Self-hosted instances yet! Please follow our Cloud guide [here](/integrations#make)
|
||||
- [n8n](#n8n)
|
||||
- [Slack](#slack)
|
||||
- Wordpress: Wordpress setup is similar to the [Cloud setup](/developer-docs/integrations/wordpress), just change the API Host to your self-hosted URL.
|
||||
- [Wordpress]: Wordpress setup is similar to steps mentioned in Cloud [here](/integrations#wordpress), just change the API Host to your self-hosted URL.
|
||||
- [Zapier](#zapier)
|
||||
|
||||
<Note>
|
||||
@@ -116,7 +116,7 @@ Integrating Google Sheets with a self-hosted Formbricks instance requires config
|
||||
1. Go to the **[Google Cloud Console](https://console.cloud.google.com/)** and **create a new project**.
|
||||
2. Enable necessary APIs:
|
||||
- Now select the project you just created and go to the **APIs & Services** section.
|
||||
- Click on the **Enable APIs and Services** button and search for **Google Sheets API** and enable it.
|
||||
- Click on the **Enable APIs and Services** button and search for **Google Sheets API** & **Google Drive API** and enable it.
|
||||
3. Configure OAuth Consent Screen:
|
||||
- Go to **OAuth Consent screen** and select the appropriate User Type (External or Internal). Select **Internal** if you want only the users of your Google Workspace to be able to use the integration.
|
||||
- Fill the required details:
|
||||
@@ -128,11 +128,12 @@ Integrating Google Sheets with a self-hosted Formbricks instance requires config
|
||||
- Click on the **Add or Remove Scopes** button and add the scopes:
|
||||
- `https://www.googleapis.com/auth/userinfo.email`
|
||||
- `https://www.googleapis.com/auth/spreadsheets`
|
||||
- `https://www.googleapis.com/auth/drive`
|
||||
- Click on the **Update** button. Verify the scopes and click on the **Save and Continue** button.
|
||||
- Skip the **Test Users** section and click on the **Save and Continue** button.
|
||||
|
||||
5. View the OAuth Consent Screen summary and click on the **Back to Dashboard** button.
|
||||
6. Register OAuth Client:
|
||||
1. View the OAuth Consent Screen summary and click on the **Back to Dashboard** button.
|
||||
2. Register OAuth Client:
|
||||
|
||||
- Navigate to **Credentials** > **Create Credentials** > **OAuth Client ID**.
|
||||
- Select **Web Application** and set:
|
||||
@@ -141,10 +142,13 @@ Integrating Google Sheets with a self-hosted Formbricks instance requires config
|
||||
- Authorized redirect URIs: `https://<your-public-facing-url>/api/google-sheet/callback`
|
||||
- Save and note the Client ID and Client Secret.
|
||||
|
||||
7. Copy the Client ID and Client Secret and set them as environment variables in your Formbricks instance:
|
||||
1. Copy the Client ID and Client Secret and set them as environment variables in your Formbricks instance:
|
||||
- `GOOGLE_SHEETS_CLIENT_ID`
|
||||
- `GOOGLE_SHEETS_CLIENT_SECRET`
|
||||
- `GOOGLE_SHEETS_REDIRECT_URL`
|
||||
2. Enable Google Drive API:
|
||||
- Go to the **APIs & Services** section and click on the **Enable APIs and Services** button.
|
||||
- Search for **Google Drive API** and enable it.
|
||||
|
||||
Now just copy **GOOGLE_SHEETS_CLIENT_ID**, **GOOGLE_SHEETS_CLIENT_SECRET** and **GOOGLE_SHEETS_REDIRECT_URL** for your integration & add it to your **Formbricks environment variables** as in the docker compose file:
|
||||
|
||||
|
||||
@@ -9,37 +9,30 @@ export const metadata = {
|
||||
|
||||
## Overview
|
||||
|
||||
The Formbricks Core source code is licensed under AGPLv3 and available on GitHub. Additionally, we offer features for bigger organisations & enterprises for self-hostesr under a separate Enterprise License.
|
||||
|
||||
<Note>
|
||||
Want to present a proof of concept? Request a free 30-day Enterprise Edition trial by [filling out the form below.](#30-day-trial-license-request) No call needed or strings attached: Just give us 24h to set up the key and send it over 🤙
|
||||
</Note>
|
||||
Formbricks core source code is under AGPL license on GitHub. Additionally, we also have additional features for bigger teams & enterprises under the Enterprise license for Self Hosting customers. Let’s understand both in depth below:
|
||||
|
||||
## Enterprise Edition License
|
||||
|
||||
Additional to the AGPLv3 licensed Formbricks core, the Formbricks repository contains code licensed under our **[Enterprise License](https://github.com/formbricks/formbricks/blob/main/packages/ee/LICENSE)**. This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an **Enterprise License Key** to unlock it. For the pricing, please refer to [Formbricks Pricing](https://formbricks.com/pricing).
|
||||
Additional to the AGPL licensed Formbricks core, the Formbricks repository contains code licensed under an **[Enterprise license](https://github.com/formbricks/formbricks/blob/main/packages/ee/LICENSE)**. This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an **Enterprise License Key** to unlock it. For the pricing, please refer to [Formbricks Pricing](https://formbricks.com/pricing).
|
||||
|
||||
### When do I need an Enterprise License?
|
||||
|
||||
| | Community Edition | Enterprise License |
|
||||
| -------------------------------------------------------- | ----------------- | -------------------- |
|
||||
| Self-host for commercial purposes | ✅ | No EE license needed |
|
||||
| Make changes to the code base (have to publish all changes) | ✅ | No EE license needed |
|
||||
| To make changes to the code base (happy to publish them) | ✅ | No EE license needed |
|
||||
| Unlimited responses | ✅ | No EE license needed |
|
||||
| Unlimited surveys | ✅ | No EE license needed |
|
||||
| Remove branding | ✅ | No EE license needed |
|
||||
| SSO | ✅ | No EE license needed |
|
||||
| Use any of the other 100 features | ✅ | No EE license needed |
|
||||
| Team access roles | ❌ | ✅ |
|
||||
| Team roles | ❌ | ✅ |
|
||||
| Multi-language surveys | ❌ | ✅ |
|
||||
| Advanced targeting / Segments | ❌ | ✅ |
|
||||
| Make code changes and **keep private** | ❌ | ✅ |
|
||||
| Make code changes and keep private | ❌ | ✅ |
|
||||
|
||||
Ready to get started with the Enterprise Edition? Fill out our form below and we'll reach out to you.
|
||||
|
||||
## 30-day Trial License Request
|
||||
Many organisations want to do an internal test run with the Enterprise Edition. To make that really easy, we now offer a 30-day trial license. Just fill out the form below and we'll send you a license key within 24 hours (business days):
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
@@ -57,11 +50,11 @@ Many organisations want to do an internal test run with the Enterprise Edition.
|
||||
|
||||
### The AGPL Formbricks Core
|
||||
|
||||
The Formbricks core application is licensed under the **[AGPLv3 Open Source License](https://github.com/formbricks/formbricks/blob/main/LICENSE)**. The core application is fully functional and includes everything you need to design & run link surveys, website surveys and in-app surveys. You can use the software for free for personal and commercial use. You're also allowed to create and distribute modified versions as long as you document the changes you make incl. date and **publish your complete code under the AGPLv3 license as well.**
|
||||
The Formbricks core application is licensed under the **[AGPLv3 Open Source License](https://github.com/formbricks/formbricks/blob/main/LICENSE)**. The core application is fully functional and includes everything you need to design & run link surveys, website surveys and in-app surveys. You can use the software for free for personal and commercial use. You're also allowed to create and distribute modified versions as long as you document the changes you make incl. date. The AGPL license requires you to publish your modified version under the AGPLv3 license as well.
|
||||
|
||||
### The Enterprise Edition
|
||||
|
||||
Additional to the AGPL licensed Formbricks core, this repository contains code licensed under an Enterprise License. The **[code](https://github.com/formbricks/formbricks/tree/main/packages/ee)** and **[license](https://github.com/formbricks/formbricks/blob/main/packages/ee/LICENSE)** for the enterprise functionality can be found in the `/packages/ee` folder of this repository. This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an **Enterprise License Key** to unlock it.
|
||||
Additional to the AGPL licensed Formbricks core, this repository contains code licensed under an Enterprise license. The **[code](https://github.com/formbricks/formbricks/tree/main/packages/ee)** and **[license](https://github.com/formbricks/formbricks/blob/main/packages/ee/LICENSE)** for the enterprise functionality can be found in the `/packages/ee` folder of this repository. This additional functionality is not part of the AGPLv3 licensed Formbricks core and is designed to meet the needs of larger teams and enterprises. This advanced functionality is already included in the Docker images, but you need an **Enterprise License Key** to unlock it.
|
||||
|
||||
## White-Labeling Formbricks and Other Licensing Needs
|
||||
|
||||
@@ -69,6 +62,6 @@ We currently do not offer Formbricks white-labeled. Any other needs? [Send us an
|
||||
|
||||
## Why charge for Enterprise Features?
|
||||
|
||||
The Enterprise Edition allows us to fund the development of Formbricks sustainably. It guarantees that the open-source surveying infrastructure we're building will be around for decades to come.
|
||||
The Enterprise Edition and White-Label Licenses allow us to fund the development of Formbricks sustainably. It guarantees that the open-source surveying infrastructure we're building will be around for decades to come.
|
||||
|
||||
**Any more questions?**: [Send us an email](mailto:johannes@formbricks.com) or [book a call with us.](https://cal.com/johannes/license)
|
||||
**Having more questions?**: [Join our Discord!](https://formbricks.com/discord)
|
||||
|
||||
@@ -17,16 +17,10 @@ Formbricks v2.0 comes with huge features such as Multi-Language Surveys and Adva
|
||||
the upgrade. Follow the below steps thoroughly to upgrade your Formbricks instance to v2.0.
|
||||
</Note>
|
||||
|
||||
<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/)
|
||||
</Note>
|
||||
and
|
||||
|
||||
<Note>
|
||||
We are moving from DockerHub to Github Packages for our images. If you are still pulling the images from
|
||||
DockerHub please change `image: formbricks/formbricks:latest` to
|
||||
`image:ghcr.io/formbricks/formbricks:latest` in your `docker-compose.yml` file.
|
||||
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
|
||||
@@ -41,7 +35,7 @@ To run all these steps, please navigate to the `formbricks` folder where your `d
|
||||
<CodeGroup title="Backup Postgres">
|
||||
|
||||
```bash
|
||||
docker exec formbricks-postgres-1 pg_dump -Fc -U postgres -d formbricks > formbricks_pre_v2.0_$(date +%Y%m%d_%H%M%S).dump
|
||||
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
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -57,19 +51,7 @@ docker exec formbricks-postgres-1 pg_dump -Fc -U postgres -d formbricks > formbr
|
||||
restore scenario you will need to use `psql` then with an empty `formbricks` database.
|
||||
</Note>
|
||||
|
||||
2. Pull the latest version of Formbricks:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Stop the containers">
|
||||
|
||||
```bash
|
||||
docker-compose pull
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
3. Stop the running Formbricks instance & remove the related containers:
|
||||
2. Stop the running Formbricks instance & remove the related containers:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Stop the containers">
|
||||
@@ -81,7 +63,7 @@ docker-compose down
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
4. Restarting the containers with the latest version of Formbricks:
|
||||
3. Restarting the containers will automatically pull the latest version of Formbricks:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Restart the containers">
|
||||
@@ -93,7 +75,7 @@ docker-compose up -d
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
5. Now let's migrate the data to the latest schema:
|
||||
4. 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>
|
||||
|
||||
@@ -101,7 +83,6 @@ 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" \
|
||||
@@ -114,7 +95,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.
|
||||
|
||||
6. That's it! Once the migration is complete, you can **now access your Formbricks instance** at the same URL as before.
|
||||
5. 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
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export const Layout = ({
|
||||
</Link>
|
||||
</div>
|
||||
<Header />
|
||||
<Navigation className="hidden lg:mt-10 lg:block" isMobile={false} />
|
||||
<Navigation className="hidden lg:mt-10 lg:block" />
|
||||
</div>
|
||||
</motion.header>
|
||||
<div className="relative flex h-full flex-col px-4 pt-14 sm:px-6 lg:px-8">
|
||||
|
||||
@@ -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 isMobile={true} />
|
||||
<Navigation />
|
||||
</motion.div>
|
||||
</Transition.Child>
|
||||
</Dialog.Panel>
|
||||
|
||||
@@ -46,41 +46,25 @@ const NavLink = ({
|
||||
active = false,
|
||||
isAnchorLink = false,
|
||||
}: {
|
||||
href?: string;
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
active: boolean;
|
||||
isAnchorLink?: boolean;
|
||||
}) => {
|
||||
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"
|
||||
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>
|
||||
);
|
||||
|
||||
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 }) => {
|
||||
@@ -147,7 +131,6 @@ const NavigationGroup = ({
|
||||
setActiveGroup,
|
||||
openGroups,
|
||||
setOpenGroups,
|
||||
isMobile,
|
||||
}: {
|
||||
group: NavGroup;
|
||||
className?: string;
|
||||
@@ -155,7 +138,6 @@ const NavigationGroup = ({
|
||||
setActiveGroup: (group: NavGroup | null) => void;
|
||||
openGroups: string[];
|
||||
setOpenGroups: (groups: string[]) => void;
|
||||
isMobile: boolean;
|
||||
}) => {
|
||||
const isInsideMobileNavigation = useIsInsideMobileNavigation();
|
||||
const pathname = usePathname();
|
||||
@@ -189,15 +171,13 @@ const NavigationGroup = ({
|
||||
{group.links.map((link) => (
|
||||
<motion.li key={link.title} layout="position" className="relative">
|
||||
{link.href ? (
|
||||
<NavLink
|
||||
href={isMobile && link.children ? "" : link.href}
|
||||
active={!!pathname?.startsWith(link.href)}>
|
||||
<NavLink href={link.href} active={!!pathname?.startsWith(link.href)}>
|
||||
{link.title}
|
||||
</NavLink>
|
||||
) : (
|
||||
<div onClick={() => toggleParentTitle(link.title)}>
|
||||
<NavLink
|
||||
href={!isMobile ? link.children?.[0]?.href || "" : undefined}
|
||||
href={link.children?.[0]?.href || ""}
|
||||
active={
|
||||
!!(
|
||||
isParentOpen(link.title) &&
|
||||
@@ -217,7 +197,7 @@ const NavigationGroup = ({
|
||||
</div>
|
||||
)}
|
||||
<AnimatePresence mode="popLayout" initial={false}>
|
||||
{isActiveGroup && link.children && isParentOpen(link.title) && (
|
||||
{link.children && isParentOpen(link.title) && (
|
||||
<motion.ul
|
||||
role="list"
|
||||
initial={{ opacity: 0 }}
|
||||
@@ -241,11 +221,7 @@ const NavigationGroup = ({
|
||||
);
|
||||
};
|
||||
|
||||
interface NavigationProps extends React.ComponentPropsWithoutRef<"nav"> {
|
||||
isMobile: boolean;
|
||||
}
|
||||
|
||||
export const Navigation = ({ isMobile, ...props }: NavigationProps) => {
|
||||
export const Navigation = (props: React.ComponentPropsWithoutRef<"nav">) => {
|
||||
const [activeGroup, setActiveGroup] = useState<NavGroup | null>(navigation[0]);
|
||||
const [openGroups, setOpenGroups] = useState<string[]>([]);
|
||||
|
||||
@@ -261,7 +237,6 @@ export const Navigation = ({ isMobile, ...props }: NavigationProps) => {
|
||||
setActiveGroup={setActiveGroup}
|
||||
openGroups={openGroups}
|
||||
setOpenGroups={setOpenGroups}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
))}
|
||||
<li className="sticky bottom-0 z-10 mt-6 min-[416px]:hidden">
|
||||
|
||||
@@ -37,7 +37,7 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Recontact Options", href: "/app-surveys/recontact" },
|
||||
{ title: "Multi Language Surveys", href: "/global/multi-language-surveys" }, // global
|
||||
{ title: "User Metadata", href: "/global/metadata" }, // global
|
||||
{ title: "Custom Styling", href: "/global/overwrite-styling" }, // global
|
||||
{ title: "Custom Styling", href: "/global/custom-styling" }, // global
|
||||
{ title: "Conditional Logic", href: "/global/conditional-logic" }, // global
|
||||
{ title: "Custom Start & End Conditions", href: "/global/custom-start-end-conditions" }, // global
|
||||
{ title: "Recall Functionality", href: "/global/recall" }, // global
|
||||
@@ -59,7 +59,7 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Recontact Options", href: "/app-surveys/recontact" },
|
||||
{ title: "Multi Language Surveys", href: "/global/multi-language-surveys" }, // global
|
||||
{ title: "User Metadata", href: "/global/metadata" }, // global
|
||||
{ title: "Custom Styling", href: "/global/overwrite-styling" }, // global
|
||||
{ title: "Custom Styling", href: "/global/custom-styling" }, // global
|
||||
{ title: "Conditional Logic", href: "/global/conditional-logic" }, // global
|
||||
{ title: "Custom Start & End Conditions", href: "/global/custom-start-end-conditions" }, // global
|
||||
{ title: "Recall Functionality", href: "/global/recall" }, // global
|
||||
@@ -84,7 +84,7 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Embed Surveys Anywhere", href: "/link-surveys/embed-surveys" },
|
||||
{ title: "Multi Language Surveys", href: "/global/multi-language-surveys" },
|
||||
{ title: "User Metadata", href: "/global/metadata" },
|
||||
{ title: "Custom Styling", href: "/global/overwrite-styling" }, // global
|
||||
{ title: "Custom Styling", href: "/global/custom-styling" },
|
||||
{ title: "Conditional Logic", href: "/global/conditional-logic" },
|
||||
{ title: "Custom Start & End Conditions", href: "/global/custom-start-end-conditions" },
|
||||
{ title: "Recall Functionality", href: "/global/recall" },
|
||||
@@ -95,13 +95,6 @@ export const navigation: Array<NavGroup> = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Core Features",
|
||||
links: [
|
||||
{ title: "Access Roles", href: "/global/access-roles" },
|
||||
{ title: "Styling Theme", href: "/global/styling-theme" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Self-Hosting",
|
||||
links: [
|
||||
|
||||
@@ -111,11 +111,6 @@ const nextConfig = {
|
||||
destination: "/developer-docs/integrations/:path",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/global/custom-styling",
|
||||
destination: "/global/overwrite-styling",
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -18,11 +18,12 @@ RUN turbo prune @formbricks/web --docker
|
||||
FROM base AS installer
|
||||
|
||||
# Enable corepack and prepare pnpm
|
||||
RUN corepack enable
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
# 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"
|
||||
@@ -58,7 +59,7 @@ RUN jq -r '.devDependencies.prisma' packages/database/package.json > /prisma_ver
|
||||
## step 3: setup production runner
|
||||
#
|
||||
FROM base AS runner
|
||||
RUN corepack enable
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
RUN apk add --no-cache curl \
|
||||
&& apk add --no-cache supercronic \
|
||||
@@ -91,5 +92,5 @@ RUN mkdir -p /home/nextjs/apps/web/uploads/
|
||||
VOLUME /home/nextjs/apps/web/uploads/
|
||||
|
||||
CMD supercronic -quiet /app/docker/cronjobs & \
|
||||
(cd packages/database && npm run db:migrate:deploy) && \
|
||||
(cd packages/database && pnpm db:migrate:deploy) && \
|
||||
exec node apps/web/server.js
|
||||
|
||||
@@ -171,7 +171,7 @@ export const FileUploadQuestionForm = ({
|
||||
onToggle={() => updateQuestion(questionIdx, { allowMultipleFiles: !question.allowMultipleFiles })}
|
||||
htmlId="allowMultipleFile"
|
||||
title="Allow Multiple Files"
|
||||
description="Let people upload up to 25 files at the same time."
|
||||
description="Let people upload up to 10 files at the same time."
|
||||
childBorder
|
||||
customContainerClass="p-0"></AdvancedOptionToggle>
|
||||
|
||||
|
||||
@@ -19,12 +19,7 @@ import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/u
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import {
|
||||
findQuestionsWithCyclicLogic,
|
||||
isCardValid,
|
||||
validateQuestion,
|
||||
validateSurveyQuestionsInBatch,
|
||||
} from "../lib/validation";
|
||||
import { isCardValid, validateQuestion, validateSurveyQuestionsInBatch } from "../lib/validation";
|
||||
import { AddQuestionButton } from "./AddQuestionButton";
|
||||
import { EditThankYouCard } from "./EditThankYouCard";
|
||||
import { EditWelcomeCard } from "./EditWelcomeCard";
|
||||
@@ -94,12 +89,8 @@ export const QuestionsView = ({
|
||||
const isFirstQuestion = question.id === localSurvey.questions[0].id;
|
||||
let temp = structuredClone(invalidQuestions);
|
||||
if (validateQuestion(question, surveyLanguages, isFirstQuestion)) {
|
||||
// If question is valid, we now check for cyclic logic
|
||||
const questionsWithCyclicLogic = findQuestionsWithCyclicLogic(localSurvey.questions);
|
||||
if (!questionsWithCyclicLogic.includes(question.id)) {
|
||||
temp = invalidQuestions.filter((id) => id !== question.id);
|
||||
setInvalidQuestions(temp);
|
||||
}
|
||||
temp = invalidQuestions.filter((id) => id !== question.id);
|
||||
setInvalidQuestions(temp);
|
||||
} else if (!invalidQuestions.includes(question.id)) {
|
||||
temp.push(question.id);
|
||||
setInvalidQuestions(temp);
|
||||
|
||||
@@ -370,7 +370,7 @@ export const TargetingCard = ({
|
||||
<UpgradePlanNotice
|
||||
message="For advanced targeting, please"
|
||||
textForUrl="request an Enterprise license."
|
||||
url={`/environments/${environmentId}/settings/enterprise`}
|
||||
url="https://formbricks.com/docs/self-hosting/enterprise"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -246,13 +246,12 @@ export const validateId = (
|
||||
return true;
|
||||
};
|
||||
|
||||
// Checks if there is a cycle present in the survey data logic and returns all questions responsible for the cycle.
|
||||
export const findQuestionsWithCyclicLogic = (questions: TSurveyQuestions): string[] => {
|
||||
// Checks if there is a cycle present in the survey data logic.
|
||||
export const isSurveyLogicCyclic = (questions: TSurveyQuestions) => {
|
||||
const visited: Record<string, boolean> = {};
|
||||
const recStack: Record<string, boolean> = {};
|
||||
const cyclicQuestions: Set<string> = new Set();
|
||||
|
||||
const checkForCyclicLogic = (questionId: string): boolean => {
|
||||
const checkForCycle = (questionId: string) => {
|
||||
if (!visited[questionId]) {
|
||||
visited[questionId] = true;
|
||||
recStack[questionId] = true;
|
||||
@@ -262,14 +261,12 @@ export const findQuestionsWithCyclicLogic = (questions: TSurveyQuestions): strin
|
||||
for (const logic of question.logic) {
|
||||
const destination = logic.destination;
|
||||
if (!destination) {
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!visited[destination] && checkForCyclicLogic(destination)) {
|
||||
cyclicQuestions.add(questionId);
|
||||
if (!visited[destination] && checkForCycle(destination)) {
|
||||
return true;
|
||||
} else if (recStack[destination]) {
|
||||
cyclicQuestions.add(questionId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -277,7 +274,7 @@ export const findQuestionsWithCyclicLogic = (questions: TSurveyQuestions): strin
|
||||
// Handle default behavior
|
||||
const nextQuestionIndex = questions.findIndex((question) => question.id === questionId) + 1;
|
||||
const nextQuestion = questions[nextQuestionIndex];
|
||||
if (nextQuestion && !visited[nextQuestion.id] && checkForCyclicLogic(nextQuestion.id)) {
|
||||
if (nextQuestion && !visited[nextQuestion.id] && checkForCycle(nextQuestion.id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -289,10 +286,12 @@ export const findQuestionsWithCyclicLogic = (questions: TSurveyQuestions): strin
|
||||
|
||||
for (const question of questions) {
|
||||
const questionId = question.id;
|
||||
checkForCyclicLogic(questionId);
|
||||
if (checkForCycle(questionId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(cyclicQuestions);
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isSurveyValid = (
|
||||
@@ -456,9 +455,7 @@ export const isSurveyValid = (
|
||||
}
|
||||
|
||||
// Detecting any cyclic dependencies in survey logic.
|
||||
const questionsWithCyclicLogic = findQuestionsWithCyclicLogic(survey.questions);
|
||||
if (questionsWithCyclicLogic.length > 0) {
|
||||
setInvalidQuestions(questionsWithCyclicLogic);
|
||||
if (isSurveyLogicCyclic(survey.questions)) {
|
||||
toast.error("Cyclic logic detected. Please fix it before saving.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { formbricksEnabled } from "@/app/lib/formbricks";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import formbricks from "@formbricks/js/app";
|
||||
import { env } from "@formbricks/lib/env";
|
||||
@@ -15,23 +15,22 @@ export const FormbricksClient = ({ session }) => {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
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 && session?.user && formbricks) {
|
||||
initializeFormbricksAndSetupRouteChanges();
|
||||
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, pathname, searchParams, initializeFormbricksAndSetupRouteChanges]);
|
||||
}, [session]);
|
||||
|
||||
useEffect(() => {
|
||||
if (formbricksEnabled && formbricks) {
|
||||
formbricks?.registerRouteChange();
|
||||
}
|
||||
}, [pathname, searchParams]);
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export const AttributeClassesTable = ({ attributeClasses }: AttributeClassesTabl
|
||||
{displayedAttributeClasses.map((attributeClass, index) => (
|
||||
<button
|
||||
onClick={() => handleOpenAttributeDetailModalClick(attributeClass)}
|
||||
className="w-full cursor-default"
|
||||
className="w-full"
|
||||
key={attributeClass.id}>
|
||||
<AttributeClassDataRow attributeClass={attributeClass} key={index} />
|
||||
</button>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Badge } from "@formbricks/ui/Badge";
|
||||
|
||||
export const AttributeClassDataRow = ({ attributeClass }) => {
|
||||
return (
|
||||
<div className="m-2 grid h-16 cursor-pointer grid-cols-5 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
||||
<div className="m-2 grid h-16 grid-cols-5 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
||||
<div className="col-span-5 flex items-center pl-6 text-sm sm:col-span-3">
|
||||
<div className="flex items-center">
|
||||
<TagIcon className="h-5 w-5 flex-shrink-0 text-slate-500" />
|
||||
|
||||
@@ -1,51 +1,42 @@
|
||||
import { PeopleSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PeopleSecondaryNavigation";
|
||||
import { TagIcon } from "lucide-react";
|
||||
|
||||
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
|
||||
import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<>
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="Attributes">
|
||||
<PeopleSecondaryNavigation activeId="attributes" loading />
|
||||
</PageHeader>
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="grid h-12 grid-cols-5 content-center border-b text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6 ">Name</div>
|
||||
<div className="text-center">Created</div>
|
||||
<div className="text-center">Last Updated</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="m-2 grid h-16 grid-cols-5 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<TagIcon className="h-5 w-5 flex-shrink-0 animate-pulse text-slate-500" />
|
||||
<div className="ml-4 text-left">
|
||||
<div className="font-medium text-slate-900">
|
||||
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-slate-400">
|
||||
<div className="h-2 w-24 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-5 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6 ">Name</div>
|
||||
<div className="text-center">Created</div>
|
||||
<div className="text-center">Last Updated</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<div key={index} className="m-2 grid h-16 grid-cols-5 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="h-10 w-10 flex-shrink-0">
|
||||
<TagIcon className="h-8 w-8 flex-shrink-0 animate-pulse text-slate-500" />
|
||||
</div>
|
||||
<div className="ml-4 text-left">
|
||||
<div className="font-medium text-slate-900">
|
||||
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="m-4 h-4 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="m-4 h-4 animate-pulse rounded-full bg-slate-200"></div>
|
||||
<div className="mt-1 text-xs text-slate-400">
|
||||
<div className="h-2 w-24 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="m-4 h-4 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="m-4 h-4 animate-pulse rounded-full bg-slate-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</PageContentWrapper>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,8 +24,8 @@ export const ActivityTimeline = ({
|
||||
|
||||
{!isUserTargetingEnabled ? (
|
||||
<UpgradePlanNotice
|
||||
message="Upgrade your plan to store action history."
|
||||
textForUrl="More info."
|
||||
message="Upgrade to the User Targeting plan to store action history."
|
||||
textForUrl="Upgrade now."
|
||||
url={`/environments/${environment.id}/settings/billing`}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -2,11 +2,10 @@ import { SecondaryNavigation } from "@formbricks/ui/SecondaryNavigation";
|
||||
|
||||
interface PeopleSegmentsTabsProps {
|
||||
activeId: string;
|
||||
environmentId?: string;
|
||||
loading?: boolean;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export const PeopleSecondaryNavigation = ({ activeId, environmentId, loading }: PeopleSegmentsTabsProps) => {
|
||||
export const PeopleSecondaryNavigation = ({ activeId, environmentId }: PeopleSegmentsTabsProps) => {
|
||||
const navigation = [
|
||||
{
|
||||
id: "people",
|
||||
@@ -25,5 +24,5 @@ export const PeopleSecondaryNavigation = ({ activeId, environmentId, loading }:
|
||||
},
|
||||
];
|
||||
|
||||
return <SecondaryNavigation navigation={navigation} activeId={activeId} loading={loading} />;
|
||||
return <SecondaryNavigation navigation={navigation} activeId={activeId} />;
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ export const PersonCard = async ({ person }: { person: TPerson }) => {
|
||||
href={`/environments/${person.environmentId}/people/${person.id}`}
|
||||
key={person.id}
|
||||
className="w-full">
|
||||
<div className="m-2 grid h-16 grid-cols-7 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
||||
<div className="m-2 grid h-16 grid-cols-7 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="ph-no-capture h-10 w-10 flex-shrink-0">
|
||||
|
||||
@@ -1,45 +1,35 @@
|
||||
import { PeopleSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PeopleSecondaryNavigation";
|
||||
|
||||
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
|
||||
import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<>
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="People">
|
||||
<PeopleSecondaryNavigation activeId="people" loading />
|
||||
</PageHeader>
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="grid h-12 grid-cols-7 content-center border-b text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6">User</div>
|
||||
<div className="col-span-2 text-center">User ID</div>
|
||||
<div className="col-span-2 text-center">Email</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="m-2 grid h-16 grid-cols-7 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="ph-no-capture h-10 w-10 flex-shrink-0 animate-pulse rounded-full bg-gray-200"></div>{" "}
|
||||
<div className="ml-4">
|
||||
<div className="ph-no-capture h-4 w-28 animate-pulse rounded-full bg-gray-200 font-medium text-slate-900"></div>
|
||||
</div>{" "}
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-7 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6">User</div>
|
||||
<div className="col-span-2 text-center">User ID</div>
|
||||
<div className="col-span-2 text-center">Email</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="m-2 grid h-16 grid-cols-7 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="ph-no-capture h-10 w-10 flex-shrink-0 animate-pulse rounded-full bg-gray-200"></div>
|
||||
<div className="ml-4">
|
||||
<div className="ph-no-capture h-4 w-28 animate-pulse rounded-full bg-gray-200 font-medium text-slate-900"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="ph-no-capture m-12 h-4 animate-pulse rounded-full bg-gray-200 text-slate-900"></div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="ph-no-capture m-12 h-4 animate-pulse rounded-full bg-gray-200 text-slate-900"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="ph-no-capture m-12 h-4 animate-pulse rounded-full bg-gray-200 text-slate-900"></div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="ph-no-capture m-12 h-4 animate-pulse rounded-full bg-gray-200 text-slate-900"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PageContentWrapper>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -218,14 +218,14 @@ export const BasicCreateSegmentModal = ({
|
||||
{isFormbricksCloud ? (
|
||||
<UpgradePlanNotice
|
||||
message="For advanced targeting, please"
|
||||
textForUrl="upgrade your plan."
|
||||
textForUrl="upgrade to the User Identification plan."
|
||||
url={`/environments/${environmentId}/settings/billing`}
|
||||
/>
|
||||
) : (
|
||||
<UpgradePlanNotice
|
||||
message="For advanced targeting, please"
|
||||
textForUrl="request an Enterprise License."
|
||||
url={`/environments/${environmentId}/settings/enterprise`}
|
||||
textForUrl="request an Enterprise license."
|
||||
url="https://formbricks.com/docs/self-hosting/enterprise"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -214,14 +214,14 @@ export const BasicSegmentSettings = ({
|
||||
{isFormbricksCloud ? (
|
||||
<UpgradePlanNotice
|
||||
message="For advanced targeting, please"
|
||||
textForUrl="upgrade your plan."
|
||||
textForUrl="upgrade to the User Identification plan."
|
||||
url={`/environments/${environmentId}/settings/billing`}
|
||||
/>
|
||||
) : (
|
||||
<UpgradePlanNotice
|
||||
message="For advanced targeting, please"
|
||||
textForUrl="request an Enterprise License."
|
||||
url={`/environments/${environmentId}/settings/enterprise`}
|
||||
textForUrl="request an Enterprise license."
|
||||
url="https://formbricks.com/docs/self-hosting/enterprise"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -18,28 +18,22 @@ export const SegmentTable = ({
|
||||
isAdvancedTargetingAllowed,
|
||||
}: TSegmentTableProps) => {
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="grid h-12 grid-cols-7 content-center border-b text-left text-sm font-semibold text-slate-900">
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-7 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-4 pl-6">Title</div>
|
||||
<div className="col-span-1 hidden text-center sm:block">Surveys</div>
|
||||
<div className="col-span-1 hidden text-center sm:block">Updated</div>
|
||||
<div className="col-span-1 hidden text-center sm:block">Created</div>
|
||||
</div>
|
||||
{segments.length === 0 ? (
|
||||
<p className="py-6 text-center text-sm text-slate-400">Create your first Segment to get started.</p>
|
||||
) : (
|
||||
<>
|
||||
{segments.map((segment) => (
|
||||
<SegmentTableDataRowContainer
|
||||
currentSegment={segment}
|
||||
segments={segments}
|
||||
actionClasses={actionClasses}
|
||||
attributeClasses={attributeClasses}
|
||||
isAdvancedTargetingAllowed={isAdvancedTargetingAllowed}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{segments.map((segment) => (
|
||||
<SegmentTableDataRowContainer
|
||||
currentSegment={segment}
|
||||
segments={segments}
|
||||
actionClasses={actionClasses}
|
||||
attributeClasses={attributeClasses}
|
||||
isAdvancedTargetingAllowed={isAdvancedTargetingAllowed}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -34,11 +34,11 @@ export const SegmentTableDataRow = ({
|
||||
<>
|
||||
<div
|
||||
key={id}
|
||||
className="m-2 grid h-16 cursor-pointer grid-cols-7 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100"
|
||||
className="m-2 grid h-16 cursor-pointer grid-cols-7 content-center rounded-lg hover:bg-slate-100"
|
||||
onClick={() => setIsEditSegmentModalOpen(true)}>
|
||||
<div className="col-span-4 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="ph-no-capture w-8 flex-shrink-0 text-slate-500">
|
||||
<div className="ph-no-capture h-8 w-8 flex-shrink-0 text-slate-700">
|
||||
<UsersIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
|
||||