fix build warnings, use next-image instead of img

This commit is contained in:
Matthias Nannt
2022-06-24 12:56:37 +09:00
20 changed files with 651 additions and 103 deletions
+2 -3
View File
@@ -7,9 +7,8 @@ import {
} from "@heroicons/react/outline";
import { DotsHorizontalIcon, TrashIcon } from "@heroicons/react/solid";
import Link from "next/link";
import Router from "next/router";
import { Fragment, useState } from "react";
import { createForm, useForms } from "../lib/forms";
import { useForms } from "../lib/forms";
import { classNames } from "../lib/utils";
import NewFormModal from "./form/NewFormModal";
import EmptyPageFiller from "./layout/EmptyPageFiller";
@@ -52,7 +51,7 @@ export default function FormList() {
hintText="Start by creating a form."
buttonText="create form"
borderStyles="border-4 border-dotted border-red"
button={true}
hasButton={true}
>
<DocumentAddIcon className="w-24 h-24 mx-auto text-ui-gray-medium stroke-thin" />
</EmptyPageFiller>
+5 -1
View File
@@ -1,5 +1,5 @@
import { FaReact, FaVuejs } from "react-icons/fa";
import { DocumentSearchIcon } from "@heroicons/react/outline";
import { DocumentSearchIcon, TerminalIcon } from "@heroicons/react/outline";
import { classNames } from "../../lib/utils";
import StandardButton from "../StandardButton";
import Link from "next/link";
@@ -44,6 +44,10 @@ export default function FormCode({ formId }) {
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
+200
View File
@@ -0,0 +1,200 @@
/* This example requires Tailwind CSS v2.0+ */
import { Dialog, RadioGroup, Transition } from "@headlessui/react";
import { CheckCircleIcon, XIcon } from "@heroicons/react/solid";
import { useRouter } from "next/router";
import { Fragment, useState } from "react";
import { BsPlus } from "react-icons/bs";
import { createForm } from "../../lib/forms";
import { createNoCodeForm } from "../../lib/noCodeForm";
import { classNames } from "../../lib/utils";
import StandardButton from "../StandardButton";
const formTypes = [
{
id: "NOCODE",
title: "No-Code Builder",
description:
"Use the Notion-like builder to build your form without a single line of code.",
},
{
id: "CODE",
title: "Code",
description:
"Use the snoopReact library to code the form yourself and manage the data here.",
additionalDescription: "",
},
];
type FormOnboardingModalProps = {
open: boolean;
setOpen: (v: boolean) => void;
};
export default function NewFormModal({
open,
setOpen,
}: FormOnboardingModalProps) {
const router = useRouter();
const [name, setName] = useState("");
const [formType, setFormType] = useState(formTypes[0]);
const submitForm = async (e) => {
e.preventDefault();
const form = await createForm({
name,
formType: formType.id,
});
if (form.formType === "NOCODE") {
await createNoCodeForm(form.id);
}
router.push(`/forms/${form.id}/form`);
};
return (
<Transition.Root show={open} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={setOpen}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-30 backdrop-blur-md" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex items-end justify-center min-h-full p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative px-4 pt-5 pb-4 text-left transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:max-w-lg sm:w-full sm:p-6">
<div className="absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
<button
type="button"
className="text-gray-400 bg-white rounded-md hover:text-gray-500 focus:outline-none focus:ring-0 focus:ring-offset-2"
onClick={() => setOpen(false)}
>
<span className="sr-only">Close</span>
<XIcon className="w-6 h-6" aria-hidden="true" />
</button>
</div>
<div className="flex flex-row justify-between">
<h2 className="flex-none p-2 text-xl font-bold text-ui-gray-dark">
Create new form
</h2>
</div>
<form
onSubmit={(e) => submitForm(e)}
className="inline-block w-full p-2 overflow-hidden text-left align-bottom transition-all transform sm:align-middle"
>
<div>
<label
htmlFor="email"
className="text-sm font-light text-ui-gray-dark"
>
Name your form
</label>
<div className="mt-2">
<input
type="text"
name="name"
className="block w-full p-2 mb-6 border-none rounded bg-ui-gray-light focus:ring-2 focus:ring-red sm:text-sm placeholder:font-extralight placeholder:text-ui-gray-medium"
placeholder="e.g. Customer Research Survey"
value={name}
onChange={(e) => setName(e.target.value)}
autoFocus
required
/>
</div>
</div>
<RadioGroup value={formType} onChange={setFormType}>
<RadioGroup.Label className="text-sm font-light text-ui-gray-dark">
How do you build your form?
</RadioGroup.Label>
<div className="grid grid-cols-1 mt-4 gap-y-6 sm:grid-cols-2 sm:gap-x-4">
{formTypes.map((formType) => (
<RadioGroup.Option
key={formType.id}
value={formType}
className={({ checked, active }) =>
classNames(
checked ? "border-transparent" : "",
active
? "border-red ring-2 ring-red"
: "bg-ui-gray-lighter",
"relative bg-white border rounded shadow-sm p-4 flex cursor-pointer focus:outline-none"
)
}
>
{({ checked, active }) => (
<>
<span className="flex flex-1">
<span className="flex flex-col">
<RadioGroup.Label
as="span"
className="block font-bold text-md text-ui-gray-dark"
>
{formType.title}
</RadioGroup.Label>
<RadioGroup.Description
as="span"
className="flex items-center mt-1 text-xs whitespace-pre-wrap text-ui-gray-dark"
>
{formType.description}
</RadioGroup.Description>
</span>
</span>
<CheckCircleIcon
className={classNames(
!checked ? "hidden" : "",
"h-5 w-5 text-red"
)}
aria-hidden="true"
/>
<div
className={classNames(
checked ? "hidden" : "",
"h-4 w-4 rounded-full border-2 border-ui-gray-light"
)}
aria-hidden="true"
/>
<span
className={classNames(
active ? "border" : "border-2",
checked ? "border-red" : "border-transparent",
"absolute -inset-px rounded pointer-events-none"
)}
aria-hidden="true"
/>
</>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
<div className="mt-5 sm:mt-6">
<StandardButton fullwidth type="submit">
create form
<BsPlus className="w-6 h-6 ml-1"></BsPlus>
</StandardButton>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}
+45
View File
@@ -0,0 +1,45 @@
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;
+3 -3
View File
@@ -8,7 +8,7 @@ interface Props {
hintText: string;
buttonText?: string;
borderStyles?: string;
button?: boolean;
hasButton?: boolean;
}
const EmptyPageFiller: React.FC<Props> = ({
@@ -18,7 +18,7 @@ const EmptyPageFiller: React.FC<Props> = ({
hintText,
buttonText,
borderStyles,
button = false,
hasButton = false,
}) => {
return (
<div
@@ -32,7 +32,7 @@ const EmptyPageFiller: React.FC<Props> = ({
{alertText}
</h3>
<p className="mt-1 text-xs font-light text-ui-gray-medium">{hintText}</p>
{button && (
{hasButton && (
<div className="mt-6">
<StandardButton onClick={onClick}>{buttonText}</StandardButton>
</div>
+6 -4
View File
@@ -27,7 +27,7 @@ export default function Layout({ children }) {
<Disclosure as="nav" className="bg-white shadow-sm">
{({ open }) => (
<>
<div className="max-w-5xl px-4 mx-auto sm:px-6 lg:px-8">
<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">
@@ -107,10 +107,12 @@ export default function Layout({ children }) {
<div className="pt-4 pb-3 border-t border-ui-gray-light">
<div className="flex items-center px-4">
<div className="flex-shrink-0">
<img
<Image
className="w-10 h-10 rounded-full"
src="https://images.unsplash.com/photo-1517365830460-955ce3ccd263?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=256&h=256&q=80"
alt=""
src="/img/avatar-placeholder.png"
alt="user avatar"
width={20}
height={20}
/>
</div>
</div>
+9 -9
View File
@@ -1,10 +1,10 @@
import Head from "next/head";
import MenuBreadcrumbs from "./MenuBreadcrumbs";
import MenuSteps from "./MenuSteps";
import MenuProfile from "./MenuProfile";
import { classNames } from "../../lib/utils";
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,
@@ -26,9 +26,9 @@ export default function LayoutFormResults({
}
const resultModes = [
{ name: "Summary", id: "summary" },
{ name: "Responses", id: "responses" },
{ name: "Analytics", id: "analytics" },
{ name: "Summary", id: "summary", icon: "" },
{ name: "Responses", id: "responses", icon: "" },
{ name: "Analytics", id: "analytics", icon: "" },
];
return (
<>
@@ -48,7 +48,7 @@ export default function LayoutFormResults({
</div>
</div>
</div>
<div className="relative z-10 flex flex-shrink-0 h-16 border-b border-ui-gray-light shadow-inner bg-gray-50">
<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) => (
+6 -3
View File
@@ -1,20 +1,23 @@
import { Menu, Transition } from "@headlessui/react";
import { signOut } from "next-auth/react";
import Image from "next/image";
import { Fragment } from "react";
import { classNames } from "../../lib/utils";
export default function MenuProfile({}) {
return (
<Menu as="div" className="relative flex-shrink-0">
<Menu as="div" className="relative z-50 flex-shrink-0">
{({ open }) => (
<>
<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>
<img
<Image
className="w-8 h-8 rounded-full"
src="/img/avatar-placeholder.png"
alt=""
alt="user avatar"
width={20}
height={20}
/>
</Menu.Button>
</div>
+1 -1
View File
@@ -7,7 +7,7 @@ interface Props {
// button component, consuming props
const SecondNavBar: React.FC<Props> = ({ children }) => {
return (
<div className="relative z-10 flex flex-shrink-0 h-16 py-2 bg-ui-gray-lighter">
<div className="relative flex flex-shrink-0 h-16 py-2 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}
+47 -29
View File
@@ -1,10 +1,11 @@
import { ClockIcon, InboxIcon, UsersIcon } from "@heroicons/react/outline";
import Image from "next/image";
import { useMemo } from "react";
import {
getSubmissionAnalytics,
useSubmissionSessions,
} from "../../lib/submissionSessions";
import { timeSince } from "../../lib/utils";
import AnalyticsCard from "../layout/AnalyticsCard";
export default function ResultsAnalytics({ formId }) {
const { submissionSessions, isLoadingSubmissionSessions } =
@@ -22,56 +23,73 @@ export default function ResultsAnalytics({ formId }) {
{
id: "uniqueUsers",
name: "Unique Users",
stat: analytics.uniqueUsers,
icon: UsersIcon,
stat: analytics.uniqueUsers || "--",
toolTipText: "placeholder",
trend: 12,
},
{
id: "totalSubmissions",
name: "Total Submissions",
stat: analytics.totalSubmissions,
icon: InboxIcon,
stat: analytics.totalSubmissions || "--",
trend: 10,
},
{
id: "uniqueUsers",
id: "lastSubmission",
name: "Last Submission",
stat: timeSince(analytics.lastSubmissionAt) || "-",
icon: ClockIcon,
stat: timeSince(analytics.lastSubmissionAt) || "--",
typeText: true,
},
];
}
}, [analytics]);
return (
<main>
<div className="mx-auto max-w-7xl sm:px-6 lg:px-8">
<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) => (
<div
<AnalyticsCard
key={item.id}
className="relative px-4 bg-white rounded-lg shadow pt-5overflow-hidden sm:pt-6 sm:px-6"
>
<dt>
<div className="absolute p-3 rounded-md bg-red-500">
<item.icon
className="w-6 h-6 text-white"
aria-hidden="true"
/>
</div>
<p className="ml-16 text-sm font-medium text-gray-500 truncate">
{item.name}
</p>
</dt>
<dd className="flex items-baseline ml-16 sm:pb-7">
<p className="text-xl font-semibold text-gray-800">
{item.stat}
</p>
</dd>
</div>
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>
</div>
</div>
</main>
);
+1 -1
View File
@@ -28,7 +28,7 @@ export default function ResultsResponses({ formId }: ResultsResponseProps) {
if (!isLoadingSubmissionSessions && submissionSessions.length > 0) {
setActiveSubmissionSession(submissionSessions[0]);
}
}, [isLoadingSubmissionSessions]);
}, [isLoadingSubmissionSessions, submissionSessions]);
if (isLoadingSubmissionSessions) {
return <Loading />;
+37 -45
View File
@@ -1,11 +1,13 @@
import { ClockIcon, InboxIcon, UsersIcon } from "@heroicons/react/outline";
import { useMemo } from "react";
import { useForm } from "../../lib/forms";
import {
getSubmissionAnalytics,
getSubmissionSummary,
useSubmissionSessions,
} from "../../lib/submissionSessions";
import { SubmissionSummary } from "../../lib/types";
import { timeSince } from "../../lib/utils";
import AnalyticsCard from "../layout/AnalyticsCard";
import Loading from "../Loading";
import TextResults from "./summary/TextResults";
@@ -15,6 +17,12 @@ export default function ResultsSummary({ formId }) {
const { form, isLoadingForm } = useForm(formId);
const analytics = useMemo(() => {
if (!isLoadingSubmissionSessions) {
return getSubmissionAnalytics(submissionSessions);
}
}, [isLoadingSubmissionSessions, submissionSessions]);
const summary: SubmissionSummary | undefined = useMemo(() => {
if (!isLoadingSubmissionSessions && !isLoadingForm) {
return getSubmissionSummary(submissionSessions, form?.schema);
@@ -22,79 +30,63 @@ export default function ResultsSummary({ formId }) {
}, [isLoadingSubmissionSessions, submissionSessions, isLoadingForm, form]);
const stats = useMemo(() => {
if (summary) {
if (analytics) {
return [
{
id: "uniqueUsers",
name: "Unique Users",
stat: 10,
icon: UsersIcon,
stat: analytics.uniqueUsers || "--",
toolTipText: "placeholder",
trend: 12,
},
{
id: "totalSubmissions",
name: "Total Submissions",
stat: 10,
icon: InboxIcon,
stat: analytics.totalSubmissions || "--",
trend: 10,
},
{
id: "uniqueUsers",
id: "lastSubmission",
name: "Last Submission",
stat: 10,
icon: ClockIcon,
stat: timeSince(analytics.lastSubmissionAt) || "--",
typeText: true,
},
];
}
}, [summary]);
}, [analytics]);
if (!summary) {
if (!summary || !analytics) {
return <Loading />;
}
return (
<main className="bg-gray-50">
<div className="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div>
<dl className="grid grid-cols-1 gap-5 mt-8 sm:grid-cols-2 lg:grid-cols-3">
{stats.map((item) => (
<div
key={item.id}
className="relative px-4 bg-white rounded-lg shadow pt-5overflow-hidden sm:pt-6 sm:px-6"
>
<dt>
<div className="absolute p-3 rounded-md bg-red-500">
<item.icon
className="w-6 h-6 text-white"
aria-hidden="true"
/>
</div>
<p className="ml-16 text-sm font-medium text-gray-500 truncate">
{item.name}
</p>
</dt>
<dd className="flex items-baseline ml-16 sm:pb-7">
<p className="text-xl font-semibold text-gray-800">
{item.stat}
</p>
</dd>
</div>
))}
</dl>
</div>
<hr className="my-8" />
<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(
(page, pageIdx) =>
(page) =>
page.type === "form" && (
<div key={page.name}>
<h2 className="text-xl font-bold text-gray-700">
Page {pageIdx + 1}
</h2>
{page.elements.map((element) =>
element.type === "text" || element.type === "textarea" ? (
<TextResults element={element} />
) : null
)}
<hr className="my-8" />
</div>
)
)}
+1 -1
View File
@@ -6,7 +6,7 @@ export default function TextResults({ element }) {
<div className="flow-root px-8 my-4 mt-6 overflow-y-scroll text-center max-h-64">
<ul className="-my-5 divide-y divide-ui-gray-light">
{element.summary.map((answer) => (
<li key={answer} className="py-5">
<li key={answer} className="py-8">
<div className="relative focus-within:ring-2 focus-within:ring-indigo-500">
<h3 className="text-sm text-gray-700">
{/* Extend touch target to entire panel */}