feat: Formbricks App Redesign (#2581)

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
Johannes
2024-05-09 17:30:44 +02:00
committed by GitHub
parent 556ee870b1
commit 50407498ec
339 changed files with 3451 additions and 3173 deletions

View File

@@ -35,3 +35,4 @@ yarn-error.log*
next-env.d.ts
public/sitemap*.xml
public/robots.txt

View File

@@ -2,7 +2,6 @@ import { MdxImage } from "@/components/MdxImage";
import { Libraries } from "./components/Libraries";
import SetupChecklist from "./images/env-id.png";
import ReactApp from "./images/react-in-app-survey-app-popup-form.webp";
import WidgetConnected from "./images/widget-connected.webp";
import WidgetNotConnected from "./images/widget-not-connected.webp";
@@ -27,14 +26,7 @@ for something else, please [join our Discord!](https://formbricks.com/discord) a
Before getting started, make sure you have:
1. A web application (behind your user authentication system) in your desired framework is set up and running.
2. A Formbricks account with access to your environment ID and API host. You can find these in the **Setup Checklist** in the Settings:
<MdxImage
src={SetupChecklist}
alt="Step 2 - Setup Checklist"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
2. A Formbricks account with access to your environment ID and API host. You can find these in the **Setup Checklist** in the Settings.
---

View File

@@ -2,7 +2,6 @@ import { MdxImage } from "@/components/MdxImage";
import { Libraries } from "./components/Libraries";
import SetupChecklist from "./images/env-id.png";
import ReactApp from "./images/react-in-app-survey-app-popup-form.webp";
import WidgetConnected from "./images/widget-connected.webp";
import WidgetNotConnected from "./images/widget-not-connected.webp";
@@ -31,14 +30,7 @@ Detailed Website Survey SDK documentation can be found [here](/developer-docs/we
Before getting started, make sure you have:
1. A **public-facing** web application in your desired framework is set up and running.
2. A Formbricks account with access to your environment ID and API host. You can find these in the **Setup Checklist** in the Settings:
<MdxImage
src={SetupChecklist}
alt="Step 2 - Setup Checklist"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
2. A Formbricks account with access to your environment ID and API host. You can find these in the **Setup Checklist** in the Settings.
---

View File

@@ -3,9 +3,11 @@
import { navigation } from "@/lib/navigation";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { FaDiscord, FaGithub, FaXTwitter } from "react-icons/fa6";
import { Button } from "./Button";
import { DiscordIcon } from "./icons/DiscordIcon";
import { GithubIcon } from "./icons/GithubIcon";
import { TwitterIcon } from "./icons/TwitterIcon";
function PageLink({
label,
@@ -98,13 +100,13 @@ function SmallPrint() {
Formbricks GmbH &copy; {currentYear}. All rights reserved.
</p>
<div className="flex gap-4">
<SocialLink href="https://twitter.com/formbricks" icon={FaXTwitter}>
<SocialLink href="https://twitter.com/formbricks" icon={TwitterIcon}>
Follow us on Twitter
</SocialLink>
<SocialLink href="https://github.com/formbricks/formbricks" icon={FaGithub}>
<SocialLink href="https://github.com/formbricks/formbricks" icon={GithubIcon}>
Follow us on GitHub
</SocialLink>
<SocialLink href="https://formbricks.com/discord" icon={FaDiscord}>
<SocialLink href="https://formbricks.com/discord" icon={DiscordIcon}>
Join our Discord server
</SocialLink>
</div>

View File

@@ -0,0 +1,15 @@
export const DiscordIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 640 512"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
{...props}>
<path d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"></path>
</svg>
);
};

View File

@@ -0,0 +1,15 @@
export const GithubIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 496 512"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
{...props}>
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path>
</svg>
);
};

View File

@@ -0,0 +1,15 @@
export const TwitterIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 512 512"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
{...props}>
<path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path>
</svg>
);
};

View File

