Duplicate Questions, Add Survey Name to Summary, Update Login Screen (#322)

* Duplicate Questions, Add Survey Name to Summary, Update Login Screen

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Johannes
2023-05-31 09:52:58 +02:00
committed by GitHub
parent 66c747d1ca
commit 78f7b4d03e
15 changed files with 178 additions and 81 deletions

View File

@@ -4,13 +4,15 @@ import FormWrapper from "@/components/auth/FormWrapper";
export default function SignInPage() {
return (
<div className="grid min-h-screen w-full bg-gradient-to-tr from-slate-200 to-slate-50 lg:grid-cols-2">
<div className="hidden lg:flex">
<div className="grid min-h-screen w-full bg-gradient-to-tr from-slate-100 to-slate-50 lg:grid-cols-5">
<div className="col-span-2 hidden lg:flex">
<Testimonial />
</div>
<FormWrapper>
<SigninForm />
</FormWrapper>
<div className="col-span-3 flex flex-col items-center justify-center">
<FormWrapper>
<SigninForm />
</FormWrapper>
</div>
</div>
);
}

View File

@@ -5,29 +5,31 @@ import Testimonial from "@/components/auth/Testimonial";
export default function SignUpPage() {
return (
<div className="grid min-h-screen w-full bg-gradient-to-tr from-slate-200 to-slate-50 lg:grid-cols-2">
<div className="hidden lg:flex">
<div className="grid min-h-screen w-full bg-gradient-to-tr from-slate-100 to-slate-50 lg:grid-cols-5">
<div className="col-span-2 hidden lg:flex">
<Testimonial />
</div>
<FormWrapper>
{process.env.NEXT_PUBLIC_SIGNUP_DISABLED === "1" ? (
<>
<h1 className="leading-2 mb-4 text-center font-bold">Sign up disabled</h1>
<p className="text-center">
The account creation is disabled in this instance. Please contact the site administrator to
create an account.
</p>
<hr className="my-4" />
<Link
href="/"
className="mt-5 flex w-full justify-center rounded-md border border-slate-400 bg-white px-4 py-2 text-sm font-medium text-slate-600 shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2">
Login
</Link>
</>
) : (
<SignupForm />
)}
</FormWrapper>
<div className="col-span-3 flex flex-col items-center justify-center">
<FormWrapper>
{process.env.NEXT_PUBLIC_SIGNUP_DISABLED === "1" ? (
<>
<h1 className="leading-2 mb-4 text-center font-bold">Sign up disabled</h1>
<p className="text-center">
The account creation is disabled in this instance. Please contact the site administrator to
create an account.
</p>
<hr className="my-4" />
<Link
href="/"
className="mt-5 flex w-full justify-center rounded-md border border-slate-400 bg-white px-4 py-2 text-sm font-medium text-slate-600 shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2">
Login
</Link>
</>
) : (
<SignupForm />
)}
</FormWrapper>
</div>
</div>
);
}

View File

@@ -23,5 +23,5 @@ export default function SurveyResultsTab({ activeId, environmentId, surveyId }:
},
];
return <SecondNavbar tabs={tabs} activeId={activeId} />;
return <SecondNavbar tabs={tabs} activeId={activeId} surveyId={surveyId} environmentId={environmentId} />;
}

View File

