diff --git a/apps/formbricks-com/app/docs/_components/Button.tsx b/apps/formbricks-com/app/docs/_components/Button.tsx deleted file mode 100644 index 556e2f4238..0000000000 --- a/apps/formbricks-com/app/docs/_components/Button.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import Link from 'next/link' -import clsx from 'clsx' - -function ArrowIcon(props: React.ComponentPropsWithoutRef<'svg'>) { - return ( - - ) -} - -const variantStyles = { - primary: - 'rounded-full bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-emerald-400/10 dark:text-emerald-400 dark:ring-1 dark:ring-inset dark:ring-emerald-400/20 dark:hover:bg-emerald-400/10 dark:hover:text-emerald-300 dark:hover:ring-emerald-300', - secondary: - 'rounded-full bg-zinc-100 py-1 px-3 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800/40 dark:text-zinc-400 dark:ring-1 dark:ring-inset dark:ring-zinc-800 dark:hover:bg-zinc-800 dark:hover:text-zinc-300', - filled: - 'rounded-full bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-emerald-500 dark:text-white dark:hover:bg-emerald-400', - outline: - 'rounded-full py-1 px-3 text-zinc-700 ring-1 ring-inset ring-zinc-900/10 hover:bg-zinc-900/2.5 hover:text-zinc-900 dark:text-zinc-400 dark:ring-white/10 dark:hover:bg-white/5 dark:hover:text-white', - text: 'text-emerald-500 hover:text-emerald-600 dark:text-emerald-400 dark:hover:text-emerald-500', -} - -type ButtonProps = { - variant?: keyof typeof variantStyles - arrow?: 'left' | 'right' -} & ( - | React.ComponentPropsWithoutRef - | (React.ComponentPropsWithoutRef<'button'> & { href?: undefined }) -) - -export function Button({ - variant = 'primary', - className, - children, - arrow, - ...props -}: ButtonProps) { - className = clsx( - 'inline-flex gap-0.5 justify-center overflow-hidden text-sm font-medium transition', - variantStyles[variant], - className, - ) - - let arrowIcon = ( - - ) - - let inner = ( - <> - {arrow === 'left' && arrowIcon} - {children} - {arrow === 'right' && arrowIcon} - - ) - - if (typeof props.href === 'undefined') { - return ( - - ) - } - - return ( - - {inner} - - ) -} diff --git a/apps/formbricks-com/app/docs/_components/Code.tsx b/apps/formbricks-com/app/docs/_components/Code.tsx deleted file mode 100644 index be25238926..0000000000 --- a/apps/formbricks-com/app/docs/_components/Code.tsx +++ /dev/null @@ -1,376 +0,0 @@ -'use client' - -import { - Children, - createContext, - isValidElement, - useContext, - useEffect, - useRef, - useState, -} from 'react' -import { Tab } from '@headlessui/react' -import clsx from 'clsx' -import { create } from 'zustand' - -import { Tag } from '@/app/docs/_components/Tag' - -const languageNames: Record = { - js: 'JavaScript', - ts: 'TypeScript', - javascript: 'JavaScript', - typescript: 'TypeScript', - php: 'PHP', - python: 'Python', - ruby: 'Ruby', - go: 'Go', -} - -function getPanelTitle({ - title, - language, -}: { - title?: string - language?: string -}) { - if (title) { - return title - } - if (language && language in languageNames) { - return languageNames[language] - } - return 'Code' -} - -function ClipboardIcon(props: React.ComponentPropsWithoutRef<'svg'>) { - return ( - - ) -} - -function CopyButton({ code }: { code: string }) { - let [copyCount, setCopyCount] = useState(0) - let copied = copyCount > 0 - - useEffect(() => { - if (copyCount > 0) { - let timeout = setTimeout(() => setCopyCount(0), 1000) - return () => { - clearTimeout(timeout) - } - } - }, [copyCount]) - - return ( - - ) -} - -function CodePanelHeader({ tag, label }: { tag?: string; label?: string }) { - if (!tag && !label) { - return null - } - - return ( -
- {tag && ( -
- {tag} -
- )} - {tag && label && ( - - )} - {label && ( - {label} - )} -
- ) -} - -function CodePanel({ - children, - tag, - label, - code, -}: { - children: React.ReactNode - tag?: string - label?: string - code?: string -}) { - let child = Children.only(children) - - if (isValidElement(child)) { - tag = child.props.tag ?? tag - label = child.props.label ?? label - code = child.props.code ?? code - } - - if (!code) { - throw new Error( - '`CodePanel` requires a `code` prop, or a child with a `code` prop.' - ) - } - - return ( -
- -
-
{children}
- -
-
- ) -} - -function CodeGroupHeader({ - title, - children, - selectedIndex, -}: { - title: string - children: React.ReactNode - selectedIndex: number -}) { - let hasTabs = Children.count(children) > 1 - - if (!title && !hasTabs) { - return null - } - - return ( -
- {title && ( -

- {title} -

- )} - {hasTabs && ( - - {Children.map(children, (child, childIndex) => ( - - {getPanelTitle(isValidElement(child) ? child.props : {})} - - ))} - - )} -
- ) -} - -function CodeGroupPanels({ - children, - ...props -}: React.ComponentPropsWithoutRef) { - let hasTabs = Children.count(children) > 1 - - if (hasTabs) { - return ( - - {Children.map(children, (child) => ( - - {child} - - ))} - - ) - } - - return {children} -} - -function usePreventLayoutShift() { - let positionRef = useRef(null) - let rafRef = useRef() - - useEffect(() => { - return () => { - if (typeof rafRef.current !== 'undefined') { - window.cancelAnimationFrame(rafRef.current) - } - } - }, []) - - return { - positionRef, - preventLayoutShift(callback: () => void) { - if (!positionRef.current) { - return - } - - let initialTop = positionRef.current.getBoundingClientRect().top - - callback() - - rafRef.current = window.requestAnimationFrame(() => { - let newTop = - positionRef.current?.getBoundingClientRect().top ?? initialTop - window.scrollBy(0, newTop - initialTop) - }) - }, - } -} - -const usePreferredLanguageStore = create<{ - preferredLanguages: Array - addPreferredLanguage: (language: string) => void -}>()((set) => ({ - preferredLanguages: [], - addPreferredLanguage: (language) => - set((state) => ({ - preferredLanguages: [ - ...state.preferredLanguages.filter( - (preferredLanguage) => preferredLanguage !== language - ), - language, - ], - })), -})) - -function useTabGroupProps(availableLanguages: Array) { - let { preferredLanguages, addPreferredLanguage } = usePreferredLanguageStore() - let [selectedIndex, setSelectedIndex] = useState(0) - let activeLanguage = [...availableLanguages].sort( - (a, z) => preferredLanguages.indexOf(z) - preferredLanguages.indexOf(a) - )[0] - let languageIndex = availableLanguages.indexOf(activeLanguage) - let newSelectedIndex = languageIndex === -1 ? selectedIndex : languageIndex - if (newSelectedIndex !== selectedIndex) { - setSelectedIndex(newSelectedIndex) - } - - let { positionRef, preventLayoutShift } = usePreventLayoutShift() - - return { - as: 'div' as const, - ref: positionRef, - selectedIndex, - onChange: (newSelectedIndex: number) => { - preventLayoutShift(() => - addPreferredLanguage(availableLanguages[newSelectedIndex]) - ) - }, - } -} - -const CodeGroupContext = createContext(false) - -export function CodeGroup({ - children, - title, - ...props -}: React.ComponentPropsWithoutRef & { title: string }) { - let languages = - Children.map(children, (child) => - getPanelTitle(isValidElement(child) ? child.props : {}) - ) ?? [] - let tabGroupProps = useTabGroupProps(languages) - let hasTabs = Children.count(children) > 1 - - let containerClassName = - 'not-prose my-6 overflow-hidden rounded-2xl bg-zinc-900 shadow-md dark:ring-1 dark:ring-white/10' - let header = ( - - {children} - - ) - let panels = {children} - - return ( - - {hasTabs ? ( - - {header} - {panels} - - ) : ( -
- {header} - {panels} -
- )} -
- ) -} - -export function Code({ - children, - ...props -}: React.ComponentPropsWithoutRef<'code'>) { - let isGrouped = useContext(CodeGroupContext) - - if (isGrouped) { - if (typeof children !== 'string') { - throw new Error( - '`Code` children must be a string when nested inside a `CodeGroup`.' - ) - } - return - } - - return {children} -} - -export function Pre({ - children, - ...props -}: React.ComponentPropsWithoutRef) { - let isGrouped = useContext(CodeGroupContext) - - if (isGrouped) { - return children - } - - return {children} -} diff --git a/apps/formbricks-com/app/docs/_components/Feedback.tsx b/apps/formbricks-com/app/docs/_components/Feedback.tsx deleted file mode 100644 index 4fa92ed4d1..0000000000 --- a/apps/formbricks-com/app/docs/_components/Feedback.tsx +++ /dev/null @@ -1,105 +0,0 @@ -'use client' - -import { forwardRef, Fragment, useState } from 'react' -import { Transition } from '@headlessui/react' - -function CheckIcon(props: React.ComponentPropsWithoutRef<'svg'>) { - return ( - - ) -} - -function FeedbackButton( - props: Omit, 'type' | 'className'>, -) { - return ( - -

- - ))} - - - ) -} diff --git a/apps/formbricks-com/app/docs/_components/Header.tsx b/apps/formbricks-com/app/docs/_components/Header.tsx deleted file mode 100644 index d06ca8ae80..0000000000 --- a/apps/formbricks-com/app/docs/_components/Header.tsx +++ /dev/null @@ -1,100 +0,0 @@ -'use client' - -import { forwardRef } from 'react' -import Link from 'next/link' -import clsx from 'clsx' -import { motion, useScroll, useTransform } from 'framer-motion' - -import { Button } from '@/app/docs/_components/Button' -import { Logo } from '@/app/docs/_components/Logo' -import { - MobileNavigation, - useIsInsideMobileNavigation, -} from '@/app/docs/_components/MobileNavigation' -import { useMobileNavigationStore } from '@/app/docs/_components/MobileNavigation' -import { MobileSearch, Search } from '@/app/docs/_components/Search' -import { ThemeToggle } from '@/app/docs/_components/ThemeToggle' - -function TopLevelNavItem({ - href, - children, -}: { - href: string - children: React.ReactNode -}) { - return ( -
  • - - {children} - -
  • - ) -} - -export const Header = forwardRef< - React.ElementRef<'div'>, - { className?: string } ->(function Header({ className }, ref) { - let { isOpen: mobileNavIsOpen } = useMobileNavigationStore() - let isInsideMobileNavigation = useIsInsideMobileNavigation() - - let { scrollY } = useScroll() - let bgOpacityLight = useTransform(scrollY, [0, 72], [0.5, 0.9]) - let bgOpacityDark = useTransform(scrollY, [0, 72], [0.2, 0.8]) - - return ( - -
    - -
    - - - - -
    -
    - -
    -
    - - -
    -
    - -
    -
    - - ) -}) diff --git a/apps/formbricks-com/app/docs/_components/Layout.tsx b/apps/formbricks-com/app/docs/_components/Layout.tsx deleted file mode 100644 index e6ce78846b..0000000000 --- a/apps/formbricks-com/app/docs/_components/Layout.tsx +++ /dev/null @@ -1,49 +0,0 @@ -'use client' - -import Link from 'next/link' -import { usePathname } from 'next/navigation' -import { motion } from 'framer-motion' - -import { Footer } from '@/app/docs/_components/Footer' -import { Header } from '@/app/docs/_components/Header' -import { Logo } from '@/app/docs/_components/Logo' -import { Navigation } from '@/app/docs/_components/Navigation' -import { - type Section, - SectionProvider, -} from '@/app/docs/_components/SectionProvider' - -export function Layout({ - children, - allSections, -}: { - children: React.ReactNode - allSections: Record> -}) { - let pathname = usePathname() - - return ( - -
    - -
    -
    - - - -
    -
    - -
    -
    -
    -
    {children}
    -
    -
    -
    -
    - ) -} diff --git a/apps/formbricks-com/app/docs/_components/Libraries.tsx b/apps/formbricks-com/app/docs/_components/Libraries.tsx deleted file mode 100644 index 80cad2542f..0000000000 --- a/apps/formbricks-com/app/docs/_components/Libraries.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import Image from 'next/image' - -import { Button } from '@/app/docs/_components/Button' -import { Heading } from '@/app/docs/_components/Heading' -import logoGo from '@/images/logos/go.svg' -import logoNode from '@/images/logos/node.svg' -import logoPhp from '@/images/logos/php.svg' -import logoPython from '@/images/logos/python.svg' -import logoRuby from '@/images/logos/ruby.svg' - -const libraries = [ - { - href: '#', - name: 'PHP', - description: - 'A popular general-purpose scripting language that is especially suited to web development.', - logo: logoPhp, - }, - { - href: '#', - name: 'Ruby', - description: - 'A dynamic, open source programming language with a focus on simplicity and productivity.', - logo: logoRuby, - }, - { - href: '#', - name: 'Node.js', - description: - 'Node.js® is an open-source, cross-platform JavaScript runtime environment.', - logo: logoNode, - }, - { - href: '#', - name: 'Python', - description: - 'Python is a programming language that lets you work quickly and integrate systems more effectively.', - logo: logoPython, - }, - { - href: '#', - name: 'Go', - description: - 'An open-source programming language supported by Google with built-in concurrency.', - logo: logoGo, - }, -] - -export function Libraries() { - return ( -
    - - Official libraries - -
    - {libraries.map((library) => ( -
    -
    -

    - {library.name} -

    -

    - {library.description} -

    -

    - -

    -
    - -
    - ))} -
    -
    - ) -} diff --git a/apps/formbricks-com/app/docs/_components/Logo.tsx b/apps/formbricks-com/app/docs/_components/Logo.tsx deleted file mode 100644 index ac0eb273f9..0000000000 --- a/apps/formbricks-com/app/docs/_components/Logo.tsx +++ /dev/null @@ -1,14 +0,0 @@ -export function Logo(props: React.ComponentPropsWithoutRef<'svg'>) { - return ( - - ) -} diff --git a/apps/formbricks-com/app/docs/_components/Navigation.tsx b/apps/formbricks-com/app/docs/_components/Navigation.tsx deleted file mode 100644 index 1ce2de76e8..0000000000 --- a/apps/formbricks-com/app/docs/_components/Navigation.tsx +++ /dev/null @@ -1,279 +0,0 @@ -'use client' - -import { useRef } from 'react' -import Link from 'next/link' -import { usePathname } from 'next/navigation' -import clsx from 'clsx' -import { AnimatePresence, motion, useIsPresent } from 'framer-motion' - -import { Button } from '@/app/docs/_components/Button' -import { useIsInsideMobileNavigation } from '@/app/docs/_components/MobileNavigation' -import { useSectionStore } from '@/app/docs/_components/SectionProvider' -import { Tag } from '@/app/docs/_components/Tag' -import { remToPx } from '@/lib/remToPx' - -interface NavGroup { - title: string - links: Array<{ - title: string - href: string - }> -} - -function useInitialValue(value: T, condition = true) { - let initialValue = useRef(value).current - return condition ? initialValue : value -} - -function TopLevelNavItem({ - href, - children, -}: { - href: string - children: React.ReactNode -}) { - return ( -
  • - - {children} - -
  • - ) -} - -function NavLink({ - href, - children, - tag, - active = false, - isAnchorLink = false, -}: { - href: string - children: React.ReactNode - tag?: string - active?: boolean - isAnchorLink?: boolean -}) { - return ( - - {children} - {tag && ( - - {tag} - - )} - - ) -} - -function VisibleSectionHighlight({ - group, - pathname, -}: { - group: NavGroup - pathname: string -}) { - let [sections, visibleSections] = useInitialValue( - [ - useSectionStore((s) => s.sections), - useSectionStore((s) => s.visibleSections), - ], - useIsInsideMobileNavigation() - ) - - let isPresent = useIsPresent() - let firstVisibleSectionIndex = Math.max( - 0, - [{ id: '_top' }, ...sections].findIndex( - (section) => section.id === visibleSections[0] - ) - ) - let itemHeight = remToPx(2) - let height = isPresent - ? Math.max(1, visibleSections.length) * itemHeight - : itemHeight - let top = - group.links.findIndex((link) => link.href === pathname) * itemHeight + - firstVisibleSectionIndex * itemHeight - - return ( - - ) -} - -function ActivePageMarker({ - group, - pathname, -}: { - group: NavGroup - pathname: string -}) { - let itemHeight = remToPx(2) - let offset = remToPx(0.25) - let activePageIndex = group.links.findIndex((link) => link.href === pathname) - let top = offset + activePageIndex * itemHeight - - return ( - - ) -} - -function NavigationGroup({ - group, - className, -}: { - group: NavGroup - className?: string -}) { - // If this is the mobile navigation then we always render the initial - // state, so that the state does not change during the close animation. - // The state will still update when we re-open (re-render) the navigation. - let isInsideMobileNavigation = useIsInsideMobileNavigation() - let [pathname, sections] = useInitialValue( - [usePathname(), useSectionStore((s) => s.sections)], - isInsideMobileNavigation - ) - - let isActiveGroup = - group.links.findIndex((link) => link.href === pathname) !== -1 - - return ( -
  • - - {group.title} - -
    - - {isActiveGroup && ( - - )} - - - - {isActiveGroup && ( - - )} - -
      - {group.links.map((link) => ( - - - {link.title} - - - {link.href === pathname && sections.length > 0 && ( - - {sections.map((section) => ( -
    • - - {section.title} - -
    • - ))} -
      - )} -
      -
      - ))} -
    -
    -
  • - ) -} - -export const navigation: Array = [ - { - title: 'Guides', - links: [ - { title: 'Introduction', href: '/docs/' }, - { title: 'Quickstart', href: '/docs/quickstart' }, - { title: 'SDKs', href: '/docs/sdks' }, - { title: 'Authentication', href: '/docs/authentication' }, - { title: 'Pagination', href: '/docs/pagination' }, - { title: 'Errors', href: '/docs/errors' }, - { title: 'Webhooks', href: '/docs/webhooks' }, - ], - }, - { - title: 'Resources', - links: [ - { title: 'Contacts', href: '/docs/contacts' }, - { title: 'Conversations', href: '/docs/conversations' }, - { title: 'Messages', href: '/docs/messages' }, - { title: 'Groups', href: '/docs/groups' }, - { title: 'Attachments', href: '/docs/attachments' }, - ], - }, -] - -export function Navigation(props: React.ComponentPropsWithoutRef<'nav'>) { - return ( - - ) -} diff --git a/apps/formbricks-com/app/docs/_components/Resources.tsx b/apps/formbricks-com/app/docs/_components/Resources.tsx deleted file mode 100644 index e5dd052154..0000000000 --- a/apps/formbricks-com/app/docs/_components/Resources.tsx +++ /dev/null @@ -1,186 +0,0 @@ -'use client' - -import Link from 'next/link' -import { - type MotionValue, - motion, - useMotionTemplate, - useMotionValue, -} from 'framer-motion' - -import { GridPattern } from '@/app/docs/_components/GridPattern' -import { Heading } from '@/app/docs/_components/Heading' -import { ChatBubbleIcon } from '@/app/docs/_components/icons/ChatBubbleIcon' -import { EnvelopeIcon } from '@/app/docs/_components/icons/EnvelopeIcon' -import { UserIcon } from '@/app/docs/_components/icons/UserIcon' -import { UsersIcon } from '@/app/docs/_components/icons/UsersIcon' - -interface Resource { - href: string - name: string - description: string - icon: React.ComponentType<{ className?: string }> - pattern: Omit< - React.ComponentPropsWithoutRef, - 'width' | 'height' | 'x' - > -} - -const resources: Array = [ - { - href: '/docs/contacts', - name: 'Contacts', - description: - 'Learn about the contact model and how to create, retrieve, update, delete, and list contacts.', - icon: UserIcon, - pattern: { - y: 16, - squares: [ - [0, 1], - [1, 3], - ], - }, - }, - { - href: '/docs/conversations', - name: 'Conversations', - description: - 'Learn about the conversation model and how to create, retrieve, update, delete, and list conversations.', - icon: ChatBubbleIcon, - pattern: { - y: -6, - squares: [ - [-1, 2], - [1, 3], - ], - }, - }, - { - href: '/docs/messages', - name: 'Messages', - description: - 'Learn about the message model and how to create, retrieve, update, delete, and list messages.', - icon: EnvelopeIcon, - pattern: { - y: 32, - squares: [ - [0, 2], - [1, 4], - ], - }, - }, - { - href: '/docs/groups', - name: 'Groups', - description: - 'Learn about the group model and how to create, retrieve, update, delete, and list groups.', - icon: UsersIcon, - pattern: { - y: 22, - squares: [[0, 1]], - }, - }, -] - -function ResourceIcon({ icon: Icon }: { icon: Resource['icon'] }) { - return ( -
    - -
    - ) -} - -function ResourcePattern({ - mouseX, - mouseY, - ...gridProps -}: Resource['pattern'] & { - mouseX: MotionValue - mouseY: MotionValue -}) { - let maskImage = useMotionTemplate`radial-gradient(180px at ${mouseX}px ${mouseY}px, white, transparent)` - let style = { maskImage, WebkitMaskImage: maskImage } - - return ( -
    -
    - -
    - - - - -
    - ) -} - -function Resource({ resource }: { resource: Resource }) { - let mouseX = useMotionValue(0) - let mouseY = useMotionValue(0) - - function onMouseMove({ - currentTarget, - clientX, - clientY, - }: React.MouseEvent) { - let { left, top } = currentTarget.getBoundingClientRect() - mouseX.set(clientX - left) - mouseY.set(clientY - top) - } - - return ( -
    - -
    -
    - -

    - - - {resource.name} - -

    -

    - {resource.description} -

    -
    -
    - ) -} - -export function Resources() { - return ( -
    - - Resources - -
    - {resources.map((resource) => ( - - ))} -
    -
    - ) -} diff --git a/apps/formbricks-com/app/docs/actions/code/page.mdx b/apps/formbricks-com/app/docs/actions/code/page.mdx new file mode 100644 index 0000000000..067d451f73 --- /dev/null +++ b/apps/formbricks-com/app/docs/actions/code/page.mdx @@ -0,0 +1,25 @@ +export const meta = { + title: "Code Actions", + description: + "Integrate code actions in Formbricks using formbricks.track() to trigger surveys based on user actions, like button clicks, for precise insights. All open-source.", +}; + +[Actions]() + +# Code Actions + +Actions can also be set in the code base. You can fire an action using `formbricks.track()` + +```javascript +formbricks.track("Action Name"); +``` + +Here is an example of how to fire an action when a user clicks a button: + +```javascript +const handleClick = () => { + formbricks.track("Button Clicked"); +}; + +return ; +``` diff --git a/apps/formbricks-com/app/docs/actions/no-code/page.mdx b/apps/formbricks-com/app/docs/actions/no-code/page.mdx new file mode 100644 index 0000000000..49e728e302 --- /dev/null +++ b/apps/formbricks-com/app/docs/actions/no-code/page.mdx @@ -0,0 +1,30 @@ +export const meta = { + title: "No-Code Actions", + description: + "Utilize Formbricks' No-Code Actions like Page URL, innerText, and CSS Selector for easy survey triggers and enhanced user insights.", +}; + +[Actions]() + +# No-Code Actions + +No-Code actions can be set up within Formbricks with just a few clicks. There are three types of No-Code actions: + +## Page URL Action + +The page URL action is triggered, when a user visits a specific page in your application. There are several match conditions: + +- `exactMatch`: The URL should exactly match the provided string. +- `contains`: The URL should contain the specified string as a substring. +- `startsWith`: The URL should start with the specified string. +- `endsWith`: The URL should end with the specified string. +- `notMatch`: The URL should not match the specified condition. +- `notContains`: The URL should not contain the specified string as a substring. + +## innerText Action + +The innerText action checks if the `innerText` of a clicked HTML element matches a specific text, e.g. the label of a button. Display a survey on any button click! + +## CSS Selector Action + +The CSS Selector action checks if the provided CSS selector matches the selector of a clicked HTML element. The CSS selector can be a class, id or any other CSS selector within your website. Display a survey on any element click! diff --git a/apps/formbricks-com/app/docs/actions/why/page.mdx b/apps/formbricks-com/app/docs/actions/why/page.mdx new file mode 100644 index 0000000000..6b0d102a64 --- /dev/null +++ b/apps/formbricks-com/app/docs/actions/why/page.mdx @@ -0,0 +1,23 @@ +export const meta = { + title: "What are actions and why are they useful?", + description: + "Actions in Formbricks enable targeted survey displays during specific user journey moments. Enhance user segmentation by tracking actions for granular surveying.", +}; + +[Actions]() + +# What are actions and why are they useful? + +You want to understand what your users think and feel during specific moments in the user journey. To be able to ask at exactly the right point in time, you need actions. + +## What are actions? + +Actions are a little notification sent from your application to Formbricks. You decide which actions are sent either in your [Code](/docs/actions/code) or by setting up a [No-Code](/docs/actions/no-code) action within Formbricks. + +## How do actions work? + +When a predefined action happens in your app, the Formbricks widget notices. This action can then trigger a survey to be shown to the user and is stored in the database. + +## Why are actions useful? + +Actions help you to display your surveys at the right time. Later on, you will be able to segment your users based on the actions they have triggered in the past. This way, you can create much more granular user segments, e.g. only target users that already have used a specific feature. diff --git a/apps/formbricks-com/app/docs/api/api-key-setup/add-api-key.png b/apps/formbricks-com/app/docs/api/api-key-setup/add-api-key.png new file mode 100644 index 0000000000..8ea4a144a1 Binary files /dev/null and b/apps/formbricks-com/app/docs/api/api-key-setup/add-api-key.png differ diff --git a/apps/formbricks-com/app/docs/api/api-key-setup/api-key-secret.png b/apps/formbricks-com/app/docs/api/api-key-setup/api-key-secret.png new file mode 100644 index 0000000000..c30d79529a Binary files /dev/null and b/apps/formbricks-com/app/docs/api/api-key-setup/api-key-secret.png differ diff --git a/apps/formbricks-com/app/docs/api/api-key-setup/page.mdx b/apps/formbricks-com/app/docs/api/api-key-setup/page.mdx new file mode 100644 index 0000000000..9a03843f62 --- /dev/null +++ b/apps/formbricks-com/app/docs/api/api-key-setup/page.mdx @@ -0,0 +1,40 @@ +import Image from "next/image"; + +import AddApiKey from "./add-api-key.png"; +import ApiKeySecret from "./api-key-secret.png"; + +export const meta = { + title: "API Key Setup", + description: + "Generate, store, and delete personal API keys for secure Formbricks access. Ensure safekeeping to prevent unauthorized account control.", +}; + +[API]() + +# API Key Setup + +## Auth: Personal API key + +The API requests are authorized with a personal API key. This API key gives you the same rights as if you were logged in at formbricks.com - **don't share it around!** + +### How to generate an API key + +1. Go to your settings on [app.formbricks.com](https://app.formbricks.com). +2. Go to page “API keys” + Add API Key +3. Create a key for the development or production environment. +4. Copy the key immediately. You won’t be able to see it again. + API Key Secret + + + ## Store API key safely {{ class: "text-white" }} + Anyone who has your API key has full control over your account. For security reasons, you cannot view the API + key again. + + +### Delete a personal API key + +1. Go to settings on [app.formbricks.com](https://app.formbricks.com/). +2. Go to page “API keys”. +3. Find the key you wish to revoke and select “Delete”. +4. Your API key will stop working immediately. diff --git a/apps/formbricks-com/app/docs/api/get-responses/page.mdx b/apps/formbricks-com/app/docs/api/get-responses/page.mdx new file mode 100644 index 0000000000..a462b39040 --- /dev/null +++ b/apps/formbricks-com/app/docs/api/get-responses/page.mdx @@ -0,0 +1,104 @@ +export const meta = { + title: "API: Get Responses", + description: "Fetch all the responses for your environment.", +}; + +[API]() + +# API: Get Responses + +## List all Responses {{ tag: 'GET', label: '/api/v1/responses' }} + + + + + Retrieve all the responses you have received for all your surveys. + + ### Mandatory Headers + + + + Your Formbricks API key. + + + + ### Optional Query Params + + + SurveyId to filter responses by. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl --location \ + 'https://app.formbricks.com/api/v1/responses' \ + --header \ + 'x-api-key: ' + ``` + + + + + + ```json {{title:'200 Success'}} + { + "data":[ + { + "id":"cllnfybxd051rpl0gieavthr9", + "createdAt":"2023-08-23T07:56:33.121Z", + "updatedAt":"2023-08-23T07:56:41.793Z", + "surveyId":"cllnfy2780fromy0hy7uoxvtn", + "finished":true, + "data":{ + "ec7agikkr58j8uonhioinkyk":"Loved it!", + "gml6mgy71efgtq8np3s9je5p":"clicked", + "klvpwd4x08x8quesihvw5l92":"Product Manager", + "kp62fbqe8cfzmvy8qwpr81b2":"Very disappointed", + "lkjaxb73ulydzeumhd51sx9g":"Not interesed.", + "ryo75306flyg72iaeditbv51":"Would love if it had dark theme." + }, + "meta":{ + "url":"https://app.formbricks.com/s/cllnfy2780fromy0hy7uoxvtn", + "userAgent":{ + "os":"Linux", + "browser":"Chrome" + } + }, + "personAttributes":null, + "person":null, + "notes":[], + "tags":[] + } + ] + } + ``` + + ```json {{ title: '404 Not Found' }} + { + "code": "not_found", + "message": "cllnfy2780fromy0hy7uoxvt not found", + "details": { + "resource_id": "survey", + "resource_type": "cllnfy2780fromy0hy7uoxvt" + } + } + ``` + + ```json {{ title: '401 Not Authenticated' }} + { + "code": "not_authenticated", + "message": "Not authenticated", + "details": { + "x-Api-Key": "Header not provided or API Key invalid" + } + } + ``` + + + + diff --git a/apps/formbricks-com/app/docs/api/overview/page.mdx b/apps/formbricks-com/app/docs/api/overview/page.mdx new file mode 100644 index 0000000000..2aad095ddb --- /dev/null +++ b/apps/formbricks-com/app/docs/api/overview/page.mdx @@ -0,0 +1,27 @@ +export const meta = { + title: "API Overview", + description: + "Explore Formbricks' APIs: Public Client API for client-side tasks, and User API for account management with secure API Key authentication.", +}; + +[API]() + +# API Overview + +Formbricks offers two types of APIs: the Public Client API and the User API. Each API serves a different purpose, has different authentication requirements, and provides access to different data and settings. + +## Public Client API + +The Public Client API is designed for the JavaScript SDK and does not require authentication. It's primarily used for creating persons, sessions, and responses within the Formbricks platform. This API is ideal for client-side interactions, as it doesn't expose sensitive information. + +## User API + +The User API provides access to all data and settings that are visible in the Formbricks App. This API requires a personal API Key for authentication, which can be generated in the Settings section of the Formbricks App. With the User API, you can manage your Formbricks account programmatically, accessing and modifying data and settings as needed. + +**Auth:** Personal API Key + +API requests made to the User API are authorized using a personal API key. This key grants the same rights and access as if you were logged in at formbricks.com. It's essential to keep your API key secure and not share it with others. + +To generate, store, or delete an API key, follow the instructions provided on the following page [API Key](/docs/api/api-key-setup). + +By understanding the differences between these two APIs, you can choose the appropriate one for your needs, ensuring a secure and efficient integration with the Formbricks platform. diff --git a/apps/formbricks-com/app/docs/attachments/page.mdx b/apps/formbricks-com/app/docs/attachments/page.mdx deleted file mode 100644 index cd36364d74..0000000000 --- a/apps/formbricks-com/app/docs/attachments/page.mdx +++ /dev/null @@ -1,363 +0,0 @@ -export const metadata = { - title: 'Attachments', - description: - 'On this page, we’ll dive into the different attachment endpoints you can use to manage attachments programmatically.', -} - -# Attachments - -Attachments are how you share things in Protocol — they allow you to send all sorts of files to your contacts and groups. On this page, we'll dive into the different attachment endpoints you can use to manage attachments programmatically. We'll look at how to query, upload, update, and delete attachments. {{ className: 'lead' }} - -## The attachment model - -The attachment model contains all the information about the files you send to your contacts and groups, including the name, type, and size. - -### Properties - - - - Unique identifier for the attachment. - - - Unique identifier for the message associated with the attachment. - - - The filename for the attachment. - - - The URL for the attached file. - - - The MIME type of the attached file. - - - The file size of the attachment in bytes. - - - Timestamp of when the attachment was created. - - - ---- - -## List all attachments {{ tag: 'GET', label: '/v1/attachments' }} - - - - - This endpoint allows you to retrieve a paginated list of all your attachments (in a conversation if a conversation id is provided). By default, a maximum of ten attachments are shown per page. - - ### Optional attributes - - - - Limit to attachments from a given conversation. - - - Limit the number of attachments returned. - - - - - - - - - ```bash {{ title: 'cURL' }} - curl -G https://api.protocol.chat/v1/attachments \ - -H "Authorization: Bearer {token}" \ - -d conversation_id="xgQQXg3hrtjh7AvZ" \ - -d limit=10 - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.attachments.list() - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.attachments.list() - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->attachments->list(); - ``` - - - - ```json {{ title: 'Response' }} - { - "has_more": false, - "data": [ - { - "id": "Nc6yKKMpcxiiFxp6", - "message_id": "LoPsJaMcPBuFNjg1", - "filename": "Invoice_room_service__Plaza_Hotel.pdf", - "file_url": "https://assets.protocol.chat/attachments/Invoice_room_service__Plaza_Hotel.pdf", - "file_type": "application/pdf", - "file_size": 21352, - "created_at": 692233200 - }, - { - "id": "hSIhXBhNe8X1d8Et" - // ... - } - ] - } - ``` - - - - ---- - -## Create an attachment {{ tag: 'POST', label: '/v1/attachments' }} - - - - - This endpoint allows you to upload a new attachment to a conversation. See the code examples for how to send the file to the Protocol API. - - ### Required attributes - - - - The file you want to add as an attachment. - - - - - - - - - ```bash {{ title: 'cURL' }} - curl https://api.protocol.chat/v1/attachments \ - -H "Authorization: Bearer {token}" \ - -F file="../Invoice_room_service__Plaza_Hotel.pdf" - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.attachments.create({ file }) - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.attachments.create(file=file) - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->attachments->create([ - 'file' => $file, - ]); - ``` - - - - ```json {{ title: 'Response' }} - { - "id": "Nc6yKKMpcxiiFxp6", - "message_id": "LoPsJaMcPBuFNjg1", - "filename": "Invoice_room_service__Plaza_Hotel.pdf", - "file_url": "https://assets.protocol.chat/attachments/Invoice_room_service__Plaza_Hotel.pdf", - "file_type": "application/pdf", - "file_size": 21352, - "created_at": 692233200 - } - ``` - - - - ---- - -## Retrieve an attachment {{ tag: 'GET', label: '/v1/attachments/:id' }} - - - - - This endpoint allows you to retrieve an attachment by providing the attachment id. Refer to [the list](#the-attachment-model) at the top of this page to see which properties are included with attachment objects. - - - - - - - ```bash {{ title: 'cURL' }} - curl https://api.protocol.chat/v1/attachments/Nc6yKKMpcxiiFxp6 \ - -H "Authorization: Bearer {token}" - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.attachments.get('Nc6yKKMpcxiiFxp6') - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.attachments.get("Nc6yKKMpcxiiFxp6") - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->attachments->get('Nc6yKKMpcxiiFxp6'); - ``` - - - - ```json {{ title: 'Response' }} - { - "id": "Nc6yKKMpcxiiFxp6", - "message_id": "LoPsJaMcPBuFNjg1", - "filename": "Invoice_room_service__Plaza_Hotel.pdf", - "file_url": "https://assets.protocol.chat/attachments/Invoice_room_service__Plaza_Hotel.pdf", - "file_type": "application/pdf", - "file_size": 21352, - "created_at": 692233200 - } - ``` - - - - ---- - -## Update an attachment {{ tag: 'PUT', label: '/v1/attachments/:id' }} - - - - - This endpoint allows you to perform an update on an attachment. Currently, the only supported type of update is changing the filename. - - ### Optional attributes - - - - The new filename for the attachment. - - - - - - - - - ```bash {{ title: 'cURL' }} - curl -X PUT https://api.protocol.chat/v1/attachments/Nc6yKKMpcxiiFxp6 \ - -H "Authorization: Bearer {token}" \ - -d filename="Invoice_room_service__Plaza_Hotel_updated.pdf" - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.attachments.update('Nc6yKKMpcxiiFxp6', { - filename: 'Invoice_room_service__Plaza_Hotel_updated.pdf', - }) - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.attachments.update("Nc6yKKMpcxiiFxp6", filename="Invoice_room_service__Plaza_Hotel_updated.pdf") - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->attachments->update('Nc6yKKMpcxiiFxp6', [ - 'filename' => 'Invoice_room_service__Plaza_Hotel_updated.pdf', - ]); - ``` - - - - ```json {{ title: 'Response' }} - { - "id": "Nc6yKKMpcxiiFxp6", - "message_id": "LoPsJaMcPBuFNjg1", - "filename": "Invoice_room_service__Plaza_Hotel.pdf", - "file_url": "https://assets.protocol.chat/attachments/Invoice_room_service__Plaza_Hotel_updated.pdf", - "file_type": "application/pdf", - "file_size": 21352, - "created_at": 692233200 - } - ``` - - - - ---- - -## Delete an attachment {{ tag: 'DELETE', label: '/v1/attachments/:id' }} - - - - - This endpoint allows you to delete attachments. Note: This will permanently delete the file. - - - - - - - ```bash {{ title: 'cURL' }} - curl -X DELETE https://api.protocol.chat/v1/attachments/Nc6yKKMpcxiiFxp6 \ - -H "Authorization: Bearer {token}" - ``` - - ```js - import ApiClient from '@example/protocol-api' - - const client = new ApiClient(token) - - await client.attachments.delete('Nc6yKKMpcxiiFxp6') - ``` - - ```python - from protocol_api import ApiClient - - client = ApiClient(token) - - client.attachments.delete("Nc6yKKMpcxiiFxp6") - ``` - - ```php - $client = new \Protocol\ApiClient($token); - - $client->attachments->delete('Nc6yKKMpcxiiFxp6'); - ``` - - - - - diff --git a/apps/formbricks-com/app/docs/attributes/custom-attributes/page.mdx b/apps/formbricks-com/app/docs/attributes/custom-attributes/page.mdx new file mode 100644 index 0000000000..1e39033fe2 --- /dev/null +++ b/apps/formbricks-com/app/docs/attributes/custom-attributes/page.mdx @@ -0,0 +1,27 @@ +export const meta = { + title: "Setting attributes with code", + description: + "Set attributes in code using setAttribute function. Enhance user segmentation, target surveys effectively, and gather valuable insights for better decisions. All open-source.", +}; + +[Attributes]() + +# Setting attributes with code + +One way to send attributes to Formbricks is in your code. In Formbricks, there are two special attributes for [user identification](/docs/attributes/identify-users)(user ID & email) and custom attributes. An example: + +### Setting Custom User Attributes + +You can use the setAttribute function to set any custom attribute for the user (e.g. name, plan, etc.): + +```javascript +formbricks.setAttribute("Plan", "Pro"); +``` + +Generally speaking, the setAttribute function works like this: + +```javascript +formbricks.setAttribute("attribute_key", "attribute_value"); +``` + +Where `attributeName` is the name of the attribute you want to set, and `attributeValue` is the value of the attribute you want to set. diff --git a/apps/formbricks-com/app/docs/attributes/identify-users/page.mdx b/apps/formbricks-com/app/docs/attributes/identify-users/page.mdx new file mode 100644 index 0000000000..42cd5a0dec --- /dev/null +++ b/apps/formbricks-com/app/docs/attributes/identify-users/page.mdx @@ -0,0 +1,45 @@ +export const meta = { + title: "Identifying Users", + description: + "Identify users with Formbricks by setting User ID, email, and custom attributes. Enhance survey targeting and recontacting while maintaining user privacy.", +}; + +[Attributes]() + +# Identifying Users + +At Formbricks, we value user privacy. By default, Formbricks doesn't collect or store any personal information from your users. However, we understand that it can be helpful for you to know which user submitted the feedback and also functionality like recontacting users and controlling the waiting period between surveys requires identifying the users. That's why we provide a way for you to share existing user data from your app, so you can view it in our dashboard. + +Once the Formbricks widget is loaded on your web app, our SDK exposes methods for identifying user attributes. Let's set it up! + +## Setting User ID + +You can use the `setUserId` function to identify a user with any string. It's best to use the default identifier you use in your app (e.g. unique id from database) but you can also anonymize these as long as they are unique for every user. This function can be called multiple times with the same value safely and stores the identifier in local storage. We recommend you set the User ID whenever the user logs in to your website, as well as after the installation snippet (if the user is already logged in). + +```javascript +formbricks.setUserId("USER_ID"); +``` + +## Setting User Email + +You can use the setEmail function to set the user's email: + +```javascript +formbricks.setEmail("user@example.com"); +``` + +### Setting Custom User Attributes + +You can use the setAttribute function to set any custom attribute for the user (e.g. name, plan, etc.): + +```javascript +formbricks.setAttribute("attribute_key", "attribute_value"); +``` + +### Logging Out Users + +When a user logs out of your webpage, make sure to log them out of Formbricks as well. This will prevent new activity from being associated with an incorrect user. Use the logout function: + +```javascript +formbricks.logout(); +``` diff --git a/apps/formbricks-com/app/docs/attributes/why/page.mdx b/apps/formbricks-com/app/docs/attributes/why/page.mdx new file mode 100644 index 0000000000..d635ba0ecb --- /dev/null +++ b/apps/formbricks-com/app/docs/attributes/why/page.mdx @@ -0,0 +1,23 @@ +export const meta = { + title: "What are attributes and why are they useful?", + description: + "How to use attributes for user segmentation, enhancing survey targeting & results. Improve feedback quality and make data-driven decisions.", +}; + +[Attributes]() + +# What are attributes and why are they useful? + +Surveying your user base without segmentation leads to weak results and survey fatigue. Attributes help you segment your users into groups. + +## What are attributes? + +Attributes are key-value pairs that you can set for each person individually. For example, the attribute "Plan" can be set to "Free" or "Paid". + +## How do attributes work? + +Attributes are sent from your application to Formbricks and are associated with the current user. We store it in our database and allow you to use it the next time you create a survey. + +## Why are attributes useful? + +Attributes help show surveys to the right group of people. For example, you can show a survey to all users who have a "Plan" attribute set to "Paid". diff --git a/apps/formbricks-com/app/docs/authentication/page.mdx b/apps/formbricks-com/app/docs/authentication/page.mdx deleted file mode 100644 index e9e2f8e0f7..0000000000 --- a/apps/formbricks-com/app/docs/authentication/page.mdx +++ /dev/null @@ -1,44 +0,0 @@ -export const metadata = { - title: 'Authentication', - description: - 'In this guide, we’ll look at how authentication works. Protocol offers two ways to authenticate your API requests: Basic authentication and OAuth2 with a token.', -} - -# Authentication - -You'll need to authenticate your requests to access any of the endpoints in the Protocol API. In this guide, we'll look at how authentication works. Protocol offers two ways to authenticate your API requests: Basic authentication and OAuth2 with a token — OAuth2 is the recommended way. {{ className: 'lead' }} - -## Basic authentication - -With basic authentication, you use your username and password to authenticate your HTTP requests. Unless you have a very good reason, you probably shouldn't use basic auth. Here's how to authenticate using cURL: - -```bash {{ title: 'Example request with basic auth' }} -curl https://api.protocol.chat/v1/conversations \ - -u username:password -``` - -Please don't commit your Protocol password to GitHub! - -## OAuth2 with bearer token - -The recommended way to authenticate with the Protocol API is by using OAuth2. When establishing a connection using OAuth2, you will need your access token — you will find it in the [Protocol dashboard](#) under API settings. Here's how to add the token to the request header using cURL: - -```bash {{ title: 'Example request with bearer token' }} -curl https://api.protocol.chat/v1/conversations \ - -H "Authorization: Bearer {token}" -``` - -Always keep your token safe and reset it if you suspect it has been compromised. - -## Using an SDK - -If you use one of our official SDKs, you won't have to worry about any of the above — fetch your access token from the [Protocol dashboard](#) under API settings, and the client library will take care of the rest. All the client libraries use OAuth2 behind the scenes. - -
    -
    diff --git a/apps/formbricks-com/app/docs/best-practices/cancel-subscription/change-text.png b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/change-text.png new file mode 100644 index 0000000000..1908b5f5be Binary files /dev/null and b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/change-text.png differ diff --git a/apps/formbricks-com/app/docs/best-practices/cancel-subscription/create-cancel-flow.png b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/create-cancel-flow.png new file mode 100644 index 0000000000..7526a42b9a Binary files /dev/null and b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/create-cancel-flow.png differ diff --git a/apps/formbricks-com/app/docs/best-practices/cancel-subscription/page.mdx b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/page.mdx new file mode 100644 index 0000000000..416822080d --- /dev/null +++ b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/page.mdx @@ -0,0 +1,126 @@ +import Image from "next/image"; +import DemoPreview from "@/components/dummyUI/DemoPreview"; + +import CreateChurnFlow from "./create-cancel-flow.png"; +import ChangeText from "./change-text.png"; +import TriggerInnerText from "./trigger-inner-text.png"; +import TriggerCSS from "./trigger-css-selector.png"; +import TriggerPageUrl from "./trigger-page-url.png"; +import RecontactOptions from "./recontact-options.png"; +import PublishSurvey from "./publish-survey.png"; +import SelectAction from "./select-action.png"; + +export const meta = { + title: "Learn from Churn", + description: "To know how to decrease churn, you have to understand it. Use a micro-survey.", +}; + +[Best Practices]() + +# Learn from Churn + +Churn is hard, but can teach you a lot. Whenever a user decides that your product isn’t worth it anymore, you have a unique opportunity to get deep insights. These insights are pure gold to reduce churn. + +## Purpose + +The Churn Survey is among the most effective ways to identify weaknesses in you offering. People were willing to pay but now are not anymore: What changed? Let’s find out! + +## Preview + + + +## Formbricks Approach + +- Ask at exactly the right point in time +- Follow-up to prevent bad reviews +- Coming soon: Make survey mandatory + +## Overview + +To run the Churn Survey in your app you want to proceed as follows: + +1. Create new Churn Survey at [app.formbricks.com](http://app.formbricks.com/) +2. Set up the user action to display survey at right point in time +3. Choose correct recontact options to never miss a feedback +4. Prevent that churn! + + + ## Formbricks Widget running? {{ class: "text-white" }} + We assume that you have already installed the Formbricks Widget in your web app. It’s required to display messages + and surveys in your app. If not, please follow the [Quick Start Guide (takes 15mins max.)](/docs/getting-started/quickstart) + + +### 1. Create new Churn Survey + +If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup) + +Click on "Create Survey" and choose the template “Churn Survey”: + +Create churn survey by template + +### 2. Update questions (if you like) + +You’re free to update the question and answer options. However, based on our experience, we suggest giving the provided template a go 😊 + +Change text content + +_Want to change the button color? You can do so in the product settings._ + +Save, and move over to the “Audience” tab. + +### 3. Pre-segment your audience + +In this case, you don’t really need to pre-segment your audience. You likely want to ask everyone who hits the “Cancel subscription” button. + +### 4. Set up a trigger + +To create the trigger for your Churn Survey, you have two options to choose from: + +1. **Trigger by innerText:** You likely have a “Cancel Subscription” button in your app. You can setup a user Action with the according `innerText` to trigger the survey, like so: + +Set the trigger by inner Text + +2. **Trigger by CSS Selector:** In case you have more than one button saying “Cancel Subscription” in your app and only want to display the survey when one of them is clicked, you want to be more specific. The best way to do that is to give this button the HTML `id=“cancel-subscription”` and set your user action up like so: + +Set the trigger by CSS Selector + +3. **Trigger by pageURL:** Lastly, you could also display your survey on a subpage “/subscription-cancelled” where you forward users once they cancelled the trial subscription. You can then create a user Action with the type `pageURL` with the following settings: + +Set the trigger by page URL + +Whenever a user visits this page, matches the filter conditions above and the recontact options (below) the survey will be displayed ✅ + +Here is our complete [Actions manual](/docs/actions/why) covering [Code](/docs/actions/code) and [No-Code](/docs/actions/no-code) Actions. + + + ## Pre-churn flow coming soon {{ class: "text-white" }} + We’re currently building full-screen survey pop-ups. You’ll be able to prevent users from closing the survey + unless they respond to it. It’s certainly debatable if you want that but you could force them to click through + the survey before letting them cancel 🤷 + + +### 5. Select Action in the “When to ask” card + +Select feedback button action + +### 6. Last step: Set Recontact Options correctly + +Lastly, scroll down to “Recontact Options”. Here you have to choose the correct settings to make sure you milk these super valuable insights. You want to make sure that this survey is always displayed, no matter if the user has already seen a survey in the past days: + +Set recontact options + +These settings make sure the survey is always displayed, when a user wants to Cancel their subscription. + +### 7. Congrats! You’re ready to publish your survey 💃 + +Publish survey + + + ## Formbricks Widget running? {{ class: "text-white" }} + You need to have the Formbricks Widget installed to display the Churn Survey in your app. Please follow [this + tutorial (Step 4 onwards)](/docs/getting-started/quickstart) to install the widget. + + +### + +# Get those insights! 🎉 diff --git a/apps/formbricks-com/app/docs/best-practices/cancel-subscription/publish-survey.png b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/publish-survey.png new file mode 100644 index 0000000000..5d21ed596f Binary files /dev/null and b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/publish-survey.png differ diff --git a/apps/formbricks-com/app/docs/best-practices/cancel-subscription/recontact-options.png b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/recontact-options.png new file mode 100644 index 0000000000..5995f97fdd Binary files /dev/null and b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/recontact-options.png differ diff --git a/apps/formbricks-com/app/docs/best-practices/cancel-subscription/select-action.png b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/select-action.png new file mode 100644 index 0000000000..94cc8eb004 Binary files /dev/null and b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/select-action.png differ diff --git a/apps/formbricks-com/app/docs/best-practices/cancel-subscription/trigger-css-selector.png b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/trigger-css-selector.png new file mode 100644 index 0000000000..806170f0d3 Binary files /dev/null and b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/trigger-css-selector.png differ diff --git a/apps/formbricks-com/app/docs/best-practices/cancel-subscription/trigger-inner-text.png b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/trigger-inner-text.png new file mode 100644 index 0000000000..103d0764fc Binary files /dev/null and b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/trigger-inner-text.png differ diff --git a/apps/formbricks-com/app/docs/best-practices/cancel-subscription/trigger-page-url.png b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/trigger-page-url.png new file mode 100644 index 0000000000..c66b870928 Binary files /dev/null and b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/trigger-page-url.png differ diff --git a/apps/formbricks-com/app/docs/best-practices/docs-feedback/add-action.png b/apps/formbricks-com/app/docs/best-practices/docs-feedback/add-action.png new file mode 100644 index 0000000000..7ebb5853f7 Binary files /dev/null and b/apps/formbricks-com/app/docs/best-practices/docs-feedback/add-action.png differ diff --git a/apps/formbricks-com/app/docs/best-practices/docs-feedback/change-id.png b/apps/formbricks-com/app/docs/best-practices/docs-feedback/change-id.png new file mode 100644 index 0000000000..fc83c4ef02 Binary files /dev/null and b/apps/formbricks-com/app/docs/best-practices/docs-feedback/change-id.png differ diff --git a/apps/formbricks-com/app/docs/best-practices/docs-feedback/copy-ids.png b/apps/formbricks-com/app/docs/best-practices/docs-feedback/copy-ids.png new file mode 100644 index 0000000000..f2e8481112 Binary files /dev/null and b/apps/formbricks-com/app/docs/best-practices/docs-feedback/copy-ids.png differ diff --git a/apps/formbricks-com/app/docs/best-practices/docs-feedback/docs-navi.png b/apps/formbricks-com/app/docs/best-practices/docs-feedback/docs-navi.png new file mode 100644 index 0000000000..df5b0a319f Binary files /dev/null and b/apps/formbricks-com/app/docs/best-practices/docs-feedback/docs-navi.png differ diff --git a/apps/formbricks-com/app/docs/best-practices/docs-feedback/docs-template.png b/apps/formbricks-com/app/docs/best-practices/docs-feedback/docs-template.png new file mode 100644 index 0000000000..c689d8f440 Binary files /dev/null and b/apps/formbricks-com/app/docs/best-practices/docs-feedback/docs-template.png differ diff --git a/apps/formbricks-com/app/docs/best-practices/docs-feedback/page.mdx b/apps/formbricks-com/app/docs/best-practices/docs-feedback/page.mdx new file mode 100644 index 0000000000..f4b21a8c9e --- /dev/null +++ b/apps/formbricks-com/app/docs/best-practices/docs-feedback/page.mdx @@ -0,0 +1,386 @@ +import Image from "next/image"; + +import DocsFeedback from "@/components/docs/DocsFeedback"; +import AddAction from "./add-action.png"; +import ChangeId from "./change-id.png"; +import DocsNavi from "./docs-navi.png"; +import DocsTemplate from "./docs-template.png"; +import SelectNonevent from "./select-nonevent.png"; +import SwitchToDev from "./switch-to-dev.png"; +import WhenToAsk from "./when-to-ask.png"; +import CopyIds from "./copy-ids.png"; + +export const meta = { + title: "Docs Feedback", + description: "Docs Feedback allows you to measure how clear your documentation is.", +}; + +[Best Practices]() + +# Docs Feedback + +Docs Feedback allows you to measure how clear your documentation is. + +## Purpose + +Unlike yourself, your users don't spend 5-7 days per week thinking about your product. To fight the "Curse of Knowledge" you have to measure how clear your docs are. + +## Preview + + + +## Installation + +To get this running, you'll need a bit of time. Here are the steps we're going through: + +1. Set up Formbricks Cloud +2. Build the frontend +3. Connect to API +4. Test + +### 1. Setting up Formbricks Cloud + +1. To get started, create an account for the [Formbricks Cloud](https://app.formbricks.com/auth/signup). + +2. In the Menu (top right) you see that you can switch between a “Development” and a “Production” environment. These are two separate environments so your test data doesn’t mess up the insights from prod. Switch to “Development”: + +switch to dev environment + +3. Then, create a survey using the template “Docs Feedback”: + +select docs template + +4. Change the Internal Question ID of the first question to **“isHelpful”** to make your life easier 😉 + +switch to dev environment + +5. In the same way, you can change the Internal Question ID of the _Please elaborate_ question to **“additionalFeedback”** and the one of the _Page URL_ question to **“pageUrl”**. + + + ## Answers need to be identical {{ class: "text-white" }} + If you want different answers than “Yes 👍” and “No 👎” you need update the choices accordingly. They have to + be identical to the frontend we're building in the next step. + + +6. Click on “Continue to Settings or select the audience tab manually. Scroll down to “When to ask” and create a new Action: + +set up when to ask card + +7. Our goal is to create an event that never fires. This is a bit nonsensical because it is a workaround. Stick with me 😃 Fill the action out like on the screenshot: + +add action + +8. Select the Non-Event in the dropdown. Now you see that the “Publish survey” button is active. Publish your survey 🤝 + +select nonevent + +**You’re all setup in Formbricks Cloud for now 👍** + +### 2. Build the frontend + + + ## Your frontend might work differently {{ class: "text-white" }} + Your frontend likely looks and works differently. This is an example specific to our tech stack. We want to illustrate + what you should consider building yours 😊 + + +Before we start, lets talk about the widget. It works like this: + +- Once the user selects yes/no, a partial response is sent to the Formbricks API. It includes the feedback and the current page url. +- Then the user is presented with an additional open text field to further explain their choice. Once it's submitted, the previous response is updated with the additional feedback. + +This allows us to capture and analyze partial feedback where the user is not willing to provide additional information. + +**Let's do this 👇** + +1. Open the code editor where you handle your docs page. + +2. Likely, you have a template file or similar which renders the navigation at the bottom of the page: + +doc navigation + +Locate that file. We are using the [Tailwind Template “Syntax”](https://tailwindui.com/templates/syntax) for our docs. Here is our [Layout.tsx](https://github.com/formbricks/formbricks/blob/main/apps/formbricks-com/components/docs/Layout.tsx) file. + +3. Write the frontend code for the widget. Here is the full component (we break it down right below): + +```tsx +import { useState } from "react"; +import { handleFeedbackSubmit, updateFeedback } from "../../lib/handleFeedbackSubmit"; +import { Popover, PopoverTrigger, PopoverContent, Button } from "@formbricks/ui"; +import { useRouter } from "next/router"; + +export default function DocsFeedback() { + const router = useRouter(); + const [isOpen, setIsOpen] = useState(false); + const [sharedFeedback, setSharedFeedback] = useState(false); + const [responseId, setResponseId] = useState(null); + const [freeText, setFreeText] = useState(""); + + if ( + !process.env.NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID || + !process.env.NEXT_PUBLIC_FORMBRICKS_COM_API_HOST || + !process.env.NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID + ) { + return null; + } + + return ( +
    + {!sharedFeedback ? ( +
    + Was this page helpful? + +
    + {["Yes 👍", " No 👎"].map((option) => ( + { + const id = await handleFeedbackSubmit(option, router.asPath); + setResponseId(id); + }}> + {option} + + ))} +
    + +
    +