mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-26 11:48:27 -05:00
update layout approach to have a unified look
This commit is contained in:
@@ -37,7 +37,7 @@ export default function FormList() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="h-full px-6 py-8 lg:px-8">
|
||||
{forms &&
|
||||
(forms.length === 0 ? (
|
||||
<div className="mt-5 text-center">
|
||||
@@ -53,7 +53,7 @@ export default function FormList() {
|
||||
</EmptyPageFiller>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 place-content-stretch">
|
||||
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 place-content-stretch ">
|
||||
<button onClick={() => newForm()}>
|
||||
<li className="col-span-1">
|
||||
<div className="overflow-hidden font-light text-white rounded-md shadow bg-snoopfade">
|
||||
@@ -67,7 +67,7 @@ export default function FormList() {
|
||||
{forms
|
||||
.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
.map((form, formIdx) => (
|
||||
<li key={form.id} className="relative col-span-1 realative ">
|
||||
<li key={form.id} className="relative col-span-1 ">
|
||||
<div className="flex flex-col justify-between h-full bg-white rounded-md shadow">
|
||||
<div className="px-4 py-5 text-lg sm:p-6">
|
||||
{form.name}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import {
|
||||
DocumentAddIcon,
|
||||
EyeIcon,
|
||||
PaperAirplaneIcon,
|
||||
ShareIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
import { NoCodeForm } from "@prisma/client";
|
||||
import { useRouter } from "next/router";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { useForm } from "../../lib/forms";
|
||||
import { persistNoCodeForm, useNoCodeForm } from "../../lib/noCodeForm";
|
||||
import SecondNavBar from "../layout/SecondNavBar";
|
||||
import Loading from "../Loading";
|
||||
import Page from "./Page";
|
||||
import ShareModal from "./ShareModal";
|
||||
import SecondNavBar from "../layout/SecondNavBar";
|
||||
import SecondNavBarItem from "../layout/SecondNavBarItem";
|
||||
import {
|
||||
DocumentAddIcon,
|
||||
PlusIcon,
|
||||
EyeIcon,
|
||||
ShareIcon,
|
||||
PaperAirplaneIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
|
||||
export default function Builder({ formId }) {
|
||||
const router = useRouter();
|
||||
const { form, isLoadingForm } = useForm(formId);
|
||||
const { noCodeForm, isLoadingNoCodeForm, mutateNoCodeForm } =
|
||||
useNoCodeForm(formId);
|
||||
@@ -123,31 +123,39 @@ export default function Builder({ formId }) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const noCodeSecondNavigation = [
|
||||
{
|
||||
id: "addPage",
|
||||
onClick: () => addPage(),
|
||||
Icon: DocumentAddIcon,
|
||||
//Icon: PlusIcon
|
||||
label: "Page",
|
||||
},
|
||||
{
|
||||
id: "preview",
|
||||
onClick: () => {
|
||||
router.push(`/forms/${formId}/preview`);
|
||||
},
|
||||
Icon: EyeIcon,
|
||||
label: "Preview",
|
||||
},
|
||||
{
|
||||
id: "publish",
|
||||
onClick: () => publishChanges(),
|
||||
Icon: PaperAirplaneIcon,
|
||||
label: "Publish",
|
||||
},
|
||||
{
|
||||
id: "share",
|
||||
onClick: () => setOpenShareModal(true),
|
||||
Icon: ShareIcon,
|
||||
label: "Share",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecondNavBar>
|
||||
<SecondNavBarItem>
|
||||
<PlusIcon className="w-8 h-8 mx-auto stroke-1" />
|
||||
Element
|
||||
</SecondNavBarItem>
|
||||
<SecondNavBarItem onClick={() => addPage()}>
|
||||
<DocumentAddIcon className="w-8 h-8 mx-auto stroke-1" />
|
||||
Page
|
||||
</SecondNavBarItem>
|
||||
<SecondNavBarItem link href={`/forms/${formId}/preview`}>
|
||||
<EyeIcon className="w-8 h-8 mx-auto stroke-1" />
|
||||
Preview
|
||||
</SecondNavBarItem>
|
||||
<SecondNavBarItem onClick={() => publishChanges()}>
|
||||
<PaperAirplaneIcon className="w-8 h-8 mx-auto stroke-1" />
|
||||
Publish
|
||||
</SecondNavBarItem>
|
||||
<SecondNavBarItem onClick={() => setOpenShareModal(true)}>
|
||||
<ShareIcon className="w-8 h-8 mx-auto stroke-1" />
|
||||
Share
|
||||
</SecondNavBarItem>
|
||||
</SecondNavBar>
|
||||
|
||||
<SecondNavBar navItems={noCodeSecondNavigation} />
|
||||
<div className="w-full bg-ui-gray-lighter">
|
||||
<div className="flex justify-center w-full">
|
||||
<div className="grid w-full grid-cols-1">
|
||||
|
||||
@@ -29,10 +29,13 @@ export default function PageToolbar({
|
||||
page.type === "thankyou"
|
||||
? "bg-red-400 text-white hover:bg-red-500"
|
||||
: "bg-white text-gray-400 hover:bg-gray-50",
|
||||
"relative inline-flex items-center px-4 py-2 text-sm font-medium border border-gray-300 rounded-l-md focus:z-10 focus:outline-none focus:ring-1 focus:ring-red-500 focus:border-red-500"
|
||||
"has-tooltip relative inline-flex items-center px-4 py-2 text-sm font-medium border border-gray-300 rounded-l-md focus:z-10 focus:outline-none focus:ring-1 focus:ring-red-500 focus:border-red-500"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Annotate</span>
|
||||
<span className="sr-only">Thank You Page</span>
|
||||
<span className="w-32 p-1 -mt-16 -ml-10 text-xs text-white bg-gray-600 rounded shadow-lg tooltip">
|
||||
Is Thank You Page
|
||||
</span>
|
||||
<MdWavingHand className="w-4 h-4" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
@@ -42,9 +45,12 @@ export default function PageToolbar({
|
||||
deletePageAction(pageIdx);
|
||||
}
|
||||
}}
|
||||
className="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-400 bg-white border border-gray-300 rounded-r-md hover:bg-gray-50 focus:z-10 focus:outline-none focus:ring-1 focus:ring-red-500 focus:border-red-500"
|
||||
className="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-400 bg-white border border-gray-300 has-tooltip rounded-r-md hover:bg-gray-50 focus:z-10 focus:outline-none focus:ring-1 focus:ring-red-500 focus:border-red-500"
|
||||
>
|
||||
<span className="sr-only">Delete</span>
|
||||
<span className="w-24 p-1 -mt-16 -ml-8 text-xs text-white bg-gray-600 rounded shadow-lg tooltip">
|
||||
Delete Page
|
||||
</span>
|
||||
<TrashIcon className="w-4 h-4" aria-hidden="true" />
|
||||
</button>
|
||||
</span>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
/* This example requires Tailwind CSS v2.0+ */
|
||||
import { InformationCircleIcon } from "@heroicons/react/solid";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function UsageIntro() {
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
return (
|
||||
!dismissed && (
|
||||
<div className="p-4 border border-gray-700 rounded-md bg-ui-gray-light">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<InformationCircleIcon
|
||||
className="w-5 h-5 text-blue-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 ml-3 md:flex md:justify-between">
|
||||
<p className="text-sm text-gray-700">
|
||||
Welcome to the snoopForms No-Code Editor. Use 'tab' to
|
||||
add new blocks or change their options. You can also drag 'n
|
||||
drop blocks to reorder them.
|
||||
</p>
|
||||
<p className="mt-3 text-sm md:mt-0 md:ml-6">
|
||||
<a
|
||||
onClick={() => setDismissed(true)}
|
||||
className="font-medium text-gray-700 whitespace-nowrap hover:text-gray-600"
|
||||
>
|
||||
Dismiss
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
+130
-147
@@ -1,17 +1,16 @@
|
||||
import { DocumentSearchIcon } from "@heroicons/react/outline";
|
||||
import Link from "next/link";
|
||||
import { FaReact, FaVuejs } from "react-icons/fa";
|
||||
import { DocumentSearchIcon, TerminalIcon } from "@heroicons/react/outline";
|
||||
import { toast } from "react-toastify";
|
||||
import { classNames } from "../../lib/utils";
|
||||
import StandardButton from "../StandardButton";
|
||||
import Link from "next/link";
|
||||
import SecondNavBar from "../layout/SecondNavBar";
|
||||
import SecondNavBarItem from "../layout/SecondNavBarItem";
|
||||
|
||||
export default function FormCode({ formId }) {
|
||||
const libs = [
|
||||
{
|
||||
id: "react",
|
||||
name: "React",
|
||||
href: `forms/${formId}/react`,
|
||||
href: `/forms/${formId}/form/react`,
|
||||
bgColor: "bg-blue",
|
||||
version: "v0.1",
|
||||
icon: FaReact,
|
||||
@@ -38,169 +37,153 @@ export default function FormCode({ formId }) {
|
||||
href: "https://docs.snoopforms.com",
|
||||
bgColor: "bg-ui-gray-dark",
|
||||
icon: DocumentSearchIcon,
|
||||
target: "_blank",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecondNavBar>
|
||||
<SecondNavBarItem link href={`/forms/${formId}/form`}>
|
||||
<TerminalIcon className="w-8 h-8 mx-auto stroke-1" />
|
||||
formID
|
||||
</SecondNavBarItem>
|
||||
<SecondNavBarItem link href={`/forms/${formId}/react`}>
|
||||
<FaReact className="w-8 h-8 mx-auto stroke-1" />
|
||||
React
|
||||
</SecondNavBarItem>
|
||||
<SecondNavBarItem disabled>
|
||||
<FaReact className="w-8 h-8 mx-auto stroke-1" />
|
||||
React Native
|
||||
</SecondNavBarItem>
|
||||
<SecondNavBarItem disabled>
|
||||
<FaVuejs className="w-8 h-8 mx-auto stroke-1" />
|
||||
Vue
|
||||
</SecondNavBarItem>
|
||||
<SecondNavBarItem link outbound href="https://docs.snoopforms.com">
|
||||
<DocumentSearchIcon className="w-8 h-8 mx-auto stroke-1" />
|
||||
Docs
|
||||
</SecondNavBarItem>
|
||||
</SecondNavBar>
|
||||
<header>
|
||||
<div className="max-w-5xl">
|
||||
<div className="mx-auto mt-8">
|
||||
<h1 className="text-3xl font-bold leading-tight text-ui-gray-dark">
|
||||
Connect your form
|
||||
</h1>
|
||||
<div className="mx-auto mt-8">
|
||||
<h1 className="text-3xl font-bold leading-tight text-ui-gray-dark">
|
||||
Connect your form
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mt-4 mb-12">
|
||||
<p className="text-ui-gray-dark">
|
||||
To send all form submissions to this dashboard, update the form ID in
|
||||
the <code>{"<snoopForm>"}</code> component.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-10">
|
||||
<div>
|
||||
<label htmlFor="formId" className="block text-base text-ui-gray-dark">
|
||||
Your form ID
|
||||
</label>
|
||||
<div className="mt-3">
|
||||
<input
|
||||
id="formId"
|
||||
type="text"
|
||||
className="w-full mb-3 border-gray-300 rounded-sm shadow-sm text-md disabled:bg-gray-100"
|
||||
value={formId}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<StandardButton
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(formId);
|
||||
toast("Copied form ID to clipboard");
|
||||
}}
|
||||
fullwidth
|
||||
>
|
||||
copy
|
||||
</StandardButton>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div className="max-w-5xl">
|
||||
<div className="p-8 font-light text-gray-200 bg-black rounded-md">
|
||||
<p>
|
||||
<code>
|
||||
{"<"}
|
||||
<span className="text-yellow-200">SnoopForm</span>
|
||||
{""}
|
||||
</code>
|
||||
</p>
|
||||
<p>
|
||||
<code>{`domain="${window?.location.host}"`}</code>
|
||||
</p>
|
||||
<p>
|
||||
<code>{`protocol="${window?.location.protocol.replace(
|
||||
":",
|
||||
""
|
||||
)}"`}</code>
|
||||
</p>
|
||||
<p>
|
||||
<code>{`formId=${formId}`}</code>
|
||||
</p>
|
||||
<p>
|
||||
<code>{">"}</code>
|
||||
</p>
|
||||
<p>
|
||||
<code>
|
||||
<span className="text-gray-600">{`{...}`}</span>
|
||||
</code>
|
||||
</p>
|
||||
<code>
|
||||
{"</"}
|
||||
<span className="text-yellow-200">SnoopForm</span>
|
||||
{">"}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-16">
|
||||
<h2 className="text-xl font-bold text-ui-gray-dark">Code your form</h2>
|
||||
<div className="mt-4 mb-12">
|
||||
<p className="text-ui-gray-dark">
|
||||
To send all form submissions to this dashboard, update the form ID
|
||||
in the <code>{"<snoopForm>"}</code> component.
|
||||
Build your form with the code library of your choice. Manage your
|
||||
data in this dashboard.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-10">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="formId"
|
||||
className="block text-base text-ui-gray-dark"
|
||||
<ul
|
||||
role="list"
|
||||
className="grid grid-cols-1 gap-5 mt-3 sm:gap-6 sm:grid-cols-2"
|
||||
>
|
||||
{libs.map((lib) => (
|
||||
<a
|
||||
className="flex col-span-1 rounded-md shadow-sm"
|
||||
key={lib.id}
|
||||
href={lib.href}
|
||||
target={lib.target || ""}
|
||||
rel="noreferrer"
|
||||
>
|
||||
Your form ID
|
||||
</label>
|
||||
<div className="mt-3">
|
||||
<input
|
||||
id="formId"
|
||||
type="text"
|
||||
className="w-full mb-3 text-lg font-bold border-gray-300 rounded-sm shadow-sm disabled:bg-gray-100"
|
||||
value={formId}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<StandardButton
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(formId);
|
||||
}}
|
||||
fullwidth
|
||||
<li
|
||||
className={classNames(
|
||||
lib.comingSoon
|
||||
? "text-ui-gray-medium"
|
||||
: "shadow-sm text-ui-gray-dark hover:text-black",
|
||||
"flex col-span-1 rounded-md w-full"
|
||||
)}
|
||||
>
|
||||
copy
|
||||
</StandardButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-8 font-light text-gray-200 bg-black rounded-md">
|
||||
<p>
|
||||
<code>
|
||||
{"<"}
|
||||
<span className="text-yellow-200">snoopForm</span>
|
||||
{""}
|
||||
</code>
|
||||
</p>
|
||||
<p>
|
||||
<code>{'domain="localhost:3000"'}</code>
|
||||
</p>
|
||||
<p>
|
||||
<code>{'protocol="http"'}</code>
|
||||
</p>
|
||||
<p>
|
||||
<code className="font-bold text-red">{'formId="luHwCdbz"'}</code>
|
||||
</p>
|
||||
<p>
|
||||
<code>{"onSubmit={({ submission, schema }) =>{}/>"}</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-16">
|
||||
<h2 className="text-xl font-bold text-ui-gray-dark">
|
||||
Code your form
|
||||
</h2>
|
||||
<div className="mt-4 mb-12">
|
||||
<p className="text-ui-gray-dark">
|
||||
Build your form with the code library of your choice. Manage your
|
||||
data in this dashboard.
|
||||
</p>
|
||||
</div>
|
||||
<ul
|
||||
role="list"
|
||||
className="grid grid-cols-1 gap-5 mt-3 sm:gap-6 sm:grid-cols-2"
|
||||
>
|
||||
{libs.map((lib) => (
|
||||
<a
|
||||
className="flex col-span-1 rounded-md shadow-sm"
|
||||
key={lib.id}
|
||||
href={lib.href}
|
||||
>
|
||||
<li
|
||||
<div
|
||||
className={classNames(
|
||||
lib.comingSoon
|
||||
? "text-ui-gray-medium"
|
||||
: "shadow-sm text-ui-gray-dark hover:text-black",
|
||||
"flex col-span-1 rounded-md w-full"
|
||||
lib.bgColor,
|
||||
"flex-shrink-0 flex items-center justify-center w-20 text-white text-sm font-medium rounded-l-md"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
<lib.icon
|
||||
className={classNames(
|
||||
lib.bgColor,
|
||||
"flex-shrink-0 flex items-center justify-center w-20 text-white text-sm font-medium rounded-md"
|
||||
lib.comingSoon
|
||||
? "text-ui-gray-medium"
|
||||
: "text-white stroke-1",
|
||||
"w-10 h-10"
|
||||
)}
|
||||
>
|
||||
<lib.icon
|
||||
className={classNames(
|
||||
lib.comingSoon
|
||||
? "text-ui-gray-medium"
|
||||
: "text-white stroke-1",
|
||||
"w-12 h-12"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
lib.comingSoon ? "border-dashed" : "",
|
||||
"flex items-center justify-between flex-1 truncate bg-white rounded-r-md"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
lib.comingSoon ? "border-dashed" : "",
|
||||
"flex items-center justify-between flex-1 truncate bg-white rounded-r-md"
|
||||
)}
|
||||
>
|
||||
<div className="inline-flex px-4 py-6 text-lg truncate">
|
||||
<p className="font-light">{lib.name}</p>
|
||||
{lib.comingSoon && (
|
||||
<div className="p-1 px-3 ml-3 bg-green-100 rounded">
|
||||
<p className="text-xs text-black">coming soon</p>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="inline-flex px-4 py-8 text-lg truncate">
|
||||
<p className="font-light">{lib.name}</p>
|
||||
{lib.comingSoon && (
|
||||
<div className="p-1 px-3 ml-3 bg-green-100 rounded">
|
||||
<p className="text-xs text-black">coming soon</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</a>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</a>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="my-12 font-light text-center text-ui-gray-medium">
|
||||
<p>
|
||||
Your form is running? Go to{" "}
|
||||
<Link href={`/forms/${formId}/preview`}>
|
||||
<a className="underline text-red">Pipelines</a>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div className="my-12 font-light text-center text-ui-gray-medium">
|
||||
<p>
|
||||
Your form is running? Go to{" "}
|
||||
<Link href={`/forms/${formId}/preview`}>
|
||||
<a className="underline text-red">Pipelines</a>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import React from "react";
|
||||
import { QuestionMarkCircleIcon } from "@heroicons/react/outline";
|
||||
import { classNames } from "../../lib/utils";
|
||||
|
||||
interface Props {
|
||||
KPI: string | number;
|
||||
typeText?: boolean;
|
||||
label: string;
|
||||
toolTipText: string;
|
||||
trend: number;
|
||||
}
|
||||
|
||||
const AnalyticsCard: React.FC<Props> = ({
|
||||
KPI,
|
||||
typeText = false,
|
||||
label,
|
||||
toolTipText,
|
||||
trend,
|
||||
}) => {
|
||||
return (
|
||||
<div className="grid content-between px-4 py-2 bg-white rounded-md shadow-md">
|
||||
<div className="inline-flex items-center text-sm text-ui-gray-dark">
|
||||
{label}{" "}
|
||||
{toolTipText && (
|
||||
<QuestionMarkCircleIcon className="w-4 h-4 ml-1 text-red hover:text-ui-gray-dark" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
`font-bold leading-none flex justify-between items-end`,
|
||||
typeText ? "text-3xl tracking-tight leading-10" : "text-7xl"
|
||||
)}
|
||||
>
|
||||
{KPI}
|
||||
{trend && (
|
||||
<div className="flex items-center h-6 px-6 py-2 mb-2.5 text-sm font-light text-green-600 bg-green-200 rounded-full">
|
||||
{trend} %
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalyticsCard;
|
||||
@@ -0,0 +1,73 @@
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import Head from "next/head";
|
||||
import { classNames } from "../../lib/utils";
|
||||
import Loading from "../Loading";
|
||||
import MenuBreadcrumbs from "./MenuBreadcrumbs";
|
||||
import MenuProfile from "./MenuProfile";
|
||||
import MenuSteps from "./MenuSteps";
|
||||
import NewFormNavButton from "./NewFormNavButton";
|
||||
|
||||
interface BaseLayoutAuthorizedProps {
|
||||
title: string;
|
||||
breadcrumbs: any;
|
||||
steps?: any;
|
||||
currentStep?: string;
|
||||
children: React.ReactNode;
|
||||
limitHeightScreen?: boolean;
|
||||
}
|
||||
|
||||
export default function BaseLayoutAuthorized({
|
||||
title,
|
||||
breadcrumbs,
|
||||
steps,
|
||||
currentStep,
|
||||
children,
|
||||
limitHeightScreen = false,
|
||||
}: BaseLayoutAuthorizedProps) {
|
||||
const { data: session, status } = useSession();
|
||||
|
||||
if (status === "loading") {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
signIn();
|
||||
return <div>You need to be authenticated to view this page.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
</Head>
|
||||
<div
|
||||
className={classNames(
|
||||
limitHeightScreen ? "h-screen" : "min-h-screen",
|
||||
"flex bg-ui-gray-lighter h-full"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col flex-1 h-full">
|
||||
<header className="w-full">
|
||||
<div className="relative z-10 flex flex-shrink-0 h-16 bg-white border-b shadow-sm border-ui-gray-light">
|
||||
<div className="flex justify-between flex-1">
|
||||
<div className="inline-flex flex-1 gap-8">
|
||||
<NewFormNavButton />
|
||||
<MenuBreadcrumbs breadcrumbs={breadcrumbs} />
|
||||
</div>
|
||||
<div className="flex flex-1">
|
||||
{steps && (
|
||||
<MenuSteps steps={steps} currentStep={currentStep} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-end flex-1 mr-4 space-x-2 text-right sm:ml-6 sm:space-x-4">
|
||||
<MenuProfile />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const FullWidth: React.FC<Props> = ({ children }) => {
|
||||
return <main className="w-full h-full">{children}</main>;
|
||||
};
|
||||
|
||||
export default FullWidth;
|
||||
@@ -1,136 +0,0 @@
|
||||
/* This example requires Tailwind CSS v2.0+ */
|
||||
import { Fragment } from "react";
|
||||
import Image from "next/image";
|
||||
import { Disclosure, Menu, Transition } from "@headlessui/react";
|
||||
import { MenuIcon, XIcon } from "@heroicons/react/outline";
|
||||
import { signIn, signOut, useSession } from "next-auth/react";
|
||||
import Loading from "../Loading";
|
||||
|
||||
function classNames(...classes) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
export default function Layout({ children }) {
|
||||
const { data: session, status } = useSession();
|
||||
|
||||
if (status === "loading") {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
signIn();
|
||||
return <div>You need to be authenticated to view this page.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<Disclosure as="nav" className="bg-white shadow-sm">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div className="px-4 mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16">
|
||||
<div className="flex">
|
||||
<div className="flex items-center flex-shrink-0 w-48">
|
||||
<Image
|
||||
src="/img/snoopforms-logo.svg"
|
||||
alt="snoopForms logo"
|
||||
width={300}
|
||||
height={53}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden sm:ml-6 sm:flex sm:items-center">
|
||||
{/* Profile dropdown */}
|
||||
<Menu as="div" className="relative ml-3">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<Menu.Button className="flex text-sm bg-white rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<Image
|
||||
width={32}
|
||||
height={32}
|
||||
src="/img/avatar-placeholder.png"
|
||||
alt="profile"
|
||||
className="rounded-full"
|
||||
/>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
show={open}
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
static
|
||||
className="absolute right-0 w-48 py-1 mt-2 origin-top-right bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||
>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={() => signOut()}
|
||||
className={classNames(
|
||||
active ? "bg-gray-100" : "",
|
||||
"block px-4 py-2 text-sm text-ui-gray-dark hover:text-black w-full text-left"
|
||||
)}
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
<div className="flex items-center -mr-2 sm:hidden">
|
||||
{/* Mobile menu button */}
|
||||
<Disclosure.Button className="inline-flex items-center justify-center p-2 bg-white rounded-md text-ui-gray-dark hover:text-ui-gray-dark hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red">
|
||||
<span className="sr-only">Open main menu</span>
|
||||
{open ? (
|
||||
<XIcon className="block w-6 h-6" aria-hidden="true" />
|
||||
) : (
|
||||
<MenuIcon className="block w-6 h-6" aria-hidden="true" />
|
||||
)}
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Disclosure.Panel className="sm:hidden">
|
||||
<div className="pt-4 pb-3 border-t border-ui-gray-light">
|
||||
<div className="flex items-center px-4">
|
||||
<div className="flex-shrink-0">
|
||||
<Image
|
||||
className="w-10 h-10 rounded-full"
|
||||
src="/img/avatar-placeholder.png"
|
||||
alt="user avatar"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 space-y-1">
|
||||
<button
|
||||
onClick={() => signOut()}
|
||||
className="block px-4 py-2 text-base font-medium text-ui-gray-dark hover:text-ui-gray-dark hover:bg-gray-100"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import Head from "next/head";
|
||||
import MenuBreadcrumbs from "./MenuBreadcrumbs";
|
||||
import MenuSteps from "./MenuSteps";
|
||||
import MenuProfile from "./MenuProfile";
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import Loading from "../Loading";
|
||||
|
||||
export default function LayoutShare({ title, formId, currentStep, children }) {
|
||||
const { data: session, status } = useSession();
|
||||
|
||||
if (status === "loading") {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
signIn();
|
||||
return <div>You need to be authenticated to view this page.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
</Head>
|
||||
<div className="flex min-h-screen overflow-hidden bg-ui-gray-lighter">
|
||||
<div className="flex flex-col flex-1 overflow-hidden">
|
||||
<header className="w-full">
|
||||
<div className="relative z-10 flex flex-shrink-0 h-16 bg-white border-b shadow-sm border-ui-gray-light">
|
||||
<div className="flex flex-1 px-4 sm:px-6">
|
||||
<MenuBreadcrumbs formId={formId} />
|
||||
<MenuSteps formId={formId} currentStep={currentStep} />
|
||||
<div className="flex items-center justify-end flex-1 space-x-2 text-right sm:ml-6 sm:space-x-4">
|
||||
{/* Profile dropdown */}
|
||||
<MenuProfile />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main content */}
|
||||
<main>
|
||||
<div className="max-w-6xl mx-auto sm:px-6 lg:px-8">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import Head from "next/head";
|
||||
import Loading from "../Loading";
|
||||
import MenuBreadcrumbs from "./MenuBreadcrumbs";
|
||||
import MenuProfile from "./MenuProfile";
|
||||
import MenuSteps from "./MenuSteps";
|
||||
|
||||
export default function LayoutFormResults({
|
||||
title,
|
||||
formId,
|
||||
currentStep,
|
||||
children,
|
||||
}) {
|
||||
const { data: session, status } = useSession();
|
||||
|
||||
if (status === "loading") {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
signIn();
|
||||
return <div>You need to be authenticated to view this page.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
</Head>
|
||||
<div className="flex min-h-screen overflow-hidden bg-white">
|
||||
<div className="flex flex-col flex-1 overflow-hidden">
|
||||
<header className="w-full">
|
||||
<div className="relative z-10 flex flex-shrink-0 h-16 bg-white border-b shadow-sm border-ui-gray-light">
|
||||
<div className="flex flex-1 px-4 sm:px-6">
|
||||
<MenuBreadcrumbs formId={formId} />
|
||||
<MenuSteps formId={formId} currentStep={currentStep} />
|
||||
<div className="flex items-center justify-end flex-1 space-x-2 text-right sm:ml-6 sm:space-x-4">
|
||||
{/* Profile dropdown */}
|
||||
<MenuProfile />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main content */}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import Head from "next/head";
|
||||
import { classNames } from "../../lib/utils";
|
||||
import Loading from "../Loading";
|
||||
import MenuBreadcrumbs from "./MenuBreadcrumbs";
|
||||
import MenuProfile from "./MenuProfile";
|
||||
import MenuSteps from "./MenuSteps";
|
||||
|
||||
export default function LayoutFormResults({
|
||||
title,
|
||||
formId,
|
||||
resultMode,
|
||||
setResultMode,
|
||||
currentStep,
|
||||
children,
|
||||
}) {
|
||||
const { data: session, status } = useSession();
|
||||
|
||||
if (status === "loading") {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
signIn();
|
||||
return <div>You need to be authenticated to view this page.</div>;
|
||||
}
|
||||
|
||||
const resultModes = [
|
||||
{ name: "Summary", id: "summary", icon: "" },
|
||||
{ name: "Responses", id: "responses", icon: "" },
|
||||
{ name: "Analytics", id: "analytics", icon: "" },
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
</Head>
|
||||
<div className="flex min-h-screen overflow-hidden bg-ui-gray-lighter">
|
||||
<div className="flex flex-col flex-1 overflow-hidden">
|
||||
<header className="w-full">
|
||||
<div className="relative z-10 flex flex-shrink-0 h-16 bg-white border-b shadow-sm border-ui-gray-light">
|
||||
<div className="flex flex-1 px-4 sm:px-6">
|
||||
<MenuBreadcrumbs formId={formId} />
|
||||
<MenuSteps formId={formId} currentStep={currentStep} />
|
||||
<div className="flex items-center justify-end flex-1 space-x-2 text-right sm:ml-6 sm:space-x-4">
|
||||
{/* Profile dropdown */}
|
||||
<MenuProfile />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative z-10 flex flex-shrink-0 h-16 border-b shadow-inner border-ui-gray-light bg-gray-50">
|
||||
<div className="flex items-center justify-center flex-1 px-4">
|
||||
<nav className="flex space-x-4" aria-label="resultModes">
|
||||
{resultModes.map((mode) => (
|
||||
<button
|
||||
key={mode.name}
|
||||
onClick={() => setResultMode(mode.id)}
|
||||
className={classNames(
|
||||
mode.id === resultMode
|
||||
? "bg-ui-gray-light text-gray-800"
|
||||
: "text-gray-600 hover:text-gray-800",
|
||||
"px-3 py-2 font-medium text-sm rounded-md"
|
||||
)}
|
||||
aria-current={mode.id === resultMode ? "page" : undefined}
|
||||
>
|
||||
{mode.name}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main content */}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const LimitedWidth: React.FC<Props> = ({ children }) => {
|
||||
return <main className="h-full mx-auto max-w-7xl">{children}</main>;
|
||||
};
|
||||
|
||||
export default LimitedWidth;
|
||||
@@ -1,38 +1,11 @@
|
||||
import { HomeIcon, PlusIcon } from "@heroicons/react/outline";
|
||||
import { HomeIcon } from "@heroicons/react/outline";
|
||||
import Link from "next/link";
|
||||
import { useForm } from "../../lib/forms";
|
||||
import Router from "next/router";
|
||||
import StandardButton from "../StandardButton";
|
||||
import { createForm } from "../../lib/forms";
|
||||
|
||||
export default function MenuBreadcrumbs({ formId }) {
|
||||
const newForm = async () => {
|
||||
const form = await createForm();
|
||||
await Router.push(`/forms/${form.id}/welcome`);
|
||||
};
|
||||
|
||||
const { form, isLoadingForm } = useForm(formId);
|
||||
|
||||
const pages = [
|
||||
{ name: "Forms", href: "/forms", current: false },
|
||||
{ name: form.name, href: "#", current: true },
|
||||
];
|
||||
|
||||
if (isLoadingForm) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
export default function MenuBreadcrumbs({ breadcrumbs }) {
|
||||
return (
|
||||
<div className="hidden sm:flex sm:flex-1">
|
||||
<nav className="hidden lg:flex" aria-label="Breadcrumb">
|
||||
<ol className="flex items-center space-x-4">
|
||||
<li>
|
||||
<div>
|
||||
<StandardButton icon secondary onClick={() => newForm()}>
|
||||
<PlusIcon className="w-6 h-6" />
|
||||
</StandardButton>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
<Link href="/forms/">
|
||||
@@ -46,8 +19,8 @@ export default function MenuBreadcrumbs({ formId }) {
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
{pages.map((page) => (
|
||||
<li key={page.name}>
|
||||
{breadcrumbs.map((crumb) => (
|
||||
<li key={crumb.name}>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="flex-shrink-0 w-5 h-5 text-ui-gray-medium"
|
||||
@@ -59,11 +32,10 @@ export default function MenuBreadcrumbs({ formId }) {
|
||||
<path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z" />
|
||||
</svg>
|
||||
<a
|
||||
href={page.href}
|
||||
href={crumb.href}
|
||||
className="ml-4 text-sm font-medium text-ui-gray-dark hover:text-ui-gray-dark"
|
||||
aria-current={page.current ? "page" : undefined}
|
||||
>
|
||||
{page.name}
|
||||
{crumb.name}
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -12,13 +12,15 @@ export default function MenuProfile({}) {
|
||||
<div className="inline-flex items-center ">
|
||||
<Menu.Button className="flex ml-3 text-sm bg-white rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<Image
|
||||
className="w-8 h-8 rounded-full"
|
||||
src="/img/avatar-placeholder.png"
|
||||
alt="user avatar"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
<div className="w-8 h-8">
|
||||
<Image
|
||||
className="rounded-full"
|
||||
src="/img/avatar-placeholder.png"
|
||||
alt="user avatar"
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
</div>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
|
||||
@@ -1,74 +1,57 @@
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useForm } from "../../lib/forms";
|
||||
import { classNames } from "../../lib/utils";
|
||||
|
||||
interface Step {
|
||||
id: string;
|
||||
name: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
type MenuStepsProps = {
|
||||
formId: string;
|
||||
currentStep: "build" | "share" | "results";
|
||||
steps: Step[];
|
||||
currentStep: string;
|
||||
};
|
||||
|
||||
export default function MenuSteps({ formId, currentStep }: MenuStepsProps) {
|
||||
const { form, isLoadingForm } = useForm(formId);
|
||||
export default function MenuSteps({ steps, currentStep }: MenuStepsProps) {
|
||||
const router = useRouter();
|
||||
const tabs = [
|
||||
{
|
||||
name: "Form",
|
||||
id: "form",
|
||||
href: `/forms/${form.id}/form`,
|
||||
},
|
||||
{
|
||||
name: "Pipelines",
|
||||
id: "pipelines",
|
||||
href: `/forms/${form.id}/pipelines`,
|
||||
},
|
||||
{
|
||||
name: "Results",
|
||||
id: "results",
|
||||
href: `/forms/${form.id}/results`,
|
||||
},
|
||||
];
|
||||
|
||||
if (isLoadingForm) {
|
||||
return <div />;
|
||||
}
|
||||
return (
|
||||
<div className="flex items-center flex-1 justify-left sm:justify-center">
|
||||
<div className="w-full sm:hidden">
|
||||
<label htmlFor="tabs" className="sr-only">
|
||||
<label htmlFor="steps" className="sr-only">
|
||||
Select a view
|
||||
</label>
|
||||
<select
|
||||
id="tabs"
|
||||
name="tabs"
|
||||
className="block w-full py-2 pl-3 pr-10 text-base border-ui-gray-medium rounded-md focus:outline-none focus:ring-red focus:border-red sm:text-sm"
|
||||
defaultValue={tabs.find((tab) => tab.id === currentStep).name}
|
||||
id="steps"
|
||||
name="steps"
|
||||
className="block w-full py-2 pl-3 pr-10 text-base rounded-md border-ui-gray-medium focus:outline-none focus:ring-red focus:border-red sm:text-sm"
|
||||
defaultValue={steps.find((step) => step.id === currentStep).name}
|
||||
onChange={(e) => {
|
||||
const stepId = e.target.children[e.target.selectedIndex].id;
|
||||
router.push(`/forms/${form.id}/${stepId}`);
|
||||
router.push(steps.find((s) => s.id === stepId).href);
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<option key={tab.name} id={tab.id}>
|
||||
{tab.name}
|
||||
{steps.map((step) => (
|
||||
<option key={step.name} id={step.id}>
|
||||
{step.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="hidden sm:block">
|
||||
<nav className="flex -mb-px space-x-8" aria-label="Tabs">
|
||||
{tabs.map((tab) => (
|
||||
<Link key={tab.name} href={tab.href}>
|
||||
<nav className="flex -mb-px space-x-8" aria-label="steps">
|
||||
{steps.map((step) => (
|
||||
<Link key={step.name} href={step.href}>
|
||||
<a
|
||||
className={classNames(
|
||||
tab.id === currentStep
|
||||
step.id === currentStep
|
||||
? "border-red text-red"
|
||||
: "border-transparent text-ui-gray-dark hover:text-ui-gray-dark hover:border-ui-gray-medium",
|
||||
"whitespace-nowrap py-5 px-1 border-b-2 font-medium text-sm"
|
||||
)}
|
||||
aria-current={tab.id === currentStep ? "page" : undefined}
|
||||
aria-current={step.id === currentStep ? "page" : undefined}
|
||||
>
|
||||
{tab.name}
|
||||
{step.name}
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { PlusIcon } from "@heroicons/react/outline";
|
||||
import { useState } from "react";
|
||||
import NewFormModal from "../form/NewFormModal";
|
||||
|
||||
export default function NewFormNavButton({}) {
|
||||
const [openNewFormModal, setOpenNewFormModal] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="items-center hidden text-sm border-r border-ui-gray-light sm:flex bg-ui-gray-lighter text-ui-gray-dark hover:text-white hover:bg-red-500"
|
||||
onClick={() => setOpenNewFormModal(true)}
|
||||
>
|
||||
<nav className="hidden lg:flex" aria-label="Breadcrumb">
|
||||
<ol className="flex items-center space-x-4">
|
||||
<li>
|
||||
<div className="inline-flex items-center px-6 py-2 text-sm font-medium leading-4 bg-transparent border border-transparent hover:text-white focus:outline-none">
|
||||
<PlusIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Start New Form
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</button>
|
||||
<NewFormModal open={openNewFormModal} setOpen={setOpenNewFormModal} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,47 @@
|
||||
import React from "react";
|
||||
import { classNames } from "../../lib/utils";
|
||||
|
||||
interface NavItem {
|
||||
id: string;
|
||||
onClick: () => void;
|
||||
Icon?: React.ElementType;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
navItems: NavItem[];
|
||||
currentItemId?: string;
|
||||
}
|
||||
|
||||
// button component, consuming props
|
||||
const SecondNavBar: React.FC<Props> = ({ children }) => {
|
||||
const SecondNavBar: React.FC<Props> = ({ navItems, currentItemId }) => {
|
||||
return (
|
||||
<div className="relative flex flex-shrink-0 h-16 py-2 border-b border-ui-gray-light bg-ui-gray-lighter">
|
||||
<div className="relative flex flex-shrink-0 h-16 border-b border-ui-gray-light bg-ui-gray-lighter">
|
||||
<div className="flex items-center justify-center flex-1 px-4 py-2">
|
||||
<nav className="flex space-x-4" aria-label="resultModes">
|
||||
{children}
|
||||
<nav className="flex space-x-10" aria-label="resultModes">
|
||||
{navItems.map((navItem) => (
|
||||
<button
|
||||
key={navItem.id}
|
||||
className={classNames(
|
||||
`h-16 text-xs`,
|
||||
!navItem.disabled &&
|
||||
(navItem.id === currentItemId
|
||||
? "text-red border-b-2 border-red"
|
||||
: "hover:border-b-2 hover:border-gray-300 text-ui-gray-dark hover:text-red bg-transparent"),
|
||||
navItem.disabled
|
||||
? "text-ui-gray-medium"
|
||||
: "hover:border-b-2 hover:border-red text-ui-gray-dark hover:text-red"
|
||||
)}
|
||||
onClick={navItem.onClick}
|
||||
disabled={navItem.disabled}
|
||||
>
|
||||
{navItem.Icon && (
|
||||
<navItem.Icon className="w-6 h-6 mx-auto mb-1 stroke-1" />
|
||||
)}
|
||||
{navItem.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import React from "react";
|
||||
import { classNames } from "../../lib/utils";
|
||||
import Link from "next/link";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
itemLabel?: string;
|
||||
disabled?: boolean;
|
||||
link?: boolean;
|
||||
outbound?: boolean;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
// button component, consuming props
|
||||
const SecondNavBarItem: React.FC<Props> = ({
|
||||
children,
|
||||
onClick = () => {},
|
||||
itemLabel,
|
||||
disabled = false,
|
||||
link = false,
|
||||
outbound = false,
|
||||
href,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
{link ? (
|
||||
<Link href={href}>
|
||||
<a
|
||||
target={outbound ? "_blank" : "_self"}
|
||||
className="inline-flex p-2 text-xs bg-transparent rounded-sm hover:bg-ui-gray-light hover:cursor-pointer text-ui-gray-dark hover:text-red"
|
||||
>
|
||||
<span>{children}</span>
|
||||
{itemLabel}
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
className={classNames(
|
||||
`p-2 text-xs rounded-sm`,
|
||||
disabled
|
||||
? "text-ui-gray-medium"
|
||||
: "bg-transparent hover:bg-ui-gray-light text-ui-gray-dark hover:text-red"
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
>
|
||||
<span>{children}</span>
|
||||
{itemLabel}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecondNavBarItem;
|
||||
@@ -0,0 +1,75 @@
|
||||
import { QuestionMarkCircleIcon } from "@heroicons/react/outline";
|
||||
import { ArrowSmDownIcon, ArrowSmUpIcon } from "@heroicons/react/solid";
|
||||
import React from "react";
|
||||
import { classNames } from "../../lib/utils";
|
||||
|
||||
interface Props {
|
||||
value: string | number;
|
||||
label: string;
|
||||
toolTipText: string;
|
||||
trend?: number;
|
||||
smallerText?: boolean;
|
||||
}
|
||||
|
||||
const AnalyticsCard: React.FC<Props> = ({
|
||||
value,
|
||||
label,
|
||||
toolTipText,
|
||||
trend,
|
||||
smallerText,
|
||||
}) => {
|
||||
return (
|
||||
<div className="bg-white rounded-md shadow-md">
|
||||
<div key={label} className="px-4 py-5 sm:p-6">
|
||||
<dt className="inline-flex text-base font-normal text-gray-900 has-tooltip">
|
||||
{label}{" "}
|
||||
{toolTipText && (
|
||||
<QuestionMarkCircleIcon className="w-4 h-4 ml-1 text-red hover:text-ui-gray-dark" />
|
||||
)}
|
||||
<span className="flex p-1 px-4 -mt-6 -ml-8 text-xs text-center text-white bg-gray-600 rounded shadow-lg grow tooltip">
|
||||
{toolTipText}
|
||||
</span>
|
||||
</dt>
|
||||
<dd className="flex items-baseline justify-between mt-1 md:block lg:flex">
|
||||
<div
|
||||
className={classNames(
|
||||
smallerText ? "text-lg" : "text-xl",
|
||||
"flex items-baseline text-xl font-semibold text-gray-800"
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
|
||||
{trend && (
|
||||
<div
|
||||
className={classNames(
|
||||
trend >= 0
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-red-100 text-red-800",
|
||||
"inline-flex items-baseline px-2.5 py-0.5 rounded-full text-sm font-medium md:mt-2 lg:mt-0"
|
||||
)}
|
||||
>
|
||||
{trend >= 0 ? (
|
||||
<ArrowSmUpIcon
|
||||
className="-ml-1 mr-0.5 flex-shrink-0 self-center h-5 w-5 text-green-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<ArrowSmDownIcon
|
||||
className="-ml-1 mr-0.5 flex-shrink-0 self-center h-5 w-5 text-red-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
<span className="sr-only">
|
||||
{trend >= 0 ? "Increased" : "Decreased"} by
|
||||
</span>
|
||||
{trend} %
|
||||
</div>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalyticsCard;
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
useSubmissionSessions,
|
||||
} from "../../lib/submissionSessions";
|
||||
import { timeSince } from "../../lib/utils";
|
||||
import AnalyticsCard from "../layout/AnalyticsCard";
|
||||
import AnalyticsCard from "./AnalyticsCard";
|
||||
|
||||
export default function ResultsAnalytics({ formId }) {
|
||||
const { submissionSessions, isLoadingSubmissionSessions } =
|
||||
@@ -24,73 +24,71 @@ export default function ResultsAnalytics({ formId }) {
|
||||
id: "uniqueUsers",
|
||||
name: "Unique Users",
|
||||
stat: analytics.uniqueUsers || "--",
|
||||
toolTipText: "placeholder",
|
||||
trend: 12,
|
||||
toolTipText: "Tracked without cookies using fingerprinting technique",
|
||||
trend: undefined,
|
||||
},
|
||||
{
|
||||
id: "totalSubmissions",
|
||||
name: "Total Submissions",
|
||||
stat: analytics.totalSubmissions || "--",
|
||||
trend: 10,
|
||||
trend: undefined,
|
||||
},
|
||||
{
|
||||
id: "lastSubmission",
|
||||
name: "Last Submission",
|
||||
stat: timeSince(analytics.lastSubmissionAt) || "--",
|
||||
typeText: true,
|
||||
smallerText: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
}, [analytics]);
|
||||
return (
|
||||
<main>
|
||||
<div className="max-w-5xl mx-auto sm:px-6 lg:px-8">
|
||||
<h2 className="mt-8 text-xl font-bold text-ui-gray-dark">Analytics</h2>
|
||||
<div>
|
||||
{stats ? (
|
||||
<dl className="grid grid-cols-1 gap-5 mt-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{stats.map((item) => (
|
||||
<AnalyticsCard
|
||||
key={item.id}
|
||||
KPI={item.stat}
|
||||
label={item.name}
|
||||
toolTipText={item.toolTipText}
|
||||
typeText={item.typeText}
|
||||
trend={item.trend}
|
||||
/>
|
||||
))}
|
||||
</dl>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<h2 className="mt-16 text-xl font-bold text-ui-gray-dark">
|
||||
Optimize Form
|
||||
</h2>
|
||||
<div className="px-3 py-2 ml-2 text-xs text-green-800 rounded-sm bg-green-50">
|
||||
<p>coming soon</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-10 mt-8">
|
||||
<div className="relative p-8 bg-white rounded-md shadow-md h-60">
|
||||
<Image
|
||||
src="/../../img/drop-offs-v1.svg"
|
||||
alt="drop-off"
|
||||
objectFit="cover"
|
||||
layout="fill"
|
||||
className="rounded-md"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative p-8 bg-white rounded-md shadow-md h-60">
|
||||
<Image
|
||||
src="/../../img/a-b-test-v1.svg"
|
||||
alt="drop-off"
|
||||
objectFit="cover"
|
||||
layout="fill"
|
||||
className="rounded-md"
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<h2 className="mt-8 text-xl font-bold text-ui-gray-dark">Analytics</h2>
|
||||
<div>
|
||||
{stats ? (
|
||||
<dl className="grid grid-cols-1 gap-5 mt-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{stats.map((item) => (
|
||||
<AnalyticsCard
|
||||
key={item.id}
|
||||
value={item.stat}
|
||||
label={item.name}
|
||||
toolTipText={item.toolTipText}
|
||||
trend={item.trend}
|
||||
smallerText={item.smallerText}
|
||||
/>
|
||||
))}
|
||||
</dl>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<h2 className="mt-16 text-xl font-bold text-ui-gray-dark">
|
||||
Optimize Form
|
||||
</h2>
|
||||
<div className="px-3 py-2 ml-2 text-xs text-green-800 rounded-sm bg-green-50">
|
||||
<p>coming soon</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<div className="grid grid-cols-2 gap-10 mt-8">
|
||||
<div className="p-5 bg-white rounded-md shadow-md">
|
||||
<Image
|
||||
src="/../../img/drop-offs-v1.svg"
|
||||
alt="drop-off"
|
||||
layout="responsive"
|
||||
width={500}
|
||||
height={273}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-5 bg-white rounded-md shadow-md">
|
||||
<Image
|
||||
src="/../../img/a-b-test-v1.svg"
|
||||
alt="drop-off"
|
||||
layout="responsive"
|
||||
width={500}
|
||||
height={273}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function ResultsResponses({ formId }: ResultsResponseProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 w-full mx-auto overflow-visible max-w-screen">
|
||||
<div className="flex flex-col flex-1 w-full h-full mx-auto overflow-visible max-w-screen">
|
||||
<div className="relative z-0 flex flex-1 overflow-visible">
|
||||
<main className="relative z-0 flex-1 overflow-y-auto focus:outline-none xl:order-last">
|
||||
<div className="overflow-visible sm:rounded-lg">
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "../../lib/submissionSessions";
|
||||
import { SubmissionSummary } from "../../lib/types";
|
||||
import { timeSince } from "../../lib/utils";
|
||||
import AnalyticsCard from "../layout/AnalyticsCard";
|
||||
import AnalyticsCard from "./AnalyticsCard";
|
||||
import Loading from "../Loading";
|
||||
import TextResults from "./summary/TextResults";
|
||||
|
||||
@@ -36,20 +36,20 @@ export default function ResultsSummary({ formId }) {
|
||||
id: "uniqueUsers",
|
||||
name: "Unique Users",
|
||||
stat: analytics.uniqueUsers || "--",
|
||||
toolTipText: "placeholder",
|
||||
trend: 12,
|
||||
toolTipText: "Tracked without cookies using fingerprinting technique",
|
||||
trend: undefined,
|
||||
},
|
||||
{
|
||||
id: "totalSubmissions",
|
||||
name: "Total Submissions",
|
||||
stat: analytics.totalSubmissions || "--",
|
||||
trend: 10,
|
||||
trend: undefined,
|
||||
},
|
||||
{
|
||||
id: "lastSubmission",
|
||||
name: "Last Submission",
|
||||
stat: timeSince(analytics.lastSubmissionAt) || "--",
|
||||
typeText: true,
|
||||
smallerText: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -60,25 +60,25 @@ export default function ResultsSummary({ formId }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="bg-gray-50">
|
||||
<div className="max-w-5xl mx-auto sm:px-6 lg:px-8">
|
||||
<h2 className="mt-8 text-xl font-bold text-ui-gray-dark">
|
||||
Responses Overview
|
||||
</h2>
|
||||
<dl className="grid grid-cols-1 gap-5 mt-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{stats.map((item) => (
|
||||
<AnalyticsCard
|
||||
key={item.id}
|
||||
KPI={item.stat}
|
||||
label={item.name}
|
||||
toolTipText={item.toolTipText}
|
||||
typeText={item.typeText}
|
||||
trend={item.trend}
|
||||
/>
|
||||
))}
|
||||
</dl>
|
||||
<div>
|
||||
{summary.pages.map(
|
||||
<>
|
||||
<h2 className="mt-8 text-xl font-bold text-ui-gray-dark">
|
||||
Responses Overview
|
||||
</h2>
|
||||
<dl className="grid grid-cols-1 gap-5 mt-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{stats.map((item) => (
|
||||
<AnalyticsCard
|
||||
key={item.id}
|
||||
value={item.stat}
|
||||
label={item.name}
|
||||
toolTipText={item.toolTipText}
|
||||
trend={item.trend}
|
||||
smallerText={item.smallerText}
|
||||
/>
|
||||
))}
|
||||
</dl>
|
||||
<div>
|
||||
{summary?.pages &&
|
||||
summary.pages.map(
|
||||
(page) =>
|
||||
page.type === "form" && (
|
||||
<div key={page.name}>
|
||||
@@ -90,8 +90,7 @@ export default function ResultsSummary({ formId }) {
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user