redesigned metadata

This commit is contained in:
Dhruwang
2023-09-26 16:48:24 +05:30
parent c272b8a75c
commit 36355ef54b
11 changed files with 187 additions and 97 deletions
@@ -8,97 +8,75 @@ interface SummaryMetadataProps {
survey: TSurveyWithAnalytics;
}
const StatCard = ({ label, percentage, value, tooltipText }) => (
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger>
<div className="flex h-full cursor-default flex-col justify-between space-y-2 rounded-lg border border-slate-200 bg-white p-4 text-left shadow-sm">
<p className="text-sm text-slate-600">
{label}
{percentage && percentage !== "NaN%" && (
<span className="ml-1 rounded-xl bg-slate-100 px-2 py-1 text-xs">{percentage}</span>
)}
</p>
<p className="text-2xl font-bold text-slate-800">{value}</p>
</div>
</TooltipTrigger>
<TooltipContent>
<p>{tooltipText}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
export default function SummaryMetadata({ responses, survey }: SummaryMetadataProps) {
const completedResponsesLength = responses.filter((r) => r.finished).length;
return (
<>
<div className="mb-4 ">
<div className="flex flex-col-reverse gap-y-2 lg:grid lg:grid-cols-2 lg:gap-x-2">
<div className="grid grid-cols-2 gap-4 md:grid md:grid-cols-4 md:gap-x-2">
<div className="flex flex-col justify-between space-y-2 rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
<p className="text-sm text-slate-600">displays</p>
<p className="text-2xl font-bold text-slate-800">
{survey.analytics.numDisplays === 0 ? <span>-</span> : survey.analytics.numDisplays}
</p>
</div>
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger>
<div className="flex h-full cursor-default flex-col justify-between space-y-2 rounded-lg border border-slate-200 bg-white p-4 text-left shadow-sm">
<p className="text-sm text-slate-600">
Starts
<span className="ml-1 rounded-xl bg-slate-100 px-2 py-1 text-xs">
{Math.round((responses.length / survey.analytics.numDisplays) * 100)}%
</span>
</p>
<p className="text-2xl font-bold text-slate-800">
{responses.length === 0 ? <span>-</span> : responses.length}
</p>
</div>
</TooltipTrigger>
<TooltipContent>
<p>People who started the survey.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger>
<div className="flex h-full cursor-default flex-col justify-between space-y-2 rounded-lg border border-slate-200 bg-white p-4 text-left shadow-sm">
<p className="text-sm text-slate-600">
Responses
<span className="ml-1 rounded-xl bg-slate-100 px-2 py-1 text-xs">
{Math.round((completedResponsesLength / survey.analytics.numDisplays) * 100)}%
</span>
</p>
<p className="text-2xl font-bold text-slate-800">
<span>{completedResponsesLength}</span>
</p>
</div>
</TooltipTrigger>
<TooltipContent>
<p>People who completed the survey.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger>
<div className="flex cursor-default flex-col justify-between space-y-2 rounded-lg border border-slate-200 bg-white p-4 text-left shadow-sm">
<p className="text-sm text-slate-600">
Drop Offs
<span className="ml-1 rounded-xl bg-slate-100 px-2 py-1 text-xs">
{Math.round(
((survey.analytics.numDisplays - survey.analytics.numResponses) /
survey.analytics.numDisplays) *
100
)}
%
</span>
</p>
<p className="text-2xl font-bold text-slate-800">
{responses.length === 0 ? (
<span>-</span>
) : (
<span>{survey.analytics.numDisplays - survey.analytics.numResponses}</span>
)}
</p>
</div>
</TooltipTrigger>
<TooltipContent>
<p>People who didn&apos;t respond to survey.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className="mb-4">
<div className="flex flex-col-reverse gap-y-2 lg:grid lg:grid-cols-2 lg:gap-x-2">
<div className="grid grid-cols-2 gap-4 md:grid md:grid-cols-4 md:gap-x-2">
<div className="flex flex-col justify-between space-y-2 rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
<p className="text-sm text-slate-600">displays</p>
<p className="text-2xl font-bold text-slate-800">
{survey.analytics.numDisplays === 0 ? <span>-</span> : survey.analytics.numDisplays}
</p>
</div>
<div className="flex flex-col justify-between lg:col-span-1">
<div className="text-right text-xs text-slate-400">
Last updated: {timeSinceConditionally(survey.updatedAt.toISOString())}
</div>
<StatCard
label="Starts"
percentage={`${Math.round((responses.length / survey.analytics.numDisplays) * 100)}%`}
value={responses.length === 0 ? <span>-</span> : responses.length}
tooltipText="People who started the survey."
/>
<StatCard
label="Responses"
percentage={`${Math.round((completedResponsesLength / survey.analytics.numDisplays) * 100)}%`}
value={responses.length === 0 ? <span>-</span> : completedResponsesLength}
tooltipText="People who completed the survey."
/>
<StatCard
label="Drop Offs"
percentage={`${Math.round(
((survey.analytics.numDisplays - survey.analytics.numResponses) /
survey.analytics.numDisplays) *
100
)}%`}
value={
responses.length === 0 ? (
<span>-</span>
) : (
survey.analytics.numDisplays - survey.analytics.numResponses
)
}
tooltipText="People who didn't respond to survey."
/>
</div>
<div className="flex flex-col justify-between lg:col-span-1">
<div className="text-right text-xs text-slate-400">
Last updated: {timeSinceConditionally(survey.updatedAt.toISOString())}
</div>
</div>
</div>
</>
</div>
);
}
@@ -1,5 +1,5 @@
import { responses } from "@/lib/api/response";
import { markDisplayResponded } from "@formbricks/lib/services/displays";
import { markDisplayResponded, updateDisplay } from "@formbricks/lib/services/displays";
import { NextResponse } from "next/server";
export async function OPTIONS(): Promise<NextResponse> {
@@ -27,3 +27,21 @@ export async function POST(_: Request, { params }: { params: { displayId: string
return responses.internalServerErrorResponse(error.message);
}
}
export async function PUT(
request: Request,
{ params }: { params: { displayId: string } }
): Promise<NextResponse> {
const { displayId } = params;
if (!displayId) {
return responses.badRequestResponse("Missing displayId");
}
const displayUpdate = await request.json();
try {
const display = await updateDisplay(displayId, displayUpdate);
return responses.successResponse(display);
} catch (error) {
return responses.internalServerErrorResponse(error.message);
}
}
@@ -81,6 +81,6 @@ export async function PUT(
response: response,
});
}
console.log("returning sucees reposne");
return responses.successResponse(response, true);
}
+8 -2
View File
@@ -34,6 +34,7 @@ export default function LinkSurvey({
const isPreview = searchParams?.get("preview") === "true";
const [surveyState, setSurveyState] = useState(new SurveyState(survey.id));
const [activeQuestionId, setActiveQuestionId] = useState<string>(survey.questions[0].id);
const [displayId, setDisplayId] = useState();
const prefillResponseData: TResponseData | undefined = prefillAnswer
? getPrefillResponseData(survey.questions[0], survey, prefillAnswer)
: undefined;
@@ -89,9 +90,14 @@ export default function LinkSurvey({
survey={survey}
brandColor={product.brandColor}
formbricksSignature={product.formbricksSignature}
onDisplay={() => createDisplay({ surveyId: survey.id }, window?.location?.origin)}
onDisplay={async () => {
const display = await createDisplay({ surveyId: survey.id }, window?.location?.origin);
setDisplayId(display.id);
}}
onResponse={(responseUpdate) => {
responseQueue.add(responseUpdate);
let responseTemp = { ...responseUpdate };
responseTemp.displayId = displayId;
responseQueue.add(responseTemp);
}}
onActiveQuestionChange={(questionId) => setActiveQuestionId(questionId)}
activeQuestionId={activeQuestionId}
@@ -50,6 +50,12 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
// Deletes a single survey
else if (req.method === "DELETE") {
const submissionId = req.query.submissionId?.toString();
const deletedDisplay = await prisma.display.delete({
where: {
responseId: submissionId,
},
});
console.log(deletedDisplay);
const prismaRes = await prisma.response.delete({
where: { id: submissionId },
});
@@ -0,0 +1,11 @@
/*
Warnings:
- A unique constraint covering the columns `[responseId]` on the table `Display` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "Display" ADD COLUMN "responseId" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX "Display_responseId_key" ON "Display"("responseId");
+9 -8
View File
@@ -162,14 +162,15 @@ enum DisplayStatus {
}
model Display {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade)
surveyId String
person Person? @relation(fields: [personId], references: [id], onDelete: Cascade)
personId String?
status DisplayStatus @default(seen)
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade)
surveyId String
person Person? @relation(fields: [personId], references: [id], onDelete: Cascade)
personId String?
responseId String? @unique
status DisplayStatus @default(seen)
}
model SurveyTrigger {
+21
View File
@@ -6,6 +6,7 @@ export const createResponse = async (responseInput: TResponseInput, apiHost: str
headers: { "Content-Type": "application/json" },
body: JSON.stringify(responseInput),
});
console.log(res);
if (!res.ok) {
console.error(res.text);
throw new Error("Could not create response");
@@ -19,14 +20,34 @@ export const updateResponse = async (
responseId: string,
apiHost: string
): Promise<TResponse> => {
console.log(responseInput);
const res = await fetch(`${apiHost}/api/v1/client/responses/${responseId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(responseInput),
});
console.log(res);
if (!res.ok) {
throw new Error("Could not update response");
}
const resJson = await res.json();
return resJson.data;
};
export const updateDisplay = async (
displayId: string,
displayInput: any,
apiHost: string
): Promise<TResponse> => {
const res = await fetch(`${apiHost}/api/v1/client/displays/${displayId}/responded`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(displayInput),
});
console.log(res);
if (!res.ok) {
throw new Error("Could not update display");
}
const resJson = await res.json();
return resJson.data;
};
+4 -1
View File
@@ -1,5 +1,5 @@
import { TResponseUpdate } from "@formbricks/types/v1/responses";
import { createResponse, updateResponse } from "./client/response";
import { createResponse, updateResponse, updateDisplay } from "./client/response";
import SurveyState from "./surveyState";
interface QueueConfig {
@@ -74,6 +74,9 @@ export class ResponseQueue {
{ ...responseUpdate, surveyId: this.surveyState.surveyId, personId: this.config.personId || null },
this.config.apiHost
);
if (responseUpdate.displayId) {
await updateDisplay(responseUpdate.displayId, { responseId: response.id }, this.config.apiHost);
}
this.surveyState.updateResponseId(response.id);
if (this.config.setSurveyState) {
this.config.setSurveyState(this.surveyState);
+45
View File
@@ -38,6 +38,31 @@ const selectDisplay = {
status: true,
};
export const updateDisplay = async (
displayId: string,
displayInput: Partial<TDisplayInput>
): Promise<TDisplay> => {
// validateInputs([displayInput, ZDisplayInput]);
try {
console.log(displayId);
const updateDisplay = await prisma.display.update({
where: {
id: displayId,
},
data: displayInput,
});
console.log(updateDisplay);
return updateDisplay;
} catch (error) {
console.log(error);
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
};
export const createDisplay = async (displayInput: TDisplayInput): Promise<TDisplay> => {
validateInputs([displayInput, ZDisplayInput]);
try {
@@ -165,3 +190,23 @@ export const getDisplaysOfPerson = cache(
}
}
);
export const deleteDisplayByResponseId = async (responseId: string): Promise<void> => {
validateInputs([responseId, ZId]);
try {
await prisma.display.delete({
where: {
responseId,
},
});
// revalidate person
revalidateTag(responseId);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
};
+1
View File
@@ -7,6 +7,7 @@ export const ZDisplay = z.object({
updatedAt: z.date(),
surveyId: z.string().cuid2(),
person: ZPerson.nullable(),
responseId: z.string().cuid2(),
status: z.enum(["seen", "responded"]),
});