Clean up + Bugfixes (#443)

* format code

* fix building issue in database package

* update turbo config and package jsons

* update error package with more information and license

* fix typescript issues in ui package

* update package-lock

* update packages

* clean dependencies in web

* clean up dependencies in demo & formbricks-com

* remove unsused template file

* remove legacy capture endpoint

* remove unfinished client endpoints

* clean up unused functions

* fix formbricks not loading on invalid session

* update readme
This commit is contained in:
Matti Nannt
2023-06-28 13:07:51 +02:00
committed by GitHub
parent 9d1d0576a2
commit 9cc836a775
48 changed files with 1088 additions and 2020 deletions

View File

@@ -3,7 +3,7 @@ Copyright (c) 2023 Matthias Nannt, Johannes Dancker
Portions of this software are licensed as follows:
- All content that resides under the "packages/ee/" directory of this repository, if that directory exists, is licensed under the license defined in "packages/ee/LICENSE".
- All content that resides under the "packages/js/" directory of this repository, if that directory exists, is licensed under the "MIT" license as defined in "packages/js/LICENSE".
- All content that resides under the "packages/js/", "packages/errors/" and "packages/api/" directories of this repository, if that directories exist, is licensed under the "MIT" license as defined in the "LICENSE" files of these packages.
- All third party components incorporated into the Formbricks Software are licensed under the original license provided by the owner of the applicable component.
- Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
@@ -67,7 +67,7 @@ modification follow.
TERMS AND CONDITIONS
0. Definitions.
1. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.

View File

@@ -12,67 +12,83 @@
</p>
<p align="center">
<a href="https://github.com/formbricks/formbricks/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPL-purple" alt="License"></a> <a href="https://formbricks.com/discord"><img src="https://img.shields.io/discord/979077669410979880?label=Discord&logo=discord&logoColor=%23fff" alt="Join Formbricks Discord"></a> <a href="https://github.com/formbricks/formbricks/stargazers"><img src="https://img.shields.io/github/stars/formbricks/formbricks?logo=github" alt="Github Stars"></a>
<a href="https://github.com/formbricks/formbricks/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-AGPL-purple" alt="License"></a> <a href="https://formbricks.com/discord"><img src="https://img.shields.io/discord/979077669410979880?label=Discord&logo=discord&logoColor=%23fff" alt="Join Formbricks Discord"></a> <a href="https://github.com/formbricks/formbricks/stargazers"><img src="https://img.shields.io/github/stars/formbricks/formbricks?logo=github" alt="Github Stars"></a>
<a href="https://news.ycombinator.com/item?id=32303986"><img src="https://img.shields.io/badge/Hacker%20News-122-%23FF6600" alt="Hacker News"></a>
<a href="https://www.producthunt.com/products/snoopforms"><img src="https://img.shields.io/badge/Product%20Hunt-%232%20Product%20of%20the%20Day-orange?logo=producthunt&logoColor=%23fff" alt="Product Hunt"></a>
<a href="https://github.blog/2023-04-12-github-accelerator-our-first-cohort-and-whats-next/"><img src="https://img.shields.io/badge/2023-blue?logo=github&label=Github%20Accelerator" alt="Github Accelerator"></a>
<a href="https://github.com/formbricks/formbricks/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
</p>
<br/>
## About Formbricks
<p align="center">
<i>Trusted by</i>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://github.com/calcom/cal.com/"><img src="https://github.com/formbricks/formbricks/assets/675065/1a8763cf-f47e-4960-90f6-334f6dc12a17" height="20px"></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://github.com/CrowdDotDev/crowd.dev"><img src="https://github.com/formbricks/formbricks/assets/675065/59b1a4d4-25e4-4ef3-b0bf-4426446fbfd0" height="20px"></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://clovyr.io/"><img src="https://github.com/formbricks/formbricks/assets/675065/9291c8df-9aac-423a-a430-a9a581240075" height="20px"></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://neverinstall.com/"><img src="https://github.com/formbricks/formbricks/assets/675065/72e5e37b-8ef7-4340-b06e-f1d12a05330f" height="20px"></a>
</p>
<img width="1527" alt="formbricks-sneak" src="https://user-images.githubusercontent.com/675065/227726212-6ebf930e-6a20-4ffa-b966-56cd41bdf363.png">
## ✨ About Formbricks
Formbricks productizes best practices for qualitative in-app user discovery. Use micro-surveys to target the right users at the right time without making surveys annoying.
<img width="1527" alt="formbricks-sneak" src="https://github-production-user-asset-6210df.s3.amazonaws.com/675065/249441967-ccb89ea3-82b4-4bf2-8d2c-528721ec313b.png">
Formbricks is your go-to solution for in-product micro-surveys that will supercharge your product experience. Use micro-surveys to target the right users at the right time without making surveys annoying.
**Try it out in the cloud at [formbricks.com](https://formbricks.com)**
### Mission: Base your decisions on qualitative data.
## 💪 Mission: Make customer-centric decisions based on data.
Formbricks helps you apply best practices from data-driven work and experience management to make better business decisions. Use Formbricks to collect and manage insights from your users; run a product market fit survey to know which audience to focus on and whether your value proposition is being recognized.
Formbricks helps you apply best practices from data-driven work and experience management to make better business decisions. Ask users as they experience your product - and leverage a significantly higher conversion rate. Gather all insights you can - including partial submissions and build conviction for the next product decision. Better data, better business.
### Features
- 📲 Create in-product surveys with our no code editor with multiple question types
- 📚 Choose from a variety of best-practice templates
- 👩🏻 Launch and target your surveys to specific user groups without changing your application code
- 🔗 Create shareable link surveys
- 👨‍👩‍👦 Invite your team members to collaborate on your surveys
- 🔌 Integrate Formbricks with Slack, Posthog, Zapier and more
- 🔒 All open source, transparent and self-hostable
- 📲 Create **in-product surveys** with our no code editor with multiple question types
- 📚 Choose from a variety of best-practice **templates**
- 👩🏻 Launch and **target your surveys to specific user groups** without changing your application code
- 🔗 Create shareable **link surveys**
- 👨‍👩‍👦 Invite your team members to **collaborate** on your surveys
- 🔌 Integrate Formbricks with **Slack, Posthog, Zapier and more**
- 🔒 All **open source**, transparent and self-hostable
### Built With
### Built on Open Source
- [Typescript](https://www.typescriptlang.org/)
- [Next.js](https://nextjs.org/)
- [React](https://reactjs.org/)
- [TailwindCSS](https://tailwindcss.com/)
- [Prisma](https://prisma.io/)
- 💻 [Typescript](https://www.typescriptlang.org/)
- 🚀 [Next.js](https://nextjs.org/)
- ⚛️ [React](https://reactjs.org/)
- 🎨 [TailwindCSS](https://tailwindcss.com/)
- 📚 [Prisma](https://prisma.io/)
- 🔒 [Auth.js](https://authjs.dev/)
- 🧘‍♂️ [Zod](https://zod.dev/)
### Upcoming Features
## 🚀 Getting started
| | Feature |
| --- | ------------------------------------------ |
| 👷 | Zapier, Slack & Posthog Integration |
| 👷 | Webhooks |
| 🗒️ | Filtering Options in Survey Analysis |
| 🗒️ | Multi-Language Functionality |
| 🗒️ | Auto-complete Surveys after at x responses |
| 🗒️ | Pre-Fill Link-Surveys |
| 🗒️ | E-Mail Surveys |
### ☁️ Cloud Version
_👷 In Progress | 🗒️ Up Next_
Formbricks has a hosted cloud offering with a generous free plan to get you up and running as quickly as possible. To get started, please visit [formbricks.com](https://formbricks.com)
## Cloud vs. self-hosted
### 🐳 Self-hosted version
Formbricks is available Open-Source under AGPLv3 license. You can host Formbricks on your own servers without a subscription. Check out our [docs](https://formbricks.com/docs/self-hosting/deployment) to see how to self-host Formbricks.
We also have a hosted cloud offering with a generous free plan to get you up and running as quickly as possible. For more information, please visit [formbricks.com](https://formbricks.com)
Formbricks is available Open-Source under AGPLv3 license. You can host Formbricks on your own servers using Docker without a subscription. To get started with self-hosting, take a look at our [self-hosting docs](https://formbricks.com/docs/self-hosting/deployment).
(In the future we may develop additional features that aren't in the free Open-Source version)
## Contributing
## ✍️ Contribution
We are very happy if you are interested in contributing to Formbricks 🤗
There are many ways to contribute to Formbricks with writing Issues, fixing bugs, building new features or updating the docs. Please check out [our contribution guide](https://formbricks.com/docs/contributing/introduction) for more information.
Here are a few options:
- Star this repo
- Create issues every time you feel something is missing or goes wrong
- Upvote issues with 👍 reaction so we know what's the demand for particular issue to prioritize it within roadmap
Please check out [our contribution guide](https://formbricks.com/docs/contributing/introduction) and our [list of open issues](https://github.com/formbricks/formbricks/issues) for more information.
## ⚖️ License
Distributed under the AGPLv3 License. See `LICENSE` for more information.
## 🔒 Security
We take security very seriously. If you come across any security vulnerabilities, please disclose them by sending an email to security@formbricks.com. We appreciate your help in making our platform as secure as possible and are committed to working with you to resolve any issues quickly and efficiently. See `SECURITY.md` for more information.

View File

@@ -12,22 +12,21 @@
"dependencies": {
"@formbricks/js": "workspace:*",
"@heroicons/react": "^2.0.17",
"@types/node": "18.15.11",
"@types/react": "18.0.33",
"@types/react-dom": "18.0.11",
"console-feed": "^3.5.0",
"eslint": "8.37.0",
"eslint-config-formbricks": "workspace:*",
"next": "13.2.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "5.0.3"
"react-dom": "18.2.0"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.3",
"@types/node": "18.15.11",
"@types/react-dom": "18.0.11",
"@types/react": "18.0.33",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.21",
"rimraf": "^5.0.0",
"tailwindcss": "^3.3.1"
"tailwindcss": "^3.3.1",
"typescript": "5.0.3"
}
}

View File

@@ -1,6 +0,0 @@
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -12,44 +12,44 @@
},
"dependencies": {
"@calcom/embed-react": "^1.1.1",
"@docsearch/react": "^3.3.3",
"@docsearch/react": "^3.5.1",
"@formbricks/lib": "workspace:*",
"@formbricks/types": "workspace:*",
"@formbricks/ui": "workspace:*",
"@headlessui/react": "^1.7.14",
"@heroicons/react": "^2.0.17",
"@headlessui/react": "^1.7.15",
"@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.3.0",
"add": "^2.0.6",
"@next/mdx": "^13.4.7",
"@paralleldrive/cuid2": "^2.2.0",
"clsx": "^1.2.1",
"lottie-web": "^5.11.0",
"next": "13.3.0",
"next-plausible": "^3.7.2",
"next-sitemap": "^4.0.7",
"prism-react-renderer": "^1.3.5",
"lottie-web": "^5.12.2",
"next": "13.4.7",
"next-plausible": "^3.8.0",
"next-sitemap": "^4.1.3",
"prism-react-renderer": "^2.0.6",
"prismjs": "^1.29.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.43.9",
"react-icons": "^4.8.0",
"react-responsive-embed": "^2.1.0",
"remark-gfm": "^3.0.1",
"sharp": "^0.32.0"
"sharp": "^0.32.1"
},
"devDependencies": {
"@formbricks/tsconfig": "workspace:*",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.9",
"@types/node": "18.15.11",
"@types/node": "20.3.2",
"@types/prismjs": "^1.26.0",
"@types/react": "^18.0.35",
"@types/react-dom": "^18.0.11",
"@types/react": "18.2.7",
"@types/react-dom": "18.2.4",
"autoprefixer": "^10.4.14",
"eslint-config-formbricks": "workspace:*",
"postcss": "^8.4.22",
"rimraf": "^5.0.0",
"tailwindcss": "^3.3.1",
"typescript": "^5.0.4"
"postcss": "^8.4.24",
"rimraf": "^5.0.1",
"tailwindcss": "^3.3.2",
"typescript": "5.0.4"
}
}

View File

@@ -0,0 +1,11 @@
"use client";
import { signOut } from "next-auth/react";
import { useEffect } from "react";
export default function ClientLogout() {
useEffect(() => {
signOut();
});
return null;
}

View File

@@ -1,63 +0,0 @@
/*
THIS FILE IS WORK IN PROGRESS
PLEASE DO NOT USE IT YET
*/
import { responses } from "@/lib/api/response";
import { prisma } from "@formbricks/database";
import { NextResponse } from "next/server";
export async function OPTIONS(): Promise<NextResponse> {
return responses.successResponse({}, true);
}
export async function POST(request: Request): Promise<NextResponse> {
const { sessionId, environmentId, eventName, properties } = await request.json();
if (!sessionId) {
return responses.missingFieldResponse("sessionId", true);
}
if (!environmentId) {
return responses.missingFieldResponse("environmentId", true);
}
if (!eventName) {
return responses.missingFieldResponse("eventName", true);
}
const action = await prisma.event.create({
data: {
properties,
session: {
connect: {
id: sessionId,
},
},
eventClass: {
connectOrCreate: {
where: {
name_environmentId: {
name: eventName,
environmentId,
},
},
create: {
name: eventName,
type: "code",
environment: {
connect: {
id: environmentId,
},
},
},
},
},
},
select: {
id: true,
},
});
return responses.successResponse(action, true);
}

View File

@@ -1,33 +0,0 @@
/*
THIS FILE IS WORK IN PROGRESS
PLEASE DO NOT USE IT YET
*/
import { createSession } from "@/lib/api/clientSession";
import { getSettings } from "@/lib/api/clientSettings";
import { responses } from "@/lib/api/response";
import { captureTelemetry } from "@formbricks/lib/telemetry";
import { NextResponse } from "next/server";
export async function OPTIONS(): Promise<NextResponse> {
return responses.successResponse({}, true);
}
export async function POST(request: Request): Promise<NextResponse> {
const { personId, environmentId } = await request.json();
if (!personId) {
return responses.missingFieldResponse("sessionId", true);
}
if (!environmentId) {
return responses.missingFieldResponse("environmentId", true);
}
const session = await createSession(personId);
const { surveys, noCodeEvents, brandColor } = await getSettings(environmentId, personId);
captureTelemetry("session created");
return responses.successResponse({ session, surveys, noCodeEvents, brandColor }, true);
}

View File

@@ -1,43 +0,0 @@
/*
THIS FILE IS WORK IN PROGRESS
PLEASE DO NOT USE IT YET
*/
import { getSettings } from "@/lib/api/clientSettings";
import { responses } from "@/lib/api/response";
import { prisma } from "@formbricks/database";
import { captureTelemetry } from "@formbricks/lib/telemetry";
import { NextResponse } from "next/server";
export async function OPTIONS(): Promise<NextResponse> {
return responses.successResponse({}, true);
}
export async function POST(request: Request): Promise<NextResponse> {
const { userCuid } = await request.json();
if (!userCuid) {
return responses.missingFieldResponse("userCuid", true);
}
// get user
const user = await prisma.person.findUnique({
where: {
id: userCuid,
},
select: {
id: true,
environmentId: true,
},
});
if (!user) {
return responses.notFoundResponse("User", userCuid, true);
}
const { surveys, noCodeEvents, brandColor } = await getSettings(user.environmentId, user.id);
captureTelemetry("session created");
return responses.successResponse({ surveys, noCodeEvents, brandColor }, true);
}

View File

@@ -1,135 +0,0 @@
/*
THIS FILE IS WORK IN PROGRESS
PLEASE DO NOT USE IT YET
*/
import { getSettings } from "@/lib/api/clientSettings";
import { responses } from "@/lib/api/response";
import { prisma } from "@formbricks/database";
import { NextResponse } from "next/server";
export async function OPTIONS(): Promise<NextResponse> {
return responses.successResponse({}, true);
}
export async function POST(request: Request, params: { userCuid: string }): Promise<NextResponse> {
const { userCuid } = params;
const { key, value } = await request.json();
if (!key) {
return responses.missingFieldResponse("key", true);
}
if (!value) {
return responses.missingFieldResponse("value", true);
}
const currentPerson = await prisma.person.findUnique({
where: {
id: userCuid,
},
select: {
id: true,
environmentId: true,
attributes: {
select: {
id: true,
attributeClass: {
select: {
name: true,
},
},
},
},
},
});
if (!currentPerson) {
return responses.notFoundResponse("User", userCuid, true);
}
const environmentId = currentPerson.environmentId;
// find attribute class
let attributeClass = await prisma.attributeClass.findUnique({
where: {
name_environmentId: {
name: key,
environmentId,
},
},
select: {
id: true,
},
});
// create new attribute class if not found
if (attributeClass === null) {
attributeClass = await prisma.attributeClass.create({
data: {
name: key,
type: "code",
environment: {
connect: {
id: environmentId,
},
},
},
select: {
id: true,
},
});
}
// upsert attribute (update or create)
const attribute = await prisma.attribute.upsert({
where: {
attributeClassId_personId: {
attributeClassId: attributeClass.id,
personId: userCuid,
},
},
update: {
value,
},
create: {
attributeClass: {
connect: {
id: attributeClass.id,
},
},
person: {
connect: {
id: userCuid,
},
},
value,
},
select: {
person: {
select: {
id: true,
environmentId: true,
attributes: {
select: {
id: true,
value: true,
attributeClass: {
select: {
id: true,
name: true,
},
},
},
},
},
},
},
});
const user = attribute.person;
const { surveys, noCodeEvents, brandColor } = await getSettings(environmentId, user.id);
return responses.successResponse({ user, surveys, noCodeEvents, brandColor }, true);
}

View File

@@ -1,142 +0,0 @@
/*
THIS FILE IS WORK IN PROGRESS
PLEASE DO NOT USE IT YET
*/
import { getSettings } from "@/lib/api/clientSettings";
import { responses } from "@/lib/api/response";
import { prisma } from "@formbricks/database";
import { NextResponse } from "next/server";
export async function OPTIONS(): Promise<NextResponse> {
return responses.successResponse({}, true);
}
export async function POST(request: Request, params: { userCuid: string }): Promise<NextResponse> {
const { userCuid } = params;
const { userId, sessionId } = await request.json();
if (!userId) {
return responses.missingFieldResponse("userId", true);
}
if (!sessionId) {
return responses.missingFieldResponse("sessionId", true);
}
let returnedUser;
// find person
const person = await prisma.person.findUnique({
where: {
id: userCuid,
},
select: {
id: true,
environmentId: true,
},
});
if (!person) {
return responses.notFoundResponse("User", userCuid, true);
}
const environmentId = person.environmentId;
// check if person with this userId already exists
const existingPerson = await prisma.person.findFirst({
where: {
environmentId,
attributes: {
some: {
attributeClass: {
name: "userId",
},
value: userId,
},
},
},
select: {
id: true,
environmentId: true,
attributes: {
select: {
id: true,
value: true,
attributeClass: {
select: {
id: true,
name: true,
},
},
},
},
},
});
// if person exists, reconnect ression and delete old user
if (existingPerson) {
// reconnect session to new person
await prisma.session.update({
where: {
id: sessionId,
},
data: {
person: {
connect: {
id: existingPerson.id,
},
},
},
});
// delete old person
await prisma.person.delete({
where: {
id: userCuid,
},
});
returnedUser = existingPerson;
} else {
// update person
returnedUser = await prisma.person.update({
where: {
id: userCuid,
},
data: {
attributes: {
create: {
value: userId,
attributeClass: {
connect: {
name_environmentId: {
name: "userId",
environmentId,
},
},
},
},
},
},
select: {
id: true,
environmentId: true,
attributes: {
select: {
id: true,
value: true,
attributeClass: {
select: {
id: true,
name: true,
},
},
},
},
},
});
}
const { surveys, noCodeEvents, brandColor } = await getSettings(environmentId, returnedUser.id);
return responses.successResponse({ user: returnedUser, surveys, noCodeEvents, brandColor }, true);
}

View File

@@ -1,28 +0,0 @@
/*
THIS FILE IS WORK IN PROGRESS
PLEASE DO NOT USE IT YET
*/
import { createPerson } from "@/lib/api/clientPerson";
import { createSession } from "@/lib/api/clientSession";
import { getSettings } from "@/lib/api/clientSettings";
import { responses } from "@/lib/api/response";
import { NextResponse } from "next/server";
export async function OPTIONS(): Promise<NextResponse> {
return responses.successResponse({}, true);
}
export async function POST(request: Request): Promise<NextResponse> {
const { environmentId } = await request.json();
if (!environmentId) {
return responses.missingFieldResponse("environmentId", true);
}
const user = await createPerson(environmentId);
const session = await createSession(user.id);
const { surveys, noCodeEvents, brandColor } = await getSettings(environmentId, user.id);
return responses.successResponse({ user, session, surveys, noCodeEvents, brandColor }, true);
}

View File

@@ -32,7 +32,9 @@ export default function AudienceView({ environmentId, localSurvey, setLocalSurve
environmentId={environmentId}
/>
{localSurvey.type==="link" && <ResponseOptionsCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />}
{localSurvey.type === "link" && (
<ResponseOptionsCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />
)}
<RecontactOptionsCard
localSurvey={localSurvey}

View File

@@ -1,16 +0,0 @@
import Modal from "components/shared/Modal";
interface SurveyCreatedSuccessModalProps {
open: boolean;
setOpen: (open: boolean) => void;
}
export default function SurveyCreatedSuccessModal({ open, setOpen }: SurveyCreatedSuccessModalProps) {
return (
<>
<Modal open={open} setOpen={setOpen} title="Survey published 🎉">
Your survey is live and collecting valuable insights!
</Modal>
</>
);
}

View File

@@ -1,44 +0,0 @@
"use client";
import { createSurvey } from "@/lib/surveys/surveys";
import type { Template } from "@formbricks/types/templates";
import { Button } from "@formbricks/ui";
import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/solid";
import { useRouter } from "next/navigation";
import { useState } from "react";
interface TemplateMenuBarProps {
activeTemplate: Template | null;
environmentId: string;
}
export default function TemplateMenuBar({ activeTemplate, environmentId }: TemplateMenuBarProps) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const addSurvey = async (activeTemplate) => {
setLoading(true);
const survey = await createSurvey(environmentId, activeTemplate.preset);
router.push(`/environments/${environmentId}/surveys/${survey.id}/edit`);
};
return (
<div className="border-b border-slate-200 bg-white px-5 py-3 sm:flex sm:items-center sm:justify-between">
<div className="flex items-center space-x-4">
<Button variant="minimal" className="px-0" onClick={() => router.back()}>
<ArrowLeftIcon className="h-5 w-5 text-slate-700" />
</Button>
<h1 className="font-slate-700 text-lg font-semibold">Start with a template</h1>
</div>
<div className="mt-3 flex sm:ml-4 sm:mt-0">
<Button
variant="highlight"
disabled={activeTemplate === null}
loading={loading}
onClick={() => addSurvey(activeTemplate)}
EndIcon={ArrowRightIcon}>
Create Survey
</Button>
</div>
</div>
);
}

View File

@@ -6,12 +6,9 @@ import { useProduct } from "@/lib/products/products";
import { replacePresetPlaceholders } from "@/lib/templates";
import type { Template } from "@formbricks/types/templates";
import { ErrorComponent } from "@formbricks/ui";
/* import { PaintBrushIcon } from "@heroicons/react/24/solid";
import Link from "next/link"; */
import { useEffect, useState } from "react";
import PreviewSurvey from "../PreviewSurvey";
import TemplateList from "./TemplateList";
/* import TemplateMenuBar from "./TemplateMenuBar"; */
import { templates } from "./templates";
export default function SurveyTemplatesPage({ params }) {
@@ -36,7 +33,6 @@ export default function SurveyTemplatesPage({ params }) {
return (
<div className="flex h-full flex-col ">
{/* <TemplateMenuBar activeTemplate={activeTemplate} environmentId={environmentId} /> */}
<div className="relative z-0 flex flex-1 overflow-hidden">
<div className="flex-1 flex-col overflow-auto bg-slate-50">
<h1 className="ml-6 mt-6 text-2xl font-bold text-slate-800">Create a new survey</h1>

View File

@@ -1,5 +1,6 @@
import { WEBAPP_URL } from "@/../../packages/lib/constants";
import ClientLogout from "@/app/ClientLogout";
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { WEBAPP_URL } from "@formbricks/lib/constants";
import type { Session } from "next-auth";
import { getServerSession } from "next-auth";
import { headers } from "next/headers";
@@ -14,6 +15,8 @@ async function getEnvironment() {
});
if (!res.ok) {
const error = await res.json();
console.error(error);
throw new Error("Failed to fetch data");
}
@@ -31,10 +34,16 @@ export default async function Home() {
return redirect(`/onboarding`);
}
const environment = await getEnvironment();
let environment;
try {
environment = await getEnvironment();
} catch (error) {
console.error("error getting environment", error);
}
if (!environment) {
throw Error("No environment found for user");
console.error("Failed to get first environment of user; signing out");
return <ClientLogout />;
}
return redirect(`/environments/${environment.id}`);

View File

@@ -460,5 +460,3 @@ export const GrinningSquintingFace: React.FC<React.SVGProps<SVGCircleElement>> =
</svg>
);
};
export let icons = [<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg"></svg>];

View File

@@ -21,8 +21,8 @@ export const SignupForm = () => {
const handleSubmit = async (e: any) => {
e.preventDefault();
if(!isValid){
return
if (!isValid) {
return;
}
setSigningUp(true);
try {
@@ -48,8 +48,8 @@ export const SignupForm = () => {
const [isButtonEnabled, setButtonEnabled] = useState(true);
const [isPasswordFocused, setIsPasswordFocused] = useState(false);
const formRef = useRef<HTMLFormElement>(null);
const [password, setPassword] = useState<string|null>(null)
const [isValid, setIsValid] = useState(false)
const [password, setPassword] = useState<string | null>(null);
const [isValid, setIsValid] = useState(false);
const checkFormValidity = () => {
// If all fields are filled, enable the button
@@ -145,7 +145,7 @@ export const SignupForm = () => {
)}
<Button
onClick={(e: any) => {
e.preventDefault()
e.preventDefault();
if (!showLogin) {
setShowLogin(true);
setButtonEnabled(false);
@@ -158,7 +158,7 @@ export const SignupForm = () => {
variant="darkCTA"
className="w-full justify-center"
loading={signingUp}
disabled={formRef.current ? (!isButtonEnabled || !isValid): !isButtonEnabled}>
disabled={formRef.current ? !isButtonEnabled || !isValid : !isButtonEnabled}>
Continue with Email
</Button>
</form>

View File

@@ -1,33 +1,30 @@
import React from 'react'
import { useEffect,useState } from 'react'
import React from "react";
import { useEffect, useState } from "react";
export default function RedirectCountDown({
initiateCountdown
}:{
initiateCountdown:boolean|undefined
}) {
export default function RedirectCountDown({ initiateCountdown }: { initiateCountdown: boolean | undefined }) {
const [timeRemaining, setTimeRemaining] = useState(3);
const [timeRemaining, setTimeRemaining] = useState(3)
useEffect(() => {
const interval = setInterval(() => {
setTimeRemaining((prevTime) => prevTime - 1);
}, 1000);
if (timeRemaining === 0) {
clearInterval(interval);
}
// Clean up the interval when the component is unmounted
return () => clearInterval(interval);
}, [timeRemaining]);
useEffect(() => {
const interval = setInterval(() => {
setTimeRemaining((prevTime) => prevTime - 1);
}, 1000);
return (
<div>
{initiateCountdown && <div className="rounded-md bg-slate-100 p-2 text-sm mt-10">
<span>You&apos;re redirected in </span>
<span>{timeRemaining}</span>
</div>}
if (timeRemaining === 0) {
clearInterval(interval);
}
// Clean up the interval when the component is unmounted
return () => clearInterval(interval);
}, [timeRemaining]);
return (
<div>
{initiateCountdown && (
<div className="mt-10 rounded-md bg-slate-100 p-2 text-sm">
<span>You&apos;re redirected in </span>
<span>{timeRemaining}</span>
</div>
)
)}
</div>
);
}

View File

@@ -9,7 +9,12 @@ interface ThankYouCardProps {
initiateCountdown?: boolean;
}
export default function ThankYouCard({ headline, subheader, brandColor,initiateCountdown }: ThankYouCardProps) {
export default function ThankYouCard({
headline,
subheader,
brandColor,
initiateCountdown,
}: ThankYouCardProps) {
return (
<div className="text-center">
<div className="flex items-center justify-center" style={{ color: brandColor }}>
@@ -33,7 +38,7 @@ export default function ThankYouCard({ headline, subheader, brandColor,initiateC
<div>
<Headline headline={headline} questionId="thankYouCard" />
<Subheader subheader={subheader} questionId="thankYouCard" />
<RedirectCountDown initiateCountdown={initiateCountdown}/>
<RedirectCountDown initiateCountdown={initiateCountdown} />
</div>
</div>
);

View File

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

View File

@@ -12,29 +12,6 @@ export const useApiKeys = (environmentId: string) => {
};
};
export const useApiKey = (environmentId: string, id: string) => {
const { data, error, mutate } = useSWR(`/api/v1/environments/${environmentId}/api-keys/${id}`, fetcher);
return {
apiKey: data,
isLoadingApiKey: !error && !data,
isErrorApiKey: error,
mutateApiKey: mutate,
};
};
export const persistApiKey = async (environmentId: string, apiKey) => {
try {
await fetch(`/api/v1/environments/${environmentId}/api-keys/${apiKey.id}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(apiKey),
});
} catch (error) {
console.error(error);
}
};
export const createApiKey = async (environmentId: string, apiKey = {}) => {
try {
const res = await fetch(`/api/v1/environments/${environmentId}/api-keys`, {

View File

@@ -9,20 +9,3 @@ export async function verifyPassword(password: string, hashedPassword: string) {
const isValid = await compare(password, hashedPassword);
return isValid;
}
export function requireAuthentication(gssp: any) {
return async (context: any) => {
const { req, resolvedUrl } = context;
const token = req.cookies.userToken;
if (!token) {
return {
redirect: {
destination: `/auth/login?callbackUrl=${encodeURIComponent(resolvedUrl)}`,
statusCode: 302,
},
};
}
return await gssp(context); // Continue on to call `getServerSideProps` logic
};
}

View File

@@ -16,7 +16,7 @@ interface sendEmailData {
html: string;
}
export const sendEmail = async (emailData: sendEmailData) => {
const sendEmail = async (emailData: sendEmailData) => {
let transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,

View File

@@ -1,18 +1,7 @@
import platform from "platform";
export function capitalizeFirstLetter(string = "") {
return string.charAt(0).toUpperCase() + string.slice(1);
}
export const onlyUnique = (value, index, self) => {
return self.indexOf(value) === index;
};
export const parseUserAgent = (userAgent: string) => {
const info = platform.parse(userAgent);
return info.description;
};
// write a function that takes a string and truncates it to the specified length
export const truncate = (str: string, length: number) => {
if (!str) return "";
@@ -53,12 +42,3 @@ export function isLight(color) {
}
return r * 0.299 + g * 0.587 + b * 0.114 > 128;
}
export const toJson = (obj: any): Object | null => {
try {
return JSON.parse(JSON.stringify(obj));
} catch (error) {
console.error(error);
return null;
}
};

View File

@@ -15,31 +15,22 @@
"@formbricks/js": "workspace:*",
"@formbricks/lib": "workspace:*",
"@formbricks/ui": "workspace:*",
"@headlessui/react": "^1.7.15",
"@heroicons/react": "^2.0.18",
"@json2csv/node": "^7.0.1",
"@paralleldrive/cuid2": "^2.2.0",
"@radix-ui/react-collapsible": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.4",
"@types/node": "20.2.3",
"@types/react": "18.2.7",
"@types/react-dom": "18.2.4",
"bcryptjs": "^2.4.3",
"class-variance-authority": "^0.6.0",
"date-fns": "^2.30.0",
"dotenv-webpack": "^8.0.1",
"eslint": "8.41.0",
"eslint-config-next": "^13.4.3",
"jsonwebtoken": "^9.0.0",
"lodash": "^4.17.21",
"lucide-react": "^0.221.0",
"markdown-it": "^13.0.1",
"next": "13.4.3",
"next-auth": "^4.22.1",
"nodemailer": "^6.9.2",
"platform": "^1.3.6",
"posthog-js": "^1.57.3",
"posthog-node": "^3.1.1",
"preact": "10.15.0",
"prismjs": "^1.29.0",
"react": "18.2.0",
"react-beautiful-dnd": "^13.1.1",
@@ -47,10 +38,7 @@
"react-hook-form": "^7.43.9",
"react-hot-toast": "^2.4.1",
"react-icons": "^4.8.0",
"react-use": "^17.4.0",
"stripe": "^12.6.0",
"swr": "^2.1.5",
"typescript": "5.0.4",
"ua-parser-js": "^1.0.35",
"zod": "^3.21.4"
},
@@ -63,12 +51,14 @@
"@types/bcryptjs": "^2.4.2",
"@types/lodash": "^4.14.195",
"@types/markdown-it": "^12.2.3",
"@types/node": "20.2.3",
"@types/react": "18.2.7",
"@types/react-dom": "18.2.4",
"autoprefixer": "^10.4.14",
"eslint-config-formbricks": "workspace:*",
"postcss": "^8.4.23",
"rimraf": "^5.0.1",
"tailwind-merge": "^1.12.0",
"tailwindcss": "^3.3.2",
"tailwindcss-animate": "^1.0.5"
"typescript": "5.0.4"
}
}

View File

@@ -1,39 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
const formId = req.query.formId?.toString();
// CORS
if (req.method === "OPTIONS") {
res.status(200).end();
}
// POST/capture/forms/[formId]/schema
// Update form schema
// Required fields in body: -
// Optional fields in body: customerId, data
else if (req.method === "POST") {
if (process.env.FORMBRICKS_LEGACY_HOST) {
const response = await fetch(
`${process.env.FORMBRICKS_LEGACY_HOST}/api/capture/forms/${formId}/schema`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.FORMBRICKS_LEGACY_HOST}`,
},
body: JSON.stringify(req.body),
}
);
const responseData = await response.json();
res.json(responseData);
} else {
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}
// Unknown HTTP Method
else {
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}

View File

@@ -1,41 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
const formId = req.query.formId?.toString();
const submissionId = req.query.submissionId?.toString();
// CORS
if (req.method === "OPTIONS") {
res.status(200).end();
}
// PUT /capture/forms/[formId]/submissions/[submissionId]
// Extend an existing form submission
// Required fields in body: -
// Optional fields in body: customerId, data
else if (req.method === "PUT") {
// redirect request and make a request to legacy.formbricks.com
if (process.env.FORMBRICKS_LEGACY_HOST) {
const response = await fetch(
`${process.env.FORMBRICKS_LEGACY_HOST}/api/capture/forms/${formId}/submissions/${submissionId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.FORMBRICKS_LEGACY_HOST}`,
},
body: JSON.stringify(req.body),
}
);
const responseData = await response.json();
res.json(responseData);
} else {
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}
// Unknown HTTP Method
else {
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}

View File

@@ -1,39 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
const formId = req.query.formId?.toString();
// CORS
if (req.method === "OPTIONS") {
res.status(200).end();
}
// POST/capture/forms/[formId]/submissions
// Create a new form submission
// Required fields in body: -
// Optional fields in body: customerId, data
else if (req.method === "POST") {
if (process.env.FORMBRICKS_LEGACY_HOST) {
const response = await fetch(
`${process.env.FORMBRICKS_LEGACY_HOST}/api/capture/forms/${formId}/submissions`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.FORMBRICKS_LEGACY_HOST}`,
},
body: JSON.stringify(req.body),
}
);
const responseData = await response.json();
res.json(responseData);
} else {
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}
// Unknown HTTP Method
else {
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}

View File

@@ -1,119 +0,0 @@
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
const environmentId = req.query?.environmentId?.toString();
if (!environmentId) {
return res.status(400).json({ message: "Missing environmentId" });
}
const hasAccess = await hasEnvironmentAccess(req, res, environmentId);
if (!hasAccess) {
return res.status(403).json({ message: "Not authorized" });
}
// POST
if (req.method === "POST") {
// lastSyncedAt is the last time the environment was synced (iso string)
const { lastSyncedAt } = req.body;
let lastSyncedCondition = lastSyncedAt
? {
OR: [
{
createdAt: {
gt: lastSyncedAt,
},
},
{
updatedAt: {
gt: lastSyncedAt,
},
},
],
}
: {};
// Get all displays that have been created or updated since lastSyncedAt
const displays = await prisma.display.findMany({
where: {
survey: {
environmentId,
},
...lastSyncedCondition,
},
select: {
id: true,
createdAt: true,
updatedAt: true,
person: {
select: {
attributes: {
select: {
id: true,
value: true,
attributeClass: {
select: {
name: true,
},
},
},
},
},
},
},
});
// Get all responses that have been created or updated since lastSyncedAt
const responses = await prisma.response.findMany({
where: {
survey: {
environmentId,
},
...lastSyncedCondition,
},
select: {
id: true,
createdAt: true,
updatedAt: true,
person: {
select: {
attributes: {
select: {
id: true,
value: true,
attributeClass: {
select: {
name: true,
},
},
},
},
},
},
},
});
const events = [
...displays.map((display) => ({
name: "formbricks_survey_displayed",
timestamp: display.createdAt,
userId: display.person?.attributes?.find((attr) => attr.attributeClass.name === "userId")?.value,
})),
...responses.map((response) => ({
name: "formbricks_response_created",
timestamp: response.createdAt,
userId: response.person?.attributes?.find((attr) => attr.attributeClass.name === "userId")?.value,
})),
];
return res.json({ events, lastSyncedAt: new Date().toISOString() });
}
// Unknown HTTP Method
else {
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}

View File

@@ -1,121 +0,0 @@
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
interface FormbricksUser {
userId: string;
attributes: { [key: string]: string };
}
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
const environmentId = req.query?.environmentId?.toString();
if (!environmentId) {
return res.status(400).json({ message: "Missing environmentId" });
}
const hasAccess = await hasEnvironmentAccess(req, res, environmentId);
if (!hasAccess) {
return res.status(403).json({ message: "Not authorized" });
}
// POST
if (req.method === "POST") {
// lastSyncedAt is the last time the environment was synced (iso string)
const { users }: { users: FormbricksUser[] } = req.body;
for (const user of users) {
// check if user with this userId as attribute already exists
const existingUser = await prisma.person.findFirst({
where: {
attributes: {
some: {
attributeClass: {
name: "userId",
environmentId,
},
value: user.userId,
},
},
},
select: {
id: true,
attributes: {
select: {
id: true,
value: true,
attributeClass: {
select: {
name: true,
},
},
},
},
},
});
if (existingUser) {
// user already exists, loop through attributes and update or create them
const attributeType: "noCode" = "noCode";
for (const key of Object.keys(user.attributes)) {
const existingAttribute = existingUser.attributes.find(
(attribute) => attribute.attributeClass.name === key
);
if (existingAttribute) {
// skip if value is the same
if (existingAttribute.value === user.attributes[key]) {
continue;
}
await prisma.attribute.update({
where: {
id: existingAttribute.id,
},
data: {
value: user.attributes[key].toString(),
},
});
} else {
// create attribute
await prisma.attribute.create({
data: {
value: user.attributes[key],
attributeClass: {
connectOrCreate: {
where: {
name_environmentId: {
name: key,
environmentId,
},
},
create: {
name: key,
description: "Created by Posthog Import",
type: attributeType,
environment: {
connect: {
id: environmentId,
},
},
},
},
},
person: {
connect: {
id: existingUser.id,
},
},
},
});
}
}
}
}
return res.status(200).end();
}
// Unknown HTTP Method
else {
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
}
}

View File

@@ -1,7 +1,7 @@
version: "3.3"
services:
postgres:
restart: unless-stopped
restart: always
image: postgres:15-alpine
volumes:
- postgres:/var/lib/postgresql/data
@@ -9,7 +9,7 @@ services:
- POSTGRES_PASSWORD=postgres
formbricks:
restart: unless-stopped
restart: always
build:
context: .
dockerfile: ./apps/web/Dockerfile

View File

@@ -1,5 +1,5 @@
{
"extends": "@formbricks/tsconfig/js-library.json",
"include": ["**/*.ts", "tsup.config.ts"],
"exclude": ["node_modules"]
"exclude": ["node_modules", "dist"]
}

View File

@@ -24,21 +24,21 @@
"studio": "prisma studio"
},
"dependencies": {
"@formbricks/types": "workspace:*",
"@prisma/client": "^4.15.0",
"prisma-json-types-generator": "^2.4.0",
"@prisma/client": "^4.16.1",
"prisma-json-types-generator": "^2.5.0",
"zod": "^3.21.4",
"zod-prisma": "^0.5.4"
},
"devDependencies": {
"@formbricks/tsconfig": "workspace:*",
"eslint": "^8.41.0",
"@formbricks/types": "workspace:*",
"eslint": "^8.43.0",
"eslint-config-formbricks": "workspace:*",
"prisma": "^4.15.0",
"prisma": "^4.16.1",
"prisma-dbml-generator": "^0.10.0",
"rimraf": "^5.0.1",
"tsup": "^6.7.0",
"tsup": "^7.1.0",
"tsx": "^3.12.7",
"typescript": "^5.0.4"
"typescript": "^5.1.3"
}
}

View File

@@ -8,7 +8,7 @@ datasource db {
generator client {
provider = "prisma-client-js"
previewFeatures = ["filteredRelationCount", "extendedWhereUnique"]
previewFeatures = ["extendedWhereUnique"]
//provider = "prisma-dbml-generator"
}

View File

@@ -1,5 +1,5 @@
{
"extends": "@formbricks/tsconfig/node16.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "tsup.config.ts"],
"exclude": ["node_modules"]
"exclude": ["node_modules", "dist", "zod"]
}

View File

@@ -19,6 +19,7 @@
},
"dependencies": {
"@formbricks/database": "workspace:*",
"next": "^13.4.4"
"next": "^13.4.4",
"stripe": "^12.6.0"
}
}

9
packages/errors/LICENSE Normal file
View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2023 Matthias Nannt, Johannes Dancker
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,8 +1,8 @@
{
"name": "@formbricks/errors",
"license": "MIT",
"description": "A helper package containing general error classes for Formbricks",
"private": true,
"version": "1.0.0",
"version": "0.1.0",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
@@ -15,8 +15,8 @@
"dev": "tsup --watch",
"lint": "eslint ./src --fix",
"clean": "rimraf .turbo node_modules dist"
},
"devDependencies": {
},
"devDependencies": {
"@formbricks/tsconfig": "workspace:*",
"eslint": "^8.41.0",
"eslint-config-formbricks": "workspace:*",

View File

@@ -1,5 +1,5 @@
{
"extends": "@formbricks/tsconfig/node16.json",
"include": ["**/*.ts", "tsup.config.ts"],
"exclude": ["node_modules"]
"exclude": ["node_modules", "dist"]
}

View File

@@ -23,7 +23,7 @@
},
"scripts": {
"clean": "rimraf .turbo node_modules dist",
"dev": "microbundle --css inline",
"dev": "microbundle --css inline --watch",
"lint": "eslint '{src,tests}/**/*.{ts,tsx}'",
"test": "jest",
"build": "microbundle --css inline"

View File

@@ -58,5 +58,6 @@
"references": [
{ "path": "../../../types/tsconfig.json" } // Add this line, adjust the path to the actual location
],
"include": ["src", "../types", "../lib/client"]
"include": ["src", "../types", "../lib/client"],
"exclude": ["node_modules", "dist", "coverage"]
}

View File

@@ -14,7 +14,11 @@
"dependencies": {
"@formbricks/database": "*",
"@formbricks/types": "*",
"@formbricks/errors": "*"
"@formbricks/errors": "*",
"date-fns": "^2.30.0",
"markdown-it": "^13.0.1",
"posthog-node": "^3.1.1",
"tailwind-merge": "^1.12.0"
},
"devDependencies": {
"@formbricks/tsconfig": "*",

View File

@@ -25,9 +25,8 @@ const PasswordInput = ({ className, ...rest }: PasswordInputProps) => {
/>
<button
type="button"
className={cn("absolute top-1/2 right-3 transform -translate-y-1/2")}
onClick={togglePasswordVisibility}
>
className={cn("absolute right-3 top-1/2 -translate-y-1/2 transform")}
onClick={togglePasswordVisibility}>
{showPassword ? (
<EyeSlashIcon className="h-5 w-5 text-slate-400 " />
) : (
@@ -39,4 +38,3 @@ const PasswordInput = ({ className, ...rest }: PasswordInputProps) => {
};
export { PasswordInput };

View File

@@ -10,9 +10,7 @@
},
"types": "./index.tsx",
"scripts": {
"build": "tsup index.tsx --format esm,cjs --dts --external react",
"clean": "rimraf .turbo node_modules dist",
"dev": "tsup index.tsx --format esm,cjs --dts --external react --watch"
"clean": "rimraf .turbo node_modules dist"
},
"devDependencies": {
"@formbricks/tsconfig": "workspace:*",
@@ -24,7 +22,6 @@
"postcss": "^8.4.24",
"react": "^18.2.0",
"rimraf": "^5.0.1",
"tsup": "^6.7.0",
"typescript": "^5.0.4"
},
"dependencies": {
@@ -47,6 +44,7 @@
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.6",
"boring-avatars": "^1.7.0",
"class-variance-authority": "^0.6.0",
"clsx": "^1.2.1",
"cmdk": "^0.2.0",
"lucide-react": "^0.233.0",

1827
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"],
"env": [
"FORMBRICKS_LEGACY_HOST",
"GITHUB_ID",
"GITHUB_SECRET",
"GOOGLE_CLIENT_ID",
@@ -80,7 +79,8 @@
"outputs": []
},
"dev": {
"cache": false
"cache": false,
"persistent": true
},
"start": {
"outputs": []