@@ -1,5 +1,5 @@
{
"name": "@formbricks/formbricks-com",
"name": "@formbricks/docs",
"version": "1.0.0",
"private": true,
"scripts": {
@@ -50,7 +50,6 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"react-highlight-words": "^0.20.0",
"react-icons": "^5.2.1",
"react-markdown": "^9.0.1",
"react-responsive-embed": "^2.1.0",
"remark": "^15.0.1",

View File

@@ -1,7 +1,7 @@
{
"extends": "@formbricks/tsconfig/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "../../packages/types/*.d.ts"],
"exclude": ["../../.env"],
"exclude": ["../../.env", "node_modules"],
"compilerOptions": {
"baseUrl": ".",
"paths": {

View File

@@ -0,0 +1,57 @@
import FormbricksClient from "@/app/(app)/components/FormbricksClient";
import PosthogIdentify from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { AuthorizationError } from "@formbricks/types/errors";
import { DevEnvironmentBanner } from "@formbricks/ui/DevEnvironmentBanner";
import ToasterClient from "@formbricks/ui/ToasterClient";
export default async function EnvLayout({ children, params }) {
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);
}
const hasAccess = await hasUserEnvironmentAccess(session.user.id, params.environmentId);
if (!hasAccess) {
throw new AuthorizationError("Not authorized");
}
const team = await getTeamByEnvironmentId(params.environmentId);
if (!team) {
throw new Error("Team not found");
}
const environment = await getEnvironment(params.environmentId);
if (!environment) {
throw new Error("Environment not found");
}
return (
<>
<ResponseFilterProvider>
<PosthogIdentify
session={session}
environmentId={params.environmentId}
teamId={team.id}
teamName={team.name}
inAppSurveyBillingStatus={team.billing.features.inAppSurvey.status}
linkSurveyBillingStatus={team.billing.features.linkSurvey.status}
userTargetingBillingStatus={team.billing.features.userTargeting.status}
/>
<FormbricksClient session={session} />
<ToasterClient />
<div className="flex h-screen flex-col">
<DevEnvironmentBanner environment={environment} />
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
</div>
</ResponseFilterProvider>
</>
);
}

View File

@@ -61,7 +61,7 @@ export const deleteSurveyAction = async (surveyId: string) => {
await deleteSurvey(surveyId);
};
export const refetchProduct = async (productId: string): Promise<TProduct | null> => {
export const refetchProductAction = async (productId: string): Promise<TProduct | null> => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");

View File

@@ -1,12 +1,12 @@
"use client";
import { CreateNewActionTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/CreateNewActionTab";
import { SavedActionsTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SavedActionsTab";
import { TActionClass } from "@formbricks/types/actionClasses";
import { TSurvey } from "@formbricks/types/surveys";
import { ModalWithTabs } from "@formbricks/ui/ModalWithTabs";
import { CreateNewActionTab } from "./CreateNewActionTab";
import { SavedActionsTab } from "./SavedActionsTab";
interface AddActionModalProps {
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;

View File

@@ -1,7 +1,6 @@
import LogicEditor from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditor";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
import LogicEditor from "./LogicEditor";
import UpdateQuestionId from "./UpdateQuestionId";
interface AdvancedSettingsProps {

View File

@@ -23,7 +23,7 @@ interface BackgroundStylingCardProps {
isUnsplashConfigured: boolean;
}
export default function BackgroundStylingCard({
export const BackgroundStylingCard = ({
open,
setOpen,
styling,
@@ -33,7 +33,7 @@ export default function BackgroundStylingCard({
disabled,
environmentId,
isUnsplashConfigured,
}: BackgroundStylingCardProps) {
}: BackgroundStylingCardProps) => {
const { bgType, brightness } = styling?.background ?? {};
const handleBgChange = (color: string, type: TSurveyBackgroundBgType) => {
@@ -150,4 +150,4 @@ export default function BackgroundStylingCard({
</Collapsible.CollapsibleContent>
</Collapsible.Root>
);
}
};

View File

@@ -27,7 +27,7 @@ type CardStylingSettingsProps = {
localProduct: TProduct;
};
const CardStylingSettings = ({
export const CardStylingSettings = ({
setStyling,
styling,
isSettingsPage = false,
@@ -300,5 +300,3 @@ const CardStylingSettings = ({
</Collapsible.Root>
);
};
export default CardStylingSettings;

View File

@@ -21,7 +21,7 @@ type FormStylingSettingsProps = {
disabled?: boolean;
};
const FormStylingSettings = ({
export const FormStylingSettings = ({
styling,
setStyling,
open,
@@ -201,5 +201,3 @@ const FormStylingSettings = ({
</Collapsible.Root>
);
};
export default FormStylingSettings;

View File

@@ -1,6 +1,5 @@
"use client";
import { validateId } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/lib/validation";
import * as Collapsible from "@radix-ui/react-collapsible";
import { FC, useState } from "react";
import { toast } from "react-hot-toast";
@@ -13,6 +12,8 @@ import { Label } from "@formbricks/ui/Label";
import { Switch } from "@formbricks/ui/Switch";
import { Tag } from "@formbricks/ui/Tag";
import { validateId } from "../lib/validation";
interface HiddenFieldsCardProps {
localSurvey: TSurvey;
setLocalSurvey: (survey: TSurvey) => void;

View File

@@ -182,7 +182,7 @@ export default function HowToSendCard({ localSurvey, setLocalSurvey, environment
</p>
<p className="text-xs font-normal">
<Link
href={`/environments/${environment.id}/settings/setup`}
href={`/environments/${environment.id}/product/setup`}
className="underline hover:text-amber-900"
target="_blank">
Connect Formbricks

View File

@@ -1,8 +1,8 @@
import { HelpCircle, TrashIcon } from "lucide-react";
import { ChevronDown, SplitIcon } from "lucide-react";
import { CornerDownRightIcon, MoveDownIcon } from "lucide-react";
import { useMemo } from "react";
import { toast } from "react-hot-toast";
import { BsArrowDown, BsArrowReturnRight } from "react-icons/bs";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
@@ -269,7 +269,7 @@ export default function LogicEditor({
<div className="mt-2 space-y-3">
{question?.logic?.map((logic, logicIdx) => (
<div key={logicIdx} className="flex items-center space-x-2 space-y-1 text-xs xl:text-sm">
<BsArrowReturnRight className="h-4 w-4" />
<CornerDownRightIcon className="h-4 w-4" />
<p className="text-slate-800">If this answer</p>
<Select value={logic.condition} onValueChange={(e) => updateLogic(logicIdx, { condition: e })}>
@@ -385,7 +385,7 @@ export default function LogicEditor({
</div>
))}
<div className="flex flex-wrap items-center space-x-2 py-1 text-sm">
<BsArrowDown className="h-4 w-4" />
<MoveDownIcon className="h-4 w-4" />
<p className="text-slate-700">All other answers will continue to the next question</p>
</div>
</div>

View File

@@ -1,6 +1,5 @@
"use client";
import { isLabelValidForAllLanguages } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/lib/validation";
import { PlusIcon, TrashIcon } from "lucide-react";
import { useState } from "react";
import { toast } from "react-hot-toast";
@@ -11,6 +10,8 @@ import { Button } from "@formbricks/ui/Button";
import { Label } from "@formbricks/ui/Label";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
import { isLabelValidForAllLanguages } from "../lib/validation";
interface MatrixQuestionFormProps {
localSurvey: TSurvey;
question: TSurveyMatrixQuestion;

View File

@@ -1,6 +1,5 @@
"use client";
import { isLabelValidForAllLanguages } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/lib/validation";
import { createId } from "@paralleldrive/cuid2";
import { PlusIcon, TrashIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
@@ -20,6 +19,8 @@ import { Label } from "@formbricks/ui/Label";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
import { isLabelValidForAllLanguages } from "../lib/validation";
interface OpenQuestionFormProps {
localSurvey: TSurvey;
question: TSurveyMultipleChoiceSingleQuestion;

View File

@@ -1,10 +1,9 @@
"use client";
import { getPlacementStyle } from "@/app/lib/preview";
import { cn } from "@formbricks/lib/cn";
import { TPlacement } from "@formbricks/types/common";
import { Label } from "@formbricks/ui/Label";
import { getPlacementStyle } from "@formbricks/ui/PreviewSurvey/lib/utils";
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/RadioGroup";
const placements = [

View File

@@ -1,19 +1,5 @@
"use client";
import { AddressQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddressQuestionForm";
import { AdvancedSettings } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/AdvancedSettings";
import { CTAQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/CTAQuestionForm";
import { CalQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/CalQuestionForm";
import { ConsentQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConsentQuestionForm";
import { DateQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/DateQuestionForm";
import { FileUploadQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/FileUploadQuestionForm";
import { MatrixQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/MatrixQuestionForm";
import { MultipleChoiceMultiForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/MultipleChoiceMultiForm";
import { MultipleChoiceSingleForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/MultipleChoiceSingleForm";
import { NPSQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/NPSQuestionForm";
import { OpenQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/OpenQuestionForm";
import { PictureSelectionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/PictureSelectionForm";
import { RatingQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/RatingQuestionForm";
import { getTSurveyQuestionTypeName } from "@/app/lib/questions";
import * as Collapsible from "@radix-ui/react-collapsible";
import {
@@ -44,7 +30,21 @@ import { Label } from "@formbricks/ui/Label";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
import { Switch } from "@formbricks/ui/Switch";
import QuestionDropdown from "./QuestionMenu";
import { AddressQuestionForm } from "./AddressQuestionForm";
import { AdvancedSettings } from "./AdvancedSettings";
import { CTAQuestionForm } from "./CTAQuestionForm";
import { CalQuestionForm } from "./CalQuestionForm";
import { ConsentQuestionForm } from "./ConsentQuestionForm";
import { DateQuestionForm } from "./DateQuestionForm";
import { FileUploadQuestionForm } from "./FileUploadQuestionForm";
import { MatrixQuestionForm } from "./MatrixQuestionForm";
import { MultipleChoiceMultiForm } from "./MultipleChoiceMultiForm";
import { MultipleChoiceSingleForm } from "./MultipleChoiceSingleForm";
import { NPSQuestionForm } from "./NPSQuestionForm";
import { OpenQuestionForm } from "./OpenQuestionForm";
import { PictureSelectionForm } from "./PictureSelectionForm";
import { QuestionDropdown } from "./QuestionMenu";
import { RatingQuestionForm } from "./RatingQuestionForm";
interface QuestionCardProps {
localSurvey: TSurvey;

View File

@@ -10,13 +10,13 @@ interface QuestionDropdownProps {
moveQuestion: (questionIdx: number, up: boolean) => void;
}
export default function QuestionActions({
export const QuestionDropdown = ({
questionIdx,
lastQuestion,
duplicateQuestion,
deleteQuestion,
moveQuestion,
}: QuestionDropdownProps) {
}: QuestionDropdownProps) => {
return (
<div className="flex space-x-2">
<ArrowUpIcon
@@ -57,4 +57,4 @@ export default function QuestionActions({
/>
</div>
);
}
};

View File

@@ -1,11 +1,5 @@
"use client";
import HiddenFieldsCard from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/HiddenFieldsCard";
import {
isCardValid,
validateQuestion,
validateSurveyQuestionsInBatch,
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/lib/validation";
import { createId } from "@paralleldrive/cuid2";
import { useEffect, useMemo, useState } from "react";
import { DragDropContext } from "react-beautiful-dnd";
@@ -18,9 +12,11 @@ import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/u
import { TProduct } from "@formbricks/types/product";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
import { isCardValid, validateQuestion, validateSurveyQuestionsInBatch } from "../lib/validation";
import AddQuestionButton from "./AddQuestionButton";
import EditThankYouCard from "./EditThankYouCard";
import EditWelcomeCard from "./EditWelcomeCard";
import HiddenFieldsCard from "./HiddenFieldsCard";
import QuestionCard from "./QuestionCard";
import { StrictModeDroppable } from "./StrictModeDroppable";

View File

@@ -148,7 +148,7 @@ export default function RecontactOptionsCard({
This setting overwrites your{" "}
<Link
className="decoration-brand-dark underline"
href={`/environments/${environmentId}/settings/product`}
href={`/environments/${environmentId}/product/general`}
target="_blank">
waiting period
</Link>

View File

@@ -1,5 +1,3 @@
import SurveyPlacementCard from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyPlacementCard";
import { AdvancedTargetingCard } from "@formbricks/ee/advancedTargeting/components/AdvancedTargetingCard";
import { TActionClass } from "@formbricks/types/actionClasses";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
@@ -11,6 +9,7 @@ import { TSurvey } from "@formbricks/types/surveys";
import HowToSendCard from "./HowToSendCard";
import RecontactOptionsCard from "./RecontactOptionsCard";
import ResponseOptionsCard from "./ResponseOptionsCard";
import SurveyPlacementCard from "./SurveyPlacementCard";
import TargetingCard from "./TargetingCard";
import WhenToSendCard from "./WhenToSendCard";

View File

@@ -1,6 +1,3 @@
import BackgroundStylingCard from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/BackgroundStylingCard";
import CardStylingSettings from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/CardStylingSettings";
import FormStylingSettings from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/FormStylingSettings";
import { RotateCcwIcon } from "lucide-react";
import Link from "next/link";
import React, { useEffect, useMemo, useState } from "react";
@@ -13,6 +10,10 @@ import { AlertDialog } from "@formbricks/ui/AlertDialog";
import { Button } from "@formbricks/ui/Button";
import { Switch } from "@formbricks/ui/Switch";
import { BackgroundStylingCard } from "./BackgroundStylingCard";
import { CardStylingSettings } from "./CardStylingSettings";
import { FormStylingSettings } from "./FormStylingSettings";
type StylingViewProps = {
environment: TEnvironment;
product: TProduct;

View File

@@ -1,13 +1,5 @@
"use client";
import { refetchProduct } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
import { LoadingSkeleton } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/LoadingSkeleton";
import { QuestionsAudienceTabs } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsStylingSettingsTabs";
import { QuestionsView } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView";
import { SettingsView } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SettingsView";
import { StylingView } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/StylingView";
import { SurveyMenuBar } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyMenuBar";
import { PreviewSurvey } from "@/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey";
import { useCallback, useEffect, useRef, useState } from "react";
import { extractLanguageCodes, getEnabledLanguages } from "@formbricks/lib/i18n/utils";
@@ -20,6 +12,15 @@ import { TMembershipRole } from "@formbricks/types/memberships";
import { TProduct } from "@formbricks/types/product";
import { TSegment } from "@formbricks/types/segment";
import { TSurvey, TSurveyEditorTabs, TSurveyStyling } from "@formbricks/types/surveys";
import { PreviewSurvey } from "@formbricks/ui/PreviewSurvey";
import { refetchProductAction } from "../actions";
import { LoadingSkeleton } from "./LoadingSkeleton";
import { QuestionsAudienceTabs } from "./QuestionsStylingSettingsTabs";
import { QuestionsView } from "./QuestionsView";
import { SettingsView } from "./SettingsView";
import { StylingView } from "./StylingView";
import { SurveyMenuBar } from "./SurveyMenuBar";
interface SurveyEditorProps {
survey: TSurvey;
@@ -64,7 +65,7 @@ export default function SurveyEditor({
const [localStylingChanges, setLocalStylingChanges] = useState<TSurveyStyling | null>(null);
const fetchLatestProduct = useCallback(async () => {
const latestProduct = await refetchProduct(localProduct.id);
const latestProduct = await refetchProductAction(localProduct.id);
if (latestProduct) {
setLocalProduct(latestProduct);
}
@@ -91,7 +92,7 @@ export default function SurveyEditor({
const listener = () => {
if (document.visibilityState === "visible") {
const fetchLatestProduct = async () => {
const latestProduct = await refetchProduct(localProduct.id);
const latestProduct = await refetchProductAction(localProduct.id);
if (latestProduct) {
setLocalProduct(latestProduct);
}

View File

@@ -1,7 +1,6 @@
"use client";
import { SurveyStatusDropdown } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown";
import { isSurveyValid } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/lib/validation";
import { isEqual } from "lodash";
import { AlertTriangleIcon, ArrowLeftIcon, SettingsIcon } from "lucide-react";
import { useRouter } from "next/navigation";
@@ -19,6 +18,7 @@ import { Input } from "@formbricks/ui/Input";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
import { updateSurveyAction } from "../actions";
import { isSurveyValid } from "../lib/validation";
interface SurveyMenuBarProps {
localSurvey: TSurvey;
@@ -53,7 +53,7 @@ export const SurveyMenuBar = ({
const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false);
const [isSurveyPublishing, setIsSurveyPublishing] = useState(false);
const [isSurveySaving, setIsSurveySaving] = useState(false);
const cautionText = "This survey received responses, make changes with caution.";
const cautionText = "This survey received responses.";
const faultyQuestions: string[] = [];
@@ -216,13 +216,6 @@ export const SurveyMenuBar = ({
return (
<>
{environment?.type === "development" && (
<nav className="top-0 z-10 w-full border-b border-slate-200 bg-white">
<div className="h-6 w-full bg-[#A33700] p-0.5 text-center text-sm text-white">
You&apos;re in development mode. Use it to test surveys, actions and attributes.
</div>
</nav>
)}
<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-2 whitespace-nowrap">
<Button

View File

@@ -1,6 +1,5 @@
"use client";
import Placement from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/Placement";
import * as Collapsible from "@radix-ui/react-collapsible";
import { CheckIcon } from "lucide-react";
import Link from "next/link";
@@ -11,6 +10,8 @@ import { TSurvey, TSurveyProductOverwrites } from "@formbricks/types/surveys";
import { Label } from "@formbricks/ui/Label";
import { Switch } from "@formbricks/ui/Switch";
import Placement from "./Placement";
interface SurveyPlacementCardProps {
localSurvey: TSurvey;
setLocalSurvey: (survey: TSurvey) => void;

View File

@@ -1,6 +1,5 @@
"use client";
import { validateId } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/lib/validation";
import { useState } from "react";
import toast from "react-hot-toast";
@@ -9,6 +8,8 @@ import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { validateId } from "../lib/validation";
interface UpdateQuestionIdProps {
localSurvey: TSurvey;
question: TSurveyQuestion;

View File

@@ -131,7 +131,8 @@ export default function WhenToSendCard({
className="w-full rounded-lg border border-slate-300 bg-white">
<Collapsible.CollapsibleTrigger
asChild
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50">
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50"
id="whenToSendCardTrigger">
<div className="inline-flex px-4 py-4">
<div className="flex items-center pl-2 pr-5">
{containsEmptyTriggers ? (

View File

@@ -0,0 +1,5 @@
import { LoadingSkeleton } from "./components/LoadingSkeleton";
export default function Loading() {
return <LoadingSkeleton />;
}

View File

@@ -0,0 +1,45 @@
import { TSurvey } from "@formbricks/types/surveys";
export const minimalSurvey: TSurvey = {
id: "someUniqueId1",
createdAt: new Date(),
updatedAt: new Date(),
name: "Minimal Survey",
type: "app",
environmentId: "someEnvId1",
createdBy: null,
status: "draft",
displayOption: "displayOnce",
autoClose: null,
triggers: [],
redirectUrl: null,
recontactDays: null,
welcomeCard: {
enabled: false,
headline: { default: "Welcome!" },
html: { default: "Thanks for providing your feedback - let's go!" },
timeToFinish: false,
showResponseCount: false,
},
questions: [],
thankYouCard: {
enabled: false,
},
hiddenFields: {
enabled: false,
},
delay: 0, // No delay
displayPercentage: null,
autoComplete: null,
runOnDate: null,
closeOnDate: null,
surveyClosedMessage: {
enabled: false,
},
productOverwrites: null,
singleUse: null,
styling: null,
resultShareKey: null,
segment: null,
languages: [],
};

View File

@@ -0,0 +1,20 @@
"use client";
import { ArrowLeftIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { Button } from "@formbricks/ui/Button";
export const BackButton = () => {
const router = useRouter();
return (
<Button
variant="secondary"
StartIcon={ArrowLeftIcon}
onClick={() => {
router.back();
}}>
Back
</Button>
);
};

View File

@@ -0,0 +1,15 @@
"use client";
import { BackButton } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/BackButton";
export const MenuBar = () => {
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-2 whitespace-nowrap">
<BackButton />
</div>
</div>
</>
);
};

View File

@@ -1,17 +1,17 @@
"use client";
import { PreviewSurvey } from "@/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey";
import { TemplateList } from "@/app/(app)/environments/[environmentId]/surveys/templates/TemplateList";
import { replacePresetPlaceholders } from "@/app/lib/templates";
import { useEffect, useState } from "react";
import { MenuBar } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/MenuBar";
import { useState } from "react";
import type { TEnvironment } from "@formbricks/types/environment";
import type { TProduct } from "@formbricks/types/product";
import type { TTemplate } from "@formbricks/types/templates";
import { TUser } from "@formbricks/types/user";
import { PreviewSurvey } from "@formbricks/ui/PreviewSurvey";
import { SearchBox } from "@formbricks/ui/SearchBox";
import { TemplateList } from "@formbricks/ui/TemplateList";
import { minimalSurvey, templates } from "./templates";
import { minimalSurvey } from "../../lib/minimalSurvey";
type TemplateContainerWithPreviewProps = {
environmentId: string;
@@ -29,21 +29,15 @@ export default function TemplateContainerWithPreview({
const [activeTemplate, setActiveTemplate] = useState<TTemplate | null>(null);
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
const [templateSearch, setTemplateSearch] = useState<string | null>(null);
useEffect(() => {
if (product && templates?.length) {
const newTemplate = replacePresetPlaceholders(templates[0], product);
setActiveTemplate(newTemplate);
setActiveQuestionId(newTemplate.preset.questions[0].id);
}
}, [product]);
return (
<div className="flex h-full flex-col ">
<div className="flex h-full flex-col">
<MenuBar />
<div className="relative z-0 flex flex-1 overflow-hidden">
<div className="flex-1 flex-col overflow-auto bg-slate-50">
<div className="flex flex-col items-center justify-between md:flex-row md:items-start">
<h1 className="ml-6 mt-6 text-2xl font-bold text-slate-800">Create a new survey</h1>
<div className="ml-6 mt-6 px-6">
<div className="ml-6 mt-6 flex flex-col items-center justify-between md:flex-row md:items-start">
<h1 className="text-2xl font-bold text-slate-800">Create a new survey</h1>
<div className="px-6">
<SearchBox
autoFocus
value={templateSearch ?? ""}

View File

@@ -4,7 +4,7 @@ import { authOptions } from "@formbricks/lib/authOptions";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import TemplateContainerWithPreview from "./TemplateContainer";
import TemplateContainerWithPreview from "./components/TemplateContainer";
export default async function SurveyTemplatesPage({ params }) {
const session = await getServerSession(authOptions);

View File

@@ -4,7 +4,6 @@ import { useEffect, useState } from "react";
import { Button } from "@formbricks/ui/Button";
import { Confetti } from "@formbricks/ui/Confetti";
import { ContentWrapper } from "@formbricks/ui/ContentWrapper";
interface ConfirmationPageProps {
environmentId: string;
@@ -15,25 +14,24 @@ export default function ConfirmationPage({ environmentId }: ConfirmationPageProp
useEffect(() => {
setShowConfetti(true);
}, []);
return (
<div className="h-full w-full">
{showConfetti && <Confetti />}
<ContentWrapper>
<div className="mx-auto max-w-sm py-8 sm:px-6 lg:px-8">
<div className="my-6 sm:flex-auto">
<h1 className="text-center text-xl font-semibold text-slate-900">Upgrade successful</h1>
<p className="mt-2 text-center text-sm text-slate-700">
Thanks a lot for upgrading your Formbricks subscription.
</p>
</div>
<Button
variant="darkCTA"
className="w-full justify-center"
href={`/environments/${environmentId}/settings/billing`}>
Back to billing overview
</Button>
<div className="mx-auto max-w-sm py-8 sm:px-6 lg:px-8">
<div className="my-6 sm:flex-auto">
<h1 className="text-center text-xl font-semibold text-slate-900">Upgrade successful</h1>
<p className="mt-2 text-center text-sm text-slate-700">
Thanks a lot for upgrading your Formbricks subscription.
</p>
</div>
</ContentWrapper>
<Button
variant="darkCTA"
className="w-full justify-center"
href={`/environments/${environmentId}/settings/billing`}>
Back to billing overview
</Button>
</div>
</div>
);
}

View File

@@ -1,9 +1,15 @@
import ConfirmationPage from "./components/ConfirmationPage";
import ConfirmationPage from "@/app/(app)/billing-confirmation/components/ConfirmationPage";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
export const dynamic = "force-dynamic";
export default function BillingConfirmation({ searchParams }) {
const { environmentId } = searchParams;
return <ConfirmationPage environmentId={environmentId?.toString()} />;
return (
<PageContentWrapper>
<ConfirmationPage environmentId={environmentId?.toString()} />
</PageContentWrapper>
);
}

View File

@@ -1,7 +1,6 @@
"use client";
import { useState } from "react";
import { useMemo } from "react";
import { useMemo, useState } from "react";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import { Switch } from "@formbricks/ui/Switch";
@@ -9,7 +8,6 @@ import { Switch } from "@formbricks/ui/Switch";
import { AttributeDetailModal } from "./AttributeDetailModal";
import { AttributeClassDataRow } from "./AttributeRowData";
import { AttributeTableHeading } from "./AttributeTableHeading";
import { HowToAddAttributesButton } from "./HowToAddAttributesButton";
import { UploadAttributesModal } from "./UploadAttributesModal";
interface AttributeClassesTableProps {
@@ -45,16 +43,15 @@ export const AttributeClassesTable = ({ attributeClasses }: AttributeClassesTabl
return (
<>
<div className="mb-6 flex items-center justify-end text-right">
{hasArchived && (
{hasArchived && (
<div className="my-4 flex items-center justify-end text-right">
<div className="flex items-center text-sm font-medium">
Show archived
<Switch className="mx-3" checked={showArchived} onCheckedChange={toggleShowArchived} />
</div>
)}
<HowToAddAttributesButton />
</div>
<div className="rounded-lg border border-slate-200">
</div>
)}
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<AttributeTableHeading />
<div className="grid-cols-7">
{displayedAttributeClasses.map((attributeClass, index) => (

View File

@@ -5,12 +5,10 @@ import { Badge } from "@formbricks/ui/Badge";
export const AttributeClassDataRow = ({ attributeClass }) => {
return (
<div className="m-2 grid h-16 grid-cols-5 content-center rounded-lg hover:bg-slate-100">
<div className="m-2 grid h-16 grid-cols-5 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
<div className="col-span-5 flex items-center pl-6 text-sm sm:col-span-3">
<div className="flex items-center">
<div className="h-10 w-10 flex-shrink-0">
<TagIcon className="h-8 w-8 flex-shrink-0 text-slate-500" />
</div>
<TagIcon className="h-5 w-5 flex-shrink-0 text-slate-500" />
<div className="ml-4 text-left">
<div className="font-medium text-slate-900">
{attributeClass.name}

View File

@@ -1,7 +1,7 @@
export const AttributeTableHeading = () => {
return (
<>
<div className="grid h-12 grid-cols-5 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
<div className="grid h-12 grid-cols-5 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
<div className="col-span-3 pl-6 ">Name</div>
<div className="hidden text-center sm:block">Created</div>
<div className="hidden text-center sm:block">Last Updated</div>

View File

@@ -1,21 +1,8 @@
import { HelpCircleIcon, TagIcon } from "lucide-react";
import { Button } from "@formbricks/ui/Button";
import { TagIcon } from "lucide-react";
export default function Loading() {
return (
<>
<div className="mb-6 text-right">
<div className="mb-6 flex items-center justify-end text-right">
<Button
variant="secondary"
className="pointer-events-none animate-pulse cursor-not-allowed select-none">
<HelpCircleIcon className="mr-2 h-4 w-4" />
Loading Attributes
</Button>
</div>
</div>
<div className="rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-5 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
<div className="col-span-3 pl-6 ">Name</div>

View File

@@ -0,0 +1,38 @@
import { PeopleSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PeopleSecondaryNavigation";
import { CircleHelpIcon } from "lucide-react";
import { Metadata } from "next";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { Button } from "@formbricks/ui/Button";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
import { AttributeClassesTable } from "./components/AttributeClassesTable";
export const metadata: Metadata = {
title: "Attributes",
};
export default async function AttributesPage({ params }) {
let attributeClasses = await getAttributeClasses(params.environmentId);
const HowToAddAttributesButton = (
<Button
size="sm"
href="https://formbricks.com/docs/app-surveys/user-identification#setting-custom-user-attributes"
variant="secondary"
target="_blank"
EndIcon={CircleHelpIcon}>
How to add attributes
</Button>
);
return (
<PageContentWrapper>
<PageHeader pageTitle="People" cta={HowToAddAttributesButton}>
<PeopleSecondaryNavigation activeId="attributes" environmentId={params.environmentId} />
</PageHeader>
<AttributeClassesTable attributeClasses={attributeClasses} />
</PageContentWrapper>
);
}

View File

@@ -1,4 +1,4 @@
import ActivityTimeline from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ActivityTimeline";
import ActivityTimeline from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ActivityTimeline";
import { getActionsByPersonId } from "@formbricks/lib/action/service";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";

View File

@@ -1,21 +1,20 @@
"use client";
import { deletePersonAction } from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/actions";
import { deletePersonAction } from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/actions";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
import { TMembershipRole } from "@formbricks/types/memberships";
import { DeleteDialog } from "@formbricks/ui/DeleteDialog";
interface DeletePersonButtonProps {
environmentId: string;
personId: string;
membershipRole?: TMembershipRole;
isViewer: boolean;
}
export function DeletePersonButton({ environmentId, personId }: DeletePersonButtonProps) {
export const DeletePersonButton = ({ environmentId, personId, isViewer }: DeletePersonButtonProps) => {
const router = useRouter();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@@ -34,6 +33,11 @@ export function DeletePersonButton({ environmentId, personId }: DeletePersonButt
setIsDeletingPerson(false);
}
};
if (isViewer) {
return null;
}
return (
<>
<button
@@ -51,4 +55,4 @@ export function DeletePersonButton({ environmentId, personId }: DeletePersonButt
/>
</>
);
}
};

View File

@@ -1,4 +1,4 @@
import ResponseTimeline from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ResponseTimeline";
import ResponseTimeline from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseTimeline";
import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";

View File

@@ -1,6 +1,6 @@
"use client";
import ResponseFeed from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ResponsesFeed";
import ResponseFeed from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponsesFeed";
import { ArrowDownUpIcon } from "lucide-react";
import { useEffect, useState } from "react";

View File

@@ -1,7 +1,7 @@
import {
ActivityItemIcon,
ActivityItemPopover,
} from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ActivityItemComponents";
} from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ActivityItemComponents";
import { ArrowDownUpIcon } from "lucide-react";
import { TrashIcon } from "lucide-react";

View File

@@ -0,0 +1,76 @@
import ActivitySection from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ActivitySection";
import AttributesSection from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/AttributesSection";
import { DeletePersonButton } from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/DeletePersonButton";
import ResponseSection from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseSection";
import { getServerSession } from "next-auth";
import { getAttributes } from "@formbricks/lib/attribute/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getPerson } from "@formbricks/lib/person/service";
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
export default async function PersonPage({ params }) {
const [environment, environmentTags, product, session, team, person, attributes] = await Promise.all([
getEnvironment(params.environmentId),
getTagsByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
getServerSession(authOptions),
getTeamByEnvironmentId(params.environmentId),
getPerson(params.personId),
getAttributes(params.personId),
]);
if (!product) {
throw new Error("Product not found");
}
if (!environment) {
throw new Error("Environment not found");
}
if (!session) {
throw new Error("Session not found");
}
if (!team) {
throw new Error("Team not found");
}
if (!person) {
throw new Error("Person not found");
}
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const { isViewer } = getAccessFlags(currentUserMembership?.role);
const getDeletePersonButton = () => {
return (
<DeletePersonButton environmentId={environment.id} personId={params.personId} isViewer={isViewer} />
);
};
return (
<PageContentWrapper>
<PageHeader pageTitle={getPersonIdentifier(person, attributes)} cta={getDeletePersonButton()} />
<section className="pb-24 pt-6">
<div className="grid grid-cols-1 gap-x-8 md:grid-cols-4">
<AttributesSection personId={params.personId} />
<ResponseSection
environment={environment}
personId={params.personId}
environmentTags={environmentTags}
/>
<ActivitySection environmentId={params.environmentId} personId={params.personId} />
</div>
</section>
</PageContentWrapper>
);
}

View File

@@ -0,0 +1,28 @@
import { SecondaryNavigation } from "@formbricks/ui/SecondaryNavigation";
interface PeopleSegmentsTabsProps {
activeId: string;
environmentId: string;
}
export const PeopleSecondaryNavigation = ({ activeId, environmentId }: PeopleSegmentsTabsProps) => {
const navigation = [
{
id: "people",
label: "People",
href: `/environments/${environmentId}/people`,
},
{
id: "segments",
label: "Segments",
href: `/environments/${environmentId}/segments`,
},
{
id: "attributes",
label: "Attributes",
href: `/environments/${environmentId}/attributes`,
},
];
return <SecondaryNavigation navigation={navigation} activeId={activeId} />;
};

View File

@@ -2,7 +2,7 @@ import Link from "next/link";
import React from "react";
import { getAttributes } from "@formbricks/lib/attribute/service";
import { getPersonIdentifier } from "@formbricks/lib/person/util";
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
import { TPerson } from "@formbricks/types/people";
import { PersonAvatar } from "@formbricks/ui/Avatars";

View File

@@ -1,14 +1,6 @@
import HowToAddPeopleButton from "@/app/(app)/environments/[environmentId]/components/HowToAddPeopleButton";
export default function Loading() {
return (
<>
<div className="mb-6 text-right">
<div className="mb-6 flex items-center justify-end text-right">
<HowToAddPeopleButton />
</div>
</div>
<div className="rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-7 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
<div className="col-span-3 pl-6">User</div>

View File

@@ -1,10 +1,14 @@
import HowToAddPeopleButton from "@/app/(app)/environments/[environmentId]/components/HowToAddPeopleButton";
import { PeopleSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PeopleSecondaryNavigation";
import { CircleHelpIcon } from "lucide-react";
import { ITEMS_PER_PAGE } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getPeople, getPeopleCount } from "@formbricks/lib/person/service";
import { TPerson } from "@formbricks/types/people";
import { Button } from "@formbricks/ui/Button";
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
import { Pagination } from "@formbricks/ui/Pagination";
import { PersonCard } from "./components/PersonCard";
@@ -36,28 +40,38 @@ export default async function PeoplePage({
people = await getPeople(params.environmentId, pageNumber);
}
const HowToAddPeopleButton = (
<Button
size="sm"
href="https://formbricks.com/docs/app-surveys/user-identification"
variant="secondary"
target="_blank"
EndIcon={CircleHelpIcon}>
How to add people
</Button>
);
return (
<>
<div className="mb-6 text-right">
<div className="mb-6 flex items-center justify-end text-right">
<HowToAddPeopleButton />
</div>
</div>
<PageContentWrapper>
<PageHeader pageTitle="People" cta={HowToAddPeopleButton}>
<PeopleSecondaryNavigation activeId="people" environmentId={params.environmentId} />
</PageHeader>
{people.length === 0 ? (
<EmptySpaceFiller
type="table"
environment={environment}
emptyMessage="Your users will appear here as soon as they use your app ⏲️"
noWidgetRequired={true}
/>
) : (
<div className="rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-7 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="grid h-12 grid-cols-7 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
<div className="col-span-3 pl-6 ">User</div>
<div className="col-span-2 hidden text-center sm:block">User ID</div>
<div className="col-span-2 hidden text-center sm:block">Email</div>
</div>
{people.map((person) => (
<PersonCard person={person} />
<PersonCard person={person} key={person.id} />
))}
</div>
)}
@@ -69,6 +83,6 @@ export default async function PeoplePage({
itemsPerPage={ITEMS_PER_PAGE}
/>
)}
</>
</PageContentWrapper>
);
}

View File

@@ -0,0 +1,50 @@
"use server";
import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { deleteSegment, getSegment, updateSegment } from "@formbricks/lib/segment/service";
import { AuthorizationError } from "@formbricks/types/errors";
import { TSegmentUpdateInput, ZSegmentFilters } from "@formbricks/types/segment";
export const deleteBasicSegmentAction = async (environmentId: string, segmentId: string) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
if (!environmentAccess) throw new AuthorizationError("Not authorized");
const foundSegment = await getSegment(segmentId);
if (!foundSegment) {
throw new Error(`Segment with id ${segmentId} not found`);
}
return await deleteSegment(segmentId);
};
export const updateBasicSegmentAction = async (
environmentId: string,
segmentId: string,
data: TSegmentUpdateInput
) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
if (!environmentAccess) throw new AuthorizationError("Not authorized");
const { filters } = data;
if (filters) {
const parsedFilters = ZSegmentFilters.safeParse(filters);
if (!parsedFilters.success) {
const errMsg =
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
throw new Error(errMsg);
}
}
return await updateSegment(segmentId, data);
};

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