mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-13 11:09:29 -05:00
feat: add simple results dashboard
This commit is contained in:
@@ -39,11 +39,7 @@ export default function LayoutShare({ title, formId, currentStep, children }) {
|
||||
|
||||
{/* Main content */}
|
||||
<main>
|
||||
<div className="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
{/* Replace with your content */}
|
||||
{children}
|
||||
{/* /End replace */}
|
||||
</div>
|
||||
<div className="mx-auto max-w-7xl sm:px-6 lg:px-8">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,78 @@
|
||||
export default function ResultsDashboard({}) {
|
||||
return <p>Dashboard</p>;
|
||||
import { ClockIcon, InboxIcon, UsersIcon } from "@heroicons/react/outline";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
getSubmissionAnalytics,
|
||||
useSubmissionSessions,
|
||||
} from "../../lib/submissionSessions";
|
||||
import { timeSince } from "../../lib/utils";
|
||||
|
||||
export default function ResultsDashboard({ formId }) {
|
||||
const { submissionSessions, isLoadingSubmissionSessions } =
|
||||
useSubmissionSessions(formId);
|
||||
|
||||
const analytics = useMemo(() => {
|
||||
if (!isLoadingSubmissionSessions) {
|
||||
return getSubmissionAnalytics(submissionSessions);
|
||||
}
|
||||
}, [isLoadingSubmissionSessions]);
|
||||
|
||||
const stats = useMemo(() => {
|
||||
if (analytics) {
|
||||
return [
|
||||
{
|
||||
id: "uniqueUsers",
|
||||
name: "Unique Users",
|
||||
stat: analytics.uniqueUsers,
|
||||
icon: UsersIcon,
|
||||
},
|
||||
{
|
||||
id: "totalSubmissions",
|
||||
name: "Total Submissions",
|
||||
stat: analytics.totalSubmissions,
|
||||
icon: InboxIcon,
|
||||
},
|
||||
{
|
||||
id: "uniqueUsers",
|
||||
name: "Last Submission",
|
||||
stat: timeSince(analytics.lastSubmissionAt) || "-",
|
||||
icon: ClockIcon,
|
||||
},
|
||||
];
|
||||
}
|
||||
}, [analytics]);
|
||||
return (
|
||||
<main>
|
||||
<div className="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div>
|
||||
{stats ? (
|
||||
<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 bg-red-500 rounded-md">
|
||||
<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-2xl font-semibold text-gray-900">
|
||||
{item.stat}
|
||||
</p>
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,9 +38,18 @@ export default function ResultsResponses({ formId }: ResultsResponseProps) {
|
||||
<div className="flex flex-col flex-1 w-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 bg-white shadow sm:rounded-lg">
|
||||
{activeSubmissionSession && (
|
||||
<div className="px-4 py-5 sm:p-12">
|
||||
<div className="overflow-visible sm:rounded-lg">
|
||||
{!activeSubmissionSession ? (
|
||||
<button
|
||||
type="button"
|
||||
className="relative block p-12 mx-auto mt-8 text-center border-2 border-gray-300 border-dashed rounded-lg w-96 hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
<span className="block mt-2 text-sm font-medium text-gray-500">
|
||||
Select a response on the left to see the details here
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<div className="px-4 py-5 bg-white shadow sm:p-12">
|
||||
<div className="grid grid-cols-2 gap-8 divide-x">
|
||||
<div className="flow-root">
|
||||
<h1 className="mb-8 text-gray-700">
|
||||
@@ -105,53 +114,60 @@ export default function ResultsResponses({ formId }: ResultsResponseProps) {
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
<aside className="flex-shrink-0 hidden border-r border-gray-200 xl:order-first xl:flex xl:flex-col w-96">
|
||||
<aside className="flex-shrink-0 hidden border-r border-gray-200 md:order-first md:flex md:flex-col w-96">
|
||||
<DownloadResponses formId={formId} />
|
||||
<div className="pt-6 pb-1">
|
||||
<h2 className="px-5 text-lg font-medium text-gray-900">
|
||||
Responses
|
||||
</h2>
|
||||
</div>
|
||||
<RadioGroup
|
||||
value={activeSubmissionSession}
|
||||
onChange={setActiveSubmissionSession}
|
||||
className="flex-1 min-h-0 overflow-y-auto"
|
||||
>
|
||||
<div className="relative">
|
||||
<ul className="relative z-0 divide-y divide-gray-200">
|
||||
{submissionSessions.map((submissionSession) => (
|
||||
<RadioGroup.Option
|
||||
key={submissionSession.id}
|
||||
value={submissionSession}
|
||||
className={({ checked }) =>
|
||||
classNames(
|
||||
checked ? "bg-gray-100" : "",
|
||||
"relative flex items-center px-6 py-5 space-x-3 "
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<button
|
||||
onClick={() =>
|
||||
setActiveSubmissionSession(submissionSession)
|
||||
}
|
||||
className="w-full text-left focus:outline-none"
|
||||
>
|
||||
{/* Extend touch target to entire panel */}
|
||||
<span className="absolute inset-0" aria-hidden="true" />
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{convertDateTimeString(submissionSession.createdAt)}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 truncate">
|
||||
{submissionSession.events.length} events
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
{submissionSessions.length === 0 ? (
|
||||
<p className="px-5 mt-3 text-sm text-gray-500">No responses yet</p>
|
||||
) : (
|
||||
<RadioGroup
|
||||
value={activeSubmissionSession}
|
||||
onChange={setActiveSubmissionSession}
|
||||
className="flex-1 min-h-0 overflow-y-auto"
|
||||
>
|
||||
<div className="relative">
|
||||
<ul className="relative z-0 divide-y divide-gray-200">
|
||||
{submissionSessions.map((submissionSession) => (
|
||||
<RadioGroup.Option
|
||||
key={submissionSession.id}
|
||||
value={submissionSession}
|
||||
className={({ checked }) =>
|
||||
classNames(
|
||||
checked ? "bg-gray-100" : "",
|
||||
"relative flex items-center px-6 py-5 space-x-3 "
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<button
|
||||
onClick={() =>
|
||||
setActiveSubmissionSession(submissionSession)
|
||||
}
|
||||
className="w-full text-left focus:outline-none"
|
||||
>
|
||||
{/* Extend touch target to entire panel */}
|
||||
<span
|
||||
className="absolute inset-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{convertDateTimeString(submissionSession.createdAt)}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 truncate">
|
||||
{submissionSession.events.length} events
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
)}
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import useSWR from "swr";
|
||||
import { SubmissionSession } from "./types";
|
||||
import { fetcher } from "./utils";
|
||||
|
||||
export const useSubmissionSessions = (formId: string) => {
|
||||
@@ -56,3 +57,34 @@ export const getSubmission = (submissionSession, schema) => {
|
||||
}
|
||||
return submission;
|
||||
};
|
||||
|
||||
export const getSubmissionAnalytics = (
|
||||
submissionSessions: SubmissionSession[]
|
||||
) => {
|
||||
const uniqueUsers = [];
|
||||
let totalSubmissions = 0;
|
||||
let lastSubmissionAt = null;
|
||||
for (const submissionSession of submissionSessions) {
|
||||
// collect unique users
|
||||
if (!uniqueUsers.includes(submissionSession.userFingerprint)) {
|
||||
uniqueUsers.push(submissionSession.userFingerprint);
|
||||
}
|
||||
if (submissionSession.events.length > 0) {
|
||||
totalSubmissions += 1;
|
||||
const lastSubmission =
|
||||
submissionSession.events[submissionSession.events.length - 1];
|
||||
if (!lastSubmissionAt) {
|
||||
lastSubmissionAt = lastSubmission.createdAt;
|
||||
} else if (
|
||||
Date.parse(lastSubmission.createdAt) > Date.parse(lastSubmissionAt)
|
||||
) {
|
||||
lastSubmissionAt = lastSubmission.createdAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
lastSubmissionAt,
|
||||
uniqueUsers: uniqueUsers.length,
|
||||
totalSubmissions: totalSubmissions,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import intlFormat from "date-fns/intlFormat";
|
||||
import { formatDistance } from "date-fns";
|
||||
|
||||
export const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||
|
||||
@@ -94,3 +95,10 @@ export const convertTimeString = (dateString: string) => {
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const timeSince = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return formatDistance(date, new Date(), {
|
||||
addSuffix: true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -120,7 +120,6 @@ export default function SignIn({ csrfToken }: props) {
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const csrfToken = await getCsrfToken(context);
|
||||
console.log("csrfToken", JSON.stringify(csrfToken));
|
||||
return {
|
||||
props: { csrfToken },
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function Share() {
|
||||
resultMode={resultMode}
|
||||
setResultMode={setResultMode}
|
||||
>
|
||||
{resultMode === "dashboard" && <ResultsDashboard />}
|
||||
{resultMode === "dashboard" && <ResultsDashboard formId={formId} />}
|
||||
{resultMode === "responses" && (
|
||||
<>
|
||||
<ResultsResponses formId={formId} />
|
||||
|
||||
Reference in New Issue
Block a user