mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-14 10:30:31 -06: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>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
73
components/layout/BaseLayoutAuthorized.tsx
Normal file
73
components/layout/BaseLayoutAuthorized.tsx
Normal file
@@ -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
components/layout/FullWidth.tsx
Normal file
9
components/layout/FullWidth.tsx
Normal 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;
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
9
components/layout/LimitedWidth.tsx
Normal file
9
components/layout/LimitedWidth.tsx
Normal 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;
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
28
components/layout/NewFormNavButton.tsx
Normal file
28
components/layout/NewFormNavButton.tsx
Normal 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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
75
components/results/AnalyticsCard.tsx
Normal file
75
components/results/AnalyticsCard.tsx
Normal 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;
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
47
lib/navigation/formCodeSecondNavigation.ts
Normal file
47
lib/navigation/formCodeSecondNavigation.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { DocumentSearchIcon, TerminalIcon } from "@heroicons/react/outline";
|
||||
import { useRouter } from "next/router";
|
||||
import { FaReact } from "react-icons/fa";
|
||||
|
||||
export const useCodeSecondNavigation = (formId) => {
|
||||
const router = useRouter();
|
||||
return [
|
||||
{
|
||||
id: "formId",
|
||||
onClick: () => {
|
||||
router.push(`/forms/${formId}/form`);
|
||||
},
|
||||
Icon: TerminalIcon,
|
||||
label: "Form ID",
|
||||
},
|
||||
{
|
||||
id: "react",
|
||||
onClick: () => {
|
||||
router.push(`/forms/${formId}/form/react`);
|
||||
},
|
||||
Icon: FaReact,
|
||||
label: "React",
|
||||
},
|
||||
{
|
||||
id: "reactNative",
|
||||
onClick: () => {},
|
||||
Icon: FaReact,
|
||||
label: "React Native",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: "vueJs",
|
||||
onClick: () => {},
|
||||
Icon: TerminalIcon,
|
||||
label: "VueJs",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: "docs",
|
||||
onClick: () => {
|
||||
window.open("https://docs.snoopforms.com", "_ blank");
|
||||
},
|
||||
Icon: DocumentSearchIcon,
|
||||
label: "Docs",
|
||||
},
|
||||
];
|
||||
};
|
||||
17
lib/navigation/formMenuSteps.ts
Normal file
17
lib/navigation/formMenuSteps.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export const useFormMenuSteps = (formId) => [
|
||||
{
|
||||
id: "form",
|
||||
name: "Form",
|
||||
href: `/forms/${formId}/form`,
|
||||
},
|
||||
{
|
||||
id: "pipelines",
|
||||
name: "Pipelines",
|
||||
href: `/forms/${formId}/pipelines`,
|
||||
},
|
||||
{
|
||||
id: "results",
|
||||
name: "Results",
|
||||
href: `/forms/${formId}/results/summary`,
|
||||
},
|
||||
];
|
||||
36
lib/navigation/formResultsSecondNavigation.ts
Normal file
36
lib/navigation/formResultsSecondNavigation.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
ChartBarIcon,
|
||||
InboxIcon,
|
||||
TrendingUpIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const useFormResultsSecondNavigation = (formId) => {
|
||||
const router = useRouter();
|
||||
return [
|
||||
{
|
||||
id: "summary",
|
||||
onClick: () => {
|
||||
router.push(`/forms/${formId}/results/summary`);
|
||||
},
|
||||
Icon: ChartBarIcon,
|
||||
label: "Summary",
|
||||
},
|
||||
{
|
||||
id: "responses",
|
||||
onClick: () => {
|
||||
router.push(`/forms/${formId}/results/responses`);
|
||||
},
|
||||
Icon: InboxIcon,
|
||||
label: "Responses",
|
||||
},
|
||||
{
|
||||
id: "analytics",
|
||||
onClick: () => {
|
||||
router.push(`/forms/${formId}/results/analytics`);
|
||||
},
|
||||
Icon: TrendingUpIcon,
|
||||
label: "Analytics",
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import { GetServerSideProps } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import Builder from "../../../components/builder/Builder";
|
||||
import FormCode from "../../../components/form/FormCode";
|
||||
import LayoutFormBasics from "../../../components/layout/LayoutFormBasic";
|
||||
import LayoutFormBuilder from "../../../components/layout/LayoutFormBuilder";
|
||||
import Loading from "../../../components/Loading";
|
||||
import { useForm } from "../../../lib/forms";
|
||||
|
||||
export default function FormPage() {
|
||||
const router = useRouter();
|
||||
const formId = router.query.id.toString();
|
||||
const { form, isLoadingForm } = useForm(router.query.id);
|
||||
|
||||
if (isLoadingForm) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (form.formType === "NOCODE") {
|
||||
return (
|
||||
<>
|
||||
<LayoutFormBuilder title={form.name} formId={formId} currentStep="form">
|
||||
<Builder formId={formId} />
|
||||
</LayoutFormBuilder>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<LayoutFormBasics title={form.name} formId={formId} currentStep="form">
|
||||
<FormCode formId={formId} />
|
||||
</LayoutFormBasics>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
|
||||
const session = await getSession({ req });
|
||||
if (!session) {
|
||||
res.statusCode = 403;
|
||||
}
|
||||
return { props: {} };
|
||||
};
|
||||
67
pages/forms/[id]/form/index.tsx
Normal file
67
pages/forms/[id]/form/index.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { GetServerSideProps } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import Builder from "../../../../components/builder/Builder";
|
||||
import FormCode from "../../../../components/form/FormCode";
|
||||
import BaseLayoutAuthorized from "../../../../components/layout/BaseLayoutAuthorized";
|
||||
import FullWidth from "../../../../components/layout/FullWidth";
|
||||
import LimitedWidth from "../../../../components/layout/LimitedWidth";
|
||||
import SecondNavBar from "../../../../components/layout/SecondNavBar";
|
||||
import Loading from "../../../../components/Loading";
|
||||
import { useForm } from "../../../../lib/forms";
|
||||
import { useCodeSecondNavigation } from "../../../../lib/navigation/formCodeSecondNavigation";
|
||||
import { useFormMenuSteps } from "../../../../lib/navigation/formMenuSteps";
|
||||
|
||||
export default function FormPage() {
|
||||
const router = useRouter();
|
||||
const formId = router.query.id.toString();
|
||||
const { form, isLoadingForm } = useForm(router.query.id);
|
||||
const codeSecondNavigation = useCodeSecondNavigation(formId);
|
||||
const formMenuSteps = useFormMenuSteps(formId);
|
||||
|
||||
if (isLoadingForm) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const breadcrumbs = [{ name: form.name, href: "#", current: true }];
|
||||
|
||||
if (form.formType === "NOCODE") {
|
||||
return (
|
||||
<>
|
||||
<BaseLayoutAuthorized
|
||||
title={`${form.name} - snoopForms`}
|
||||
breadcrumbs={breadcrumbs}
|
||||
steps={formMenuSteps}
|
||||
currentStep="form"
|
||||
>
|
||||
<FullWidth>
|
||||
<Builder formId={formId} />
|
||||
</FullWidth>
|
||||
</BaseLayoutAuthorized>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<BaseLayoutAuthorized
|
||||
title={`${form.name} - snoopForms`}
|
||||
breadcrumbs={breadcrumbs}
|
||||
steps={formMenuSteps}
|
||||
currentStep="form"
|
||||
>
|
||||
<SecondNavBar navItems={codeSecondNavigation} currentItemId="formId" />
|
||||
|
||||
<LimitedWidth>
|
||||
<FormCode formId={formId} />
|
||||
</LimitedWidth>
|
||||
</BaseLayoutAuthorized>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
|
||||
const session = await getSession({ req });
|
||||
if (!session) {
|
||||
res.statusCode = 403;
|
||||
}
|
||||
return { props: {} };
|
||||
};
|
||||
134
pages/forms/[id]/form/react.tsx
Normal file
134
pages/forms/[id]/form/react.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { GetServerSideProps } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import { FaDiscord } from "react-icons/fa";
|
||||
import BaseLayoutAuthorized from "../../../../components/layout/BaseLayoutAuthorized";
|
||||
import LimitedWidth from "../../../../components/layout/LimitedWidth";
|
||||
import SecondNavBar from "../../../../components/layout/SecondNavBar";
|
||||
import Loading from "../../../../components/Loading";
|
||||
import { useForm } from "../../../../lib/forms";
|
||||
import { useCodeSecondNavigation } from "../../../../lib/navigation/formCodeSecondNavigation";
|
||||
|
||||
export default function ReactPage() {
|
||||
const router = useRouter();
|
||||
const formId = router.query.id.toString();
|
||||
const { form, isLoadingForm } = useForm(router.query.id);
|
||||
const codeSecondNavigation = useCodeSecondNavigation(formId);
|
||||
|
||||
if (isLoadingForm) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseLayoutAuthorized
|
||||
title={form.name}
|
||||
breadcrumbs={[{ name: form.name, href: "#", current: true }]}
|
||||
>
|
||||
<SecondNavBar navItems={codeSecondNavigation} currentItemId="react" />
|
||||
<LimitedWidth>
|
||||
<header>
|
||||
<div className="mx-auto mt-8 max-w-7xl">
|
||||
<h1 className="text-3xl font-bold leading-tight text-ui-gray-dark">
|
||||
Build your form with React
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
<div className="my-4">
|
||||
<p className="text-ui-gray-dark">
|
||||
Use our pre-built components to build your form. Manage data in
|
||||
this dashboard.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-10 mt-16">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-ui-gray-dark">
|
||||
1. Get started
|
||||
</h2>
|
||||
<p className="text-ui-gray-dark">
|
||||
Install the snoopReact Library with Node Package Manager via
|
||||
snoopforms/react.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-8 font-light text-gray-200 bg-black rounded-md">
|
||||
<code>{"npm install --save @snoopforms/react"}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-10 mt-16">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-ui-gray-dark">
|
||||
2. Build the form
|
||||
</h2>
|
||||
<p className="text-ui-gray-dark">
|
||||
Use the pre-built components snoopForm, snoopPage and
|
||||
snoopElement to build exactly the form you want.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-8 font-light text-gray-200 bg-black rounded-md">
|
||||
<code className="whitespace-pre language-js">{`<SnoopForm
|
||||
domain="localhost:3000"
|
||||
protocol="http"
|
||||
className="w-full space-y-6"
|
||||
onSubmit={({ submission, schema })=>{}}>
|
||||
|
||||
<SnoopPage name="first">
|
||||
<SnoopElement
|
||||
type="text"
|
||||
name={"name"}
|
||||
label="Your name"
|
||||
classNames={{
|
||||
label: "your-label-class",
|
||||
element: "your-input-class",}}
|
||||
required/>
|
||||
</SnoopPage>
|
||||
|
||||
<SnoopPage thankyou>
|
||||
<h1>Thank you!</h1>
|
||||
</SnoopPage>
|
||||
|
||||
</SnoopForm>`}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-10 my-16">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-ui-gray-dark">
|
||||
Questions?
|
||||
</h2>
|
||||
<p className="pb-4 text-ui-gray-dark">
|
||||
Find a more detailed explanation on how to go about build the
|
||||
form and piping your data{" "}
|
||||
<a
|
||||
href="https://docs.snoopforms.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline text-red hover:no-underline"
|
||||
>
|
||||
in the docs.
|
||||
</a>{" "}
|
||||
Or join our Discord and ask us :)
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center p-8 rounded-md bg-purple">
|
||||
<a
|
||||
className="inline-flex items-center justify-center px-4 py-2 bg-white rounded-sm hover:motion-safe:animate-bounce text-purple text-bold text-md"
|
||||
href="https://discord.gg/kT4Aq7Kq"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Join Discord <FaDiscord className="w-8 h-8 ml-2" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</LimitedWidth>
|
||||
</BaseLayoutAuthorized>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
|
||||
const session = await getSession({ req });
|
||||
if (!session) {
|
||||
res.statusCode = 403;
|
||||
}
|
||||
return { props: {} };
|
||||
};
|
||||
@@ -13,7 +13,7 @@ export default function FormIndex() {
|
||||
if (!isLoadingSubmissionSessions) {
|
||||
// redirect to /results if there is at least one submissionSession
|
||||
if (submissionSessions.length > 0) {
|
||||
router.push(`/forms/${formId}/results`);
|
||||
router.push(`/forms/${formId}/results/summary`);
|
||||
} else {
|
||||
// redirect to /form if there isn't one submissionSession
|
||||
router.push(`/forms/${formId}/form`);
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
import { CodeIcon, PuzzleIcon } from "@heroicons/react/outline";
|
||||
import { GetServerSideProps } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import LayoutFormBasics from "../../../components/layout/LayoutFormBasic";
|
||||
import {
|
||||
SiAirtable,
|
||||
SiGoogle,
|
||||
SiNotion,
|
||||
SiSlack,
|
||||
SiZapier,
|
||||
} from "react-icons/si";
|
||||
import BaseLayoutAuthorized from "../../../components/layout/BaseLayoutAuthorized";
|
||||
import EmptyPageFiller from "../../../components/layout/EmptyPageFiller";
|
||||
import LimitedWidth from "../../../components/layout/LimitedWidth";
|
||||
import Loading from "../../../components/Loading";
|
||||
import { useForm } from "../../../lib/forms";
|
||||
import {
|
||||
SiZapier,
|
||||
SiAirtable,
|
||||
SiSlack,
|
||||
SiNotion,
|
||||
SiGoogle,
|
||||
} from "react-icons/si";
|
||||
import { CodeIcon } from "@heroicons/react/outline";
|
||||
import { useFormMenuSteps } from "../../../lib/navigation/formMenuSteps";
|
||||
import { classNames } from "../../../lib/utils";
|
||||
import EmptyPageFiller from "../../../components/layout/EmptyPageFiller";
|
||||
import { PuzzleIcon } from "@heroicons/react/outline";
|
||||
|
||||
const libs = [
|
||||
{
|
||||
id: "webhook",
|
||||
name: "Webhook",
|
||||
href: "#",
|
||||
bgColor: "bg-red-500",
|
||||
comingSoon: true,
|
||||
bgColor: "bg-ui-gray-light",
|
||||
icon: CodeIcon,
|
||||
},
|
||||
{
|
||||
@@ -70,6 +72,7 @@ export default function PipelinesPage() {
|
||||
const router = useRouter();
|
||||
const formId = router.query.id.toString();
|
||||
const { form, isLoadingForm } = useForm(router.query.id);
|
||||
const formMenuSteps = useFormMenuSteps(formId);
|
||||
|
||||
if (isLoadingForm) {
|
||||
return <Loading />;
|
||||
@@ -77,92 +80,97 @@ export default function PipelinesPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<LayoutFormBasics
|
||||
title={form.title}
|
||||
formId={formId}
|
||||
<BaseLayoutAuthorized
|
||||
title={`${form.name} - snoopForms`}
|
||||
breadcrumbs={[{ name: form.name, href: "#", current: true }]}
|
||||
steps={formMenuSteps}
|
||||
currentStep="pipelines"
|
||||
>
|
||||
<header>
|
||||
<div className="mx-auto mt-8 max-w-7xl">
|
||||
<h1 className="text-3xl font-bold leading-tight text-ui-gray-dark">
|
||||
Data Pipelines
|
||||
</h1>
|
||||
<LimitedWidth>
|
||||
<header>
|
||||
<div className="mx-auto mt-8 max-w-7xl">
|
||||
<h1 className="text-3xl font-bold leading-tight text-ui-gray-dark">
|
||||
Data Pipelines
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
<div className="my-4">
|
||||
<p className="text-ui-gray-dark">
|
||||
Pipe your data exactly where you need it. Add conditions for
|
||||
variable data piping.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
<div className="my-4">
|
||||
<p className="text-ui-gray-dark">
|
||||
Pipe your data exactly where you need it. Add conditions for
|
||||
variable data piping.
|
||||
</p>
|
||||
</div>
|
||||
<EmptyPageFiller
|
||||
alertText="No active pipelines for 'form name'"
|
||||
hintText="Setup a data pipeline below."
|
||||
>
|
||||
<PuzzleIcon className="w-24 h-24 mx-auto text-ui-gray-medium stroke-thin" />
|
||||
</EmptyPageFiller>
|
||||
<div>
|
||||
<div className="mt-16">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-ui-gray-dark">
|
||||
Integrations
|
||||
</h2>
|
||||
<EmptyPageFiller
|
||||
alertText={`No active pipelines for '${form.name}'`}
|
||||
hintText="Setup a data pipeline below."
|
||||
>
|
||||
<PuzzleIcon className="w-24 h-24 mx-auto text-ui-gray-medium stroke-thin" />
|
||||
</EmptyPageFiller>
|
||||
<div>
|
||||
<div className="mt-16">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-ui-gray-dark">
|
||||
Integrations
|
||||
</h2>
|
||||
|
||||
<ul
|
||||
role="list"
|
||||
className="grid grid-cols-1 gap-5 mt-3 sm:gap-6 sm:grid-cols-3 lg:grid-cols-3"
|
||||
>
|
||||
{libs.map((lib) => (
|
||||
<a
|
||||
className="flex col-span-1 rounded-md shadow-sm"
|
||||
key={lib.id}
|
||||
>
|
||||
<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"
|
||||
)}
|
||||
<ul
|
||||
role="list"
|
||||
className="grid grid-cols-1 gap-5 mt-3 sm:gap-6 sm:grid-cols-3 lg:grid-cols-3"
|
||||
>
|
||||
{libs.map((lib) => (
|
||||
<a
|
||||
className="flex col-span-1 rounded-md shadow-sm"
|
||||
key={lib.id}
|
||||
>
|
||||
<div
|
||||
<li
|
||||
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"
|
||||
: "shadow-sm text-ui-gray-dark hover:text-black",
|
||||
"flex col-span-1 rounded-md w-full"
|
||||
)}
|
||||
>
|
||||
<lib.icon
|
||||
<div
|
||||
className={classNames(
|
||||
lib.comingSoon
|
||||
? "text-ui-gray-medium w-8 h-8"
|
||||
: "text-white stroke-1 w-10 h-10",
|
||||
""
|
||||
)}
|
||||
/>
|
||||
</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-8 text-sm truncate">
|
||||
<p className="">{lib.name}</p>
|
||||
{lib.comingSoon && (
|
||||
<div className="p-1 px-3 ml-3 bg-green-100 rounded-sm">
|
||||
<p className="text-xs text-black">coming soon</p>
|
||||
</div>
|
||||
lib.bgColor,
|
||||
"flex-shrink-0 flex items-center justify-center w-20 text-white text-sm font-medium rounded-md"
|
||||
)}
|
||||
>
|
||||
<lib.icon
|
||||
className={classNames(
|
||||
lib.comingSoon
|
||||
? "text-ui-gray-medium w-8 h-8"
|
||||
: "text-white stroke-1 w-10 h-10",
|
||||
""
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</a>
|
||||
))}
|
||||
</ul>
|
||||
<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-8 text-sm truncate">
|
||||
<p className="">{lib.name}</p>
|
||||
{lib.comingSoon && (
|
||||
<div className="p-1 px-3 ml-3 bg-green-100 rounded-sm">
|
||||
<p className="text-xs text-black">
|
||||
coming soon
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</a>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutFormBasics>
|
||||
</LimitedWidth>
|
||||
</BaseLayoutAuthorized>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
import { DocumentSearchIcon, TerminalIcon } from "@heroicons/react/outline";
|
||||
import { GetServerSideProps } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import { FaDiscord, FaReact, FaVuejs } from "react-icons/fa";
|
||||
import LayoutFormBasics from "../../../components/layout/LayoutFormBasic";
|
||||
import SecondNavBar from "../../../components/layout/SecondNavBar";
|
||||
import SecondNavBarItem from "../../../components/layout/SecondNavBarItem";
|
||||
import Loading from "../../../components/Loading";
|
||||
import { useForm } from "../../../lib/forms";
|
||||
|
||||
export default function ReactPage() {
|
||||
const router = useRouter();
|
||||
const formId = router.query.id.toString();
|
||||
const { form, isLoadingForm } = useForm(router.query.id);
|
||||
|
||||
if (isLoadingForm) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<LayoutFormBasics
|
||||
title={form.title}
|
||||
formId={formId}
|
||||
currentStep="pipelines"
|
||||
>
|
||||
<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="mx-auto mt-8 max-w-7xl">
|
||||
<h1 className="text-3xl font-bold leading-tight text-ui-gray-dark">
|
||||
Build your form with React
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
<div className="my-4">
|
||||
<p className="text-ui-gray-dark">
|
||||
Use our pre-built components to build your form. Manage data in this
|
||||
dashboard.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-10 mt-16">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-ui-gray-dark">
|
||||
1. Get started
|
||||
</h2>
|
||||
<p className="text-ui-gray-dark">
|
||||
Install the snoopReact Library with Node Package Manager via
|
||||
snoopforms/react.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-8 font-light text-gray-200 bg-black rounded-md">
|
||||
<code>{"npm install --save @snoopforms/react"}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-10 mt-16">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-ui-gray-dark">
|
||||
2. Build the form
|
||||
</h2>
|
||||
<p className="text-ui-gray-dark">
|
||||
Use the pre-built components snoopForm, snoopPage and snoopElement
|
||||
to build exactly the form you want.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-8 font-light text-gray-200 bg-black rounded-md">
|
||||
<code className="whitespace-pre language-js">{`<SnoopForm
|
||||
domain="localhost:3000"
|
||||
protocol="http"
|
||||
className="w-full space-y-6"
|
||||
onSubmit={({ submission, schema })=>{}}>
|
||||
|
||||
<SnoopPage name="first">
|
||||
<SnoopElement
|
||||
type="text"
|
||||
name={"name"}
|
||||
label="Your name"
|
||||
classNames={{
|
||||
label: "your-label-class",
|
||||
element: "your-input-class",}}
|
||||
required/>
|
||||
</SnoopPage>
|
||||
|
||||
<SnoopPage thankyou>
|
||||
<h1>Thank you!</h1>
|
||||
</SnoopPage>
|
||||
|
||||
</SnoopForm>`}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-10 my-16">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-ui-gray-dark">Questions?</h2>
|
||||
<p className="pb-4 text-ui-gray-dark">
|
||||
Find a more detailed explanation on how to go about build the form
|
||||
and piping your data{" "}
|
||||
<a
|
||||
href="https://docs.snoopforms.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline text-red hover:no-underline"
|
||||
>
|
||||
in the docs.
|
||||
</a>{" "}
|
||||
Or join our Discord and ask us :)
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center p-8 rounded-md bg-purple">
|
||||
<a
|
||||
className="inline-flex items-center justify-center px-4 py-2 bg-white rounded-sm hover:motion-safe:animate-bounce text-purple text-bold text-md"
|
||||
href="https://discord.gg/kT4Aq7Kq"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Join Discord <FaDiscord className="w-8 h-8 ml-2" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutFormBasics>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
|
||||
const session = await getSession({ req });
|
||||
if (!session) {
|
||||
res.statusCode = 403;
|
||||
}
|
||||
return { props: {} };
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import { GetServerSideProps } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import LayoutFormResults from "../../../components/layout/LayoutFormResults";
|
||||
import Loading from "../../../components/Loading";
|
||||
import ResultsSummary from "../../../components/results/ResultsSummary";
|
||||
import ResultsAnalytics from "../../../components/results/ResultsAnalytics";
|
||||
import ResultsResponses from "../../../components/results/ResultsResponses";
|
||||
import { useForm } from "../../../lib/forms";
|
||||
|
||||
export default function Share() {
|
||||
const router = useRouter();
|
||||
const formId = router.query.id.toString();
|
||||
const { form, isLoadingForm } = useForm(router.query.id);
|
||||
const [resultMode, setResultMode] = useState<string>("summary");
|
||||
|
||||
if (isLoadingForm) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<LayoutFormResults
|
||||
title={form.title}
|
||||
formId={formId}
|
||||
currentStep="results"
|
||||
resultMode={resultMode}
|
||||
setResultMode={setResultMode}
|
||||
>
|
||||
{resultMode === "summary" && <ResultsSummary formId={formId} />}
|
||||
{resultMode === "responses" && <ResultsResponses formId={formId} />}
|
||||
{resultMode === "analytics" && <ResultsAnalytics formId={formId} />}
|
||||
</LayoutFormResults>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
|
||||
const session = await getSession({ req });
|
||||
if (!session) {
|
||||
res.statusCode = 403;
|
||||
}
|
||||
return { props: {} };
|
||||
};
|
||||
50
pages/forms/[id]/results/analytics.tsx
Normal file
50
pages/forms/[id]/results/analytics.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { GetServerSideProps } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import BaseLayoutAuthorized from "../../../../components/layout/BaseLayoutAuthorized";
|
||||
import LimitedWidth from "../../../../components/layout/LimitedWidth";
|
||||
import SecondNavBar from "../../../../components/layout/SecondNavBar";
|
||||
import Loading from "../../../../components/Loading";
|
||||
import ResultsAnalytics from "../../../../components/results/ResultsAnalytics";
|
||||
import { useForm } from "../../../../lib/forms";
|
||||
import { useFormMenuSteps } from "../../../../lib/navigation/formMenuSteps";
|
||||
import { useFormResultsSecondNavigation } from "../../../../lib/navigation/formResultsSecondNavigation";
|
||||
|
||||
export default function ResultsAnalyticsPage() {
|
||||
const router = useRouter();
|
||||
const formId = router.query.id.toString();
|
||||
const { form, isLoadingForm } = useForm(router.query.id);
|
||||
const formMenuSteps = useFormMenuSteps(formId);
|
||||
const formResultsSecondNavigation = useFormResultsSecondNavigation(formId);
|
||||
|
||||
if (isLoadingForm) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseLayoutAuthorized
|
||||
title={`${form.name} - snoopForms`}
|
||||
breadcrumbs={[{ name: form.name, href: "#", current: true }]}
|
||||
steps={formMenuSteps}
|
||||
currentStep="results"
|
||||
limitHeightScreen={true}
|
||||
>
|
||||
<SecondNavBar
|
||||
navItems={formResultsSecondNavigation}
|
||||
currentItemId="analytics"
|
||||
/>
|
||||
|
||||
<LimitedWidth>
|
||||
<ResultsAnalytics formId={formId} />
|
||||
</LimitedWidth>
|
||||
</BaseLayoutAuthorized>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
|
||||
const session = await getSession({ req });
|
||||
if (!session) {
|
||||
res.statusCode = 403;
|
||||
}
|
||||
return { props: {} };
|
||||
};
|
||||
50
pages/forms/[id]/results/responses.tsx
Normal file
50
pages/forms/[id]/results/responses.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { GetServerSideProps } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import BaseLayoutAuthorized from "../../../../components/layout/BaseLayoutAuthorized";
|
||||
import FullWidth from "../../../../components/layout/FullWidth";
|
||||
import SecondNavBar from "../../../../components/layout/SecondNavBar";
|
||||
import Loading from "../../../../components/Loading";
|
||||
import ResultsResponses from "../../../../components/results/ResultsResponses";
|
||||
import { useForm } from "../../../../lib/forms";
|
||||
import { useFormMenuSteps } from "../../../../lib/navigation/formMenuSteps";
|
||||
import { useFormResultsSecondNavigation } from "../../../../lib/navigation/formResultsSecondNavigation";
|
||||
|
||||
export default function ResultsResponsesPage() {
|
||||
const router = useRouter();
|
||||
const formId = router.query.id.toString();
|
||||
const { form, isLoadingForm } = useForm(router.query.id);
|
||||
const formMenuSteps = useFormMenuSteps(formId);
|
||||
const formResultsSecondNavigation = useFormResultsSecondNavigation(formId);
|
||||
|
||||
if (isLoadingForm) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseLayoutAuthorized
|
||||
title={`${form.name} - snoopForms`}
|
||||
breadcrumbs={[{ name: form.name, href: "#", current: true }]}
|
||||
steps={formMenuSteps}
|
||||
currentStep="results"
|
||||
limitHeightScreen={true}
|
||||
>
|
||||
<SecondNavBar
|
||||
navItems={formResultsSecondNavigation}
|
||||
currentItemId="responses"
|
||||
/>
|
||||
|
||||
<FullWidth>
|
||||
<ResultsResponses formId={formId} />
|
||||
</FullWidth>
|
||||
</BaseLayoutAuthorized>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
|
||||
const session = await getSession({ req });
|
||||
if (!session) {
|
||||
res.statusCode = 403;
|
||||
}
|
||||
return { props: {} };
|
||||
};
|
||||
50
pages/forms/[id]/results/summary.tsx
Normal file
50
pages/forms/[id]/results/summary.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { GetServerSideProps } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import BaseLayoutAuthorized from "../../../../components/layout/BaseLayoutAuthorized";
|
||||
import LimitedWidth from "../../../../components/layout/LimitedWidth";
|
||||
import SecondNavBar from "../../../../components/layout/SecondNavBar";
|
||||
import Loading from "../../../../components/Loading";
|
||||
import ResultsSummary from "../../../../components/results/ResultsSummary";
|
||||
import { useForm } from "../../../../lib/forms";
|
||||
import { useFormMenuSteps } from "../../../../lib/navigation/formMenuSteps";
|
||||
import { useFormResultsSecondNavigation } from "../../../../lib/navigation/formResultsSecondNavigation";
|
||||
|
||||
export default function ResultsSummaryPage() {
|
||||
const router = useRouter();
|
||||
const formId = router.query.id.toString();
|
||||
const { form, isLoadingForm } = useForm(router.query.id);
|
||||
const formMenuSteps = useFormMenuSteps(formId);
|
||||
const formResultsSecondNavigation = useFormResultsSecondNavigation(formId);
|
||||
|
||||
if (isLoadingForm) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseLayoutAuthorized
|
||||
title={`${form.name} - snoopForms`}
|
||||
breadcrumbs={[{ name: form.name, href: "#", current: true }]}
|
||||
steps={formMenuSteps}
|
||||
currentStep="results"
|
||||
limitHeightScreen={true}
|
||||
>
|
||||
<SecondNavBar
|
||||
navItems={formResultsSecondNavigation}
|
||||
currentItemId="summary"
|
||||
/>
|
||||
|
||||
<LimitedWidth>
|
||||
<ResultsSummary formId={formId} />
|
||||
</LimitedWidth>
|
||||
</BaseLayoutAuthorized>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
|
||||
const session = await getSession({ req });
|
||||
if (!session) {
|
||||
res.statusCode = 403;
|
||||
}
|
||||
return { props: {} };
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import LayoutBasic from "../../components/layout/LayoutBasic";
|
||||
import FormList from "../../components/FormList";
|
||||
import Head from "next/head";
|
||||
import { useForms } from "../../lib/forms";
|
||||
import BaseLayoutAuthorized from "../../components/layout/BaseLayoutAuthorized";
|
||||
import LimitedWidth from "../../components/layout/LimitedWidth";
|
||||
import Loading from "../../components/Loading";
|
||||
import { useForms } from "../../lib/forms";
|
||||
|
||||
export default function Forms({}) {
|
||||
const { isLoadingForms } = useForms();
|
||||
@@ -12,16 +12,14 @@ export default function Forms({}) {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Your forms - snoopForms</title>
|
||||
</Head>
|
||||
<LayoutBasic>
|
||||
<div className="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div className="px-4 py-8 sm:px-0">
|
||||
<FormList />
|
||||
</div>
|
||||
</div>
|
||||
</LayoutBasic>
|
||||
<BaseLayoutAuthorized
|
||||
title={"Forms - snoopForms"}
|
||||
breadcrumbs={[{ name: "My Forms", href: "#", current: true }]}
|
||||
>
|
||||
<LimitedWidth>
|
||||
<FormList />
|
||||
</LimitedWidth>
|
||||
</BaseLayoutAuthorized>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -101,3 +101,11 @@
|
||||
@apply text-transparent bg-clip-text bg-gradient-to-br from-red to-pink-500 hover:to-red;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
@apply absolute invisible;
|
||||
}
|
||||
|
||||
.has-tooltip:hover .tooltip {
|
||||
@apply z-50 visible;
|
||||
}
|
||||
|
||||
@@ -4,173 +4,6 @@ module.exports = {
|
||||
"./components/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
borderRadius: {
|
||||
none: "0",
|
||||
sm: "0.25rem", // 4px
|
||||
DEFAULT: "0.5rem", //8px
|
||||
DEFAULT: "8px",
|
||||
md: "1rem", // 16px
|
||||
lg: "1.5rem", // 24px
|
||||
xl: "2rem", // 32px
|
||||
"2xl": "2.5rem", // 40px
|
||||
"3xl": "3rem",
|
||||
"4xl": "3.5rem",
|
||||
full: "9999px",
|
||||
},
|
||||
colors: {
|
||||
red: {
|
||||
DEFAULT: "#f53b57",
|
||||
50: "#fff1f2",
|
||||
100: "#ffe4e5",
|
||||
200: "#fecdd2",
|
||||
300: "#fea3ad",
|
||||
400: "#fc7081",
|
||||
500: "#f53b57",
|
||||
600: "#e21c43",
|
||||
700: "#bf1137",
|
||||
800: "#a01136",
|
||||
900: "#891235",
|
||||
},
|
||||
green: {
|
||||
DEFAULT: "#0be881",
|
||||
50: "#eefff6",
|
||||
100: "#d7ffed",
|
||||
200: "#b2ffda",
|
||||
300: "#77febe",
|
||||
400: "#35f39a",
|
||||
500: "#0be881",
|
||||
600: "#02b763",
|
||||
700: "#068f50",
|
||||
800: "#0b7042",
|
||||
900: "#0b5c38",
|
||||
},
|
||||
pink: {
|
||||
DEFAULT: "#ef5777",
|
||||
50: "#fef2f2",
|
||||
100: "#fee5e8",
|
||||
200: "#fccfd6",
|
||||
300: "#faa7b4",
|
||||
400: "#f6768e",
|
||||
500: "#ef5777",
|
||||
600: "#da2453",
|
||||
700: "#b81845",
|
||||
800: "#9a1740",
|
||||
900: "#84173d",
|
||||
},
|
||||
purple: {
|
||||
DEFAULT: "#575fcf",
|
||||
50: "#f2f3fc",
|
||||
100: "#e2e4f7",
|
||||
200: "#cbd0f2",
|
||||
300: "#a8b2e8",
|
||||
400: "#7e8bdc",
|
||||
500: "#575fcf",
|
||||
600: "#4d4cc4",
|
||||
700: "#4842b3",
|
||||
800: "#413a93",
|
||||
900: "#373375",
|
||||
},
|
||||
blue: {
|
||||
DEFAULT: "#4bcffa",
|
||||
50: "#f0faff",
|
||||
100: "#e0f4fe",
|
||||
200: "#baeafd",
|
||||
300: "#7cdbfd",
|
||||
400: "#4bcffa",
|
||||
500: "#0cb4eb",
|
||||
600: "#0190c8",
|
||||
700: "#0273a2",
|
||||
800: "#066186",
|
||||
900: "#0b506f",
|
||||
},
|
||||
teal: {
|
||||
DEFAULT: "#34e7e4",
|
||||
50: "#effefd",
|
||||
100: "#c7fffa",
|
||||
200: "#90fff6",
|
||||
300: "#50f8ef",
|
||||
400: "#34e7e4",
|
||||
500: "#04c8c7",
|
||||
600: "#009da1",
|
||||
700: "#057d80",
|
||||
800: "#0a6065",
|
||||
900: "#0d5154",
|
||||
},
|
||||
amber: {
|
||||
DEFAULT: "#ffc048",
|
||||
50: "#fff9eb",
|
||||
100: "#ffeec6",
|
||||
200: "#ffda88",
|
||||
300: "#ffc048",
|
||||
400: "#ffa820",
|
||||
500: "#f98407",
|
||||
600: "#dd5f02",
|
||||
700: "#b74006",
|
||||
800: "#94300c",
|
||||
900: "#7a290d",
|
||||
},
|
||||
orange: {
|
||||
DEFAULT: "#ffa801",
|
||||
50: "#fffdea",
|
||||
100: "#fff6c5",
|
||||
200: "#ffed85",
|
||||
300: "#ffdc46",
|
||||
400: "#ffca1b",
|
||||
500: "#ffa801",
|
||||
600: "#e27f00",
|
||||
700: "#bb5702",
|
||||
800: "#984308",
|
||||
900: "#7c370b",
|
||||
},
|
||||
yellow: {
|
||||
DEFAULT: "#ffdd59",
|
||||
50: "#fffceb",
|
||||
100: "#fff6c6",
|
||||
200: "#ffeb88",
|
||||
300: "#ffdd59",
|
||||
400: "#ffc820",
|
||||
500: "#f9a607",
|
||||
600: "#dd7d02",
|
||||
700: "#b75806",
|
||||
800: "#94430c",
|
||||
900: "#7a380d",
|
||||
},
|
||||
gray: {
|
||||
DEFAULT: "#d2dae2",
|
||||
50: "#f6f8f9",
|
||||
100: "#eceff2",
|
||||
200: "#d2dae2",
|
||||
300: "#aebdcb",
|
||||
400: "#8299ae",
|
||||
500: "#627d95",
|
||||
600: "#4e657b",
|
||||
700: "#405164",
|
||||
800: "#384654",
|
||||
900: "#323d48",
|
||||
},
|
||||
"ui-gray": {
|
||||
lighter: "#FAFAFB",
|
||||
light: "#E5EAEF",
|
||||
medium: "#B5BFC8",
|
||||
dark: "#6B7177",
|
||||
},
|
||||
black: {
|
||||
DEFAULT: "#1e272e",
|
||||
50: "#f2f7f9",
|
||||
100: "#dfebee",
|
||||
200: "#c2d7df",
|
||||
300: "#98bbc8",
|
||||
400: "#6696aa",
|
||||
500: "#4b7a8f",
|
||||
600: "#416579",
|
||||
700: "#395465",
|
||||
800: "#354855",
|
||||
900: "#1e272e",
|
||||
},
|
||||
white: {
|
||||
DEFAULT: "#ffffff",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
strokeWidth: {
|
||||
thin: "0.5px",
|
||||
@@ -185,6 +18,173 @@ module.exports = {
|
||||
text: {
|
||||
KPI: "5rem",
|
||||
},
|
||||
borderRadius: {
|
||||
none: "0",
|
||||
sm: "0.25rem", // 4px
|
||||
DEFAULT: "0.5rem", //8px
|
||||
DEFAULT: "8px",
|
||||
md: "1rem", // 16px
|
||||
lg: "1.5rem", // 24px
|
||||
xl: "2rem", // 32px
|
||||
"2xl": "2.5rem", // 40px
|
||||
"3xl": "3rem",
|
||||
"4xl": "3.5rem",
|
||||
full: "9999px",
|
||||
},
|
||||
colors: {
|
||||
red: {
|
||||
DEFAULT: "#f53b57",
|
||||
50: "#fff1f2",
|
||||
100: "#ffe4e5",
|
||||
200: "#fecdd2",
|
||||
300: "#fea3ad",
|
||||
400: "#fc7081",
|
||||
500: "#f53b57",
|
||||
600: "#e21c43",
|
||||
700: "#bf1137",
|
||||
800: "#a01136",
|
||||
900: "#891235",
|
||||
},
|
||||
green: {
|
||||
DEFAULT: "#0be881",
|
||||
50: "#eefff6",
|
||||
100: "#d7ffed",
|
||||
200: "#b2ffda",
|
||||
300: "#77febe",
|
||||
400: "#35f39a",
|
||||
500: "#0be881",
|
||||
600: "#02b763",
|
||||
700: "#068f50",
|
||||
800: "#0b7042",
|
||||
900: "#0b5c38",
|
||||
},
|
||||
pink: {
|
||||
DEFAULT: "#ef5777",
|
||||
50: "#fef2f2",
|
||||
100: "#fee5e8",
|
||||
200: "#fccfd6",
|
||||
300: "#faa7b4",
|
||||
400: "#f6768e",
|
||||
500: "#ef5777",
|
||||
600: "#da2453",
|
||||
700: "#b81845",
|
||||
800: "#9a1740",
|
||||
900: "#84173d",
|
||||
},
|
||||
purple: {
|
||||
DEFAULT: "#575fcf",
|
||||
50: "#f2f3fc",
|
||||
100: "#e2e4f7",
|
||||
200: "#cbd0f2",
|
||||
300: "#a8b2e8",
|
||||
400: "#7e8bdc",
|
||||
500: "#575fcf",
|
||||
600: "#4d4cc4",
|
||||
700: "#4842b3",
|
||||
800: "#413a93",
|
||||
900: "#373375",
|
||||
},
|
||||
blue: {
|
||||
DEFAULT: "#4bcffa",
|
||||
50: "#f0faff",
|
||||
100: "#e0f4fe",
|
||||
200: "#baeafd",
|
||||
300: "#7cdbfd",
|
||||
400: "#4bcffa",
|
||||
500: "#0cb4eb",
|
||||
600: "#0190c8",
|
||||
700: "#0273a2",
|
||||
800: "#066186",
|
||||
900: "#0b506f",
|
||||
},
|
||||
teal: {
|
||||
DEFAULT: "#34e7e4",
|
||||
50: "#effefd",
|
||||
100: "#c7fffa",
|
||||
200: "#90fff6",
|
||||
300: "#50f8ef",
|
||||
400: "#34e7e4",
|
||||
500: "#04c8c7",
|
||||
600: "#009da1",
|
||||
700: "#057d80",
|
||||
800: "#0a6065",
|
||||
900: "#0d5154",
|
||||
},
|
||||
amber: {
|
||||
DEFAULT: "#ffc048",
|
||||
50: "#fff9eb",
|
||||
100: "#ffeec6",
|
||||
200: "#ffda88",
|
||||
300: "#ffc048",
|
||||
400: "#ffa820",
|
||||
500: "#f98407",
|
||||
600: "#dd5f02",
|
||||
700: "#b74006",
|
||||
800: "#94300c",
|
||||
900: "#7a290d",
|
||||
},
|
||||
orange: {
|
||||
DEFAULT: "#ffa801",
|
||||
50: "#fffdea",
|
||||
100: "#fff6c5",
|
||||
200: "#ffed85",
|
||||
300: "#ffdc46",
|
||||
400: "#ffca1b",
|
||||
500: "#ffa801",
|
||||
600: "#e27f00",
|
||||
700: "#bb5702",
|
||||
800: "#984308",
|
||||
900: "#7c370b",
|
||||
},
|
||||
yellow: {
|
||||
DEFAULT: "#ffdd59",
|
||||
50: "#fffceb",
|
||||
100: "#fff6c6",
|
||||
200: "#ffeb88",
|
||||
300: "#ffdd59",
|
||||
400: "#ffc820",
|
||||
500: "#f9a607",
|
||||
600: "#dd7d02",
|
||||
700: "#b75806",
|
||||
800: "#94430c",
|
||||
900: "#7a380d",
|
||||
},
|
||||
gray: {
|
||||
DEFAULT: "#d2dae2",
|
||||
50: "#f6f8f9",
|
||||
100: "#eceff2",
|
||||
200: "#d2dae2",
|
||||
300: "#aebdcb",
|
||||
400: "#8299ae",
|
||||
500: "#627d95",
|
||||
600: "#4e657b",
|
||||
700: "#405164",
|
||||
800: "#384654",
|
||||
900: "#323d48",
|
||||
},
|
||||
"ui-gray": {
|
||||
lighter: "#FAFAFB",
|
||||
light: "#E5EAEF",
|
||||
medium: "#B5BFC8",
|
||||
dark: "#6B7177",
|
||||
},
|
||||
black: {
|
||||
DEFAULT: "#1e272e",
|
||||
50: "#f2f7f9",
|
||||
100: "#dfebee",
|
||||
200: "#c2d7df",
|
||||
300: "#98bbc8",
|
||||
400: "#6696aa",
|
||||
500: "#4b7a8f",
|
||||
600: "#416579",
|
||||
700: "#395465",
|
||||
800: "#354855",
|
||||
900: "#1e272e",
|
||||
},
|
||||
white: {
|
||||
DEFAULT: "#ffffff",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")],
|
||||
|
||||
Reference in New Issue
Block a user