remove old formbricks-com

This commit is contained in:
Matthias Nannt
2023-08-22 14:38:52 +02:00
parent 80c64f2bf5
commit 8c229e4685
213 changed files with 200 additions and 10548 deletions

View File

@@ -1,6 +0,0 @@
NEXT_PUBLIC_FORMBRICKS_COM_API_HOST=http://localhost:3000
NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID=
NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID=
# Strapi API Key
STRAPI_API_KEY=

View File

@@ -1,4 +0,0 @@
module.exports = {
root: true,
extends: ["formbricks"],
};

View File

@@ -1,37 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
public/sitemap*.xml

View File

@@ -1,73 +0,0 @@
import { Button, Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui";
import { useRouter } from "next/router";
import { useState } from "react";
import { handleFeedbackSubmit, updateFeedback } from "../../lib/handleFeedbackSubmit";
export const DocsFeedback: React.FC = () => {
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 (
<div className="mt-6 inline-flex cursor-default items-center rounded-md border border-slate-200 bg-white p-4 text-slate-800 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">
{!sharedFeedback ? (
<div className="text-center md:text-left">
Is everything on this page clear?
<Popover open={isOpen} onOpenChange={setIsOpen}>
<div className="mt-2 inline-flex space-x-3 md:ml-4 md:mt-0">
{["Yes 👍", "No 👎"].map((option) => (
<PopoverTrigger
key={option}
className="rounded border border-slate-200 bg-slate-50 px-4 py-2 text-slate-900 hover:bg-slate-100 hover:text-slate-600 focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1 dark:border-slate-700 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600 dark:hover:text-slate-300"
onClick={async () => {
const id = await handleFeedbackSubmit(option, router.asPath);
setResponseId(id);
}}>
{option}
</PopoverTrigger>
))}
</div>
<PopoverContent className="border-slate-300 bg-white dark:border-slate-500 dark:bg-slate-700">
<form>
<textarea
value={freeText}
onChange={(e) => setFreeText(e.target.value)}
placeholder="Please explain why..."
className="focus:border-brand-dark focus:ring-brand-dark mb-2 w-full rounded-md bg-white text-sm text-slate-900 dark:bg-slate-600 dark:text-slate-200 dark:placeholder:text-slate-200"
/>
<div className="text-right">
<Button
type="submit"
variant="primary"
onClick={(e) => {
e.preventDefault();
updateFeedback(freeText, responseId);
setIsOpen(false);
setFreeText("");
setSharedFeedback(true);
}}>
Send
</Button>
</div>
</form>
</PopoverContent>
</Popover>
</div>
) : (
<div>Thanks a lot, boss! 🤝</div>
)}
</div>
);
};
export default DocsFeedback;

View File

@@ -1,13 +0,0 @@
import { Button } from "@formbricks/ui";
declare global {
interface Window {
formbricks: any;
}
}
export const FeedbackButton: React.FC = () => {
return <Button variant="secondary">Open Feedback</Button>;
};
export default FeedbackButton;

View File

@@ -1,221 +0,0 @@
import { FooterLogo } from "@/components/shared/Logo";
import { MobileNavigation } from "@/components/shared/MobileNavigation";
import { Navigation } from "@/components/shared/Navigation";
import { Prose } from "@/components/shared/Prose";
import { Search } from "@/components/shared/Search";
import { ThemeSelector } from "@/components/shared/ThemeSelector";
import navigation from "@/lib/docsNavigation";
import { Button } from "@formbricks/ui";
import clsx from "clsx";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react";
import MetaInformation from "../shared/MetaInformation";
import DocsFeedback from "./DocsFeedback";
function GitHubIcon(props: any) {
return (
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
<path d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z" />
</svg>
);
}
function Header({ navigation }: any) {
let [isScrolled, setIsScrolled] = useState(false);
useEffect(() => {
function onScroll() {
setIsScrolled(window.scrollY > 0);
}
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => {
window.removeEventListener("scroll", onScroll);
};
}, []);
return (
<header
className={clsx(
"sticky top-0 z-50 flex flex-wrap items-center justify-between bg-slate-100 px-4 py-5 shadow-md shadow-slate-900/5 transition duration-500 dark:shadow-none sm:px-6 lg:px-8",
isScrolled
? "bg-slate-100/90 backdrop-blur dark:bg-slate-900/90 [@supports(backdrop-filter:blur(0))]:bg-slate-100/75 dark:[@supports(backdrop-filter:blur(0))]:bg-slate-900/75"
: "dark:bg-transparent"
)}>
<div className="mr-6 flex lg:hidden">
<MobileNavigation navigation={navigation} />
</div>
<div className="relative flex flex-grow basis-0 items-center">
<Link href="/" aria-label="Home page">
<FooterLogo className="h-8 w-auto sm:h-10" />
</Link>
</div>
<div className="-my-5 mr-6 sm:mr-8 md:mr-0">
<Search />
</div>
<div className="hidden items-center justify-end md:flex md:flex-1 lg:w-0">
<ThemeSelector className="relative z-10 mr-5" />
<Button
variant="secondary"
EndIcon={GitHubIcon}
endIconClassName="fill-slate-800 dark:fill-slate-200 ml-2"
href="https://github.com/formbricks/formbricks"
target="_blank">
Star us on Github
</Button>
<Button
variant="highlight"
className="ml-2"
href="https://app.formbricks.com/auth/signup"
target="_blank">
Get started
</Button>
</div>
</header>
);
}
interface LayoutProps {
children: React.ReactNode;
meta: {
title: string;
description?: string;
};
}
export const Layout: React.FC<LayoutProps> = ({ children, meta }) => {
let router = useRouter();
let allLinks = navigation.flatMap((section) => section.links);
let linkIndex = allLinks.findIndex((link) => link.href === router.pathname);
let previousPage = allLinks[linkIndex - 1];
let nextPage = allLinks[linkIndex + 1];
let section = navigation.find((section) => section.links.find((link) => link.href === router.pathname));
const linkRef = useRef<HTMLLIElement>(null);
const parentRef = useRef<HTMLDivElement>(null);
const preserveScroll = () => {
const scroll = Math.abs(linkRef.current.getBoundingClientRect().top - linkRef.current.offsetTop);
sessionStorage.setItem("scrollPosition", (scroll + 89).toString());
};
const useExternalLinks = (selector: string) => {
useEffect(() => {
const links = document.querySelectorAll(selector);
links.forEach((link) => {
link.setAttribute("target", "_blank");
link.setAttribute("rel", "noopener noreferrer");
});
return () => {
links.forEach((link) => {
link.removeAttribute("target");
link.removeAttribute("rel");
});
};
}, [selector]);
};
useExternalLinks(".prose a");
useEffect(() => {
if (parentRef.current) {
const scrollPosition = Number.parseInt(sessionStorage.getItem("scrollPosition"), 10);
if (scrollPosition) {
parentRef.current.scrollTop = scrollPosition;
}
}
}, []);
return (
<>
<MetaInformation
title={`Formbricks Docs | ${meta.title}`}
description={
meta.description ? meta.description : "Open-source Experience Management for Digital Products."
}
/>
<Header navigation={navigation} />
<div className="max-w-8xl relative mx-auto flex justify-center sm:px-2 lg:px-8 xl:px-12">
<div className="hidden lg:relative lg:block lg:flex-none">
<div className="absolute inset-y-0 right-0 w-[50vw] bg-slate-50 dark:hidden" />
<div className="absolute bottom-0 right-0 top-16 hidden h-12 w-px bg-gradient-to-t from-slate-800 dark:block" />
<div className="absolute bottom-0 right-0 top-28 hidden w-px bg-slate-800 dark:block" />
<div
className="sticky top-[4.5rem] -ml-0.5 h-[calc(100vh-4.5rem)] overflow-y-auto overflow-x-hidden py-16 pl-0.5"
ref={parentRef}>
<Navigation
navigation={navigation}
preserveScroll={preserveScroll}
linkRef={linkRef}
className="w-64 pr-8 xl:w-72 xl:pr-16"
/>
</div>
</div>
<div className="min-w-0 max-w-2xl flex-auto px-4 py-16 lg:max-w-none lg:pl-8 lg:pr-0 xl:px-16">
<article>
{(meta.title || section) && (
<header className="mb-9 space-y-1">
{section && (
<p className="font-display text-brand-light dark:text-brand-dark text-sm font-medium">
{section.title}
</p>
)}
{meta.title && (
<h1 className="font-display text-3xl tracking-tight text-slate-800 dark:text-slate-100">
{meta.title}
</h1>
)}
</header>
)}
<Prose className="">{children}</Prose>
</article>
<DocsFeedback />
<dl className="mt-12 flex border-t border-slate-200 pt-6 dark:border-slate-800">
{previousPage && (
<div>
<dt className="font-display text-brand-dark dark:text-brand-light text-sm font-medium">
Previous
</dt>
<dd className="mt-1">
<Link
href={previousPage.href}
className="text-base font-semibold text-slate-500 hover:text-slate-600 dark:text-slate-400 dark:hover:text-slate-300">
<span aria-hidden="true">&larr;</span> {previousPage.title}
</Link>
</dd>
</div>
)}
{nextPage && (
<div className="ml-auto text-right">
<dt className="font-display text-brand-dark dark:text-brand-light text-sm font-medium">
Next
</dt>
<dd className="mt-1">
<Link
href={nextPage.href}
className="text-base font-semibold text-slate-500 hover:text-slate-600 dark:text-slate-400 dark:hover:text-slate-300">
{nextPage.title} <span aria-hidden="true">&rarr;</span>
</Link>
</dd>
</div>
)}
</dl>
<div className="mt-16 rounded-xl border-2 border-slate-200 bg-slate-300 p-8 dark:border-slate-700/50 dark:bg-slate-800">
<h4 className="text-3xl font-semibold text-slate-500 dark:text-slate-50">Need help? 🤓</h4>
<p className="my-4 text-slate-500 dark:text-slate-400">
Join our Discord and ask away. We&apos;re happy to help where we can!
</p>
<Button variant="highlight" href="/discord" target="_blank">
Join Discord
</Button>
</div>
</div>
</div>
</>
);
};

View File

@@ -1,69 +0,0 @@
import { useEffect, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { Dialog } from "@headlessui/react";
import { Logomark } from "@/components/shared/Logo";
import { Navigation } from "@/components/shared/Navigation";
function MenuIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" strokeWidth="2" strokeLinecap="round" {...props}>
<path d="M4 7h16M4 12h16M4 17h16" />
</svg>
);
}
function CloseIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" strokeWidth="2" strokeLinecap="round" {...props}>
<path d="M5 5l14 14M19 5l-14 14" />
</svg>
);
}
export function MobileNavigation({ navigation }) {
let router = useRouter();
let [isOpen, setIsOpen] = useState(false);
useEffect(() => {
if (!isOpen) return;
function onRouteChange() {
setIsOpen(false);
}
router.events.on("routeChangeComplete", onRouteChange);
router.events.on("routeChangeError", onRouteChange);
return () => {
router.events.off("routeChangeComplete", onRouteChange);
router.events.off("routeChangeError", onRouteChange);
};
}, [router, isOpen]);
return (
<>
<button type="button" onClick={() => setIsOpen(true)} className="relative" aria-label="Open navigation">
<MenuIcon className="h-6 w-6 stroke-slate-500" />
</button>
<Dialog
open={isOpen}
onClose={setIsOpen}
className="fixed inset-0 z-50 flex items-start overflow-y-auto bg-slate-900/50 pr-10 backdrop-blur lg:hidden"
aria-label="Navigation">
<Dialog.Panel className="min-h-full w-full max-w-xs bg-white px-4 pt-5 pb-12 dark:bg-slate-900 sm:px-6">
<div className="flex items-center">
<button type="button" onClick={() => setIsOpen(false)} aria-label="Close navigation">
<CloseIcon className="h-6 w-6 stroke-slate-500" />
</button>
<Link href="/" className="ml-6" aria-label="Home page">
<Logomark className="h-9 w-9" />
</Link>
</div>
<Navigation navigation={navigation} className="mt-5 px-1" />
</Dialog.Panel>
</Dialog>
</>
);
}

View File

@@ -1,74 +0,0 @@
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui";
import { PlusIcon, TrashIcon } from "@heroicons/react/24/solid";
import { useState } from "react";
const DummyUI: React.FC = () => {
const eventClasses = [
{ id: "1", name: "View Dashboard" },
{ id: "2", name: "Upgrade to Pro" },
{ id: "3", name: "Cancel Plan" },
];
const [triggers, setTriggers] = useState<string[]>([eventClasses[0].id]);
const setTriggerEvent = (index: number, eventClassId: string) => {
setTriggers((prevTriggers) =>
prevTriggers.map((trigger, idx) => (idx === index ? eventClassId : trigger))
);
};
const addTriggerEvent = () => {
setTriggers((prevTriggers) => [...prevTriggers, eventClasses[0].id]);
};
const removeTriggerEvent = (index: number) => {
setTriggers((prevTriggers) => prevTriggers.filter((_, idx) => idx !== index));
};
return (
<>
{triggers.map((triggerEventClassId, idx) => (
<div className="mt-2" key={idx}>
<div className="inline-flex items-center">
<p className="mr-2 w-14 text-right text-sm text-slate-800 dark:text-slate-300">
{idx === 0 ? "When" : "or"}
</p>
<Select
value={triggerEventClassId}
onValueChange={(eventClassId) => setTriggerEvent(idx, eventClassId)}>
<SelectTrigger className="w-[180px] text-slate-800 dark:border-slate-400 dark:bg-slate-700 dark:text-slate-300">
<SelectValue className="" />
</SelectTrigger>
<SelectContent>
{eventClasses.map((eventClass) => (
<SelectItem
key={eventClass.id}
className="px-0.5 py-1 text-slate-800 dark:bg-slate-700 dark:text-slate-300 dark:ring-slate-700"
value={eventClass.id}>
{eventClass.name}
</SelectItem>
))}
</SelectContent>
</Select>
<button onClick={() => removeTriggerEvent(idx)}>
<TrashIcon className="ml-3 h-4 w-4 text-slate-400" />
</button>
</div>
</div>
))}
<div className="p-3">
<Button
variant="secondary"
className="dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600"
onClick={() => {
addTriggerEvent();
}}>
<PlusIcon className="mr-2 h-4 w-4" />
Add event
</Button>
</div>
</>
);
};
export default DummyUI;

View File

