mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-28 17:40:45 -05:00
redesigned metadata
This commit is contained in:
+62
-84
@@ -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'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);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
+6
@@ -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");
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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"]),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user