update layout approach to have a unified look

This commit is contained in:
Matthias Nannt
2022-06-24 21:26:39 +09:00
parent 8648d0bb12
commit e760cdf29d
39 changed files with 1258 additions and 1312 deletions
+3 -3
View File
@@ -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}
+40 -32
View File
@@ -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">
+9 -3
View File
@@ -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>
-36
View File
@@ -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 &apos;tab&apos; to
add new blocks or change their options. You can also drag &apos;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
View File
@@ -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>
</>
-45
View File
@@ -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>
</>
);
}
+9
View File
@@ -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;
-136
View File
@@ -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>
);
}
-48
View File
@@ -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>
</>
);
}
-51
View File
@@ -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>
</>
);
}
-80
View File
@@ -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>
</>
);
}
+9
View File
@@ -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;
+6 -34
View File
@@ -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>
+9 -7
View File
@@ -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
+24 -41
View File
@@ -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>
))}
+28
View File
@@ -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} />
</>
);
}
+36 -5
View File
@@ -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>
-58
View File
@@ -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;
+75
View File
@@ -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;
+50 -52
View File
@@ -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>
</>
);
}
+1 -1
View File
@@ -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">
+25 -26
View File
@@ -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>
</>
);
}