mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-04 11:30:38 -05:00
Introducing the new Formbricks (#210)
### New Formbricks Release: Complete Rewrite, New Features & Enhanced UI 🚀 We're thrilled to announce the release of the new Formbricks, a complete overhaul of our codebase, packed with powerful features and an improved user experience. #### What's New: 1. **Survey Builder**: Design and customize your in-product micro-surveys with our intuitive survey builder. 2. **Trigger Micro-Surveys**: Set up micro-surveys to appear at specific points within your app, allowing you to gather feedback when it matters most. 3. **JavaScript SDK**: Our new JavaScript SDK makes integration a breeze - just embed it once and you're ready to go. 4. **No-Code Events**: Set up events and triggers without writing a single line of code, making it accessible for everyone on your team. 5. **Revamped UI**: Enjoy an entirely new user interface that enhances usability and provides a smooth, delightful experience. This release marks a major step forward for Formbricks, enabling you to better understand your users and build an outstanding product experience. Please update your Formbricks integration to take advantage of these exciting new features, and let us know in the Discord if you have any questions or feedback! Happy surveying! 🎉
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
import { useState, ChangeEvent } from "react";
|
||||
import { ChevronDownIcon, ChevronRightIcon, ChevronLeftIcon } from "@heroicons/react/20/solid";
|
||||
import { ChevronDownIcon, ChevronRightIcon, ChevronLeftIcon } from "@heroicons/react/24/solid";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface APICallProps {
|
||||
method: "GET" | "POST";
|
||||
url: string;
|
||||
description: string;
|
||||
queries: {
|
||||
headers: {
|
||||
label: string;
|
||||
type: string;
|
||||
description: string;
|
||||
@@ -26,7 +26,7 @@ interface APICallProps {
|
||||
example?: string;
|
||||
}
|
||||
|
||||
export function APILayout({ method, url, description, queries, bodies, responses, example }: APICallProps) {
|
||||
export function APILayout({ method, url, description, headers, bodies, responses, example }: APICallProps) {
|
||||
const [switchState, setSwitchState] = useState(true);
|
||||
function handleOnChange() {
|
||||
setSwitchState(!switchState);
|
||||
@@ -64,14 +64,17 @@ export function APILayout({ method, url, description, queries, bodies, responses
|
||||
<div className={clsx(switchState ? "block" : "hidden", "ml-8")}>
|
||||
<p className="mt-6 mb-2 text-lg font-semibold">Parameters</p>
|
||||
<div>
|
||||
<div className="text-base">
|
||||
<p className="not-prose -mb-1 pt-2 font-bold">Query</p>
|
||||
<div>
|
||||
{queries.map((q) => (
|
||||
<Parameter key={q.label} label={q.label} type={q.type} description={q.description} />
|
||||
))}
|
||||
{headers.length > 0 && (
|
||||
<div className="text-base">
|
||||
<p className="not-prose -mb-1 pt-2 font-bold">Headers</p>
|
||||
<div>
|
||||
{headers.map((q) => (
|
||||
<Parameter key={q.label} label={q.label} type={q.type} description={q.description} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 text-base">
|
||||
<p className="not-prose -mb-1 pt-2 font-bold">Body</p>
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// components/ui/CodeBlock.tsx
|
||||
import Prism from "prismjs";
|
||||
import "prismjs/themes/prism.css";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
interface CodeBlockProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const CodeBlock: React.FC<CodeBlockProps> = ({ children }) => {
|
||||
useEffect(() => {
|
||||
Prism.highlightAll();
|
||||
}, [children]);
|
||||
|
||||
return (
|
||||
<div className="group relative mt-4 rounded-md text-sm font-light text-slate-200 sm:text-base">
|
||||
<pre>
|
||||
<code className="language-js">{children}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeBlock;
|
||||
@@ -1,92 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import { usePlausible } from "next-plausible";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
formbricks: any;
|
||||
}
|
||||
}
|
||||
|
||||
export function FeedbackButton() {
|
||||
const plausible = usePlausible();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const feedbackRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Close the feedback form if the user clicks outside of it
|
||||
function handleClickOutside(event: any) {
|
||||
if (feedbackRef.current && !feedbackRef.current.contains(event.target)) {
|
||||
if (isOpen) {
|
||||
setIsOpen(false);
|
||||
if (window) {
|
||||
window.formbricks.clean();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Bind the event listener
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
// Unbind the event listener on clean up
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [feedbackRef, isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
window.formbricks = {
|
||||
...window.formbricks,
|
||||
config: {
|
||||
hqUrl: process.env.NEXT_PUBLIC_FORMBRICKS_URL,
|
||||
formId: process.env.NEXT_PUBLIC_FORMBRICKS_FORM_ID,
|
||||
containerId: "formbricks-feedback-wrapper",
|
||||
contact: {
|
||||
name: "Matti",
|
||||
position: "Co-Founder",
|
||||
imgUrl: "https://avatars.githubusercontent.com/u/675065?s=128&v=4",
|
||||
},
|
||||
},
|
||||
};
|
||||
// @ts-ignore
|
||||
import("@formbricks/feedback");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={clsx(
|
||||
"xs:flex-row xs:right-0 xs:top-1/2 xs:w-[18rem] xs:-translate-y-1/2 fixed bottom-0 z-50 h-[22rem] w-full flex-1 transition-all duration-500 ease-in-out",
|
||||
isOpen ? "xs:-translate-x-0 translate-y-0" : "xs:translate-x-full xs:-mr-1 translate-y-full"
|
||||
)}>
|
||||
<div
|
||||
className="xs:flex-row flex h-full flex-col"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
ref={feedbackRef}>
|
||||
<button
|
||||
className="xs:-rotate-90 xs:top-1/2 xs:-left-[5.75rem] xs:-translate-y-1/2 xs:-translate-x-0 xs:w-32 xs:p-4 bg-brand-dark absolute left-1/2 w-28 -translate-x-1/2 -translate-y-full rounded-t-lg p-3 font-medium text-white"
|
||||
onClick={() => {
|
||||
if (!isOpen) {
|
||||
plausible("openFeedback");
|
||||
if (window) {
|
||||
window.formbricks.render();
|
||||
window.formbricks.resetForm();
|
||||
}
|
||||
} else {
|
||||
if (window) {
|
||||
window.formbricks.clean();
|
||||
}
|
||||
}
|
||||
setIsOpen(!isOpen);
|
||||
}}>
|
||||
{isOpen ? "Close" : "Feedback"}
|
||||
</button>
|
||||
<div
|
||||
className="xs:rounded-bl-lg xs:rounded-tr-none h-full w-full overflow-hidden rounded-bl-none rounded-tr-lg rounded-tl-lg bg-slate-50 shadow-lg"
|
||||
id="formbricks-feedback-wrapper"></div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { FooterLogo } from "./Logo";
|
||||
const navigation = {
|
||||
/* creation: [
|
||||
{ name: "React Form Builder", href: "/react-form-library", status: true },
|
||||
{ name: "No Code Builder", href: "/visual-builder", status: false },
|
||||
{ name: "No-Code Builder", href: "/visual-builder", status: false },
|
||||
{ name: "Templates", href: "#", status: false },
|
||||
],
|
||||
pipelines: [
|
||||
|
||||
@@ -35,6 +35,11 @@ export default function Header() {
|
||||
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
|
||||
Blog <p className="bg-brand inline rounded-full px-2 text-xs text-white">1</p>
|
||||
</Link>
|
||||
<Link
|
||||
href="/docs"
|
||||
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
|
||||
Docs
|
||||
</Link>
|
||||
</Popover.Group>
|
||||
<div className="hidden flex-1 items-center justify-end md:flex">
|
||||
<ThemeSelector className="relative z-10 mr-5" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/solid";
|
||||
import {
|
||||
Bars3Icon,
|
||||
BoltIcon,
|
||||
@@ -30,7 +30,7 @@ const creation = [
|
||||
status: true,
|
||||
},
|
||||
{
|
||||
name: "No Code Builder",
|
||||
name: "No-Code Builder",
|
||||
description: "Notion-like visual builder",
|
||||
href: "/visual-builder",
|
||||
icon: CursorArrowRaysIcon,
|
||||
|
||||
@@ -17,26 +17,26 @@ const BestPractices = [
|
||||
title: "Onboarding Segmentation",
|
||||
description:
|
||||
"Get to know your users right from the start. Ask a few questions early, let us enrich the profile.",
|
||||
category: "In-Moment",
|
||||
category: "Boost Retention",
|
||||
icon: OnboardingIcon,
|
||||
},
|
||||
{
|
||||
title: "Product-Market Fit Survey",
|
||||
description: "Find out how disappointed people would be if they could not use your service any more.",
|
||||
category: "In-Moment",
|
||||
category: "Boost Retention",
|
||||
icon: PMFIcon,
|
||||
href: "/pmf",
|
||||
},
|
||||
{
|
||||
title: "Feature Chaser",
|
||||
description: "Show a survey about a new feature shown only to people who used it.",
|
||||
category: "In-Moment",
|
||||
category: "Boost Retention",
|
||||
icon: DogChaserIcon,
|
||||
},
|
||||
{
|
||||
title: "Cancel Subscription Flow",
|
||||
description: "Request users going through a cancel subscription flow before cancelling.",
|
||||
category: "In-Moment",
|
||||
category: "Boost Retention",
|
||||
icon: CancelSubscriptionIcon,
|
||||
},
|
||||
{
|
||||
@@ -57,29 +57,18 @@ const BestPractices = [
|
||||
category: "Retain Users",
|
||||
icon: FeedbackIcon,
|
||||
},
|
||||
{
|
||||
title: "Bug Report Form",
|
||||
description: "Catch all bugs in your SaaS with easy and accessible bug reports.",
|
||||
category: "Retain Users",
|
||||
icon: BugBlueIcon,
|
||||
},
|
||||
|
||||
{
|
||||
title: "Rage Click Survey",
|
||||
description: "Sometimes things don’t work. Trigger this rage click survey to catch users in rage.",
|
||||
category: "Retain Users",
|
||||
icon: AngryBirdRageIcon,
|
||||
},
|
||||
{
|
||||
title: "Feature Request Widget",
|
||||
description: "Allow users to request features and pipe it to GitHub projects or Linear.",
|
||||
category: "Retain Users",
|
||||
icon: FeatureRequestIcon,
|
||||
},
|
||||
];
|
||||
|
||||
export default function InsightOppos() {
|
||||
return (
|
||||
<div className="pt-12 pb-10 md:pt-40">
|
||||
<div className="pt-12 pb-10 md:pt-20">
|
||||
<div className="px-4 py-20 text-center sm:px-6 lg:px-8" id="best-practices">
|
||||
<h1 className="text-3xl font-bold tracking-tight text-slate-800 dark:text-slate-200 sm:text-4xl md:text-5xl">
|
||||
Get started with{" "}
|
||||
@@ -88,7 +77,7 @@ export default function InsightOppos() {
|
||||
</span>
|
||||
</h1>
|
||||
<p className="mx-auto mt-3 max-w-md text-base text-slate-500 dark:text-slate-300 sm:text-lg md:mt-5 md:max-w-3xl md:text-xl">
|
||||
Proven templates for qualitative user research.
|
||||
Run battle-tested approaches for qualitative user research in minutes.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
@@ -96,13 +85,13 @@ export default function InsightOppos() {
|
||||
{BestPractices.map((bestPractice) => (
|
||||
<div
|
||||
key={bestPractice.title}
|
||||
className="drop-shadow-card duration-120 relative cursor-default rounded-lg bg-slate-100 p-8 transition-all ease-in-out hover:scale-105 dark:bg-slate-800">
|
||||
className="drop-shadow-card duration-120 relative cursor-pointer rounded-lg bg-slate-100 p-8 transition-all ease-in-out hover:scale-105 dark:bg-slate-800">
|
||||
<div
|
||||
className={clsx(
|
||||
// base styles independent what type of button it is
|
||||
"absolute right-10 rounded-full py-1 px-3",
|
||||
// different styles depending on size
|
||||
bestPractice.category === "In-Moment" &&
|
||||
// different styles depending on type
|
||||
bestPractice.category === "Boost Retention" &&
|
||||
"bg-pink-100 text-pink-500 dark:bg-pink-800 dark:text-pink-200",
|
||||
bestPractice.category === "Exploration" &&
|
||||
"bg-blue-100 text-blue-500 dark:bg-blue-800 dark:text-blue-200",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { FeedbackButton } from "@/components/shared/FeedbackButton";
|
||||
import Footer from "./Footer";
|
||||
import Header from "./Header";
|
||||
import MetaInformation from "./MetaInformation";
|
||||
@@ -14,7 +13,6 @@ export default function Layout({ title, description, children }: LayoutProps) {
|
||||
<div className="flex h-screen flex-col justify-between">
|
||||
<MetaInformation title={title} description={description} />
|
||||
<Header />
|
||||
<FeedbackButton />
|
||||
{
|
||||
<main className="max-w-8xl relative mx-auto mb-auto flex w-full flex-col justify-center sm:px-2 lg:px-8 xl:px-12">
|
||||
{children}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { FeedbackButton } from "@/components/shared/FeedbackButton";
|
||||
import Footer from "./Footer";
|
||||
import Header from "./Header";
|
||||
import MetaInformation from "./MetaInformation";
|
||||
@@ -17,7 +16,6 @@ export default function LayoutMdx({ meta, children }: Props) {
|
||||
<div className="flex h-screen flex-col justify-between">
|
||||
<MetaInformation title={meta.title} description={meta.description} />
|
||||
<Header />
|
||||
<FeedbackButton />
|
||||
<main className="min-w-0 max-w-2xl flex-auto px-4 lg:max-w-none lg:pr-0 lg:pl-8 xl:px-16">
|
||||
<article className="mx-auto my-16 max-w-3xl px-2">
|
||||
{meta.title && (
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||
import { Fragment } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
type Modal = {
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
noPadding?: boolean;
|
||||
closeOnOutsideClick?: boolean;
|
||||
};
|
||||
|
||||
const Modal: React.FC<Modal> = ({
|
||||
open,
|
||||
setOpen,
|
||||
children,
|
||||
title,
|
||||
noPadding,
|
||||
closeOnOutsideClick = true,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Transition.Root show={open} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={() => closeOnOutsideClick && setOpen(false)}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0">
|
||||
<div className="fixed inset-0 bg-slate-500 bg-opacity-30 backdrop-blur-md transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
||||
<Dialog.Panel
|
||||
className={clsx(
|
||||
"relative transform rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-xl ",
|
||||
`${noPadding ? "" : "px-4 pt-5 pb-4 sm:p-6"}`
|
||||
)}>
|
||||
<div className="absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md bg-white text-slate-400 hover:text-slate-500 focus:outline-none focus:ring-0 focus:ring-offset-2"
|
||||
onClick={() => setOpen(false)}>
|
||||
<span className="sr-only">Close</span>
|
||||
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{title && <h3 className="mb-4 text-xl font-bold text-slate-500">{title}</h3>}
|
||||
|
||||
{children}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
Reference in New Issue
Block a user