@@ -1,133 +0,0 @@
import {
Button,
Input,
Label,
RadioGroup,
RadioGroupItem,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@formbricks/ui";
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
import Modal from "../shared/Modal";
interface EventDetailModalProps {
open: boolean;
setOpen: (v: boolean) => void;
}
export const AddNoCodeEventModalDummy: React.FC<EventDetailModalProps> = ({ open, setOpen }) => {
return (
<Modal open={open} setOpen={setOpen} noPadding>
<div className="flex h-full flex-col rounded-lg bg-slate-50 dark:bg-slate-800">
<div className="rounded-t-lg bg-slate-100 dark:bg-slate-700">
<div className="flex items-center justify-between p-6">
<div className="flex items-center space-x-2">
<div className="mr-1.5 h-6 w-6 text-slate-500">
<CursorArrowRaysIcon />
</div>
<div>
<div className="text-xl font-medium text-slate-700 dark:text-slate-300">Add Action</div>
<div className="text-sm text-slate-500">
Create a new user action to display surveys when it&apos;s triggered.
</div>
</div>
</div>
</div>
</div>
<form>
<div className="flex justify-between rounded-lg p-6">
<div className="space-y-4">
<div>
<Label>Select By</Label>
<RadioGroup className="grid grid-cols-2 gap-1 md:grid-cols-3" defaultValue="pageUrl">
<div className="flex items-center space-x-2 rounded-lg border border-slate-200 p-3 dark:border-slate-500">
<RadioGroupItem value="pageUrl" id="pageUrl" className="bg-slate-50" />
<Label htmlFor="pageUrl" className="cursor-pointer dark:text-slate-200">
Page URL
</Label>
</div>
<div className="flex items-center space-x-2 rounded-lg bg-slate-50 p-3 dark:bg-slate-600">
<RadioGroupItem disabled value="innerHtml" id="innerHtml" className="bg-slate-50" />
<Label
htmlFor="innerHtml"
className="flex cursor-not-allowed items-center text-slate-500 dark:text-slate-400">
Inner Text
</Label>
</div>
<div className="hidden items-center space-x-2 rounded-lg bg-slate-50 p-3 dark:bg-slate-600 md:flex">
<RadioGroupItem disabled value="cssSelector" id="cssSelector" className="bg-slate-50" />
<Label
htmlFor="cssSelector"
className="flex cursor-not-allowed items-center text-slate-500">
CSS Selector
</Label>
</div>
</RadioGroup>
</div>
<div className="grid grid-cols-2 gap-x-2">
<div>
<Label>Name</Label>
<Input placeholder="e.g. Dashboard Page View" defaultValue="Dashboard view" />
</div>
<div>
<Label>Description</Label>
<Input placeholder="e.g. User visited dashboard" defaultValue="User visited dashboard" />
</div>
</div>
<div className="grid w-full grid-cols-3 gap-x-8">
<div className="col-span-1">
<Label>URL</Label>
<Select defaultValue="endsWith">
<SelectTrigger
className="w-[110px] dark:text-slate-200 md:w-[180px]"
onClick={(e) => e.preventDefault()}
disabled>
<SelectValue placeholder="Select match type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="exactMatch">Exactly matches</SelectItem>
<SelectItem value="contains">Contains</SelectItem>
<SelectItem value="startsWith">Starts with</SelectItem>
<SelectItem value="endsWith">Ends with</SelectItem>
<SelectItem value="notMatch">Does not exactly match</SelectItem>
<SelectItem value="notContains">Does not contain</SelectItem>
</SelectContent>
</Select>
</div>
<div className="col-span-2 flex w-full items-end">
<Input type="text" defaultValue="/dashboard" />
</div>
</div>
</div>
</div>
<div className="flex justify-end border-t border-slate-200 p-6 dark:border-slate-700">
<div className="flex space-x-2">
<Button
variant="minimal"
onClick={(e) => {
e.preventDefault();
setOpen(false);
}}>
Cancel
</Button>
<Button
variant="primary"
onClick={(e) => {
e.preventDefault();
setOpen(false);
}}>
Add event
</Button>
</div>
</div>
</form>
</div>
</Modal>
);
};
export default AddNoCodeEventModalDummy;

View File

@@ -1,45 +0,0 @@
import type { CTAQuestion } from "@formbricks/types/questions";
import Headline from "./Headline";
import HtmlBody from "./HtmlBody";
interface CTAQuestionProps {
question: CTAQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function CTAQuestion({ question, onSubmit, lastQuestion, brandColor }: CTAQuestionProps) {
return (
<div>
<Headline headline={question.headline} questionId={question.id} />
<HtmlBody htmlString={question.html || ""} questionId={question.id} />
<div className="mt-4 flex w-full justify-end">
<div></div>
{!question.required && (
<button
type="button"
onClick={() => {
onSubmit({ [question.id]: "dismissed" });
}}
className="mr-4 flex items-center rounded-md px-3 py-3 text-base font-medium leading-4 text-slate-500 hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2 dark:border-slate-400 dark:text-slate-400">
{question.dismissButtonLabel || "Skip"}
</button>
)}
<button
type="button"
onClick={() => {
if (question.buttonExternal && question.buttonUrl) {
window?.open(question.buttonUrl, "_blank")?.focus();
}
onSubmit({ [question.id]: "clicked" });
}}
className="flex items-center rounded-md border border-transparent px-3 py-3 text-base font-medium leading-4 text-white shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
style={{ backgroundColor: brandColor }}>
{question.buttonLabel || (lastQuestion ? "Finish" : "Next")}
</button>
</div>
</div>
);
}

View File

@@ -1,10 +0,0 @@
import clsx from "clsx";
interface ContentWrapperProps {
children: React.ReactNode;
className?: string;
}
export default function ContentWrapper({ children, className }: ContentWrapperProps) {
return <div className={clsx("mx-auto max-w-7xl p-6", className)}>{children}</div>;
}

View File

@@ -1,44 +0,0 @@
// DemoPreview.tsx
import React, { useEffect, useState } from "react";
import PreviewSurvey from "./PreviewSurvey";
import { findTemplateByName } from "./templates";
import type { Template } from "@formbricks/types/templates";
interface DemoPreviewProps {
template: string;
}
const DemoPreview: React.FC<DemoPreviewProps> = ({ template }) => {
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
const selectedTemplate: Template | undefined = findTemplateByName(template);
useEffect(() => {
if (selectedTemplate) {
setActiveQuestionId(selectedTemplate.preset.questions[0].id);
}
}, [selectedTemplate]);
if (!selectedTemplate) {
return <div>Template not found.</div>;
}
return (
<div className="mx-2 flex items-center justify-center rounded-xl border-2 border-slate-300 bg-slate-200 py-6 transition-transform duration-150 dark:border-slate-500 dark:bg-slate-700 md:mx-0">
<div className="flex flex-col items-center justify-around">
<p className="my-3 text-sm text-slate-500 dark:text-slate-300">Preview</p>
<div className="">
{selectedTemplate && (
<PreviewSurvey
activeQuestionId={activeQuestionId}
questions={selectedTemplate.preset.questions}
brandColor="#94a3b8"
setActiveQuestionId={setActiveQuestionId}
/>
)}
</div>
</div>
</div>
);
};
export default DemoPreview;

View File

@@ -1,41 +0,0 @@
import type { Template } from "@formbricks/types/templates";
import { useEffect, useState } from "react";
import PreviewSurvey from "./PreviewSurvey";
import TemplateList from "./TemplateList";
import { templates } from "./templates";
export default function SurveyTemplatesPage({}) {
const [activeTemplate, setActiveTemplate] = useState<Template | null>(null);
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
useEffect(() => {
if (templates.length > 0) {
setActiveTemplate(templates[0]);
setActiveQuestionId(templates[0]?.preset.questions[0]?.id || null);
}
}, []);
return (
<div className="flex h-screen flex-col overflow-x-auto">
<div className="relative z-0 flex flex-1 overflow-hidden">
<TemplateList
activeTemplate={activeTemplate}
onTemplateClick={(template) => {
setActiveQuestionId(template.preset.questions[0].id);
setActiveTemplate(template);
}}
/>
<aside className="group relative h-full flex-1 flex-shrink-0 overflow-hidden rounded-r-lg bg-slate-200 shadow-inner dark:bg-slate-700 md:flex md:flex-col">
{activeTemplate && (
<PreviewSurvey
activeQuestionId={activeQuestionId}
questions={activeTemplate.preset.questions}
brandColor="#94a3b8"
setActiveQuestionId={setActiveQuestionId}
/>
)}
</aside>
</div>
</div>
);
}

View File

@@ -1,11 +0,0 @@
export const Headline: React.FC<{ headline: string; questionId: string }> = ({ headline, questionId }) => {
return (
<label
htmlFor={questionId}
className="mb-1.5 block text-base font-semibold leading-6 text-slate-900 dark:text-slate-100">
{headline}
</label>
);
};
export default Headline;

View File

@@ -1,11 +0,0 @@
/* import { cleanHtml } from "../../lib/cleanHtml"; */
import { cleanHtml } from "@formbricks/lib/cleanHtml";
export default function HtmlBody({ htmlString, questionId }: { htmlString: string; questionId: string }) {
return (
<label
htmlFor={questionId}
className="fb-block fb-font-normal fb-leading-6 text-sm text-slate-500 dark:text-slate-300"
dangerouslySetInnerHTML={{ __html: cleanHtml(htmlString) }}></label>
);
}

View File

@@ -1,31 +0,0 @@
import { ReactNode, useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn";
export default function Modal({
children,
isOpen,
}: {
children: ReactNode;
isOpen: boolean;
reset: () => void;
}) {
const [show, setShow] = useState(false);
useEffect(() => {
setShow(isOpen);
}, [isOpen]);
return (
<div aria-live="assertive" className="flex items-end">
<div className="flex w-full flex-col items-center p-4 sm:items-end md:min-w-[390px]">
<div
className={cn(
show ? "translate-x-0 opacity-100" : "translate-x-28 opacity-0",
"pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white px-4 py-6 shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out dark:bg-slate-900 sm:p-6"
)}>
{children}
</div>
</div>
</div>
);
}

View File

@@ -1,107 +0,0 @@
import { useState, useEffect } from "react";
import { cn } from "@formbricks/lib/cn";
import type { MultipleChoiceMultiQuestion } from "@formbricks/types/questions";
import Headline from "./Headline";
import Subheader from "./Subheader";
interface MultipleChoiceMultiProps {
question: MultipleChoiceMultiQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function MultipleChoiceMultiQuestion({
question,
onSubmit,
lastQuestion,
brandColor,
}: MultipleChoiceMultiProps) {
const [selectedChoices, setSelectedChoices] = useState<string[]>([]);
const [isAtLeastOneChecked, setIsAtLeastOneChecked] = useState(false);
useEffect(() => {
setIsAtLeastOneChecked(selectedChoices.length > 0);
}, [selectedChoices]);
return (
<form
onSubmit={(e) => {
e.preventDefault();
if (question.required && selectedChoices.length <= 0) {
return;
}
const data = {
[question.id]: selectedChoices,
};
onSubmit(data);
setSelectedChoices([]); // reset value
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
<div className="mt-4">
<fieldset>
<legend className="sr-only">Options</legend>
<div className="relative space-y-2 rounded-md bg-white dark:bg-slate-900">
{question.choices &&
question.choices.map((choice) => (
<label
key={choice.id}
className={cn(
selectedChoices.includes(choice.label)
? "z-10 border-slate-400 bg-slate-50 dark:border-slate-400 dark:bg-slate-600"
: "border-slate-200 dark:border-slate-600 dark:bg-slate-700 dark:hover:bg-slate-600",
"relative flex cursor-pointer flex-col rounded-md border p-4 focus:outline-none"
)}>
<span className="flex items-center text-sm">
<input
type="checkbox"
id={choice.id}
name={question.id}
value={choice.label}
className="h-4 w-4 border border-slate-300 focus:ring-0 focus:ring-offset-0 dark:border-slate-600 dark:bg-slate-500"
aria-labelledby={`${choice.id}-label`}
checked={selectedChoices.includes(choice.label)}
onChange={(e) => {
if (e.currentTarget.checked) {
setSelectedChoices([...selectedChoices, e.currentTarget.value]);
} else {
setSelectedChoices(
selectedChoices.filter((label) => label !== e.currentTarget.value)
);
}
}}
style={{ borderColor: brandColor, color: brandColor }}
/>
<span
id={`${choice.id}-label`}
className="ml-3 font-medium text-slate-900 dark:text-slate-200">
{choice.label}
</span>
</span>
</label>
))}
</div>
</fieldset>
</div>
<input
type="text"
className="clip-[rect(0,0,0,0)] absolute m-[-1px] h-1 w-1 overflow-hidden whitespace-nowrap border-0 p-0 text-transparent caret-transparent focus:border-transparent focus:ring-0"
required={question.required}
value={isAtLeastOneChecked ? "checked" : ""}
onChange={() => {}}
/>
<div className="mt-4 flex w-full justify-between">
<div></div>
<button
type="submit"
className="flex items-center rounded-md border border-transparent px-3 py-3 text-base font-medium leading-4 text-white shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
style={{ backgroundColor: brandColor }}>
{question.buttonLabel || (lastQuestion ? "Finish" : "Next")}
</button>
</div>
</form>
);
}

View File

@@ -1,85 +0,0 @@
import { cn } from "@formbricks/lib/cn";
import type { MultipleChoiceSingleQuestion } from "@formbricks/types/questions";
import { useState } from "react";
import Headline from "./Headline";
import Subheader from "./Subheader";
interface MultipleChoiceSingleProps {
question: MultipleChoiceSingleQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function MultipleChoiceSingleQuestion({
question,
onSubmit,
lastQuestion,
brandColor,
}: MultipleChoiceSingleProps) {
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
return (
<form
onSubmit={(e) => {
e.preventDefault();
const data = {
[question.id]: e.currentTarget[question.id].value,
};
onSubmit(data);
setSelectedChoice(null); // reset form
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
<div className="mt-4">
<fieldset>
<legend className="sr-only">Options</legend>
<div className="relative space-y-2 rounded-md">
{question.choices &&
question.choices.map((choice, idx) => (
<label
key={choice.id}
className={cn(
selectedChoice === choice.label
? "z-10 border-slate-400 bg-slate-50 dark:border-slate-400 dark:bg-slate-600"
: "border-slate-200 dark:border-slate-600 dark:bg-slate-700 dark:hover:bg-slate-600",
"relative flex cursor-pointer flex-col rounded-md border p-4 hover:bg-slate-50 focus:outline-none"
)}>
<span className="flex items-center text-sm">
<input
type="radio"
id={choice.id}
name={question.id}
value={choice.label}
className="h-4 w-4 border border-slate-300 focus:ring-0 focus:ring-offset-0 dark:border-slate-600 dark:bg-slate-500"
aria-labelledby={`${choice.id}-label`}
onChange={(e) => {
setSelectedChoice(e.currentTarget.value);
}}
checked={selectedChoice === choice.label}
style={{ borderColor: brandColor, color: brandColor }}
required={question.required && idx === 0}
/>
<span
id={`${choice.id}-label`}
className="ml-3 font-medium text-slate-900 dark:text-slate-200">
{choice.label}
</span>
</span>
</label>
))}
</div>
</fieldset>
</div>
<div className="mt-4 flex w-full justify-between">
<div></div>
<button
type="submit"
className="flex items-center rounded-md border border-transparent px-3 py-3 text-base font-medium leading-4 text-white shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
style={{ backgroundColor: brandColor }}>
{question.buttonLabel || (lastQuestion ? "Finish" : "Next")}
</button>
</div>
</form>
);
}

View File

@@ -1,84 +0,0 @@
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import type { NPSQuestion } from "@formbricks/types/questions";
import Headline from "./Headline";
import Subheader from "./Subheader";
interface NPSQuestionProps {
question: NPSQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function NPSQuestion({ question, onSubmit, lastQuestion, brandColor }: NPSQuestionProps) {
const [selectedChoice, setSelectedChoice] = useState<number | null>(null);
const handleSelect = (number: number) => {
setSelectedChoice(number);
if (question.required) {
onSubmit({
[question.id]: number,
});
}
};
return (
<form
onSubmit={(e) => {
e.preventDefault();
const data = {
[question.id]: selectedChoice,
};
onSubmit(data);
// reset form
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
<div className="my-4">
<fieldset>
<legend className="sr-only">Options</legend>
<div className="flex">
{Array.from({ length: 11 }, (_, i) => i).map((number) => (
<label
key={number}
className={cn(
selectedChoice === number
? "z-10 bg-slate-50 dark:bg-slate-500"
: "dark:bg-slate-700 dark:hover:bg-slate-500",
"relative h-10 flex-1 cursor-pointer border bg-white text-center text-sm leading-10 text-slate-900 first:rounded-l-md last:rounded-r-md hover:bg-gray-100 focus:outline-none dark:border-slate-600 dark:text-white "
)}>
<input
type="radio"
name="nps"
value={number}
className="absolute h-full w-full cursor-pointer opacity-0"
onChange={() => handleSelect(number)}
required={question.required}
/>
{number}
</label>
))}
</div>
<div className="flex justify-between px-1.5 text-xs leading-6 text-slate-500">
<p>{question.lowerLabel}</p>
<p>{question.upperLabel}</p>
</div>
</fieldset>
</div>
{!question.required && (
<div className="mt-4 flex w-full justify-between">
<div></div>
<button
type="submit"
className="flex items-center rounded-md border border-transparent px-3 py-3 text-base font-medium leading-4 text-white shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
style={{ backgroundColor: brandColor }}>
{question.buttonLabel || (lastQuestion ? "Finish" : "Next")}
</button>
</div>
)}
</form>
);
}

View File

@@ -1,56 +0,0 @@
import type { OpenTextQuestion } from "@formbricks/types/questions";
import { useState } from "react";
import Headline from "./Headline";
import Subheader from "./Subheader";
interface OpenTextQuestionProps {
question: OpenTextQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function OpenTextQuestion({
question,
onSubmit,
lastQuestion,
brandColor,
}: OpenTextQuestionProps) {
const [value, setValue] = useState<string>("");
return (
<form
onSubmit={(e) => {
e.preventDefault();
const data = {
[question.id]: value,
};
setValue(""); // reset value
onSubmit(data);
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
<div className="mt-4">
<textarea
rows={3}
name={question.id}
id={question.id}
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={question.placeholder}
required={question.required}
className="block w-full rounded-md border border-slate-100 bg-slate-50 p-2 shadow-sm focus:border-slate-500 focus:ring-0 dark:border-slate-500 dark:bg-slate-700 dark:text-white sm:text-sm"></textarea>
</div>
<div className="mt-4 flex w-full justify-between">
<div></div>
<button
type="submit"
className="flex items-center rounded-md border border-transparent px-3 py-3 text-base font-medium leading-4 text-white shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
style={{ backgroundColor: brandColor }}>
{question.buttonLabel || (lastQuestion ? "Finish" : "Next")}
</button>
</div>
</form>
);
}

View File

@@ -1,27 +0,0 @@
import clsx from "clsx";
import { ReactNode, useEffect, useState } from "react";
export const Modal: React.FC<{ children: ReactNode; isOpen: boolean }> = ({ children, isOpen }) => {
const [show, setShow] = useState(false);
useEffect(() => {
setShow(isOpen);
}, [isOpen]);
return (
<div
aria-live="assertive"
className="pointer-events-none absolute inset-0 flex items-end px-4 py-6 sm:p-6">
<div className="flex w-full flex-col items-center space-y-4 sm:items-end">
<div
className={clsx(
show ? "translate-x-0 opacity-100" : "translate-x-28 opacity-0",
"pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white px-4 py-6 shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out sm:p-6"
)}>
{children}
</div>
</div>
</div>
);
};
export default Modal;

View File

@@ -1,89 +0,0 @@
import { useState } from "react";
import Modal from "./Modal";
import QuestionConditional from "./QuestionConditional";
import type { Question } from "@formbricks/types/questions";
import { Survey } from "@formbricks/types/surveys";
import ThankYouCard from "./ThankYouCard";
interface PreviewSurveyProps {
localSurvey?: Survey;
setActiveQuestionId: (id: string | null) => void;
activeQuestionId?: string | null;
questions: Question[];
brandColor: string;
}
export default function PreviewSurvey({
localSurvey,
setActiveQuestionId,
activeQuestionId,
questions,
brandColor,
}: PreviewSurveyProps) {
const [isModalOpen, setIsModalOpen] = useState(true);
const gotoNextQuestion = () => {
const currentIndex = questions.findIndex((q) => q.id === activeQuestionId);
if (currentIndex < questions.length - 1) {
setActiveQuestionId(questions[currentIndex + 1].id);
} else {
if (localSurvey?.thankYouCard?.enabled) {
setActiveQuestionId("thank-you-card");
} else {
setIsModalOpen(false);
setTimeout(() => {
setActiveQuestionId(questions[0].id);
setIsModalOpen(true);
}, 500);
if (localSurvey?.thankYouCard?.enabled) {
setActiveQuestionId("thank-you-card");
} else {
setIsModalOpen(false);
setTimeout(() => {
setActiveQuestionId(questions[0].id);
setIsModalOpen(true);
}, 500);
}
}
}
};
const resetPreview = () => {
setIsModalOpen(false);
setTimeout(() => {
setActiveQuestionId(questions[0].id);
setIsModalOpen(true);
}, 500);
};
if (!activeQuestionId) {
return null;
}
return (
<>
<Modal isOpen={isModalOpen} reset={resetPreview}>
{activeQuestionId == "thank-you-card" ? (
<ThankYouCard
brandColor={brandColor}
headline={localSurvey?.thankYouCard?.headline || ""}
subheader={localSurvey?.thankYouCard?.subheader || ""}
/>
) : (
questions.map(
(question, idx) =>
activeQuestionId === question.id && (
<QuestionConditional
key={question.id}
question={question}
brandColor={brandColor}
lastQuestion={idx === questions.length - 1}
onSubmit={gotoNextQuestion}
/>
)
)
)}
</Modal>
</>
);
}

View File

@@ -1,11 +0,0 @@
export const Progress: React.FC<{ progress: number; brandColor: string }> = ({ progress, brandColor }) => {
return (
<div className="h-1 w-full rounded-full bg-slate-200">
<div
className="h-1 rounded-full bg-slate-700"
style={{ backgroundColor: brandColor, width: `${Math.floor(progress * 100)}%` }}></div>
</div>
);
};
export default Progress;

View File

@@ -1,65 +0,0 @@
import { QuestionType, type Question } from "@formbricks/types/questions";
import OpenTextQuestion from "./OpenTextQuestion";
import MultipleChoiceSingleQuestion from "./MultipleChoiceSingleQuestion";
import MultipleChoiceMultiQuestion from "./MultipleChoiceMultiQuestion";
import NPSQuestion from "./NPSQuestion";
import CTAQuestion from "./CTAQuestion";
import RatingQuestion from "./RatingQuestion";
interface QuestionConditionalProps {
question: Question;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function QuestionConditional({
question,
onSubmit,
lastQuestion,
brandColor,
}: QuestionConditionalProps) {
return question.type === QuestionType.OpenText ? (
<OpenTextQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : question.type === QuestionType.MultipleChoiceSingle ? (
<MultipleChoiceSingleQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : question.type === QuestionType.MultipleChoiceMulti ? (
<MultipleChoiceMultiQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : question.type === QuestionType.NPS ? (
<NPSQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : question.type === QuestionType.CTA ? (
<CTAQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : question.type === QuestionType.Rating ? (
<RatingQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : null;
}

View File

@@ -1,91 +0,0 @@
import type { RatingQuestion } from "@formbricks/types/questions";
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import Headline from "./Headline";
import Subheader from "./Subheader";
interface RatingQuestionProps {
question: RatingQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function RatingQuestion({
question,
onSubmit,
lastQuestion,
brandColor,
}: RatingQuestionProps) {
const [selectedChoice, setSelectedChoice] = useState<number | null>(null);
const handleSelect = (number: number) => {
setSelectedChoice(number);
if (question.required) {
onSubmit({
[question.id]: number,
});
setSelectedChoice(null); // reset choice
}
};
return (
<form
onSubmit={(e) => {
e.preventDefault();
const data = {
[question.id]: selectedChoice,
};
setSelectedChoice(null); // reset choice
onSubmit(data);
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
<div className="my-4">
<fieldset>
<legend className="sr-only">Options</legend>
<div className="flex">
{Array.from({ length: question.range }, (_, i) => i + 1).map((number) => (
<label
key={number}
className={cn(
selectedChoice === number
? "z-10 border-slate-400 bg-slate-50"
: "bg-white hover:bg-gray-100 dark:bg-slate-700 dark:hover:bg-slate-500",
"relative h-10 flex-1 cursor-pointer border border-slate-100 text-center text-sm leading-10 text-slate-800 first:rounded-l-md last:rounded-r-md focus:outline-none dark:border-slate-500 dark:text-slate-200 "
)}>
<input
type="radio"
name="rating"
value={number}
className="absolute h-full w-full cursor-pointer opacity-0"
onChange={() => handleSelect(number)}
required={question.required}
/>
{number}
</label>
))}
</div>
<div className="flex justify-between px-1.5 text-xs leading-6 text-slate-500">
<p>{question.lowerLabel}</p>
<p>{question.upperLabel}</p>
</div>
</fieldset>
</div>
{!question.required && (
<div className="mt-4 flex w-full justify-between">
<div></div>
<button
type="submit"
className="flex items-center rounded-md border border-transparent px-3 py-3 text-base font-medium leading-4 text-white shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
style={{ backgroundColor: brandColor }}>
{question.buttonLabel || (lastQuestion ? "Finish" : "Next")}
</button>
</div>
)}
</form>
);
}

View File

@@ -1,14 +0,0 @@
export const Subheader: React.FC<{ subheader?: string; questionId: string }> = ({
subheader,
questionId,
}) => {
return (
<label
htmlFor={questionId}
className="mt-2 block text-sm font-normal leading-6 text-slate-500 dark:text-slate-400">
{subheader}
</label>
);
};
export default Subheader;

View File

@@ -1,77 +0,0 @@
import type { Template } from "@formbricks/types/templates";
import { useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { templates } from "./templates";
type TemplateList = {
onTemplateClick: (template: Template) => void;
activeTemplate: Template | null;
};
const ALL_CATEGORY_NAME = "All";
export default function TemplateList({ onTemplateClick, activeTemplate }: TemplateList) {
const [selectedFilter, setSelectedFilter] = useState(ALL_CATEGORY_NAME);
const [categories, setCategories] = useState<Array<string>>([]);
useEffect(() => {
const defaultCategories = [
/* ALL_CATEGORY_NAME, */
...(Array.from(new Set(templates.map((template) => template.category))) as string[]),
];
const fullCategories = [ALL_CATEGORY_NAME, ...defaultCategories];
setCategories(fullCategories);
const activeFilter = ALL_CATEGORY_NAME;
setSelectedFilter(activeFilter);
}, []);
return (
<main className="relative z-0 flex-1 overflow-y-auto rounded-l-lg bg-slate-100 px-8 py-6 focus:outline-none dark:bg-slate-800">
<div className="mb-6 flex flex-wrap space-x-1">
{categories.map((category) => (
<button
key={category}
type="button"
onClick={() => setSelectedFilter(category)}
className={cn(
selectedFilter === category
? "text-brand-dark border-brand-dark font-semibold"
: "border-slate-300 text-slate-700 hover:bg-slate-100 dark:border-slate-600 dark:text-slate-300",
"mt-2 rounded border bg-slate-50 px-3 py-1 text-xs transition-all duration-150 dark:bg-slate-600 dark:hover:bg-slate-500"
)}>
{category}
</button>
))}
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{templates
.filter((template) => selectedFilter === ALL_CATEGORY_NAME || template.category === selectedFilter)
.map((template: Template) => (
<button
type="button"
onClick={() => {
onTemplateClick(template); // Pass the 'template' object instead of 'activeTemplate'
}}
key={template.name}
className={cn(
activeTemplate?.name === template.name && "ring-brand ring-2",
"duration-120 group relative rounded-lg bg-white p-6 shadow transition-all duration-150 hover:scale-105 dark:bg-slate-700"
)}>
<div className="absolute right-6 top-6 rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 text-xs text-slate-500 dark:border-slate-400 dark:bg-slate-800 dark:text-slate-400">
{template.category}
</div>
<template.icon className="h-8 w-8" />
<h3 className="text-md mb-1 mt-3 text-left font-bold text-slate-700 dark:text-slate-300">
{template.name}
</h3>
<p className="text-left text-xs text-slate-600 dark:text-slate-400">{template.description}</p>
</button>
))}
</div>
</main>
);
}

View File

@@ -1,52 +0,0 @@
import Headline from "./Headline";
import Subheader from "./Subheader";
interface ThankYouCardProps {
headline: string;
subheader: string;
brandColor: string;
}
export default function ThankYouCard({ headline, subheader, brandColor }: ThankYouCardProps) {
return (
<div className="text-center">
<div className="flex items-center justify-center" style={{ color: brandColor }}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="h-24 w-24">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<span className="mb-[10px] inline-block h-1 w-16 rounded-[100%] bg-slate-300"></span>
<div>
<Headline headline={headline} questionId="thankYouCard" />
<Subheader subheader={subheader} questionId="thankYouCard" />
</div>
{/* <span
className="mb-[10px] mt-[35px] inline-block h-[2px] w-4/5 rounded-full opacity-25"
style={{ backgroundColor: brandColor }}></span>
<div>
<p className="text-xs text-slate-500">
Powered by{" "}
<b>
<a href="https://formbricks.com" target="_blank" className="hover:text-slate-700">
Formbricks
</a>
</b>
</p>
</div> */}
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,66 +0,0 @@
import { CodeFileIcon, EyeIcon, HandPuzzleIcon } from "@formbricks/ui";
import HeadingCentered from "../shared/HeadingCentered";
const features = [
{
id: "compliance",
name: "Smoothly Compliant",
description: "Use our GDPR-compliant Cloud or self-host the entire solution.",
icon: EyeIcon,
},
{
id: "customizable",
name: "Fully Customizable",
description: "Full customizability and extendability. Integrate with your stack easily.",
icon: HandPuzzleIcon,
},
{
id: "independent",
name: "Stay independent",
description: "The code is open-source. Do with it what your organization needs.",
icon: CodeFileIcon,
},
];
export const Features: React.FC = () => {
return (
<div className="relative px-4 pb-10 sm:px-6 lg:px-8 lg:pb-14 lg:pt-24">
<div className="relative mx-auto max-w-7xl">
<HeadingCentered
closer
teaser="Data Privacy at heart"
heading="The only open-source solution"
subheading="Comply with all data privacy regulation with ease. Self-host if you want."
/>
<ul role="list" className="grid grid-cols-1 gap-4 pt-8 sm:grid-cols-2 md:grid-cols-3 lg:gap-10">
{features.map((feature) => {
const IconComponent: React.ElementType = feature.icon;
return (
<li
key={feature.id}
className="relative col-span-1 mt-16 flex flex-col rounded-xl bg-slate-100 text-center dark:bg-slate-700">
<div className="absolute -mt-12 w-full">
<div className="mx-auto flex h-20 w-20 items-center justify-center rounded-3xl bg-slate-200 shadow dark:bg-slate-800">
<IconComponent className="text-brand-dark dark:text-brand-light mx-auto h-10 w-10 flex-shrink-0" />
</div>
</div>
<div className="flex flex-1 flex-col p-10">
<h3 className="my-4 text-lg font-medium text-slate-800 dark:text-slate-200">
{feature.name}
</h3>
<dl className="mt-1 flex flex-grow flex-col justify-between">
<dt className="sr-only">Description</dt>
<dd className="text-sm text-slate-600 dark:text-slate-400">{feature.description}</dd>
</dl>
</div>
</li>
);
})}
</ul>
</div>
</div>
);
};
export default Features;

View File

@@ -1,45 +0,0 @@
import GitHubMarkWhite from "@/images/github-mark-white.svg";
import GitHubMarkDark from "@/images/github-mark.svg";
import Image from "next/image";
import Link from "next/link";
export const GitHubSponsorship: React.FC = () => {
return (
<div className="mx-4 my-4 mb-12 mt-12 rounded-xl bg-gradient-to-br from-slate-100 to-slate-200 px-4 py-8 dark:from-slate-800 dark:via-slate-800 dark:to-slate-700 sm:px-6 sm:pb-12 sm:pt-8 md:max-w-none lg:mt-6 lg:px-8 lg:pt-8">
<style jsx>{`
@media (min-width: 426px);
`}</style>
<div className="right-24 lg:absolute">
<Image
src={GitHubMarkDark}
alt="GitHub Sponsors Formbricks badge"
width={100}
height={100}
className="mr-12 block dark:hidden md:mr-4 "
/>
<Image
src={GitHubMarkWhite}
alt="GitHub Sponsors Formbricks badge"
width={100}
height={100}
className="mr-12 hidden dark:block md:mr-4 "
/>
</div>
<h2 className="mt-4 text-2xl font-bold tracking-tight text-slate-800 dark:text-slate-200 lg:text-2xl">
Proudly Open-Source 🤍
</h2>
<p className="lg:text-md mt-4 max-w-3xl text-slate-500 dark:text-slate-400">
We&apos;re proud to to be supported by GitHubs Open-Source Program!{" "}
<span>
<Link
href="/blog/inaugural-batch-github-accelerator"
className="decoration-brand-dark underline underline-offset-4">
Read more.
</Link>
</span>
</p>
</div>
);
};
export default GitHubSponsorship;

View File

@@ -1,119 +0,0 @@
import CalLogoDark from "@/images/clients/cal-logo-dark.svg";
import CalLogoLight from "@/images/clients/cal-logo-light.svg";
import ClovyrLogo from "@/images/clients/clovyr-logo.svg";
import CrowdLogoDark from "@/images/clients/crowd-logo-dark.svg";
import CrowdLogoLight from "@/images/clients/crowd-logo-light.svg";
import NILogoDark from "@/images/clients/niLogoDark.svg";
import NILogoLight from "@/images/clients/niLogoWhite.svg";
import AnimationFallback from "@/public/animations/opensource-xm-platform-formbricks-fallback.png";
import { Button } from "@formbricks/ui";
import { ChevronRightIcon } from "@heroicons/react/24/outline";
import { usePlausible } from "next-plausible";
import Image from "next/image";
import { useRouter } from "next/router";
import HeroAnimation from "./HeroAnimation";
export const Hero: React.FC = ({}) => {
const plausible = usePlausible();
const router = useRouter();
return (
<div className="relative">
<div className="px-4 pb-20 pt-16 text-center sm:px-6 lg:px-8 lg:pb-32 lg:pt-20">
<a
href="https://github.com/formbricks/formbricks"
target="_blank"
className="border-brand-dark rounded-full border px-4 py-1.5 text-sm text-slate-500 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800">
We&apos;re Open-Source | Star us on GitHub{" "}
<ChevronRightIcon className="inline h-5 w-5 text-slate-300" />
</a>
<h1 className="mt-10 text-3xl font-bold tracking-tight text-slate-800 dark:text-slate-200 sm:text-4xl md:text-5xl">
<span className="xl:inline">Open-source Experience Management</span>
</h1>
<p className="xs:max-w-none mx-auto mt-3 max-w-xs text-base text-slate-500 dark:text-slate-400 sm:text-lg md:mt-5 md:text-xl">
Understand what customers think & feel about your product.
<br />
<span className="hidden md:block">
Natively integrate user research with minimal dev attention,{" "}
<span className="decoration-brand-dark underline underline-offset-4">privacy-first.</span>
</span>
</p>
<div className="mx-auto mt-5 max-w-3xl items-center px-4 sm:flex sm:justify-center md:mt-8 md:space-x-8 md:px-0">
<p className="hidden whitespace-nowrap pt-3 text-xs text-slate-400 dark:text-slate-500 md:block">
Trusted by
</p>
<div className="grid grid-cols-4 items-center gap-5 pt-2 md:gap-8">
<Image
src={CalLogoLight}
alt="Cal Logo"
className="block rounded-lg hover:opacity-100 dark:hidden md:opacity-50"
width={170}
/>
<Image
src={CalLogoDark}
alt="Cal Logo"
className="hidden rounded-lg hover:opacity-100 dark:block md:opacity-50"
width={170}
/>
<Image
src={CrowdLogoLight}
alt="Crowd.dev Logo"
className="block rounded-lg pb-1 hover:opacity-100 dark:hidden md:opacity-50"
width={200}
/>
<Image
src={CrowdLogoDark}
alt="Crowd.dev Logo"
className="hidden rounded-lg pb-1 hover:opacity-100 dark:block md:opacity-50"
width={200}
/>
<Image
src={ClovyrLogo}
alt="Clovyr Logo"
className="rounded-lg pb-1 hover:opacity-100 md:opacity-50"
width={200}
/>
<Image
src={NILogoDark}
alt="Neverinstall Logo"
className="block pb-1 hover:opacity-100 dark:hidden md:opacity-50"
width={200}
/>
<Image
src={NILogoLight}
alt="Neverinstall Logo"
className="hidden pb-1 hover:opacity-100 dark:block md:opacity-50"
width={200}
/>
</div>
</div>
<div className="hidden pt-10 md:block">
<Button
variant="highlight"
className="mr-3 px-6"
onClick={() => {
router.push("https://app.formbricks.com/auth/signup");
plausible("Hero_CTA_CreateSurvey");
}}>
Create survey
</Button>
<Button
variant="secondary"
className="px-6"
onClick={() => {
router.push("/demo");
plausible("Hero_CTA_LaunchDemo");
}}>
Live demo
</Button>
</div>
</div>
<div className="relative px-2 md:px-0">
<HeroAnimation fallbackImage={AnimationFallback} />
</div>
</div>
);
};
export default Hero;

View File

@@ -1,53 +0,0 @@
import { useEffect, useRef, useState } from "react";
import type { LottiePlayer } from "lottie-web";
import Image from "next/image";
export const HeroAnimation: React.FC<any> = ({ fallbackImage, ...props }) => {
const [loaded, setLoaded] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const [lottie, setLottie] = useState<LottiePlayer | null>(null);
useEffect(() => {
import("lottie-web").then((Lottie) => setLottie(Lottie.default));
}, []);
useEffect(() => {
if (lottie && ref.current) {
const animation = lottie.loadAnimation({
container: ref.current,
renderer: "svg",
loop: true,
autoplay: true,
// path to your animation file, place it inside public folder
path: "/animations/opensource-xm-platform-formbricks.json",
});
animation.addEventListener("DOMLoaded", () => {
setLoaded(true);
});
return () => animation.destroy();
}
}, [lottie]);
return (
<div className="relative" {...props}>
<div ref={ref} />
{!loaded && (
<div className="absolute inset-0">
<Image
src={fallbackImage}
alt="Fallback Image"
layout="fill"
objectFit="cover"
objectPosition="center"
className="transition-opacity duration-300"
style={{ opacity: loaded ? 0 : 1 }}
/>
</div>
)}
</div>
);
};
export default HeroAnimation;

View File

@@ -1,68 +0,0 @@
import ImageAttributesDark from "@/images/attributes-dark.svg";
import ImageAttributesLight from "@/images/attributes-light.svg";
import ImageEventTriggerDark from "@/images/event-trigger-dark.svg";
import ImageEventTriggerLight from "@/images/event-trigger-light.svg";
import Image from "next/image";
export const Highlights: React.FC = ({}) => {
return (
<>
<div className="mx-auto mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="pb-8 md:pb-0">
<h2 className="xs:text-3xl text-2xl font-bold leading-7 tracking-tight text-slate-800 dark:text-slate-200">
Ask at the right moment,
<br />
<span className="font-light">get the data you need.</span>
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Follow up emails are so 2010. Ask users as they experience your product - and leverage a
significantly higher conversion rate.
</p>
</div>
<div className="rounded-lg bg-slate-100 py-6 pr-4 dark:bg-slate-800 sm:py-16 sm:pr-8">
<Image
src={ImageEventTriggerLight}
alt="react library"
className="block rounded-lg dark:hidden"
/>
<Image
src={ImageEventTriggerDark}
alt="react library"
className="hidden rounded-lg dark:block"
/>
</div>
</div>
</div>
</div>
<div className="mx-auto mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="order-last rounded-lg bg-slate-100 p-4 dark:bg-slate-800 sm:p-8 md:order-first">
<Image
src={ImageAttributesLight}
alt="react library"
className="block rounded-lg dark:hidden"
/>
<Image src={ImageAttributesDark} alt="react library" className="hidden rounded-lg dark:block" />
</div>
<div className="pb-8 md:pb-0">
<h2 className="xs:text-3xl text-2xl font-bold leading-7 tracking-tight text-slate-800 dark:text-slate-100 sm:text-3xl">
Dont&apos; Spray and pray.
<br />
<span className="font-light">Pre-segment granularly.</span>
</h2>
<p className="text-md mt-6 max-w-md leading-7 text-slate-500 dark:text-slate-400">
Pre-segment who sees your survey based on custom attributes. Keep the signal, cancel out the
noise.
</p>
</div>
</div>
</div>
</div>
</>
);
};
export default Highlights;

View File

@@ -1,71 +0,0 @@
import clsx from "clsx";
import { useState } from "react";
import { IoLogoHtml5, IoLogoNpm } from "react-icons/io5";
import CodeBlock from "../shared/CodeBlock";
interface SecondNavbarProps {
tabs: { id: string; label: string; icon?: React.ReactNode }[];
activeId: string;
setActiveId: (id: string) => void;
}
export const TabBar: React.FC<SecondNavbarProps> = ({ tabs, activeId, setActiveId }) => {
return (
<div className="flex h-14 items-center justify-center rounded-lg bg-slate-200 dark:bg-slate-700">
<nav className="flex h-full items-center space-x-4" aria-label="Tabs">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveId(tab.id)}
className={clsx(
tab.id === activeId
? " border-brand-dark border-b-2 font-semibold text-slate-900 dark:text-slate-300"
: "text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200",
"flex h-full items-center px-3 text-sm font-medium"
)}
aria-current={tab.id === activeId ? "page" : undefined}>
{tab.icon && <div className="flex h-5 w-5 items-center">{tab.icon}</div>}
{tab.label}
</button>
))}
</nav>
</div>
);
};
const tabs = [
{ id: "npm", label: "NPM", icon: <IoLogoNpm /> },
{ id: "html", label: "HTML", icon: <IoLogoHtml5 /> },
];
export const SetupInstructions: React.FC = ({}) => {
const [activeTab, setActiveTab] = useState(tabs[0].id);
return (
<div>
<TabBar tabs={tabs} activeId={activeTab} setActiveId={setActiveTab} />
<div className="h-80 max-w-xs px-4 sm:max-w-lg">
{activeTab === "npm" ? (
<>
<CodeBlock>npm install @formbricks/js</CodeBlock>
<CodeBlock>{`import formbricks from "@formbricks/js";
if (typeof window !== "undefined") {
formbricks.init({
environmentId: "claV2as2kKAqF28fJ8",
apiHost: "https://app.formbricks.com",
});
}`}</CodeBlock>
</>
) : activeTab === "html" ? (
<CodeBlock>{`<script type="text/javascript">
!function(){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="./dist/index.umd.js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init("claDadXk29dak92dK9","https://app.formbricks.com")},500)}();
</script>`}</CodeBlock>
) : null}
</div>
</div>
);
};
export default SetupInstructions;

View File

@@ -1,148 +0,0 @@
import DemoPreview from "@/components/dummyUI/DemoPreview";
import DashboardMockupDark from "@/images/dashboard-mockup-dark.png";
import DashboardMockup from "@/images/dashboard-mockup.png";
import { Button } from "@formbricks/ui";
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
import Image from "next/image";
import { useState } from "react";
import AddEventDummy from "../dummyUI/AddEventDummy";
import AddNoCodeEventModalDummy from "../dummyUI/AddNoCodeEventModalDummy";
import HeadingCentered from "../shared/HeadingCentered";
import SetupTabs from "./SetupTabs";
export const Steps: React.FC = () => {
const [isAddEventModalOpen, setAddEventModalOpen] = useState(false);
return (
<>
<HeadingCentered
closer
teaser="Leave your engineers in peace"
heading="Set Formbricks up in minutes"
subheading="Formbricks is designed for as little dev attention as possible. Heres how:"
/>
<div id="howitworks" className="mx-auto mb-12 mt-16 max-w-lg md:mb-0 md:mt-8 md:max-w-none">
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="pb-8 sm:pl-10 md:pb-0">
<h4 className="text-brand-dark font-bold">Step 1</h4>
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 dark:text-slate-200">
Copy + Paste
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Simply copy a &lt;script&gt; tag to your HTML head - thats about it. Or use NPM to install
Formbricks for React, Vue, Svelte, etc.
</p>
</div>
<div className="rounded-lg bg-slate-100 dark:bg-slate-800">
<SetupTabs />
</div>
</div>
</div>
</div>
<div className="mx-auto mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="order-last w-full rounded-lg bg-slate-100 p-4 dark:bg-slate-800 sm:py-8 md:order-first">
<div className="flex h-40 items-center justify-center">
<Button
variant="primary"
className=""
onClick={() => {
setAddEventModalOpen(true);
}}>
<CursorArrowRaysIcon className="mr-2 h-5 w-5 text-white" />
Add Action
</Button>
</div>
</div>
<div className="pb-8 md:pb-0">
<h4 className="text-brand-dark font-bold">Step 2</h4>
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 dark:text-slate-100 sm:text-3xl">
No-Code: Track User Actions
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Set up user actions which can trigger your survey without writing a single line of code.
Surveys can be triggered on specific pages or after an element is clicked.
</p>
</div>
</div>
</div>
</div>
<div className="mx-auto mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="pb-8 sm:pl-10 md:pb-0">
<h4 className="text-brand-dark font-bold">Step 3</h4>
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 dark:text-slate-200 sm:text-3xl">
Create your survey
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Start from a template - or from scratch. Ask what you want, in any language. You can also
adjust the look and feel of your survey.
</p>
</div>
<div className="relative w-full rounded-lg p-1 dark:bg-slate-800 sm:p-8">
<DemoPreview template="Product Market Fit Survey (short)" />
</div>
</div>
</div>
</div>
<div className="mx-auto mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="order-last w-full rounded-lg bg-slate-100 p-4 dark:bg-slate-800 sm:py-8 md:order-first">
<div className="mx-auto md:w-3/4">
<AddEventDummy />
</div>
</div>
<div className="pb-8 md:pb-0">
<h4 className="text-brand-dark font-bold">Step 4</h4>
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 dark:text-slate-100 sm:text-3xl">
Set segment and trigger
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Create a custom segment for each survey. Use attributes and past user actions to only survey
the people who have answers. Trigger your survey on any user action in your app.
</p>
</div>
</div>
</div>
</div>
<div className="mx-auto mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="pb-8 sm:pl-10 md:pb-0">
<h4 className="text-brand-dark font-bold">Step 5</h4>
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 dark:text-slate-200 sm:text-3xl">
Make better decisions
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Gather all insights you can - including partial submissions. Build conviction for the next
product decision. Better data, better business.
</p>
</div>
<div className="sm:scale-125 sm:p-8">
<Image
src={DashboardMockup}
quality="100"
alt="Data Pipelines"
className="block rounded-lg dark:hidden"
/>
<Image
src={DashboardMockupDark}
quality="100"
alt="Data Pipelines"
className="hidden dark:block"
/>
</div>
</div>
</div>
</div>
<AddNoCodeEventModalDummy open={isAddEventModalOpen} setOpen={setAddEventModalOpen} />
</>
);
};
export default Steps;

View File

@@ -1,17 +0,0 @@
import { ResponsiveVideo } from "@formbricks/ui";
import Modal from "../shared/Modal";
interface VideoWalkThroughProps {
open: boolean;
setOpen: (v: boolean) => void;
}
export const VideoWalkThrough: React.FC<VideoWalkThroughProps> = ({ open, setOpen }) => {
return (
<Modal open={open} setOpen={setOpen}>
<div className="mt-5">
<ResponsiveVideo src="/videos/walkthrough-v1.mp4" />
</div>
</Modal>
);
};

View File

@@ -1,214 +0,0 @@
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/solid";
import clsx from "clsx";
import { useState } from "react";
interface APICallProps {
method: "GET" | "POST";
url: string;
description: string;
headers: {
label: string;
type: string;
description: string;
required?: boolean;
}[];
bodies: {
label: string;
type: string;
description: string;
required?: boolean;
}[];
responses: {
color: string;
statusCode: string;
description: string;
example?: string;
}[];
example?: string;
}
export function APILayout({ method, url, description, headers, bodies, responses, example }: APICallProps) {
const [switchState, setSwitchState] = useState(true);
function handleOnChange() {
setSwitchState(!switchState);
}
return (
<div className="rounded-lg bg-slate-200 p-8 dark:bg-slate-700">
{switchState ? (
<ChevronDownIcon
className="hover:text-brand-dark dark:hover:text-brand-dark mr-3 inline h-5 w-5 hover:cursor-pointer"
aria-hidden="true"
onClick={handleOnChange}
/>
) : (
<ChevronRightIcon
className="hover:text-brand-dark dark:hover:text-brand-dark mr-3 inline h-5 w-5 hover:cursor-pointer"
aria-hidden="true"
onClick={handleOnChange}
/>
)}
<div
className={clsx(
"mr-3 inline rounded-full p-1 px-3 font-semibold text-white",
method === "POST" && "bg-red-400 dark:bg-red-800",
method === "GET" && "bg-green-400 dark:bg-green-800"
)}>
{method}
</div>
<div className="inline text-sm text-slate-500 ">
https://app.formbricks.com
<span className="font-bold text-black dark:text-slate-300">{url}</span>
</div>
<div className="ml-8 mt-4 font-bold dark:text-slate-400">{description}</div>
<div>
<div className={clsx(switchState ? "block" : "hidden", "ml-8")}>
<p className="mb-2 mt-6 text-lg font-semibold">Parameters</p>
<div>
{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}
required={q.required}
/>
))}
</div>
</div>
)}
{bodies && (
<div className="mt-4 text-base">
<p className="not-prose -mb-1 pt-2 font-bold">Body</p>
<div>
{}
{bodies?.map((b) => (
<Parameter
key={b.label}
label={b.label}
type={b.type}
description={b.description}
required={b.required}
/>
))}
{example && (
<div>
<p className="not-prose mb-2 pt-2 font-bold">Body Example</p>
<div>
<pre>
<code>{example}</code>
</pre>
</div>
</div>
)}
</div>
</div>
)}
<div>
<div className="mt-4 text-base">
<p className="not-prose -mb-1 pt-2 font-bold">Responses</p>
<div>
{responses.map((r) => (
<Response
key={r.color}
color={r.color}
statusCode={r.statusCode}
description={r.description}
example={r.example}
/>
))}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
interface ParaProps {
label: string;
type: string;
description: string;
required?: boolean;
}
function Parameter({ label, type, description, required }: ParaProps) {
return (
<>
<div className="my-2 grid grid-cols-4 text-sm">
<div className="inline font-mono">
{label}
{required && <p className="inline font-bold text-red-500">*</p>}
</div>
<div>{type}</div>
<div className="col-span-2">{description}</div>
</div>
</>
);
}
interface RespProps {
color: string;
statusCode: string;
description: string;
example?: string;
}
function Response({ color, statusCode, description, example }: RespProps) {
const [toggleExample, setSwitchState] = useState(false);
function handleOnChange() {
setSwitchState(!toggleExample);
}
return (
<div className="my-2 grid grid-cols-2 text-sm">
<div className="text-md inline-flex items-center font-semibold">
<div
className={clsx(
"mr-3 inline h-3 w-3 rounded-full",
color === "green" && "bg-green-400",
color === "brown" && "bg-amber-800"
)}>
&nbsp;
</div>
<div>{statusCode}</div>
</div>
<div className="flex items-center justify-between">
<div>{description}</div>
<div className="font-bold">
{example &&
(toggleExample ? (
<ChevronDownIcon
className={clsx(
toggleExample ? "block" : "hidden",
"hover:text-brand-dark dark:hover:text-brand-dark mr-3 inline h-6 w-6 hover:cursor-pointer"
)}
aria-hidden="true"
onClick={handleOnChange}
/>
) : (
<ChevronLeftIcon
className={clsx(
toggleExample ? "hidden" : "block",
"hover:text-brand-dark dark:hover:text-brand-dark mr-3 inline h-6 w-6 hover:cursor-pointer"
)}
aria-hidden="true"
onClick={handleOnChange}
/>
))}
</div>
</div>
{example && toggleExample && (
<div className="col-span-2 my-3 whitespace-pre-wrap rounded-lg bg-slate-300 p-2 font-mono dark:bg-slate-600 dark:text-slate-300">
{example}
</div>
)}
</div>
);
}

View File

@@ -1,36 +0,0 @@
import Image from "next/image";
import AuthorJohannes from "@/images/blog/johannes-co-founder-formbricks-small.jpg";
interface AuthorBoxProps {
name: string;
title: string;
date: string;
duration: string;
}
export default function AuthorBox({ name, title, date, duration }: AuthorBoxProps) {
return (
<div className="mb-8 flex items-center space-x-4 rounded-lg border border-slate-200 bg-slate-100 px-6 py-3 dark:border-slate-700 dark:bg-slate-800">
<Image
className="m-0 rounded-full"
src={AuthorJohannes}
alt={name}
width={45}
height={45}
quality={100}
placeholder="blur"
style={{ objectFit: "contain" }}
/>
<div className="flex w-full items-end justify-between">
<div>
<p className="m-0 font-medium text-slate-600 dark:text-slate-300">{name}</p>
<p className="m-0 text-sm text-slate-400">{title}</p>
</div>
<div className="text-right">
<p className="m-0 font-medium text-slate-600 dark:text-slate-300">{duration} Minutes</p>
<p className="m-0 text-sm text-slate-400">{date}</p>
</div>
</div>
</div>
);
}

View File

@@ -1,114 +0,0 @@
import {
CancelSubscriptionIcon,
DogChaserIcon,
FeedbackIcon,
InterviewPromptIcon,
OnboardingIcon,
PMFIcon,
BaseballIcon,
CodeBookIcon,
} from "@formbricks/ui";
import clsx from "clsx";
import Link from "next/link";
export default function BestPracticeNavigation() {
const BestPractices = [
{
name: "Interview Prompt",
href: "/interview-prompt",
status: true,
icon: InterviewPromptIcon,
description: "Ask only power users users to book a time in your calendar. Get those juicy details.",
category: "Understand Users",
},
{
name: "Product-Market Fit Survey",
href: "/measure-product-market-fit",
status: true,
icon: PMFIcon,
description: "Find out how disappointed people would be if they could not use your service any more.",
category: "Understand Users",
},
{
name: "Onboarding Segments",
href: "/onboarding-segmentation",
status: false,
icon: OnboardingIcon,
description:
"Get to know your users right from the start. Ask a few questions early, let us enrich the profile.",
category: "Understand Users",
},
{
name: "Learn from Churn",
href: "/learn-from-churn",
status: true,
icon: CancelSubscriptionIcon,
description: "Churn is hard, but insightful. Learn from users who changed their mind.",
category: "Increase Revenue",
},
{
name: "Improve Trial CR",
href: "/improve-trial-conversion",
status: true,
icon: BaseballIcon,
description: "Take guessing out, convert more trials to paid users with insights.",
category: "Increase Revenue",
},
{
name: "Docs Feedback",
href: "/docs-feedback",
status: true,
icon: CodeBookIcon,
description: "Clear docs lead to more adoption. Understand granularly what's confusing.",
category: "Boost Retention",
},
{
name: "Feature Chaser",
href: "/feature-chaser",
status: true,
icon: DogChaserIcon,
description: "Show a survey about a new feature shown only to people who used it.",
category: "Boost Retention",
},
{
name: "Feedback Box",
href: "/feedback-box",
status: true,
icon: FeedbackIcon,
description: "Give users the chance to share feedback in a single click.",
category: "Boost Retention",
},
];
return (
<div className=" mx-auto grid grid-cols-1 gap-6 px-2 sm:grid-cols-3">
{BestPractices.map((bestPractice) => (
<Link href={bestPractice.href} key={bestPractice.name}>
<div className="drop-shadow-card duration-120 hover:border-brand-dark relative rounded-lg border border-slate-100 bg-slate-100 p-8 transition-all ease-in-out hover:scale-105 hover:cursor-pointer dark:bg-slate-800">
<div
className={clsx(
// base styles independent what type of button it is
"absolute right-10 rounded-full px-3 py-1",
// 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 === "Increase Revenue" &&
"bg-blue-100 text-blue-500 dark:bg-blue-800 dark:text-blue-200",
bestPractice.category === "Understand Users" &&
"bg-orange-100 text-orange-500 dark:bg-orange-800 dark:text-orange-200"
)}>
{bestPractice.category}
</div>
<div className="h-12 w-12">
<bestPractice.icon className="h-12 w-12 " />
</div>
<h3 className="mb-1 mt-3 text-xl font-bold text-slate-700 dark:text-slate-200">
{bestPractice.name}
</h3>
<p className="text-sm text-slate-600 dark:text-slate-400">{bestPractice.description}</p>
</div>
</Link>
))}
</div>
);
}

View File

@@ -1,37 +0,0 @@
import { Button } from "@formbricks/ui";
import { usePlausible } from "next-plausible";
import { useRouter } from "next/router";
import BestPracticeNavigation from "./BestPracticeNavigation";
export default function InsightOppos() {
const plausible = usePlausible();
const router = useRouter();
return (
<div className="pb-10 pt-12 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{" "}
<span className="from-brand-light to-brand-dark bg-gradient-to-b bg-clip-text text-transparent xl:inline">
Best Practices
</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">
Run battle-tested approaches for qualitative user research in minutes.
</p>
</div>
<BestPracticeNavigation />
<div className="mx-auto mt-4 w-fit px-4 py-2 text-center">
<Button
variant="highlight"
onClick={() => {
router.push("/demo");
plausible("subPractices_CTA_LaunchDemo");
}}>
Launch Live Demo
</Button>
</div>
</div>
);
}

View File

@@ -1,62 +0,0 @@
import { Button } from "@formbricks/ui";
import clsx from "clsx";
import { usePlausible } from "next-plausible";
import { useRouter } from "next/router";
interface Props {
teaser: string;
headline: string;
subheadline: string;
cta: string;
href: string;
inverted?: boolean;
}
export default function BreakerCTA({ inverted = false, teaser, headline, subheadline, cta, href }: Props) {
const router = useRouter();
const plausible = usePlausible();
return (
<div
className={clsx(
inverted
? "from-slate-800 via-slate-800 to-slate-700 dark:from-slate-200 dark:to-slate-300"
: "from-slate-200 to-slate-300 dark:from-slate-800 dark:via-slate-800 dark:to-slate-700",
"xs:mx-auto xs:w-full mx-4 my-4 mt-28 max-w-6xl rounded-xl bg-gradient-to-br md:mb-0 "
)}>
<div className="relative px-4 py-8 sm:px-6 sm:pb-12 sm:pt-8 lg:px-8 lg:pt-12">
<div className="xs:block xs:absolute xs:right-10 hidden md:top-1/2 md:-translate-y-1/2">
<Button
variant="highlight"
onClick={() => {
plausible("Breaker_CTAs");
router.push(`${href}`);
}}>
{cta}
</Button>
</div>
<p className="lg:text-md dark:text-brand-dark text-brand-light text-sm font-semibold uppercase">
{teaser}
</p>
<h2
className={clsx(
inverted ? "text-slate-200 dark:text-slate-800" : "text-slate-800 dark:text-slate-200",
"mt-4 text-2xl font-bold tracking-tight lg:text-3xl "
)}>
{headline}
</h2>
<p
className={clsx(
inverted ? "text-slate-300 dark:text-slate-500" : "text-slate-500 dark:text-slate-300",
"text-md mt-4 max-w-3xl lg:text-lg"
)}>
{subheadline}
</p>
<div className="xs:hidden mt-4">
<Button variant="highlight" target="_blank" onClick={() => router.push(`${href}`)}>
{cta}
</Button>
</div>
</div>
</div>
);
}

View File

@@ -1,31 +0,0 @@
import { Button } from "@formbricks/ui";
import { useRouter } from "next/router";
import HeadingCentered from "./HeadingCentered";
export default function CTA() {
const router = useRouter();
return (
<>
<div className="mx-auto px-4 py-16 sm:px-6 lg:px-8 lg:pb-40 lg:pt-24">
<HeadingCentered closer teaser="Get started" heading="Ready for the last form tool you need?" />
<div className="mt-12 grid grid-cols-1 content-center md:grid-cols-2">
<div className="-mb-4 rounded-t-xl bg-gradient-to-br from-slate-300 to-slate-200 px-8 py-24 text-center text-slate-900 dark:from-slate-800 dark:to-slate-900 dark:text-slate-100 md:-mr-5 md:mb-0 md:ml-2.5 md:rounded-l-xl lg:p-24">
<h3 className="text-3xl font-bold">Self-hosted</h3>
<p className="mb-4 mt-2">Run locally e.g. with docker-compose.</p>
<Button variant="secondary" onClick={() => router.push("/docs")} className="mt-3">
Read docs
</Button>
</div>
<div className="rounded-xl bg-gradient-to-br from-slate-400 to-slate-300 py-24 text-center text-slate-800 dark:from-slate-800 dark:to-slate-700 dark:text-slate-100">
<h3 className="text-3xl font-bold">Cloud</h3>
<p className="mb-4 mt-2">Use our free managed service.</p>
<Button variant="secondary" onClick={() => router.push("/waitlist")} className="mt-3" disabled>
Coming soon
</Button>
</div>
</div>
</div>
</>
);
}

View File

@@ -1,40 +0,0 @@
import clsx from "clsx";
import { Icon } from "@/components/shared/Icon";
const styles = {
note: {
container: "bg-slate-100 dark:bg-slate-800/60 dark:ring-1 dark:ring-slate-300/10",
title: "text-slate-900 dark:text-slate-400",
body: "text-slate-800 [--tw-prose-background:theme(colors.slate.50)] prose-a:text-slate-900 prose-code:text-slate-900 dark:text-slate-300 dark:prose-code:text-slate-300",
},
warning: {
container: "bg-amber-50 dark:bg-slate-800/60 dark:ring-1 dark:ring-slate-300/10",
title: "text-amber-900 dark:text-amber-500",
body: "text-amber-800 [--tw-prose-underline:theme(colors.amber.400)] [--tw-prose-background:theme(colors.amber.50)] prose-a:text-amber-900 prose-code:text-amber-900 dark:text-slate-300 dark:[--tw-prose-underline:theme(colors.slate.700)] dark:prose-code:text-slate-300",
},
};
const icons = {
note: (props: any) => <Icon icon="lightbulb" {...props} />,
warning: (props: any) => <Icon icon="warning" color="amber" {...props} />,
};
interface CalloutProps {
type: "note" | "warning";
title: string;
children: React.ReactNode;
}
export function Callout({ type = "note", title, children }: CalloutProps) {
let IconComponent = icons[type];
return (
<div className={clsx("my-8 flex rounded-3xl p-6", styles[type].container)}>
<IconComponent className="h-8 w-8 flex-none" />
<div className="ml-4 flex-auto">
<p className={clsx("font-display m-0 text-xl", styles[type].title)}>{title}</p>
<div className={clsx("prose mt-2.5", styles[type].body)}>{children}</div>
</div>
</div>
);
}

View File

@@ -1,76 +0,0 @@
import Link from "next/link";
import clsx from "clsx";
function ChevronRightIcon(props) {
return (
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}>
<path d="M6.75 5.75 9.25 8l-2.5 2.25" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
export function Card({ as: Component = "div", className, children }) {
return (
<Component className={clsx(className, "group relative flex flex-col items-start")}>{children}</Component>
);
}
Card.Link = function CardLink({ children, ...props }) {
return (
<>
<div className="absolute -inset-y-6 -inset-x-4 scale-95 bg-slate-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-slate-800 sm:-inset-x-6 sm:rounded-2xl" />
<Link {...props}>
<span className="absolute -inset-y-6 -inset-x-4 sm:-inset-x-6 sm:rounded-2xl" />
<span className="relative">{children}</span>
</Link>
</>
);
};
Card.Title = function CardTitle({ as: Component = "h2", href, children }) {
return (
<Component className="text-base font-semibold tracking-tight text-slate-800 dark:text-slate-100">
{href ? <Card.Link href={href}>{children}</Card.Link> : children}
</Component>
);
};
Card.Description = function CardDescription({ children }) {
return <p className="relative mt-2 text-sm text-slate-600 dark:text-slate-400">{children}</p>;
};
Card.Cta = function CardCta({ children }) {
return (
<div
aria-hidden="true"
className="relative mt-4 flex items-center text-sm font-medium text-brand-dark dark:text-brand-light">
{children}
<ChevronRightIcon className="ml-1 h-4 w-4 stroke-current" />
</div>
);
};
Card.Eyebrow = function CardEyebrow({
as: Component = "p",
decorate = false,
className,
children,
...props
}) {
return (
<Component
className={clsx(
className,
"relative order-first mb-3 flex items-center text-sm text-slate-400 dark:text-slate-500",
decorate && "pl-3.5"
)}
{...props}>
{decorate && (
<span className="absolute inset-y-0 left-0 flex items-center" aria-hidden="true">
<span className="h-4 w-0.5 rounded-full bg-slate-200 dark:bg-slate-500" />
</span>
)}
{children}
</Component>
);
};

View File

@@ -1,24 +0,0 @@
// 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;

View File

@@ -1,41 +0,0 @@
import EarlyBird from "@/images/early bird deal for open source jotform alternative typeform and surveymonkey_v2.svg";
import { Button } from "@formbricks/ui";
import { usePlausible } from "next-plausible";
import Image from "next/image";
export default function EarlyBirdDeal() {
const plausible = usePlausible();
return (
<div className="bg-brand-dark relative mx-4 max-w-7xl overflow-hidden rounded-xl p-6 pb-16 sm:p-8 sm:pb-16 md:px-12 md:py-8 lg:mx-0 lg:flex lg:items-center">
<div className="lg:w-0 lg:flex-1 ">
<h2
className="mb-1 text-2xl font-bold tracking-tight text-white sm:text-2xl"
id="newsletter-headline">
50% off for Early Birds.
</h2>
<h2 className="text-xl font-semibold tracking-tight text-slate-200 sm:text-lg">
Limited deal: Only{" "}
<span className="bg- rounded-sm bg-slate-200/40 px-2 py-0.5 text-slate-100">14</span> left.
</h2>
<div className="mt-6">
<Button
variant="secondary"
className="dark:bg-slate-200 dark:text-slate-700 dark:hover:bg-slate-300"
onClick={() => {
plausible("Pricing_CTA_EarlyBird");
window.open("https://app.formbricks.com/auth/signup", "_blank")?.focus();
}}>
Get Early Bird deal
</Button>
</div>
<p className="mb-24 mt-2 max-w-3xl text-xs tracking-tight text-slate-200 md:mb-0 md:max-w-sm lg:max-w-none">
This saves you $588 every year.
</p>
<div className="absolute -bottom-36 -right-20 mx-auto h-96 w-96 scale-75 sm:-right-10">
<Image src={EarlyBird} fill alt="formbricks favicon open source forms typeform alternative" />
</div>
</div>
</div>
);
}

View File

@@ -1,55 +0,0 @@
import { Button } from "@formbricks/ui";
import { useRouter } from "next/router";
import clsx from "clsx";
interface Props {
featureTitle: string;
text: string;
img: React.ReactNode;
isImgLeft?: boolean;
cta?: string;
href?: string;
disabled?: boolean;
}
export default function FeatureHighlights({
featureTitle,
text,
img,
isImgLeft,
cta,
href,
disabled,
}: Props) {
const router = useRouter();
return (
<div className="my-12">
<div className="mx-auto max-w-md px-4 sm:max-w-3xl sm:px-6 lg:max-w-7xl lg:px-8">
<div className="md:grid-cols-2 lg:grid lg:items-center lg:gap-24">
<div className={clsx(isImgLeft ? "order-last" : "")}>
<h2 className="text-2xl font-bold tracking-tight text-slate-800 dark:text-slate-200 sm:text-3xl">
{featureTitle}
</h2>
<div className="text-md mt-6 whitespace-pre-line leading-7 text-slate-500 dark:text-slate-400">
{text}
</div>
<div className="mt-6">
{cta && href && (
<Button
disabled={disabled}
variant="minimal"
size="sm"
className="mb-8"
onClick={() => router.push(href)}>
{cta}
</Button>
)}
</div>
</div>
{img}
</div>
</div>
</div>
);
}

View File

@@ -1,25 +0,0 @@
import { Fragment } from "react";
import { Highlight } from "prism-react-renderer";
export function Fence({ children, language }) {
return (
<Highlight code={children.trimEnd()} language={language} theme={undefined}>
{({ className, style, tokens, getTokenProps }) => (
<pre className={className} style={style}>
<code>
{tokens.map((line, lineIndex) => (
<Fragment key={lineIndex}>
{line
.filter((token) => !token.empty)
.map((token, tokenIndex) => (
<span key={tokenIndex} {...getTokenProps({ token })} />
))}
{"\n"}
</Fragment>
))}
</code>
</pre>
)}
</Highlight>
);
}

View File

@@ -1,71 +0,0 @@
import Link from "next/link";
import { FooterLogo } from "./Logo";
const navigation = {
other: [
{ name: "Community", href: "/community", status: true },
{ name: "Blog", href: "/blog", status: true },
{ name: "OSS Friends", href: "/oss-friends", status: true },
{ name: "GDPR FAQ", href: "/gdpr", status: true },
{ name: "GDPR Guide", href: "/gdpr-guide", status: true },
],
social: [
{
name: "Twitter",
href: "https://twitter.com/formbricks",
icon: (props: any) => (
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84" />
</svg>
),
},
{
name: "GitHub",
href: "https://github.com/formbricks/formbricks",
icon: (props: any) => (
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
<path
fillRule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clipRule="evenodd"
/>
</svg>
),
},
],
};
export default function Footer() {
return (
<footer
className="mt-32 bg-gradient-to-b from-slate-50 to-slate-200 dark:from-slate-900 dark:to-slate-800"
aria-labelledby="footer-heading">
<h2 id="footer-heading" className="sr-only">
Footer
</h2>
<div className="mx-auto flex max-w-7xl flex-col space-y-6 px-4 py-12 text-center sm:px-6 lg:px-8 lg:py-16">
<Link href="/">
<span className="sr-only">Formbricks</span>
<FooterLogo className="mx-auto h-8 w-auto sm:h-10" />
</Link>
<p className="text-base text-slate-500 dark:text-slate-400">Privacy-first Experience Management</p>
<div className="border-slate-500">
<p className="text-sm text-slate-400 dark:text-slate-500">
Formbricks GmbH &copy; 2022. All rights reserved.
<br />
<Link href="/imprint">Imprint</Link> | <Link href="/privacy">Privacy Policy</Link> |{" "}
<Link href="/terms">Terms</Link> | <Link href="/oss-friends">OSS Friends</Link>
</p>
</div>
<div className="flex justify-center space-x-6">
{navigation.social.map((item) => (
<Link key={item.name} href={item.href} className="text-slate-400 hover:text-slate-500">
<span className="sr-only">{item.name}</span>
<item.icon className="h-6 w-6" aria-hidden="true" />
</Link>
))}
</div>
</div>
</footer>
);
}

View File

@@ -1,396 +0,0 @@
import GitHubMarkWhite from "@/images/github-mark-white.svg";
import GitHubMarkDark from "@/images/github-mark.svg";
import {
BaseballIcon,
Button,
CancelSubscriptionIcon,
CodeBookIcon,
DogChaserIcon,
FeedbackIcon,
InterviewPromptIcon,
OnboardingIcon,
PMFIcon,
} from "@formbricks/ui";
import { Popover, Transition } from "@headlessui/react";
import { Bars3Icon, ChevronDownIcon, ChevronRightIcon, XMarkIcon } from "@heroicons/react/24/outline";
import clsx from "clsx";
import { usePlausible } from "next-plausible";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { Fragment, useState } from "react";
import { FooterLogo } from "./Logo";
import { ThemeSelector } from "./ThemeSelector";
function GitHubIcon(props: any) {
return (
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
<path d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z" />
</svg>
);
}
const UnderstandUsers = [
{
name: "Interview Prompt",
href: "/interview-prompt",
status: true,
icon: InterviewPromptIcon,
description: "Interview invites on auto-pilot",
},
{
name: "Measure PMF",
href: "/measure-product-market-fit",
status: true,
icon: PMFIcon,
description: "Improve Product-Market Fit",
},
{
name: "Onboarding Segments",
href: "/onboarding-segmentation",
status: true,
icon: OnboardingIcon,
description: "Get it right from the start",
},
];
const IncreaseRevenue = [
{
name: "Learn from Churn",
href: "/learn-from-churn",
status: true,
icon: CancelSubscriptionIcon,
description: "Churn is hard, but insightful",
},
{
name: "Improve Trial CR",
href: "/improve-trial-conversion",
status: true,
icon: BaseballIcon,
description: "Take guessing out, hit it right",
},
];
const BoostRetention = [
{
name: "Feedback Box",
href: "/feedback-box",
status: true,
icon: FeedbackIcon,
description: "Always keep an ear open",
},
{
name: "Docs Feedback",
href: "/docs-feedback",
status: true,
icon: CodeBookIcon,
description: "Clear docs, more adoption",
},
{
name: "Feature Chaser",
href: "/feature-chaser",
status: true,
icon: DogChaserIcon,
description: "Follow up, improve",
},
];
export default function Header() {
const [mobileSubOpen, setMobileSubOpen] = useState(false);
const plausible = usePlausible();
const router = useRouter();
return (
<Popover className="relative" as="header">
<div className="flex items-center justify-between px-4 py-6 sm:px-6 md:justify-start ">
<div className="flex w-0 flex-1 justify-start">
<Link href="/">
<span className="sr-only">Formbricks</span>
<FooterLogo className="h-8 w-auto sm:h-10" />
</Link>
</div>
<div className="-my-2 -mr-2 md:hidden">
<Popover.Button className="inline-flex items-center justify-center rounded-md bg-slate-100 p-2 text-slate-400 hover:bg-slate-100 hover:text-slate-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-teal-500 dark:bg-slate-700 dark:text-slate-200">
<span className="sr-only">Open menu</span>
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
</Popover.Button>
</div>
<Popover.Group as="nav" className="hidden space-x-10 md:flex">
<Popover className="relative">
{({ open }) => (
<>
<Popover.Button
className={clsx(
open
? "text-slate-600 dark:text-slate-400 "
: "text-slate-400 hover:text-slate-900 dark:hover:text-slate-100",
"group inline-flex items-center rounded-md text-base font-medium hover:text-slate-300 focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 dark:hover:text-slate-50"
)}>
<span>Best Practices</span>
<ChevronDownIcon
className={clsx(
open ? "text-slate-600" : "text-slate-400",
"ml-2 h-5 w-5 group-hover:text-slate-500"
)}
aria-hidden="true"
/>
</Popover.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1">
<Popover.Panel className="absolute z-10 -ml-4 mt-3 w-screen max-w-lg transform lg:left-1/2 lg:ml-0 lg:max-w-4xl lg:-translate-x-1/2">
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5">
<div className="relative grid gap-6 bg-white px-5 py-6 dark:bg-slate-700 sm:gap-6 sm:p-8 lg:grid-cols-3">
<div>
<h4 className="mb-6 ml-16 text-sm text-slate-400 dark:text-slate-300">
Understand Users
</h4>
{UnderstandUsers.map((brick) => (
<Link
key={brick.name}
href={brick.href}
className={clsx(
brick.status
? "cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-600"
: "cursor-default",
"-m-3 flex items-start rounded-lg p-3 py-4"
)}>
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center text-teal-500 sm:h-12 sm:w-12">
<brick.icon className="h-6 w-6" aria-hidden="true" />
</div>
<div className="ml-4">
<p
className={clsx(
brick.status ? "text-slate-900 dark:text-slate-100" : "text-slate-400",
"font-semibold"
)}>
{brick.name}
</p>
<p className="mt-0.5 text-xs text-slate-400">{brick.description}</p>
</div>
</Link>
))}
</div>
<div>
<h4 className="mb-6 ml-16 text-sm text-slate-400 dark:text-slate-300">
Increase Revenue
</h4>
{IncreaseRevenue.map((brick) => (
<Link
key={brick.name}
href={brick.href}
className={clsx(
brick.status
? "cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-600"
: "cursor-default",
"-m-3 flex items-start rounded-lg p-3 py-4"
)}>
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-md text-teal-500 sm:h-12 sm:w-12">
<brick.icon className="h-6 w-6" aria-hidden="true" />
</div>
<div className="ml-4">
<p
className={clsx(
brick.status ? "text-slate-900 dark:text-slate-100" : "text-slate-400",
" font-semibold"
)}>
{brick.name}
</p>
<p className="mt-0.5 text-xs text-slate-400">{brick.description}</p>
</div>
</Link>
))}
</div>
<div>
<h4 className="mb-6 ml-16 text-sm text-slate-400 dark:text-slate-300">
Boost Retention
</h4>
{BoostRetention.map((brick) => (
<Link
key={brick.name}
href={brick.href}
className={clsx(
brick.status
? "cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-600"
: "cursor-default",
"-m-3 flex items-start rounded-lg p-3 py-4"
)}>
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-md text-teal-500 sm:h-12 sm:w-12">
<brick.icon className="h-6 w-6" aria-hidden="true" />
</div>
<div className="ml-4">
<p
className={clsx(
brick.status ? "text-slate-900 dark:text-slate-100" : "text-slate-400",
" font-semibold"
)}>
{brick.name}
</p>
<p className="mt-0.5 text-xs text-slate-400">{brick.description}</p>
</div>
</Link>
))}
</div>
</div>
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
{/* <Link
href="/community"
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
Community
</Link>
*/}
<Link
href="https://formbricks.com/#pricing"
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
Pricing
</Link>
<Link
href="/docs"
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
Docs
</Link>
<Link
href="/blog"
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="/careers"
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
Careers <p className="bg-brand inline rounded-full px-2 text-xs text-white">2</p>
</Link> */}
<Link
href="/concierge"
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
Concierge
</Link>
</Popover.Group>
<div className="hidden flex-1 items-center justify-end md:flex">
<ThemeSelector className="relative z-10 mr-5" />
<Button
variant="secondary"
className="group px-2"
href="https://formbricks.com/github"
target="_blank">
<Image
src={GitHubMarkDark}
alt="GitHub Sponsors Formbricks badge"
width={24}
className="block dark:hidden"
/>
<Image
src={GitHubMarkWhite}
alt="GitHub Sponsors Formbricks badge"
width={24}
className="hidden dark:block"
/>
</Button>
{/* <Button variant="secondary" className="ml-2 px-2" onClick={() => setVideoModal(true)}>
<VideoWalkThrough open={videoModal} setOpen={() => setVideoModal(false)} />
<PlayCircleIcon className="h-6 w-6" />
</Button> */}
<Button
variant="highlight"
className="ml-2"
onClick={() => {
router.push("https://app.formbricks.com");
plausible("NavBar_CTA_Login");
}}>
Go to app
</Button>
</div>
</div>
<Transition
as={Fragment}
enter="duration-200 ease-out"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-100 ease-in"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95">
<Popover.Panel
focus
className="absolute inset-x-0 top-0 z-20 origin-top-right transform p-2 transition md:hidden">
<div className="dark:divide-slate divide-y-2 divide-slate-100 rounded-lg bg-slate-200 shadow-lg ring-1 ring-black ring-opacity-5 dark:divide-slate-700 dark:bg-slate-800">
<div className="px-5 pb-6 pt-5">
<div className="flex items-center justify-between">
<div>
<FooterLogo className="h-8 w-auto" />
</div>
<div className="-mr-2">
<Popover.Button className="inline-flex items-center justify-center rounded-md bg-white p-2 text-slate-400 hover:bg-slate-100 hover:text-slate-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-teal-500 dark:bg-slate-700 dark:text-slate-200">
<span className="sr-only">Close menu</span>
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</Popover.Button>
</div>
</div>
</div>
<div className="px-5 py-6">
<div className="flex flex-col space-y-5 text-center text-sm dark:text-slate-300">
<div>
{mobileSubOpen ? (
<ChevronDownIcon className="mr-2 inline h-4 w-4" />
) : (
<ChevronRightIcon className="mr-2 inline h-4 w-4" />
)}
<button onClick={() => setMobileSubOpen(!mobileSubOpen)}>Best Practices</button>
</div>
{mobileSubOpen && (
<div className="flex flex-col space-y-5 text-center text-sm dark:text-slate-300">
{UnderstandUsers.map((brick) => (
<Link href={brick.href} key={brick.name} className="font-semibold">
{brick.name}
</Link>
))}
{IncreaseRevenue.map((brick) => (
<Link href={brick.href} key={brick.name} className="font-semibold">
{brick.name}
</Link>
))}
{BoostRetention.map((brick) => (
<Link href={brick.href} key={brick.name} className="font-semibold">
{brick.name}
</Link>
))}
<hr className="mx-20 my-6 opacity-25" />
</div>
)}
<Link href="/concierge">Concierge</Link>
<Link href="#pricing">Pricing</Link>
<Link href="/docs">Docs</Link>
<Link href="/blog">Blog</Link>
{/* <Link href="/careers">Careers</Link> */}
<Button
variant="secondary"
EndIcon={GitHubIcon}
onClick={() => router.push("https://github.com/formbricks/formbricks")}
className="flex w-full justify-center fill-slate-800 dark:fill-slate-200">
View on Github
</Button>
<Button
variant="primary"
onClick={() => router.push("https://app.formbricks.com/auth/signup")}
className="flex w-full justify-center">
Get started
</Button>
</div>
</div>
</div>
</Popover.Panel>
</Transition>
</Popover>
);
}

View File

@@ -1,43 +0,0 @@
import { Button } from "@formbricks/ui";
import { Popover } from "@headlessui/react";
import { usePlausible } from "next-plausible";
import Link from "next/link";
import { useRouter } from "next/router";
import { FooterLogo } from "./Logo";
export default function HeaderLight() {
const plausible = usePlausible();
const router = useRouter();
return (
<Popover className="relative" as="header">
<div className="mx-auto flex items-center justify-between py-6 sm:px-2 md:justify-start lg:px-8 xl:px-12 ">
<div className="flex w-0 flex-1 justify-start">
<Link href="/">
<span className="sr-only">Formbricks</span>
<FooterLogo className="ml-7 h-8 w-auto sm:h-10" />
</Link>
</div>
<div className="hidden flex-1 items-center justify-end md:flex">
<Button
variant="secondary"
onClick={() => {
router.push("https://cal.com/johannes/onboarding");
plausible("Demo_CTA_TalkToUs");
}}>
Talk to us
</Button>
<Button
variant="highlight"
className="ml-2"
onClick={() => {
router.push("https://app.formbricks.com/auth/signup");
plausible("Demo_CTA_TryForFree");
}}>
Start for free
</Button>
</div>
</div>
</Popover>
);
}

View File

@@ -1,24 +0,0 @@
import clsx from "clsx";
interface Props {
teaser?: string;
heading: string;
subheading?: string;
closer?: boolean;
}
export default function HeadingCentered({ teaser, heading, subheading, closer }: Props) {
return (
<div className={clsx(closer ? "pt-16 lg:pt-24" : "pt-24 lg:pt-40", "px-2 pb-4 text-center md:pb-12")}>
<p className="text-md text-brand-dark dark:text-brand-light mx-auto mb-3 max-w-2xl font-semibold uppercase sm:mt-4">
{teaser}
</p>
<h2 className="text-3xl font-bold tracking-tight text-slate-800 dark:text-slate-100 sm:text-4xl">
{heading}
</h2>
<p className="mx-auto mt-3 max-w-3xl text-xl text-slate-500 dark:text-slate-300 sm:mt-4">
{subheading}
</p>
</div>
);
}

View File

@@ -1,28 +0,0 @@
import { useEffect, useRef, useState } from "react";
import type { LottiePlayer } from "lottie-web";
export default function HeroAnimation(props: any) {
const ref = useRef<HTMLDivElement>(null);
const [lottie, setLottie] = useState<LottiePlayer | null>(null);
useEffect(() => {
import("lottie-web").then((Lottie) => setLottie(Lottie.default));
}, []);
useEffect(() => {
if (lottie && ref.current) {
const animation = lottie.loadAnimation({
container: ref.current,
renderer: "svg",
loop: true,
autoplay: true,
// path to your animation file, place it inside public folder
path: "/animations/xm-hero-v1.json",
});
return () => animation.destroy();
}
}, [lottie]);
return <div ref={ref} {...props} />;
}

View File

@@ -1,25 +0,0 @@
interface Props {
headingPt1: string;
headingTeal?: string;
headingPt2?: string;
subheading?: string;
children?: React.ReactNode;
}
export default function HeroTitle({ headingPt1, headingTeal, headingPt2, subheading, children }: Props) {
return (
<div className="px-4 py-20 text-center sm:px-6 lg:px-8 lg:py-28">
<h1 className="text-3xl font-bold tracking-tight text-slate-800 dark:text-slate-200 sm:text-4xl md:text-5xl">
<span className="xl:inline">{headingPt1}</span>{" "}
<span className="from-brand-light to-brand-dark bg-gradient-to-b bg-clip-text text-transparent xl:inline">
{headingTeal}
</span>{" "}
<span className="inline ">{headingPt2}</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-2xl md:text-xl">
{subheading}
</p>
<div className="mx-auto mt-5 max-w-md sm:flex sm:justify-center md:mt-8">{children}</div>
</div>
);
}

View File

@@ -1,69 +0,0 @@
import { useId } from "react";
import clsx from "clsx";
import { InstallationIcon } from "@/components/shared/icons/InstallationIcon";
import { LightbulbIcon } from "@/components/shared/icons/LightbulbIcon";
import { PluginsIcon } from "@/components/shared/icons/PluginsIcon";
import { PresetsIcon } from "@/components/shared/icons/PresetsIcon";
import { ThemingIcon } from "@/components/shared/icons/ThemingIcon";
import { WarningIcon } from "@/components/shared/icons/WarningIcon";
const icons = {
installation: InstallationIcon,
presets: PresetsIcon,
plugins: PluginsIcon,
theming: ThemingIcon,
lightbulb: LightbulbIcon,
warning: WarningIcon,
};
const iconStyles = {
slate: "[--icon-foreground:theme(colors.slate.900)] [--icon-background:theme(colors.white)]",
amber: "[--icon-foreground:theme(colors.amber.900)] [--icon-background:theme(colors.amber.100)]",
};
export function Icon({ color = "slate", icon, className, ...props }) {
let id = useId();
let IconComponent = icons[icon];
return (
<svg
aria-hidden="true"
viewBox="0 0 32 32"
fill="none"
className={clsx(className, iconStyles[color])}
{...props}>
<IconComponent id={id} color={color} />
</svg>
);
}
const gradients = {
slate: [
{ stopColor: "#0EA5E9" },
{ stopColor: "#22D3EE", offset: ".527" },
{ stopColor: "#818CF8", offset: 1 },
],
amber: [
{ stopColor: "#FDE68A", offset: ".08" },
{ stopColor: "#F59E0B", offset: ".837" },
],
};
export function Gradient({ color = "slate", ...props }) {
return (
<radialGradient cx={0} cy={0} r={1} gradientUnits="userSpaceOnUse" {...props}>
{gradients[color].map((stop, stopIndex) => (
<stop key={stopIndex} {...stop} />
))}
</radialGradient>
);
}
export function LightMode({ className, ...props }) {
return <g className={clsx("dark:hidden", className)} {...props} />;
}
export function DarkMode({ className, ...props }) {
return <g className={clsx("hidden dark:inline", className)} {...props} />;
}

View File

@@ -1,24 +0,0 @@
import Footer from "./Footer";
import Header from "./Header";
import MetaInformation from "./MetaInformation";
interface LayoutProps {
children: React.ReactNode;
title: string;
description: string;
}
export default function Layout({ title, description, children }: LayoutProps) {
return (
<div className="flex h-screen flex-col justify-between">
<MetaInformation title={title} description={description} />
<Header />
{
<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}
</main>
}
<Footer />
</div>
);
}

View File

@@ -1,24 +0,0 @@
import Footer from "./Footer";
import MetaInformation from "./MetaInformation";
import HeaderLight from "./HeaderLight";
interface LayoutProps {
children: React.ReactNode;
title: string;
description: string;
}
export default function Layout({ title, description, children }: LayoutProps) {
return (
<div className="flex h-screen flex-col justify-between">
<MetaInformation title={title} description={description} />
<HeaderLight />
{
<main className="relative mx-auto mb-auto flex w-full flex-col justify-center sm:px-2 lg:px-8 xl:px-12">
{children}
</main>
}
<Footer />
</div>
);
}

View File

@@ -1,67 +0,0 @@
import { useEffect } from "react";
import Footer from "./Footer";
import Header from "./Header";
import MetaInformation from "./MetaInformation";
import { Prose } from "./Prose";
const useExternalLinks = (selector: string) => {
useEffect(() => {
const links = document.querySelectorAll(selector);
links.forEach((link) => {
link.setAttribute("target", "_blank");
link.setAttribute("rel", "noopener noreferrer");
});
return () => {
links.forEach((link) => {
link.removeAttribute("target");
link.removeAttribute("rel");
});
};
}, [selector]);
};
interface Props {
meta: {
title: string;
description: string;
publishedTime: string;
authors: string[];
section: string;
tags: string[];
};
children: JSX.Element;
}
export default function LayoutMdx({ meta, children }: Props) {
useExternalLinks(".prose a");
return (
<div className="flex h-screen flex-col justify-between">
<MetaInformation
title={meta.title}
description={meta.description}
publishedTime={meta.publishedTime}
authors={meta.authors}
section={meta.section}
tags={meta.tags}
/>
<Header />
<main className="min-w-0 max-w-2xl flex-auto px-4 lg:max-w-none lg:pl-8 lg:pr-0 xl:px-16">
<article className="mx-auto my-16 max-w-3xl px-2">
{meta.title && (
<header className="mb-9 space-y-1">
{meta.title && (
<h1 className="font-display text-3xl tracking-tight text-slate-800 dark:text-slate-100">
{meta.title}
</h1>
)}
</header>
)}
<Prose className="">{children}</Prose>
</article>
</main>
<Footer />
</div>
);
}

View File

@@ -1,36 +0,0 @@
import Image from "next/image";
import logomark from "@/images/logo/logomark.svg";
import logo from "@/images/logo/logo.svg";
import logoDark from "@/images/logo/logo_dark.svg";
import footerLogo from "@/images/logo/footerlogo.svg";
import footerLogoDark from "@/images/logo/footerlogo-dark.svg";
export function Logomark(props: any) {
return <Image src={logomark} {...props} alt="Formbricks Open source Forms & Surveys Logomark" />;
}
export function Logo(props: any) {
return (
<div>
<div className="block dark:hidden">
<Image src={logo} {...props} alt="Formbricks Open source Forms & Surveys Logo" />
</div>
<div className="hidden dark:block">
<Image src={logoDark} {...props} alt="Formbricks Open source Forms & Surveys Logo" />
</div>
</div>
);
}
export function FooterLogo(props: any) {
return (
<div>
<div className="block dark:hidden">
<Image src={footerLogo} {...props} alt="Formbricks Open source Forms & Surveys Logo" />
</div>
<div className="hidden dark:block">
<Image src={footerLogoDark} {...props} alt="Formbricks Open source Forms & Surveys Logo" />
</div>
</div>
);
}

View File

@@ -1,34 +0,0 @@
import { Button } from "@formbricks/ui";
import { useRouter } from "next/router";
export default function CTA() {
const router = useRouter();
return (
<>
<div className="mx-auto py-16 lg:pb-40 lg:pt-24">
<p className="text-md text-brand-dark dark:text-brand-light font-semibold uppercase">
It&apos;s free & open-source
</p>
<p className="my-0 text-4xl font-semibold tracking-tight text-slate-800 dark:text-slate-100">
Try Formbricks right now!
</p>
<div className="mt-12 grid grid-cols-1 content-center md:grid-cols-2">
<div className="-mb-2 rounded-t-xl bg-gradient-to-br from-slate-300 to-slate-200 text-center text-slate-900 dark:from-slate-800 dark:to-slate-900 dark:text-slate-200 md:-mr-5 md:mb-0 md:ml-2.5 md:rounded-l-xl">
<h3 className="text-3xl font-bold">Self-hosted</h3>
<p className="mb-4 mt-2 dark:text-slate-400">Run locally with docker-compose.</p>
<Button variant="secondary" onClick={() => router.push("/docs")} className="mb-8 mt-3 md:mb-0">
Read docs
</Button>
</div>
<div className="rounded-xl bg-gradient-to-br from-slate-400 to-slate-300 pb-10 text-center text-slate-800 dark:from-slate-800 dark:to-slate-700 dark:text-slate-200">
<h3 className="text-3xl font-bold">Cloud</h3>
<p className="mb-4 mt-2 dark:text-slate-400">Use our free managed service.</p>
<Button variant="secondary" onClick={() => router.push("/waitlist")} className="mt-3">
Get started
</Button>
</div>
</div>
</div>
</>
);
}

View File

@@ -1,36 +0,0 @@
import { Button } from "@formbricks/ui";
import { DocumentDuplicateIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
export default function HeadingCentered() {
const router = useRouter();
return (
<div className="mx-auto grid grid-cols-1 content-center gap-10 pb-12 pt-24 md:grid-cols-2">
<div className="">
<p className="text-md text-brand-dark dark:text-brand-light font-semibold uppercase">
What are you waiting for?
</p>
<p className="my-0 text-4xl font-semibold tracking-tight text-slate-800 dark:text-slate-100">
Try it right now!
</p>
<p className="text-slate-500 dark:text-slate-300 ">
Dive right in or browse docs for examples. Questions? Join our Discord, were happy to help
</p>
<Button variant="secondary" onClick={() => router.push("/discord")}>
Join Discord
</Button>
<Button variant="primary" className="ml-3" onClick={() => router.push("/waitlist")}>
Get started
</Button>
</div>
<div className="flex items-center pt-10">
<div className="flex h-20 w-full items-center justify-between rounded-lg bg-slate-300 px-8 text-slate-700 dark:bg-slate-800 dark:text-slate-200 ">
<p>npm install @formbricks/react</p>
<button onClick={() => navigator.clipboard.writeText("npm install @formbricks/react")}>
<DocumentDuplicateIcon className="h-8 w-8" />
</button>
</div>
</div>
</div>
);
}

View File

@@ -1,49 +0,0 @@
import Head from "next/head";
interface Props {
title: string;
description: string;
publishedTime?: string;
updatedTime?: string;
authors?: string[];
section?: string;
tags?: string[];
}
export default function MetaInformation({
title,
description,
publishedTime,
updatedTime,
authors,
section,
tags,
}: Props) {
const pageTitle = `${title} | Open-Source Experience Management, Privacy-first`;
return (
<Head>
<title>{pageTitle}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={`https://${process.env.VERCEL_URL}/social-image.png`} />
<meta property="og:image:alt" content="Formbricks - Open Source Experience Management, Privacy-first" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:locale" content="en_US" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Formbricks Privacy-first Experience Management Solution" />
<meta property="article:publisher" content="Formbricks GmbH" />
{publishedTime && <meta property="article:published_time" content={publishedTime} />}
{updatedTime && <meta property="article:updated_time" content={updatedTime} />}
{authors && <meta property="article:author" content={authors.join(", ")} />}
{section && <meta property="article:section" content={section} />}
{tags && <meta property="article:tag" content={tags.join(", ")} />}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@formbricks" />
<meta name="twitter:creator" content="@formbricks" />
<meta name="theme-color" content="#00C4B8" />
</Head>
);
}

View File

@@ -1,69 +0,0 @@
import { useEffect, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { Dialog } from "@headlessui/react";
import { Logomark } from "@/components/shared/Logo";
import { Navigation } from "@/components/shared/Navigation";
function MenuIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" strokeWidth="2" strokeLinecap="round" {...props}>
<path d="M4 7h16M4 12h16M4 17h16" />
</svg>
);
}
function CloseIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" strokeWidth="2" strokeLinecap="round" {...props}>
<path d="M5 5l14 14M19 5l-14 14" />
</svg>
);
}
export function MobileNavigation({ navigation }) {
let router = useRouter();
let [isOpen, setIsOpen] = useState(false);
useEffect(() => {
if (!isOpen) return;
function onRouteChange() {
setIsOpen(false);
}
router.events.on("routeChangeComplete", onRouteChange);
router.events.on("routeChangeError", onRouteChange);
return () => {
router.events.off("routeChangeComplete", onRouteChange);
router.events.off("routeChangeError", onRouteChange);
};
}, [router, isOpen]);
return (
<>
<button type="button" onClick={() => setIsOpen(true)} className="relative" aria-label="Open navigation">
<MenuIcon className="h-6 w-6 stroke-slate-500" />
</button>
<Dialog
open={isOpen}
onClose={setIsOpen}
className="fixed inset-0 z-50 flex items-start overflow-y-auto bg-slate-900/50 pr-10 backdrop-blur lg:hidden"
aria-label="Navigation">
<Dialog.Panel className="min-h-full w-full max-w-xs bg-white px-4 pt-5 pb-12 dark:bg-slate-900 sm:px-6">
<div className="flex items-center">
<button type="button" onClick={() => setIsOpen(false)} aria-label="Close navigation">
<CloseIcon className="h-6 w-6 stroke-slate-500" />
</button>
<Link href="/" className="ml-6" aria-label="Home page">
<Logomark className="h-9 w-9" />
</Link>
</div>
<Navigation navigation={navigation} className="mt-5 px-1" />
</Dialog.Panel>
</Dialog>
</>
);
}

View File

@@ -1,75 +0,0 @@
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-slate-100 text-left shadow-xl transition-all dark:bg-slate-800 sm:my-8 sm:w-full sm:max-w-xl ",
`${noPadding ? "" : "px-4 pb-4 pt-5 sm:p-6"}`
)}>
<div className="absolute right-0 top-0 hidden pr-4 pt-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 dark:bg-slate-900"
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;

View File

@@ -1,51 +0,0 @@
import Link from "next/link";
import { useRouter } from "next/router";
import clsx from "clsx";
interface NavigationProps {
navigation: {
title: string;
links: {
title: string;
href: string;
}[];
}[];
className: string;
preserveScroll: () => void;
linkRef: React.RefObject<HTMLLIElement>;
}
export function Navigation({ navigation, className, preserveScroll, linkRef }: NavigationProps) {
let router = useRouter();
return (
<nav className={clsx("text-base lg:text-sm", className)}>
<ul role="list" className="space-y-9">
{navigation.map((section) => (
<li key={section.title}>
<h2 className="font-display font-medium text-slate-800 dark:text-slate-100">{section.title}</h2>
<ul
role="list"
className="mt-2 space-y-2 border-l-2 border-slate-100 dark:border-slate-800 lg:mt-4 lg:space-y-4 lg:border-slate-200">
{section.links.map((link) => (
<li key={link.href} className="relative" ref={linkRef}>
<Link
onClick={preserveScroll}
href={link.href}
className={clsx(
"block w-full pl-3.5 before:pointer-events-none before:absolute before:-left-1 before:top-1/2 before:h-1.5 before:w-1.5 before:-translate-y-1/2 before:rounded-full",
link.href === router.pathname
? "text-brand before:bg-brand font-semibold"
: "text-slate-500 before:hidden before:bg-slate-300 hover:text-slate-600 hover:before:block dark:text-slate-400 dark:before:bg-slate-700 dark:hover:text-slate-300"
)}>
{link.title}
</Link>
</li>
))}
</ul>
</li>
))}
</ul>
</nav>
);
}

View File

@@ -1,61 +0,0 @@
import Friends from "@/images/newsletter-signup-gif.gif";
import { Button } from "@formbricks/ui";
import Image from "next/image";
export default function WaitlistForm() {
return (
<div className="not-prose text-md mx-auto mt-12 max-w-7xl rounded-lg bg-slate-200 p-10 text-slate-500 shadow-lg dark:bg-slate-800 dark:text-slate-400">
<p className="my-0 text-2xl font-bold text-slate-600 dark:text-slate-300">Build in public</p>
Get all the juicy details of our journey building Formbricks in public 👇
<div className="mt-8 gap-4 md:grid md:grid-cols-2">
<form method="post" action="https://listmonk.formbricks.com/subscription/form">
<div className="p-6 ">
<div>
<input type="hidden" name="nonce" />
</div>
<div>
<input
type="email"
name="email"
required
placeholder="Email"
className="block w-full rounded-xl text-slate-900 shadow-sm focus:border-slate-500 focus:ring-slate-500 sm:text-sm"
/>
<label htmlFor="email" className="ml-2 block text-sm text-slate-400 dark:text-slate-500">
Work or personal
</label>
</div>
<div>
<input
type="text"
name="name"
placeholder="Name"
required
className="mt-4 block w-full rounded-xl text-slate-900 shadow-sm focus:border-slate-500 focus:ring-slate-500 sm:text-sm"
/>
<label htmlFor="name" className="ml-2 block text-sm text-slate-400 dark:text-slate-500">
Optional but appreciated
</label>
</div>
<div className="hidden">
<input
id="e0084"
type="checkbox"
name="l"
checked
value="e0084486-8751-43e4-8cfb-58b7c3f5f318"
readOnly
/>
<label htmlFor="e0084">Stay in the loop</label>
</div>
<Button type="submit" className="mt-5 w-full justify-center">
Subscribe
</Button>
</div>
</form>
<Image src={Friends} alt="Sign up to newsletter" className="not-prose rounded-xl" />
</div>
</div>
);
}

View File

@@ -1,162 +0,0 @@
import { Button } from "@formbricks/ui";
import clsx from "clsx";
import HeadingCentered from "./HeadingCentered";
import { CheckIcon } from "@heroicons/react/24/outline";
import { usePlausible } from "next-plausible";
import { useRouter } from "next/router";
const tiers = [
{
name: "Self-hosting",
priceMonthly: "free",
paymentRythm: "/always",
button: "secondary",
discounted: false,
highlight: false,
description: "Host Formbricks on your own server.",
features: [
"All Free features",
"Easy self-hosting (Docker)",
"Unlimited surveys",
"Unlimited responses",
"Unlimited team members",
],
ctaName: "Read docs",
plausibleGoal: "Pricing_CTA_SelfHosting",
href: "/docs/self-hosting/deployment",
},
{
name: "Cloud",
href: "https://app.formbricks.com/auth/signup",
priceMonthly: "$0",
paymentRythm: "/month",
button: "highlight",
discounted: false,
highlight: true,
description: "Start with the 'Free forever' plan.",
features: [
"Unlimited surveys",
"In-product surveys",
"Link surveys",
"Remove branding",
"Granular targeting",
"30+ templates",
"API access",
"Integrations (Zapier, Make, ...)",
"Unlimited team members",
"100 responses per survey",
],
ctaName: "Get started",
plausibleGoal: "Pricing_CTA_FreePlan",
},
{
name: "Cloud Pro",
href: "https://app.formbricks.com/auth/signup",
priceMonthly: "$99",
paymentRythm: "/month",
button: "secondary",
discounted: false,
highlight: false,
description: "All features, unlimited usage.",
features: ["Everything in 'Cloud'", "Unlimited responses per survey"],
ctaName: "Start for free",
plausibleGoal: "Pricing_CTA_ProPlan",
},
];
export default function Pricing() {
const plausible = usePlausible();
const router = useRouter();
return (
<div className="-mt-10 pb-20">
<div className="mx-auto max-w-7xl py-4 sm:px-6 sm:pb-6 lg:px-8" id="pricing">
<HeadingCentered heading="One price, unlimited usage." teaser="Pricing" />
<div className="mx-auto space-y-4 px-4 lg:grid lg:grid-cols-3 lg:gap-6 lg:space-y-0 lg:px-0">
{tiers.map((tier) => (
<div
key={tier.name}
className={clsx(
`h-fit rounded-lg shadow-sm`,
tier.highlight
? "border border-slate-300 bg-slate-200 dark:border-slate-500 dark:bg-slate-800"
: "bg-slate-100 dark:bg-slate-700"
)}>
<div className="p-8">
<h2
className={clsx(
"inline-flex text-3xl font-bold",
tier.highlight
? "text-slate-700 dark:text-slate-200"
: "text-slate-500 dark:text-slate-300"
)}>
{tier.name}
</h2>
<p className="mt-4 whitespace-pre-wrap text-sm text-slate-600 dark:text-slate-300">
{tier.description}
</p>
<ul className="mt-4 space-y-4">
{tier.features.map((feature, index) => (
<li key={index} className="flex items-start">
<div className="rounded-full border border-green-300 bg-green-100 p-0.5 dark:border-green-600 dark:bg-green-900">
<CheckIcon className="h-5 w-5 p-0.5 text-green-500 dark:text-green-300" />
</div>
<span className="ml-2 text-sm text-slate-500 dark:text-slate-400">{feature}</span>
</li>
))}
</ul>
<p className="mt-8">
<span
className={clsx(
`text-4xl font-light`,
tier.highlight
? "text-slate-800 dark:text-slate-100"
: "text-slate-500 dark:text-slate-200",
tier.discounted ? "decoration-brand line-through" : ""
)}>
{tier.priceMonthly}
</span>{" "}
<span className="text-4xl font-bold text-slate-900 dark:text-slate-50">
{tier.discounted && "$49"}
</span>
<span
className={clsx(
"text-base font-medium",
tier.highlight
? "text-slate-500 dark:text-slate-400"
: "text-slate-400 dark:text-slate-500"
)}>
{tier.paymentRythm}
</span>
</p>
<Button
onClick={() => {
plausible(`${tier.plausibleGoal}`);
router.push(`${tier.href}`);
}}
className={clsx(
"mt-6 w-full justify-center py-4 text-lg shadow-sm",
tier.highlight
? ""
: "bg-slate-300 hover:bg-slate-200 dark:bg-slate-600 dark:hover:bg-slate-500"
)}
variant={tier.highlight ? "highlight" : "secondary"}>
{tier.ctaName}
</Button>
{tier.name == "Cloud Pro" && (
<p className="mt-1.5 text-center text-xs text-slate-500">No Creditcard required.</p>
)}
{tier.name == "Cloud" && (
<p className="mt-1.5 text-center text-xs text-slate-500">Free forever 🤍</p>
)}
</div>
</div>
))}
</div>
</div>
</div>
);
}

View File

@@ -1,25 +0,0 @@
import clsx from "clsx";
export function Prose({ as: Component = "div", className, ...props }) {
return (
<Component
className={clsx(
className,
"prose prose-slate max-w-none dark:prose-invert dark:text-slate-400",
// headings
"prose-headings:scroll-mt-28 prose-headings:font-display prose-headings:font-normal lg:prose-headings:scroll-mt-[8.5rem]",
// lead
"prose-lead:text-slate-500 dark:prose-lead:text-slate-400",
// links
"prose-a:font-semibold dark:prose-a:text-brand",
// link underline
"prose-a:no-underline prose-a:shadow-[inset_0_-2px_0_0_var(--tw-prose-background,#fff),inset_0_calc(-1*(var(--tw-prose-underline-size,4px)+2px))_0_0_var(--tw-prose-underline,theme(colors.brand.dark))] hover:prose-a:[--tw-prose-underline-size:6px] dark:[--tw-prose-background:theme(colors.slate.900)] dark:prose-a:shadow-[inset_0_calc(-1*var(--tw-prose-underline-size,2px))_0_0_var(--tw-prose-underline,theme(colors.slate.800))] dark:hover:prose-a:[--tw-prose-underline-size:6px]",
// pre
"prose-pre:rounded-xl prose-pre:bg-slate-900 prose-pre:shadow-lg dark:prose-pre:bg-slate-800/60 dark:prose-pre:shadow-none dark:prose-pre:ring-1 dark:prose-pre:ring-slate-300/10",
// hr
"dark:prose-hr:border-slate-800"
)}
{...props}
/>
);
}

View File

@@ -1,82 +0,0 @@
import { useCallback, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import Link from "next/link";
import Router from "next/router";
import { DocSearchModal, useDocSearchKeyboardEvents } from "@docsearch/react";
const docSearchConfig = {
appId: process.env.NEXT_PUBLIC_DOCSEARCH_APP_ID || "",
apiKey: process.env.NEXT_PUBLIC_DOCSEARCH_API_KEY || "",
indexName: process.env.NEXT_PUBLIC_DOCSEARCH_INDEX_NAME || "",
};
interface HitProps {
hit: { url: string };
children: React.ReactNode;
}
function Hit({ hit, children }: HitProps) {
return <Link href={hit.url}>{children}</Link>;
}
function SearchIcon(props: any) {
return (
<svg aria-hidden="true" viewBox="0 0 20 20" {...props}>
<path d="M16.293 17.707a1 1 0 0 0 1.414-1.414l-1.414 1.414ZM9 14a5 5 0 0 1-5-5H2a7 7 0 0 0 7 7v-2ZM4 9a5 5 0 0 1 5-5V2a7 7 0 0 0-7 7h2Zm5-5a5 5 0 0 1 5 5h2a7 7 0 0 0-7-7v2Zm8.707 12.293-3.757-3.757-1.414 1.414 3.757 3.757 1.414-1.414ZM14 9a4.98 4.98 0 0 1-1.464 3.536l1.414 1.414A6.98 6.98 0 0 0 16 9h-2Zm-1.464 3.536A4.98 4.98 0 0 1 9 14v2a6.98 6.98 0 0 0 4.95-2.05l-1.414-1.414Z" />
</svg>
);
}
export function Search() {
let [isOpen, setIsOpen] = useState(false);
let [modifierKey, setModifierKey] = useState<string>();
const onOpen = useCallback(() => {
setIsOpen(true);
}, [setIsOpen]);
const onClose = useCallback(() => {
setIsOpen(false);
}, [setIsOpen]);
useDocSearchKeyboardEvents({ isOpen, onOpen, onClose });
useEffect(() => {
setModifierKey(/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? "⌘" : "Ctrl ");
}, []);
return (
<>
<button
type="button"
className="group flex h-6 w-6 items-center justify-center sm:justify-start md:h-auto md:w-60 md:flex-none md:rounded-lg md:py-2.5 md:pl-4 md:pr-3.5 md:text-sm md:ring-1 md:ring-slate-200 md:hover:ring-slate-300 dark:md:bg-slate-800/75 dark:md:ring-inset dark:md:ring-white/5 dark:md:hover:bg-slate-700/40 dark:md:hover:ring-slate-500 xl:w-80"
onClick={onOpen}>
<SearchIcon className="h-5 w-5 flex-none fill-slate-400 group-hover:fill-slate-500 dark:fill-slate-500 md:group-hover:fill-slate-400" />
<span className="sr-only md:not-sr-only md:pl-2 md:text-slate-500 md:dark:text-slate-400">
Search docs
</span>
{modifierKey && (
<kbd className="ml-auto hidden font-medium text-slate-400 dark:text-slate-500 md:block">
<kbd className="font-sans">{modifierKey}</kbd>
<kbd className="font-sans">K</kbd>
</kbd>
)}
</button>
{isOpen &&
createPortal(
<DocSearchModal
{...docSearchConfig}
initialScrollY={window.scrollY}
onClose={onClose}
hitComponent={Hit}
navigator={{
navigate({ itemUrl }) {
Router.push(itemUrl);
},
}}
/>,
document.body
)}
</>
);
}

View File

@@ -1,111 +0,0 @@
import { useEffect, useState } from "react";
import { Listbox } from "@headlessui/react";
import clsx from "clsx";
const themes = [
{ name: "Light", value: "light", icon: LightIcon },
{ name: "Dark", value: "dark", icon: DarkIcon },
{ name: "System", value: "system", icon: SystemIcon },
];
function LightIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7 1a1 1 0 0 1 2 0v1a1 1 0 1 1-2 0V1Zm4 7a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm2.657-5.657a1 1 0 0 0-1.414 0l-.707.707a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0 0-1.414Zm-1.415 11.313-.707-.707a1 1 0 0 1 1.415-1.415l.707.708a1 1 0 0 1-1.415 1.414ZM16 7.999a1 1 0 0 0-1-1h-1a1 1 0 1 0 0 2h1a1 1 0 0 0 1-1ZM7 14a1 1 0 1 1 2 0v1a1 1 0 1 1-2 0v-1Zm-2.536-2.464a1 1 0 0 0-1.414 0l-.707.707a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0 0-1.414Zm0-8.486A1 1 0 0 1 3.05 4.464l-.707-.707a1 1 0 0 1 1.414-1.414l.707.707ZM3 8a1 1 0 0 0-1-1H1a1 1 0 0 0 0 2h1a1 1 0 0 0 1-1Z"
/>
</svg>
);
}
function DarkIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.23 3.333C7.757 2.905 7.68 2 7 2a6 6 0 1 0 0 12c.68 0 .758-.905.23-1.332A5.989 5.989 0 0 1 5 8c0-1.885.87-3.568 2.23-4.668ZM12 5a1 1 0 0 1 1 1 1 1 0 0 0 1 1 1 1 0 1 1 0 2 1 1 0 0 0-1 1 1 1 0 1 1-2 0 1 1 0 0 0-1-1 1 1 0 1 1 0-2 1 1 0 0 0 1-1 1 1 0 0 1 1-1Z"
/>
</svg>
);
}
function SystemIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 4a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3h-1.5l.31 1.242c.084.333.36.573.63.808.091.08.182.158.264.24A1 1 0 0 1 11 15H5a1 1 0 0 1-.704-1.71c.082-.082.173-.16.264-.24.27-.235.546-.475.63-.808L5.5 11H4a3 3 0 0 1-3-3V4Zm3-1a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Z"
/>
</svg>
);
}
export function ThemeSelector(props) {
let [selectedTheme, setSelectedTheme] = useState();
useEffect(() => {
if (selectedTheme) {
document.documentElement.setAttribute("data-theme", selectedTheme.value);
} else {
setSelectedTheme(
themes.find((theme) => theme.value === document.documentElement.getAttribute("data-theme"))
);
}
}, [selectedTheme]);
useEffect(() => {
let handler = () =>
setSelectedTheme(themes.find((theme) => theme.value === (window.localStorage.theme ?? "system")));
window.addEventListener("storage", handler);
return () => window.removeEventListener("storage", handler);
}, []);
return (
<Listbox as="div" value={selectedTheme} onChange={setSelectedTheme} {...props}>
<Listbox.Label className="sr-only">Theme</Listbox.Label>
<Listbox.Button
className="flex h-6 w-6 items-center justify-center rounded-lg shadow-md shadow-black/5 ring-1 ring-black/5 dark:bg-slate-700 dark:ring-inset dark:ring-white/5"
aria-label={selectedTheme?.name}>
<LightIcon className="hidden h-4 w-4 fill-brand [[data-theme=light]_&]:block" />
<DarkIcon className="hidden h-4 w-4 fill-brand [[data-theme=dark]_&]:block" />
<LightIcon className="hidden h-4 w-4 fill-slate-400 [:not(.dark)[data-theme=system]_&]:block" />
<DarkIcon className="hidden h-4 w-4 fill-slate-400 [.dark[data-theme=system]_&]:block" />
</Listbox.Button>
<Listbox.Options className="absolute top-full left-1/2 mt-3 w-36 -translate-x-1/2 space-y-1 rounded-xl bg-white p-3 text-sm font-medium shadow-md shadow-black/5 ring-1 ring-black/5 dark:bg-slate-800 dark:ring-white/5">
{themes.map((theme) => (
<Listbox.Option
key={theme.value}
value={theme}
className={({ active, selected }) =>
clsx("flex cursor-pointer select-none items-center rounded-[0.625rem] p-1", {
"text-brand-dark dark:text-brand-light": selected,
"text-slate-800 dark:text-slate-100": active && !selected,
"text-slate-700 dark:text-slate-400": !active && !selected,
"bg-slate-100 dark:bg-slate-900/40": active,
})
}>
{({ selected }) => (
<>
<div className="rounded-md bg-white p-1 shadow ring-1 ring-slate-900/5 dark:bg-slate-700 dark:ring-inset dark:ring-white/5">
<theme.icon
className={clsx(
"h-4 w-4",
selected ? "fill-brand-dark dark:fill-brand-light" : "fill-slate-400"
)}
/>
</div>
<div className="ml-3">{theme.name}</div>
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Listbox>
);
}

View File

@@ -1,38 +0,0 @@
import { Button } from "@formbricks/ui";
import { DocumentDuplicateIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
export default function HeadingCentered() {
const router = useRouter();
return (
<div className="mx-auto grid max-w-md grid-cols-1 content-center gap-10 px-4 py-12 sm:max-w-3xl sm:px-6 md:grid-cols-2 md:pb-36 md:pt-24 lg:max-w-6xl lg:px-8">
<div className="">
<p className="text-md text-brand-dark dark:text-brand-light mb-3 font-semibold uppercase">
What are you waiting for?
</p>
<h2 className="text-3xl font-bold tracking-tight text-slate-800 dark:text-slate-100 sm:text-4xl">
Try it right now!
</h2>
<p className="my-3 text-slate-500 dark:text-slate-300 sm:mb-6 sm:mt-4 md:text-lg">
Dive right in or browse docs for examples.
<br />
Questions? Join our Discord, were happy to help!
</p>
<Button variant="secondary" onClick={() => router.push("/docs")}>
Read docs
</Button>
<Button variant="primary" className="ml-3" onClick={() => router.push("/waitlist")}>
Get started
</Button>
</div>
<div className="flex items-center">
<div className="flex h-20 w-full items-center justify-between rounded-lg bg-slate-800 px-8 text-slate-100 ">
<p>npm install @formbricks/react</p>
<button onClick={() => navigator.clipboard.writeText("npm install @formbricks/react")}>
<DocumentDuplicateIcon className="h-8 w-8" />
</button>
</div>
</div>
</div>
);
}

View File

@@ -1,29 +0,0 @@
import { Button } from "@formbricks/ui";
import { useRouter } from "next/router";
interface UseCaseCTAProps {
href: string;
}
export default function UseCaseHeader({ href }: UseCaseCTAProps) {
/* const plausible = usePlausible(); */
const router = useRouter();
return (
<div className="my-8 flex space-x-2 whitespace-nowrap">
<Button variant="secondary" href={href}>
Step-by-step manual
</Button>
<div className="space-y-1 text-center">
<Button
variant="darkCTA"
onClick={() => {
router.push("https://app.formbricks.com/auth/signup");
/* plausible("BestPractice_SubPage_CTA_TryItNow"); */
}}>
Try it now
</Button>
<p className="text-xs text-slate-400">It&apos;s free</p>
</div>
</div>
);
}

View File

@@ -1,26 +0,0 @@
interface UseCaseHeaderProps {
title: string;
difficulty: string;
setupMinutes: string;
}
export default function UseCaseHeader({ title, difficulty, setupMinutes }: UseCaseHeaderProps) {
return (
<div>
<div className="mb-4 flex-wrap space-y-2">
<h1 className="mb-2 inline whitespace-nowrap pr-4 text-3xl font-semibold text-slate-800 dark:text-slate-200 ">
{title}
</h1>
<div className="inline-flex items-center justify-center whitespace-nowrap ">
<div className="rounded-full bg-indigo-200 px-4 py-1 text-sm text-indigo-700 dark:bg-indigo-800 dark:text-indigo-200 ">
{difficulty}
</div>
<div className="ml-2 rounded-full bg-slate-300 px-4 py-1 text-sm text-slate-700 dark:bg-slate-700 dark:text-slate-200 ">
{setupMinutes} minutes
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,80 +0,0 @@
import {
UsersIcon,
CubeTransparentIcon,
UserGroupIcon,
CommandLineIcon,
SwatchIcon,
SquaresPlusIcon,
} from "@heroicons/react/24/outline";
const features = [
{
name: "Futureproof",
description: "Form needs change. With Formbricks youll avoid island solutions right from the start.",
icon: CubeTransparentIcon,
},
{
name: "Privacy by design",
description: "Self-host the entire product and fly through privacy compliance reviews.",
icon: UsersIcon,
},
{
name: "Community driven",
description: "We're building for you. If you need something specific, were happy to build it!",
icon: UserGroupIcon,
},
{
name: "Great DX",
description: "We love a solid developer experience. We felt your pain and do our best to avoid it.",
icon: CommandLineIcon,
},
{
name: "Customizable",
description: "We have to build opinionated. If it doesn't suit your need, just change it up.",
icon: SwatchIcon,
},
{
name: "Extendable",
description: "Even though we try, we cannot build every single integration. With Formbricks, you can.",
icon: SquaresPlusIcon,
},
];
export default function FeatureTable({}) {
return (
<div className="mt-32 rounded-xl bg-gradient-to-br from-slate-900 via-slate-900 to-slate-800 dark:from-slate-200 dark:to-slate-300 lg:mt-56">
<div className="mx-auto max-w-4xl px-4 py-8 sm:px-6 sm:pb-12 sm:pt-8 lg:max-w-7xl lg:px-8 lg:pt-12">
<p className="text-md dark:text-brand-dark text-brand-light mb-3 max-w-2xl font-semibold uppercase sm:mt-4">
Why Formbricks?
</p>
<h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-200 dark:text-slate-800">
The only complete open source option.
</h2>
<p className="mt-4 max-w-3xl text-lg text-slate-300 dark:text-slate-500">
We experienced how form needs develop as companies grow. We could&#39nt find a solution which ticked
all of the boxes. Now we&apos;re building it.
</p>
<div className="mt-12 grid grid-cols-1 gap-x-6 gap-y-12 sm:grid-cols-2 lg:mt-16 lg:grid-cols-3 lg:gap-x-8 lg:gap-y-16">
{features.map((feature) => (
<div key={feature.name}>
<div>
<span className="flex h-12 w-12 items-center justify-center rounded-md bg-slate-800 dark:bg-slate-300">
<feature.icon
className="dark:text-brand-dark text-brand-light h-6 w-6"
aria-hidden="true"
/>
</span>
</div>
<div className="mt-6">
<h3 className="text-lg font-semibold text-slate-200 dark:text-slate-700">{feature.name}</h3>
<p className="mt-2 text-base leading-6 text-slate-400 dark:text-slate-500">
{feature.description}
</p>
</div>
</div>
))}
</div>
</div>
</div>
);
}

View File

@@ -1,33 +0,0 @@
import { DarkMode, Gradient, LightMode } from "@/components/shared/Icon";
export function InstallationIcon({ id, color }) {
return (
<>
<defs>
<Gradient id={`${id}-gradient`} color={color} gradientTransform="matrix(0 21 -21 0 12 3)" />
<Gradient id={`${id}-gradient-dark`} color={color} gradientTransform="matrix(0 21 -21 0 16 7)" />
</defs>
<LightMode>
<circle cx={12} cy={12} r={12} fill={`url(#${id}-gradient)`} />
<path
d="m8 8 9 21 2-10 10-2L8 8Z"
fillOpacity={0.5}
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</LightMode>
<DarkMode>
<path
d="m4 4 10.286 24 2.285-11.429L28 14.286 4 4Z"
fill={`url(#${id}-gradient-dark)`}
stroke={`url(#${id}-gradient-dark)`}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</DarkMode>
</>
);
}

View File

@@ -1,42 +0,0 @@
import { DarkMode, Gradient, LightMode } from "@/components/shared/Icon";
export function LightbulbIcon({ id, color }) {
return (
<>
<defs>
<Gradient id={`${id}-gradient`} color={color} gradientTransform="matrix(0 21 -21 0 20 11)" />
<Gradient
id={`${id}-gradient-dark`}
color={color}
gradientTransform="matrix(0 24.5001 -19.2498 0 16 5.5)"
/>
</defs>
<LightMode>
<circle cx={20} cy={20} r={12} fill={`url(#${id}-gradient)`} />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M20 24.995c0-1.855 1.094-3.501 2.427-4.792C24.61 18.087 26 15.07 26 12.231 26 7.133 21.523 3 16 3S6 7.133 6 12.23c0 2.84 1.389 5.857 3.573 7.973C10.906 21.494 12 23.14 12 24.995V27a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-2.005Z"
className="fill-[var(--icon-background)]"
fillOpacity={0.5}
/>
<path
d="M25 12.23c0 2.536-1.254 5.303-3.269 7.255l1.391 1.436c2.354-2.28 3.878-5.547 3.878-8.69h-2ZM16 4c5.047 0 9 3.759 9 8.23h2C27 6.508 21.998 2 16 2v2Zm-9 8.23C7 7.76 10.953 4 16 4V2C10.002 2 5 6.507 5 12.23h2Zm3.269 7.255C8.254 17.533 7 14.766 7 12.23H5c0 3.143 1.523 6.41 3.877 8.69l1.392-1.436ZM13 27v-2.005h-2V27h2Zm1 1a1 1 0 0 1-1-1h-2a3 3 0 0 0 3 3v-2Zm4 0h-4v2h4v-2Zm1-1a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2Zm0-2.005V27h2v-2.005h-2ZM8.877 20.921C10.132 22.136 11 23.538 11 24.995h2c0-2.253-1.32-4.143-2.731-5.51L8.877 20.92Zm12.854-1.436C20.32 20.852 19 22.742 19 24.995h2c0-1.457.869-2.859 2.122-4.074l-1.391-1.436Z"
className="fill-[var(--icon-foreground)]"
/>
<path
d="M20 26a1 1 0 1 0 0-2v2Zm-8-2a1 1 0 1 0 0 2v-2Zm2 0h-2v2h2v-2Zm1 1V13.5h-2V25h2Zm-5-11.5v1h2v-1h-2Zm3.5 4.5h5v-2h-5v2Zm8.5-3.5v-1h-2v1h2ZM20 24h-2v2h2v-2Zm-2 0h-4v2h4v-2Zm-1-10.5V25h2V13.5h-2Zm2.5-2.5a2.5 2.5 0 0 0-2.5 2.5h2a.5.5 0 0 1 .5-.5v-2Zm2.5 2.5a2.5 2.5 0 0 0-2.5-2.5v2a.5.5 0 0 1 .5.5h2ZM18.5 18a3.5 3.5 0 0 0 3.5-3.5h-2a1.5 1.5 0 0 1-1.5 1.5v2ZM10 14.5a3.5 3.5 0 0 0 3.5 3.5v-2a1.5 1.5 0 0 1-1.5-1.5h-2Zm2.5-3.5a2.5 2.5 0 0 0-2.5 2.5h2a.5.5 0 0 1 .5-.5v-2Zm2.5 2.5a2.5 2.5 0 0 0-2.5-2.5v2a.5.5 0 0 1 .5.5h2Z"
className="fill-[var(--icon-foreground)]"
/>
</LightMode>
<DarkMode>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16 2C10.002 2 5 6.507 5 12.23c0 3.144 1.523 6.411 3.877 8.691.75.727 1.363 1.52 1.734 2.353.185.415.574.726 1.028.726H12a1 1 0 0 0 1-1v-4.5a.5.5 0 0 0-.5-.5A3.5 3.5 0 0 1 9 14.5V14a3 3 0 1 1 6 0v9a1 1 0 1 0 2 0v-9a3 3 0 1 1 6 0v.5a3.5 3.5 0 0 1-3.5 3.5.5.5 0 0 0-.5.5V23a1 1 0 0 0 1 1h.36c.455 0 .844-.311 1.03-.726.37-.833.982-1.626 1.732-2.353 2.354-2.28 3.878-5.547 3.878-8.69C27 6.507 21.998 2 16 2Zm5 25a1 1 0 0 0-1-1h-8a1 1 0 0 0-1 1 3 3 0 0 0 3 3h4a3 3 0 0 0 3-3Zm-8-13v1.5a.5.5 0 0 1-.5.5 1.5 1.5 0 0 1-1.5-1.5V14a1 1 0 1 1 2 0Zm6.5 2a.5.5 0 0 1-.5-.5V14a1 1 0 1 1 2 0v.5a1.5 1.5 0 0 1-1.5 1.5Z"
fill={`url(#${id}-gradient-dark)`}
/>
</DarkMode>
</>
);
}

View File

@@ -1,49 +0,0 @@
import { DarkMode, Gradient, LightMode } from "@/components/shared/Icon";
export function PluginsIcon({ id, color }) {
return (
<>
<defs>
<Gradient id={`${id}-gradient`} color={color} gradientTransform="matrix(0 21 -21 0 20 11)" />
<Gradient
id={`${id}-gradient-dark-1`}
color={color}
gradientTransform="matrix(0 22.75 -22.75 0 16 6.25)"
/>
<Gradient id={`${id}-gradient-dark-2`} color={color} gradientTransform="matrix(0 14 -14 0 16 10)" />
</defs>
<LightMode>
<circle cx={20} cy={20} r={12} fill={`url(#${id}-gradient)`} />
<g
fillOpacity={0.5}
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round">
<path d="M3 9v14l12 6V15L3 9Z" />
<path d="M27 9v14l-12 6V15l12-6Z" />
</g>
<path d="M11 4h8v2l6 3-10 6L5 9l6-3V4Z" fillOpacity={0.5} className="fill-[var(--icon-background)]" />
<g
className="stroke-[color:var(--icon-foreground)]"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round">
<path d="M20 5.5 27 9l-12 6L3 9l7-3.5" />
<path d="M20 5c0 1.105-2.239 2-5 2s-5-.895-5-2m10 0c0-1.105-2.239-2-5-2s-5 .895-5 2m10 0v3c0 1.105-2.239 2-5 2s-5-.895-5-2V5" />
</g>
</LightMode>
<DarkMode strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
<path
d="M17.676 3.38a3.887 3.887 0 0 0-3.352 0l-9 4.288C3.907 8.342 3 9.806 3 11.416v9.168c0 1.61.907 3.073 2.324 3.748l9 4.288a3.887 3.887 0 0 0 3.352 0l9-4.288C28.093 23.657 29 22.194 29 20.584v-9.168c0-1.61-.907-3.074-2.324-3.748l-9-4.288Z"
stroke={`url(#${id}-gradient-dark-1)`}
/>
<path
d="M16.406 8.087a.989.989 0 0 0-.812 0l-7 3.598A1.012 1.012 0 0 0 8 12.61v6.78c0 .4.233.762.594.925l7 3.598a.989.989 0 0 0 .812 0l7-3.598c.361-.163.594-.525.594-.925v-6.78c0-.4-.233-.762-.594-.925l-7-3.598Z"
fill={`url(#${id}-gradient-dark-2)`}
stroke={`url(#${id}-gradient-dark-2)`}
/>
</DarkMode>
</>
);
}

View File

@@ -1,38 +0,0 @@
import { DarkMode, Gradient, LightMode } from "@/components/shared/Icon";
export function PresetsIcon({ id, color }) {
return (
<>
<defs>
<Gradient id={`${id}-gradient`} color={color} gradientTransform="matrix(0 21 -21 0 20 3)" />
<Gradient
id={`${id}-gradient-dark`}
color={color}
gradientTransform="matrix(0 22.75 -22.75 0 16 6.25)"
/>
</defs>
<LightMode>
<circle cx={20} cy={12} r={12} fill={`url(#${id}-gradient)`} />
<g
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
fillOpacity={0.5}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round">
<path d="M3 5v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2Z" />
<path d="M18 17v10a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V17a2 2 0 0 0-2-2h-7a2 2 0 0 0-2 2Z" />
<path d="M18 5v4a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2h-7a2 2 0 0 0-2 2Z" />
<path d="M3 25v2a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2Z" />
</g>
</LightMode>
<DarkMode fill={`url(#${id}-gradient-dark)`}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3 17V4a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1Zm16 10v-9a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-6a2 2 0 0 1-2-2Zm0-23v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-8a1 1 0 0 0-1 1ZM3 28v-3a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1Z"
/>
<path d="M2 4v13h2V4H2Zm2-2a2 2 0 0 0-2 2h2V2Zm8 0H4v2h8V2Zm2 2a2 2 0 0 0-2-2v2h2Zm0 13V4h-2v13h2Zm-2 2a2 2 0 0 0 2-2h-2v2Zm-8 0h8v-2H4v2Zm-2-2a2 2 0 0 0 2 2v-2H2Zm16 1v9h2v-9h-2Zm3-3a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1v-2Zm6 0h-6v2h6v-2Zm3 3a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2Zm0 9v-9h-2v9h2Zm-3 3a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2Zm-6 0h6v-2h-6v2Zm-3-3a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1h-2Zm2-18V4h-2v5h2Zm0 0h-2a2 2 0 0 0 2 2V9Zm8 0h-8v2h8V9Zm0 0v2a2 2 0 0 0 2-2h-2Zm0-5v5h2V4h-2Zm0 0h2a2 2 0 0 0-2-2v2Zm-8 0h8V2h-8v2Zm0 0V2a2 2 0 0 0-2 2h2ZM2 25v3h2v-3H2Zm2-2a2 2 0 0 0-2 2h2v-2Zm9 0H4v2h9v-2Zm2 2a2 2 0 0 0-2-2v2h2Zm0 3v-3h-2v3h2Zm-2 2a2 2 0 0 0 2-2h-2v2Zm-9 0h9v-2H4v2Zm-2-2a2 2 0 0 0 2 2v-2H2Z" />
</DarkMode>
</>
);
}

View File

@@ -1,55 +0,0 @@
import { DarkMode, Gradient, LightMode } from "@/components/shared/Icon";
export function ThemingIcon({ id, color }) {
return (
<>
<defs>
<Gradient id={`${id}-gradient`} color={color} gradientTransform="matrix(0 21 -21 0 12 11)" />
<Gradient
id={`${id}-gradient-dark`}
color={color}
gradientTransform="matrix(0 24.5 -24.5 0 16 5.5)"
/>
</defs>
<LightMode>
<circle cx={12} cy={20} r={12} fill={`url(#${id}-gradient)`} />
<path
d="M27 12.13 19.87 5 13 11.87v14.26l14-14Z"
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
fillOpacity={0.5}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M3 3h10v22a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V3Z"
className="fill-[var(--icon-background)]"
fillOpacity={0.5}
/>
<path
d="M3 9v16a4 4 0 0 0 4 4h2a4 4 0 0 0 4-4V9M3 9V3h10v6M3 9h10M3 15h10M3 21h10"
className="stroke-[color:var(--icon-foreground)]"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M29 29V19h-8.5L13 26c0 1.5-2.5 3-5 3h21Z"
fillOpacity={0.5}
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</LightMode>
<DarkMode>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3 2a1 1 0 0 0-1 1v21a6 6 0 0 0 12 0V3a1 1 0 0 0-1-1H3Zm16.752 3.293a1 1 0 0 0-1.593.244l-1.045 2A1 1 0 0 0 17 8v13a1 1 0 0 0 1.71.705l7.999-8.045a1 1 0 0 0-.002-1.412l-6.955-6.955ZM26 18a1 1 0 0 0-.707.293l-10 10A1 1 0 0 0 16 30h13a1 1 0 0 0 1-1V19a1 1 0 0 0-1-1h-3ZM5 18a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2H5Zm-1-5a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2H5a1 1 0 0 1-1-1Zm1-7a1 1 0 0 0 0 2h6a1 1 0 1 0 0-2H5Z"
fill={`url(#${id}-gradient-dark)`}
/>
</DarkMode>
</>
);
}

View File

@@ -1,55 +0,0 @@
import { DarkMode, Gradient, LightMode } from "@/components/shared/Icon";
export function WarningIcon({ id, color }) {
return (
<>
<defs>
<Gradient
id={`${id}-gradient`}
color={color}
gradientTransform="rotate(65.924 1.519 20.92) scale(25.7391)"
/>
<Gradient
id={`${id}-gradient-dark`}
color={color}
gradientTransform="matrix(0 24.5 -24.5 0 16 5.5)"
/>
</defs>
<LightMode>
<circle cx={20} cy={20} r={12} fill={`url(#${id}-gradient)`} />
<path
d="M3 16c0 7.18 5.82 13 13 13s13-5.82 13-13S23.18 3 16 3 3 8.82 3 16Z"
fillOpacity={0.5}
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="m15.408 16.509-1.04-5.543a1.66 1.66 0 1 1 3.263 0l-1.039 5.543a.602.602 0 0 1-1.184 0Z"
className="fill-[var(--icon-foreground)] stroke-[color:var(--icon-foreground)]"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16 23a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
fillOpacity={0.5}
stroke="currentColor"
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</LightMode>
<DarkMode>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 16C2 8.268 8.268 2 16 2s14 6.268 14 14-6.268 14-14 14S2 23.732 2 16Zm11.386-4.85a2.66 2.66 0 1 1 5.228 0l-1.039 5.543a1.602 1.602 0 0 1-3.15 0l-1.04-5.543ZM16 20a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z"
fill={`url(#${id}-gradient-dark)`}
/>
</DarkMode>
</>
);
}

View File

@@ -1,21 +0,0 @@
import glob from "fast-glob";
import * as path from "path";
async function importArticle(articleFilename: string) {
let { meta, default: component } = await import(`../pages/blog/${articleFilename}`);
return {
slug: articleFilename.replace(/(\/index)?\.mdx$/, ""),
...meta,
component,
};
}
export async function getAllArticles() {
let articleFilenames = await glob(["*.mdx", "*/index.mdx"], {
cwd: path.join(process.cwd(), "pages/blog"),
});
let articles = await Promise.all(articleFilenames.map(importArticle));
return articles.sort((a, z) => new Date(z.date).valueOf() - new Date(a.date).valueOf());
}

View File

@@ -1,79 +0,0 @@
/*!
* Sanitize an HTML string
* (c) 2021 Chris Ferdinandi, MIT License, https://gomakethings.com
* @param {String} str The HTML string to sanitize
* @return {String} The sanitized string
*/
export function cleanHtml(str: string): string {
/**
* Convert the string to an HTML document
* @return {Node} An HTML document
*/
function stringToHTML() {
let parser = new DOMParser();
let doc = parser.parseFromString(str, "text/html");
return doc.body || document.createElement("body");
}
/**
* Remove <script> elements
* @param {Node} html The HTML
*/
function removeScripts(html) {
let scripts = html.querySelectorAll("script");
for (let script of scripts) {
script.remove();
}
}
/**
* Check if the attribute is potentially dangerous
* @param {String} name The attribute name
* @param {String} value The attribute value
* @return {Boolean} If true, the attribute is potentially dangerous
*/
function isPossiblyDangerous(name, value) {
let val = value.replace(/\s+/g, "").toLowerCase();
if (["src", "href", "xlink:href"].includes(name)) {
if (val.includes("javascript:") || val.includes("data:")) return true;
}
if (name.startsWith("on")) return true;
}
/**
* Remove potentially dangerous attributes from an element
* @param {Node} elem The element
*/
function removeAttributes(elem) {
// Loop through each attribute
// If it's dangerous, remove it
let atts = elem.attributes;
for (let { name, value } of atts) {
if (!isPossiblyDangerous(name, value)) continue;
elem.removeAttribute(name);
}
}
/**
* Remove dangerous stuff from the HTML document's nodes
* @param {Node} html The HTML document
*/
function clean(html) {
let nodes = html.children;
for (let node of nodes) {
removeAttributes(node);
clean(node);
}
}
// Convert the string to HTML
let html = stringToHTML();
// Sanitize it
removeScripts(html);
clean(html);
// If the user wants HTML nodes back, return them
// Otherwise, pass a sanitized string back
return html.innerHTML;
}

View File

@@ -1,99 +0,0 @@
const navigation = [
{
title: "Introduction",
links: [
{ title: "What is Formbricks?", href: "/docs/introduction/what-is-formbricks" },
{ title: "Why is it better?", href: "/docs/introduction/why-is-it-better" },
{ title: "How does it work?", href: "/docs/introduction/how-it-works" },
],
},
{
title: "Getting Started",
links: [
{ title: "Quickstart", href: "/docs/getting-started/quickstart" },
{ title: "Next.js App Dir", href: "/docs/getting-started/nextjs-app" },
{ title: "Next.js Pages Dir", href: "/docs/getting-started/nextjs-pages" },
{ title: "Setup with Vue.js", href: "/docs/getting-started/vuejs" },
],
},
{
title: "Attributes",
links: [
{ title: "Why Attributes?", href: "/docs/attributes/why" },
{ title: "Custom Attributes", href: "/docs/attributes/custom-attributes" },
{ title: "Identify users", href: "/docs/attributes/identify-users" },
],
},
{
title: "Actions",
links: [
{ title: "Why Actions?", href: "/docs/actions/why" },
{ title: "No-Code Actions", href: "/docs/actions/no-code" },
{ title: "Code Actions", href: "/docs/actions/code" },
],
},
{
title: "Best Practices",
links: [
{ title: "Learn from Churn", href: "/docs/best-practices/cancel-subscription" },
{ title: "Interview Prompt", href: "/docs/best-practices/interview-prompt" },
{ title: "Product-Market Fit", href: "/docs/best-practices/pmf-survey" },
{ title: "Trial Conversion", href: "/docs/best-practices/improve-trial-cr" },
{ title: "Feature Chaser", href: "/docs/best-practices/feature-chaser" },
{ title: "Feedback Box", href: "/docs/best-practices/feedback-box" },
{ title: "Docs Feedback", href: "/docs/best-practices/docs-feedback" },
],
},
{
title: "Integrations",
links: [{ title: "Zapier", href: "/docs/integrations/zapier" }],
},
{
title: "Link Surveys",
links: [
{ title: "Data Prefilling", href: "/docs/link-surveys/data-prefilling" },
{ title: "User Identification", href: "/docs/link-surveys/user-identification" },
],
},
{
title: "API",
links: [
{ title: "Overview", href: "/docs/api/overview" },
{ title: "API Key Setup", href: "/docs/api/api-key-setup" },
],
},
{
title: "Client API",
links: [
{ title: "Overview", href: "/docs/client-api/overview" },
{ title: "Create Response", href: "/docs/client-api/create-response" },
{ title: "Update Response", href: "/docs/client-api/update-response" },
],
},
{
title: "Webhook API",
links: [
{ title: "Overview", href: "/docs/webhook-api/overview" },
{ title: "List Webhooks", href: "/docs/webhook-api/list-webhooks" },
{ title: "Get Webhook", href: "/docs/webhook-api/get-webhook" },
{ title: "Create Webhook", href: "/docs/webhook-api/create-webhook" },
{ title: "Delete Webhook", href: "/docs/webhook-api/delete-webhook" },
{ title: "Webhook Payload", href: "/docs/webhook-api/webhook-payload" },
],
},
{
title: "Self-hosting",
links: [{ title: "Deployment", href: "/docs/self-hosting/deployment" }],
},
{
title: "Contributing",
links: [
{ title: "Introduction", href: "/docs/contributing/introduction" },
{ title: "Setup Dev Environment", href: "/docs/contributing/setup" },
{ title: "Demo App", href: "/docs/contributing/demo" },
{ title: "Troubleshooting", href: "/docs/contributing/troubleshooting" },
],
},
];
export default navigation;

View File

@@ -1,70 +0,0 @@
export const handleFeedbackSubmit = async (YesNo, pageUrl) => {
const response_data = {
data: {
isHelpful: YesNo,
pageUrl: pageUrl,
},
};
const payload = {
response: response_data,
surveyId: process.env.NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID,
};
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_FORMBRICKS_COM_API_HOST}/api/v1/client/environments/${process.env.NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID}/responses`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}
);
if (res.ok) {
const responseJson = await res.json();
return responseJson.id; // Return the response ID
} else {
console.error("Error submitting form");
}
} catch (error) {
console.error("Error submitting form:", error);
}
};
export const updateFeedback = async (freeText, responseId) => {
if (!responseId) {
console.error("No response ID available");
return;
}
const payload = {
response: {
data: {
additionalInfo: freeText,
},
finished: true,
},
};
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_FORMBRICKS_COM_API_HOST}/api/v1/client/environments/${process.env.NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID}/responses/${responseId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}
);
if (!res.ok) {
console.error("Error updating response");
}
} catch (error) {
console.error("Error updating response:", error);
}
};

View File

@@ -1,8 +0,0 @@
export function formatDate(dateString: string) {
return new Date(`${dateString}T00:00:00Z`).toLocaleDateString("en-US", {
day: "numeric",
month: "long",
year: "numeric",
timeZone: "UTC",
});
}

View File

@@ -1,130 +0,0 @@
/** @type {import('next').NextConfig} */
import rehypePrism from "@mapbox/rehype-prism";
import nextMDX from "@next/mdx";
import { withPlausibleProxy } from "next-plausible";
import remarkGfm from "remark-gfm";
const nextConfig = {
reactStrictMode: true,
pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"],
transpilePackages: ["@formbricks/ui", "@formbricks/lib"],
images: {
remotePatterns: [
{
protocol: "https",
hostname: "seo-strapi-aws-s3.s3.eu-central-1.amazonaws.com",
port: "",
},
],
},
async redirects() {
return [
{
source: "/discord",
destination: "https://discord.gg/3YFcABF2Ts",
permanent: true,
},
{
source: "/roadmap",
destination: "https://github.com/orgs/formbricks/projects/1",
permanent: true,
},
{
source: "/github",
destination: "https://github.com/formbricks/formbricks",
permanent: true,
},
{
source: "/deal",
destination: "/concierge",
permanent: false,
},
{
source: "/privacy",
destination: "/privacy-policy",
permanent: true,
},
{
source: "/form-hq",
destination: "/",
permanent: true,
},
{
source: "/docs",
destination: "/docs/introduction/what-is-formbricks",
permanent: true,
},
{
source: "/docs/getting-started/nextjs",
destination: "/docs/getting-started/nextjs-app",
permanent: true,
},
{
source: "/docs/formbricks-hq/self-hosting",
destination: "/docs",
permanent: true,
},
{
source: "/docs/react-form-library/getting-started",
destination: "/docs",
permanent: true,
},
{
source: "/docs/react-form-library/work-with-components",
destination: "/docs",
permanent: true,
},
{
source: "/docs/react-form-library/introduction",
destination: "/docs",
permanent: true,
},
{
source: "/docs/formbricks-hq/schema",
destination: "/docs",
permanent: true,
},
{
source: "/docs/events/why",
destination: "/docs/actions/why",
permanent: true,
},
{
source: "/docs/events/code",
destination: "/docs/actions/code",
permanent: true,
},
{
source: "/docs/events/code",
destination: "/docs/actions/code",
permanent: true,
},
{
source: "/pmf",
destination: "/",
permanent: true,
},
{
source: "/blog/v1-and-how-we-got-here",
destination: "/blog/experience-management-open-source",
permanent: true,
},
];
},
};
const withMDX = nextMDX({
extension: /\.mdx?$/,
options: {
// If you use remark-gfm, you'll need to use next.config.mjs
// as the package is ESM only
// https://github.com/remarkjs/remark-gfm#install
remarkPlugins: [remarkGfm],
rehypePlugins: [rehypePrism],
// If you use `MDXProvider`, uncomment the following line.
// providerImportSource: "@mdx-js/react",
},
});
export default withPlausibleProxy({ customDomain: "https://plausible.formbricks.com" })(withMDX(nextConfig));

View File

@@ -1,48 +0,0 @@
{
"name": "@formbricks/formbricks-com-old",
"version": "0.1.0",
"private": true,
"scripts": {
"clean": "rimraf .turbo node_modules .next",
"dev": "next dev -p 3002",
"build": "next build",
"postbuild": "next-sitemap",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@calcom/embed-react": "^1.3.0",
"@docsearch/react": "^3.5.1",
"@formbricks/lib": "workspace:*",
"@formbricks/types": "workspace:*",
"@formbricks/ui": "workspace:*",
"@headlessui/react": "^1.7.16",
"@heroicons/react": "^2.0.18",
"@mapbox/rehype-prism": "^0.8.0",
"@mdx-js/loader": "^2.3.0",
"@mdx-js/react": "^2.3.0",
"@next/mdx": "^13.4.12",
"@paralleldrive/cuid2": "^2.2.1",
"clsx": "^2.0.0",
"lottie-web": "^5.12.2",
"next": "13.4.12",
"next-plausible": "^3.10.1",
"next-seo": "^6.1.0",
"next-sitemap": "^4.1.8",
"node-fetch": "^3.3.2",
"prism-react-renderer": "^2.0.6",
"prismjs": "^1.29.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-icons": "^4.10.1",
"react-markdown": "^8.0.7",
"react-responsive-embed": "^2.1.0",
"remark-gfm": "^3.0.1",
"sharp": "^0.32.4"
},
"devDependencies": {
"@formbricks/tsconfig": "workspace:*",
"@types/prismjs": "^1.26.0",
"eslint-config-formbricks": "workspace:*"
}
}

View File

@@ -1,14 +0,0 @@
// 404.js
import { Button } from "@formbricks/ui";
export default function FourOhFour() {
return (
<div className="flex h-screen flex-col items-center justify-center">
<h1 className="text-8xl font-bold text-slate-300">404</h1>
<h1 className="mb-8 text-xl text-slate-300">Page Not Found</h1>
<Button href="/" variant="highlight">
Go back home
</Button>
</div>
);
}

View File

@@ -1,11 +0,0 @@
import PlausibleProvider from "next-plausible";
import type { AppProps } from "next/app";
import "../styles/globals.css";
export default function App({ Component, pageProps }: AppProps) {
return (
<PlausibleProvider domain="formbricks.com" selfHosted={true}>
<Component {...pageProps} />
</PlausibleProvider>
);
}

View File

@@ -1,63 +0,0 @@
import { Head, Html, Main, NextScript } from "next/document";
const themeScript = `
let isDarkMode = window.matchMedia('(prefers-color-scheme: dark)')
function updateTheme(theme) {
theme = theme ?? window.localStorage.theme ?? 'system'
if (theme === 'dark' || (theme === 'system' && isDarkMode.matches)) {
document.documentElement.classList.add('dark')
} else if (theme === 'light' || (theme === 'system' && !isDarkMode.matches)) {
document.documentElement.classList.remove('dark')
}
return theme
}
function updateThemeWithoutTransitions(theme) {
updateTheme(theme)
document.documentElement.classList.add('[&_*]:!transition-none')
window.setTimeout(() => {
document.documentElement.classList.remove('[&_*]:!transition-none')
}, 0)
}
document.documentElement.setAttribute('data-theme', updateTheme())
new MutationObserver(([{ oldValue }]) => {
let newValue = document.documentElement.getAttribute('data-theme')
if (newValue !== oldValue) {
try {
window.localStorage.setItem('theme', newValue)
} catch {}
updateThemeWithoutTransitions(newValue)
}
}).observe(document.documentElement, { attributeFilter: ['data-theme'], attributeOldValue: true })
isDarkMode.addEventListener('change', () => updateThemeWithoutTransitions())
`;
export default function Document() {
return (
<Html className="scroll-smooth antialiased [font-feature-settings:'ss01']" lang="en" dir="ltr">
<Head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
<link rel="manifest" href="/favicon/site.webmanifest" />
<link rel="mask-icon" href="/favicon/safari-pinned-tab.svg" color="#002941" />
<link rel="shortcut icon" href="/favicon/favicon.ico" />
<meta name="msapplication-TileColor" content="#002941" />
<meta name="msapplication-config" content="/favicon/browserconfig.xml" />
<meta name="theme-color" content="#ffffff" />
</Head>
<body className="bg-slate-50 dark:bg-slate-900">
<Main />
<NextScript />
</body>
</Html>
);
}

View File

@@ -1,144 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// GET
if (req.method === "GET") {
return res.status(200).json({
data: [
{
name: "Appsmith",
description: "Build build custom software on top of your data.",
href: "https://www.appsmith.com",
},
{
name: "BoxyHQ",
description:
"BoxyHQs suite of APIs for security and privacy helps engineering teams build and ship compliant cloud applications faster.",
href: "https://boxyhq.com",
},
{
name: "Cal.com",
description:
"Cal.com is a scheduling tool that helps you schedule meetings without the back-and-forth emails.",
href: "https://cal.com",
},
{
name: "Crowd.dev",
description:
"Centralize community, product, and customer data to understand which companies are engaging with your open source project.",
href: "https://www.crowd.dev",
},
{
name: "Documenso",
description:
"The Open-Source DocuSign Alternative. We aim to earn your trust by enabling you to self-host the platform and examine its inner workings.",
href: "https://documenso.com",
},
{
name: "Erxes",
description:
"The Open-Source HubSpot Alternative. A single XOS enables to create unique and life-changing experiences that work for all types of business.",
href: "https://erxes.io",
},
{
name: "Formbricks",
description:
"Survey granular user segments at any point in the user journey. Gather up to 6x more insights with targeted micro-surveys. All open-source.",
href: "https://formbricks.com",
},
{
name: "Ghostfolio",
description:
"Ghostfolio is a privacy-first, open source dashboard for your personal finances. Designed to simplify asset tracking and empower informed investment decisions.",
href: "https://ghostfol.io",
},
{
name: "GitWonk",
description:
"GitWonk is an open-source technical documentation tool, designed and built focusing on the developer experience.",
href: "https://gitwonk.com",
},
{
name: "Hanko",
description:
"Open-source authentication and user management for the passkey era. Integrated in minutes, for web and mobile apps.",
href: "https://www.hanko.io",
},
{
name: "HTMX",
description:
"HTMX is a dependency-free JavaScript library that allows you to access AJAX, CSS Transitions, WebSockets, and Server Sent Events directly in HTML.",
href: "https://htmx.org",
},
{
name: "Infisical",
description:
"Open source, end-to-end encrypted platform that lets you securely manage secrets and configs across your team, devices, and infrastructure.",
href: "https://infisical.com",
},
{
name: "Mockoon",
description: "Mockoon is the easiest and quickest way to design and run mock REST APIs.",
href: "https://mockoon.com",
},
{
name: "Novu",
description:
"The open-source notification infrastructure for developers. Simple components and APIs for managing all communication channels in one place.",
href: "https://novu.co",
},
{
name: "OpenBB",
description:
"Democratizing investment research through an open source financial ecosystem. The OpenBB Terminal allows everyone to perform investment research, from everywhere.",
href: "https://openbb.co",
},
{
name: "Sniffnet",
description:
"Sniffnet is a network monitoring tool to help you easily keep track of your Internet traffic.",
href: "https://www.sniffnet.net",
},
{
name: "Tolgee",
description: "Software localization from A to Z made really easy.",
href: "https://tolgee.io/",
},
{
name: "Trigger.dev",
description:
"Create long-running Jobs directly in your codebase with features like API integrations, webhooks, scheduling and delays.",
href: "https://trigger.dev",
},
{
name: "Typebot",
description:
"Typebot gives you powerful blocks to create unique chat experiences. Embed them anywhere on your apps and start collecting results like magic.",
href: "https://typebot.io",
},
{
name: "Twenty",
description:
"A modern CRM offering the flexibility of open-source, advanced features and sleek design.",
href: "https://twenty.com",
},
{
name: "Webiny",
description:
"Open-source enterprise-grade serverless CMS. Own your data. Scale effortlessly. Customize everything.",
href: "https://www.webiny.com",
},
{
name: "Webstudio",
description: "Webstudio is an open source alternative to Webflow",
href: "https://webstudio.is",
},
],
});
}
// Unknown HTTP Method
else {
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}

View File

@@ -1,141 +0,0 @@
import Image from "next/image";
import LayoutMdx from "@/components/shared/LayoutMdx";
import AuthorBox from "@/components/shared/AuthorBox";
import Formbricks from "./open-source-survey-software-free-2023-formbricks-typeform-alternative.png";
import Typebot from "./typebot-open-source-free-conversational-form-builder-survey-software-opensource.jpg";
import LimeSurvey from "./free-survey-tool-limesurvey-open-source-software-opensource.png";
import OpnForm from "./opnform-free-open-source-form-survey-tools-builder-2023-self-hostign.jpg";
import HeaderImage from "./2023-title-best-open-source-survey-software-tools-and-alternatives.png";
import SurveyJS from "./surveyjs-free-opensource-form-survey-tool-software-to-make-surveys-2023.png";
export const meta = {
title: "5 Open Source Survey and Form Tools maintained in 2023",
description:
"Most open source projects get abandoned after a while. But these 5 open source survey tools are still alive and kicking in 2023.",
date: "2023-04-12",
publishedTime: "2023-04-12T12:00:00",
authors: ["Johannes"],
section: "Open Source Surveys",
tags: ["Open Source Surveys", "Formbricks", "Typeform", "SurveyJS", "Typebot", "OpnForm", "LimeSurvey"],
};
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
_Most open source projects get abandoned after a while. But these 5 open source survey tools are still alive and kicking in 2023._
<Image
src={HeaderImage}
alt="Open source survey tool self-hostable: Find the 5 best (and maintained) open source survey tool 2023."
className="rounded-lg"
/>
Looking for the perfect open source survey tool to help you gather valuable insights and improve your business? Look no further!
We've compiled a list of the top 5 open source form and survey tools that are still maintained in 2023. In app surveys, conversational bots, AI-generated surveys: These open source tools offer various features that cater to different needs.
## 1. Formbricks - In app micro surveys
<Image
src={Formbricks}
alt="Formbricks is a free and open source survey software for in app micro surveys. Ask any user segment at any point in the user journey."
className="rounded-lg"
/>
Formbricks is a powerful open source survey tool designed to help you get better experience data for your business. This tool allows you to survey specific customer segments at any point in the user journey, providing you with invaluable insights into what your customers think and feel about your product.
- 👍 **Pre-segment users:** Don't ask everyone, all the time. Granularly segment your user base to get deep insights
- 👍 **Event-based surveys:** Trigger surveys based on user behavior, such as page views, clicks, and more
- 👍 **Extensive template library:** Choose from a wide range of templates to create surveys that answer your question
- 👍 **Easy self-hosting:** Docker makes it possible to self-host Formbricks in minutes.
- ⚠️ **It's early for Formbricks.** Product developes rapidly but might encounter a bug here and there.
[Try it out](https://app.formbricks.com), [read more](https://formbricks.com) or dive into the [code base](https://formbricks.com/github) or comprehensive [docs](https://formbricks.com/docs).
## 2. SurveyJS - Build-it-yourself library
<Image
src={SurveyJS}
alt="SurveyJS is a comprehensive JS library to build your own form or survey application."
className="rounded-lg"
/>
SurveyJS is a collection of JavaScript Librarys to build forms. Building your own form management system has never been easier than with SurveyJS. It packs:
- 👍 **Library:** A JavaScript library to render surveys and forms
- 👍 **Analytics:** A dashboard to analyze survey and form results
- 👍 **Editor:** A visual editor to create surveys and forms
- 👍 **PDF:** A library to render survey and form rersults as PDF
- ⚠️ **Pricing:** Starts at $499 / year
[Dive into the code on GitHub](https://github.com/surveyjs)
## 3. Typebot - Truly conversational forms
<Image
src={Typebot}
alt="Open source survey and form builder SurveyJS lets you build surveys fast"
className="rounded-lg"
/>
Coming in at number three on our list is Typebot, that makes it really easy to create conversational forms and surveys. Typebot helps you engage with your audience in a more interactive way, leading to higher response rates and better data. It comes with:
- 👍 **A slick visual** editor to create conversational forms and surveys
- 👍 A library to render conversational forms and surveys **on your website**
- 👍 A dashboard to **analyze survey and form results**
- 👍 **Lots of integrations** to pipe your data to
- ⚠️ **Limited to conversational forms.** If you need more classical forms, this might not be the right tool for you.
[Dive into the code on GitHub](https://github.com/baptisteArno/typebot.io)
## 4. OpnForm - Straight-forward survey builder
<Image
src={OpnForm}
alt="OpnForm is an open source form builder for experience management"
className="rounded-lg"
/>
OpnForms is a flexible and powerful open source form and survey tool designed to make data collection easy and efficient. OpnForm packs lots of features, especially for a Beta:
- 👍 **Multiple Question Types:** Choose from a wide variety of question types to create highly customizable forms and surveys.
- 👍 **Conditional Logic:** Show or hide questions based on previous responses to create personalized surveys.
- 👍 **Export Data:** Easily export collected data in various formats for further analysis.
- ⚠️ The UI is a bit **stuffed and not very intuitive.**
[Dive into the code on GitHub](https://github.com/JhumanJ/OpnForm)
## 5. LimeSurvey - Old (but gold?)
<Image
src={LimeSurvey}
alt="LimeSurvey is open source survey builder to manage experiences with forms"
className="rounded-lg"
/>
LimeSurvey has been around for at least a decade. It's a powerful survey tool made for more classical, scientific surveying. It packs:
- 👍 **Multilingual Surveys:** Create surveys in multiple languages to reach a global audience.
- 👍 **Extensive Question Types:** Choose from a wide range of question types to create highly engaging surveys.
- 👍 **Advanced Logic:** Utilize advanced survey logic features to personalize your surveys and capture more accurate data.
- 👍 **Survey Templates:** Start with pre-built survey templates to save time and effort.
- ⚠️ **Very old-fashioned UI:** The product is quite the opposite of slick and user-friendly.
[Dive into the code on GitHub](https://github.com/LimeSurvey/LimeSurvey)
## Summary ☟
In this article, we've rounded up the top 5 open source form and survey tools that are still rocking it in 2023. Perfect for devs who are always on the lookout for the latest and greatest!
1. Formbricks: A game-changer for in app micro surveys, letting you target specific customer segments at any point in their journey. It's still early days, but this bad boy is worth keeping an eye on.
2. SurveyJS: A must-have for DIY enthusiasts, this collection of JavaScript libraries makes building your own form management system a breeze. Just remember, the starting price is $499/year.
3. Typebot: Make your forms and surveys truly conversational with Typebot's slick visual editor and user-friendly interface. Just note that it's limited to conversational forms.
4. OpnForm: A flexible and powerful tool that's all about making data collection simple and efficient. It's still in Beta, so the UI might not be super polished, but it's packed with features.
5. LimeSurvey: An oldie but a goldie. This veteran survey tool is perfect for more traditional, scientific surveying. Just be prepared to deal with its old-fashioned UI.
Take a peek at these tools, and we're sure you'll find one that suits your needs. Happy surveying!
export default ({ children }) => <LayoutMdx meta={meta}>{children}</LayoutMdx>;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Some files were not shown because too many files have changed in this diff Show More