@@ -17,8 +17,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@formbricks/ui";
import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline";
import { TrashIcon } from "@heroicons/react/24/solid";
import { QuestionMarkCircleIcon, TrashIcon } from "@heroicons/react/24/solid";
import { ChevronDown, SplitIcon } from "lucide-react";
import { useMemo } from "react";
import { BsArrowDown, BsArrowReturnRight } from "react-icons/bs";
@@ -129,10 +128,6 @@ export default function LogicEditor({
},
};
// useEffect(() => {
// console.log(question);
// }, [question]);
const addLogic = () => {
const newLogic: Logic[] = !question.logic ? [] : question.logic;
newLogic.push({
@@ -217,13 +212,13 @@ export default function LogicEditor({
{question?.logic?.map((logic, logicIdx) => (
<div key={logicIdx} className="flex items-center space-x-2 space-y-1 text-sm">
<BsArrowReturnRight className="h-4 w-4" />
<p>If this answer</p>
<p className="text-slate-700">If this answer</p>
<Select
defaultValue={logic.condition}
onValueChange={(e) => updateLogic(logicIdx, { condition: e })}>
<SelectTrigger className="min-w-fit flex-1">
<SelectValue placeholder="select condition" />
<SelectValue placeholder="Select condition" />
</SelectTrigger>
<SelectContent>
{conditions[question.type].map(
@@ -244,7 +239,7 @@ export default function LogicEditor({
defaultValue={logic.value}
onValueChange={(e) => updateLogic(logicIdx, { value: e })}>
<SelectTrigger>
<SelectValue placeholder="select match type" />
<SelectValue placeholder="Select match type" />
</SelectTrigger>
<SelectContent>
{logicConditions[logic.condition].values?.map((value) => (
@@ -259,7 +254,7 @@ export default function LogicEditor({
<DropdownMenuTrigger className="z-10 cursor-pointer" asChild>
<div className="flex h-10 w-full items-center justify-between rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ">
{logic.value?.length === 0 ? (
<p className="text-slate-400">select match type</p>
<p className="text-slate-400">Select match type</p>
) : (
<p>{logic.value.join(", ")}</p>
)}
@@ -285,13 +280,13 @@ export default function LogicEditor({
</div>
)}
<p>skip to</p>
<p className="text-slate-700">skip to</p>
<Select
defaultValue={logic.destination}
onValueChange={(e) => updateLogic(logicIdx, { destination: e })}>
<SelectTrigger className="w-fit overflow-hidden ">
<SelectValue placeholder="select question" />
<SelectValue placeholder="Select question" />
</SelectTrigger>
<SelectContent>
{localSurvey.questions.map(
@@ -314,7 +309,7 @@ export default function LogicEditor({
))}
<div className="flex flex-wrap items-center space-x-2 py-1 text-sm">
<BsArrowDown className="h-4 w-4" />
<p>All other answers will continue to the next question</p>
<p className="text-slate-700">All other answers will continue to the next question</p>
</div>
</div>
)}
@@ -322,6 +317,7 @@ export default function LogicEditor({
<div className="mt-2 flex items-center space-x-2">
<Button
id="logicJumps"
className="bg-slate-100 px-6 py-2 hover:bg-slate-50"
type="button"
name="logicJumps"
variant="secondary"
@@ -329,12 +325,12 @@ export default function LogicEditor({
onClick={() => addLogic()}>
Add Logic
</Button>
<TooltipProvider>
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger asChild>
<QuestionMarkCircleIcon className="h-5 w-5 cursor-default" />
<QuestionMarkCircleIcon className="ml-2 inline h-4 w-4 cursor-default text-slate-500" />
</TooltipTrigger>
<TooltipContent className="max-w-[200px]" side="top">
<TooltipContent className="max-w-[300px]" side="top">
With logic jumps you can skip questions based on the responses users give.
</TooltipContent>
</Tooltip>

View File

@@ -95,7 +95,7 @@ export default function MultipleChoiceMultiForm({
/>
{question.choices && question.choices.length > 2 && (
<TrashIcon
className="ml-2 h-4 w-4 text-slate-400"
className="ml-2 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
onClick={() => deleteChoice(choiceIdx)}
/>
)}

View File

@@ -18,7 +18,6 @@ export default function MultipleChoiceSingleForm({
updateQuestion,
lastQuestion,
}: OpenQuestionFormProps): JSX.Element {
// console.log(localSurvey);
const updateChoice = (choiceIdx: number, updatedAttributes: any) => {
const newChoices = !question.choices
? []
@@ -96,7 +95,7 @@ export default function MultipleChoiceSingleForm({
/>
{question.choices && question.choices.length > 2 && (
<TrashIcon
className="ml-2 h-4 w-4 text-slate-400"
className="ml-2 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
onClick={() => deleteChoice(choiceIdx)}
/>
)}

View File

@@ -1,5 +1,6 @@
"use client";
import LogicEditor from "@/app/environments/[environmentId]/surveys/[surveyId]/edit/LogicEditor";
import { getQuestionTypeName } from "@/lib/questions";
import { cn } from "@formbricks/lib/cn";
import type { Question } from "@formbricks/types/questions";
@@ -26,7 +27,6 @@ import OpenQuestionForm from "./OpenQuestionForm";
import QuestionDropdown from "./QuestionDropdown";
import RatingQuestionForm from "./RatingQuestionForm";
import UpdateQuestionId from "./UpdateQuestionId";
import LogicEditor from "@/app/environments/[environmentId]/surveys/[surveyId]/edit/LogicEditor";
interface QuestionCardProps {
localSurvey: Survey;
@@ -35,6 +35,7 @@ interface QuestionCardProps {
moveQuestion: (questionIndex: number, up: boolean) => void;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
deleteQuestion: (questionIdx: number) => void;
duplicateQuestion: (questionIdx: number) => void;
activeQuestionId: string | null;
setActiveQuestionId: (questionId: string | null) => void;
lastQuestion: boolean;
@@ -46,6 +47,7 @@ export default function QuestionCard({
questionIdx,
moveQuestion,
updateQuestion,
duplicateQuestion,
deleteQuestion,
activeQuestionId,
setActiveQuestionId,
@@ -130,6 +132,7 @@ export default function QuestionCard({
<QuestionDropdown
questionIdx={questionIdx}
lastQuestion={lastQuestion}
duplicateQuestion={duplicateQuestion}
deleteQuestion={deleteQuestion}
moveQuestion={moveQuestion}
/>

View File

@@ -1,11 +1,18 @@
"use client";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@formbricks/ui";
import { EllipsisHorizontalIcon, ArrowUpIcon, ArrowDownIcon, TrashIcon } from "@heroicons/react/24/solid";
import {
EllipsisHorizontalIcon,
ArrowUpIcon,
ArrowDownIcon,
TrashIcon,
DocumentDuplicateIcon,
} from "@heroicons/react/24/solid";
interface QuestionDropdownProps {
questionIdx: number;
lastQuestion: boolean;
duplicateQuestion: (questionIdx: number) => void;
deleteQuestion: (questionIdx: number) => void;
moveQuestion: (questionIdx: number, up: boolean) => void;
}
@@ -13,6 +20,7 @@ interface QuestionDropdownProps {
export default function QuestionDropdown({
questionIdx,
lastQuestion,
duplicateQuestion,
deleteQuestion,
moveQuestion,
}: QuestionDropdownProps) {
@@ -22,14 +30,6 @@ export default function QuestionDropdown({
<EllipsisHorizontalIcon className="h-5 w-5 text-slate-600 focus:outline-none active:outline-none" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="justify-between"
onClick={(e) => {
e.stopPropagation();
deleteQuestion(questionIdx);
}}>
Delete <TrashIcon className="ml-3 h-4" />
</DropdownMenuItem>
<DropdownMenuItem
className="justify-between"
onClick={(e) => {
@@ -49,6 +49,22 @@ export default function QuestionDropdown({
Move down
<ArrowDownIcon className="h-4" />
</DropdownMenuItem>
<DropdownMenuItem
className="justify-between"
onClick={(e) => {
e.stopPropagation();
duplicateQuestion(questionIdx);
}}>
Duplicate <DocumentDuplicateIcon className="ml-3 h-4" />
</DropdownMenuItem>
<DropdownMenuItem
className="justify-between"
onClick={(e) => {
e.stopPropagation();
deleteQuestion(questionIdx);
}}>
Delete <TrashIcon className="ml-3 h-4" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);

View File

@@ -1,13 +1,14 @@
"use client";
import type { Survey } from "@formbricks/types/surveys";
import { DragDropContext } from "react-beautiful-dnd";
import AddQuestionButton from "./AddQuestionButton";
import QuestionCard from "./QuestionCard";
import { StrictModeDroppable } from "./StrictModeDroppable";
import EditThankYouCard from "./EditThankYouCard";
import { createId } from "@paralleldrive/cuid2";
import { useMemo } from "react";
import { DragDropContext } from "react-beautiful-dnd";
import toast from "react-hot-toast";
import AddQuestionButton from "./AddQuestionButton";
import EditThankYouCard from "./EditThankYouCard";
import QuestionCard from "./QuestionCard";
import { StrictModeDroppable } from "./StrictModeDroppable";
interface QuestionsViewProps {
localSurvey: Survey;
@@ -73,6 +74,28 @@ export default function QuestionsView({
setActiveQuestionId(localSurvey.questions[questionIdx - 1].id);
}
}
toast.success("Question deleted.");
};
const duplicateQuestion = (questionIdx: number) => {
const questionToDuplicate = JSON.parse(JSON.stringify(localSurvey.questions[questionIdx]));
const newQuestionId = createId();
// create a copy of the question with a new id
const duplicatedQuestion = {
...questionToDuplicate,
id: newQuestionId,
};
// insert the new question right after the original one
const updatedSurvey = JSON.parse(JSON.stringify(localSurvey));
updatedSurvey.questions.splice(questionIdx + 1, 0, duplicatedQuestion);
setLocalSurvey(updatedSurvey);
setActiveQuestionId(newQuestionId);
internalQuestionIdMap[newQuestionId] = createId();
toast.success("Question duplicated.");
};
const addQuestion = (question: any) => {
@@ -122,6 +145,7 @@ export default function QuestionsView({
questionIdx={questionIdx}
moveQuestion={moveQuestion}
updateQuestion={updateQuestion}
duplicateQuestion={duplicateQuestion}
deleteQuestion={deleteQuestion}
activeQuestionId={activeQuestionId}
setActiveQuestionId={setActiveQuestionId}

View File

@@ -4,7 +4,7 @@ export default function FormWrapper({ children }: { children: React.ReactNode })
return (
<div className="mx-auto flex flex-1 flex-col justify-center px-4 py-12 sm:px-6 lg:flex-none lg:px-20 xl:px-24">
<div className="mx-auto w-full max-w-sm rounded-xl bg-white p-8 shadow-xl lg:w-96">
<div className="mb-6 text-center">
<div className="mb-8 text-center">
<Logo className="mx-auto w-3/4" />
</div>
{children}

View File

@@ -38,7 +38,7 @@ export const SigninForm = () => {
return (
<>
<div className="text-center">
<p className="mb-8 text-lg text-slate-700">Log in to your account</p>
<h1 className="mb-4 text-slate-700">Log in to your account</h1>
<div className="space-y-2">
<form onSubmit={handleSubmit} ref={formRef} className="space-y-2" onChange={checkFormValidity}>
{showLogin && (
@@ -98,7 +98,7 @@ export const SigninForm = () => {
className="w-full justify-center"
loading={loggingIn}
disabled={!isButtonEnabled}>
Continue with Email
Login with Email
</Button>
</form>
@@ -114,9 +114,11 @@ export const SigninForm = () => {
)}
</div>
{process.env.NEXT_PUBLIC_SIGNUP_DISABLED !== "1" && (
<div className="mt-3 text-center text-xs text-slate-600">
<Link href="/auth/signup" className="font-semibold underline">
Create new account
<div className="mt-9 text-center text-xs ">
<span className="leading-5 text-slate-500">New to Formbricks?</span>
<br />
<Link href="/auth/signup" className="font-semibold text-slate-600 underline hover:text-slate-700">
Create an account
</Link>
</div>
)}

View File

@@ -67,7 +67,7 @@ export const SignupForm = () => {
</div>
)}
<div className="text-center">
<p className="mb-8 text-lg text-slate-700">Create your Formbricks account</p>
<h1 className="mb-4 text-slate-700">Create your Formbricks account</h1>
<div className="space-y-2">
<form onSubmit={handleSubmit} ref={formRef} className="space-y-2" onChange={checkFormValidity}>
{showLogin && (
@@ -189,9 +189,10 @@ export const SignupForm = () => {
</div>
)}
<div className="mt-3 text-center text-xs text-slate-600">
Have an account?{" "}
<Link href="/auth/login" className="font-semibold underline">
<div className="mt-9 text-center text-xs ">
<span className="leading-5 text-slate-500">Have an account?</span>
<br />
<Link href="/auth/login" className="font-semibold text-slate-600 underline hover:text-slate-700">
Log in.
</Link>
</div>

View File

@@ -5,10 +5,10 @@ import CalComLogo from "@/images/cal-logo-light.svg";
export default function Testimonial() {
return (
<div className="flex flex-col items-center justify-center">
<div className="mb-10 w-3/4 space-y-8 2xl:w-1/2">
<div className="flex flex-col items-center justify-center bg-gradient-to-tr from-slate-100 to-slate-300">
<div className="3xl:w-2/3 mb-10 space-y-8 px-12 xl:px-20 ">
<div>
<h2 className="text-3xl font-bold text-slate-900">
<h2 className="text-3xl font-bold text-slate-800">
Versatile in-app surveys. Valuable user insights.
</h2>
</div>
@@ -19,19 +19,19 @@ export default function Testimonial() {
<div className="space-y-2">
<div className="flex space-x-2">
<CheckCircleIcon className="text-brand-dark h-6 w-6" />
<p className="inline text-lg text-slate-900">All features included</p>
<p className="inline text-lg text-slate-800">All features included</p>
</div>
<div className="flex space-x-2">
<CheckCircleIcon className="text-brand-dark h-6 w-6" />
<p className="inline text-lg text-slate-900">Free and open-source</p>
<p className="inline text-lg text-slate-800">Free and open-source</p>
</div>
<div className="flex space-x-2">
<CheckCircleIcon className="text-brand-dark h-6 w-6" />
<p className="inline text-lg text-slate-900">No credit card required</p>
<p className="inline text-lg text-slate-800">No credit card required</p>
</div>
</div>
<div className="border-1 rounded-xl border-slate-200 bg-slate-50 p-6 shadow-sm">
<div className="rounded-xl border border-slate-200 bg-gradient-to-tr from-slate-100 to-slate-200 p-8">
<p className="italic text-slate-700">
We measure the clarity of our docs and learn from churn all on one platform. Great product, very
responsive team!

View File

@@ -1,16 +1,30 @@
import { cn } from "@formbricks/lib/cn";
import SurveyNavBarName from "@/components/shared/SurveyNavBarName";
import Link from "next/link";
interface SecondNavbarProps {
tabs: { id: string; label: string; href: string; icon?: React.ReactNode }[];
activeId: string;
surveyId?: string;
environmentId?: string;
}
export default function SecondNavbar({ tabs, activeId, ...props }: SecondNavbarProps) {
export default function SecondNavbar({
tabs,
activeId,
surveyId,
environmentId,
...props
}: SecondNavbarProps) {
return (
<div {...props}>
<div className="flex h-14 w-full items-center justify-center border-b bg-white">
<nav className="flex h-full items-center space-x-4" aria-label="Tabs">
<div className="grid h-14 w-full grid-cols-3 items-center justify-items-stretch border-b bg-white px-4">
<div className="justify-self-start">
{surveyId && environmentId && (
<SurveyNavBarName surveyId={surveyId} environmentId={environmentId} />
)}
</div>{" "}
<nav className="flex h-full items-center space-x-4 justify-self-center" aria-label="Tabs">
{tabs.map((tab) => (
<Link
key={tab.id}
@@ -27,6 +41,7 @@ export default function SecondNavbar({ tabs, activeId, ...props }: SecondNavbarP
</Link>
))}
</nav>
<div className="justify-self-end"></div>
</div>
</div>
);

View File

@@ -0,0 +1,37 @@
"use client";
import { useProduct } from "@/lib/products/products";
import { useSurvey } from "@/lib/surveys/surveys";
interface SurveyNavBarNameProps {
surveyId: string;
environmentId: string;
}
export default function SurveyNavBarName({ surveyId, environmentId }: SurveyNavBarNameProps) {
const { survey, isLoadingSurvey, isErrorSurvey } = useSurvey(environmentId, surveyId);
const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId);
if (isLoadingSurvey || isLoadingProduct) {
return null;
}
if (isErrorProduct || isErrorSurvey) {
return null;
}
return (
<div className="hidden items-center space-x-2 whitespace-nowrap md:flex">
{/* <Button
variant="secondary"
StartIcon={ArrowLeftIcon}
onClick={() => {
router.back();
}}>
Back
</Button> */}
<p className="pl-4 font-semibold">{product.name} / </p>
<span>{survey.name}</span>
</div>
);
}