);
}
@@ -116,7 +188,15 @@ export default function FormResponsesPage() {
const fields = (form.schema as { fields?: FormFieldConfig[] }).fields || [];
const responses = responsesData.responses;
- const isQuiz = !!(form.schema as { isQuiz?: boolean }).isQuiz;
+ const isQuiz =
+ !!(form.schema as { isQuiz?: boolean; is_quiz?: boolean }).isQuiz ||
+ !!(form.schema as { isQuiz?: boolean; is_quiz?: boolean }).is_quiz ||
+ fields.some(
+ (f) =>
+ f.correctAnswer !== undefined &&
+ f.correctAnswer !== null &&
+ f.correctAnswer !== "",
+ );
const handleDeleteResponse = async (responseId: number) => {
setDeleteConfirmId(null);
@@ -128,8 +208,10 @@ export default function FormResponsesPage() {
count: responsesData!.count - 1,
responses: responsesData!.responses.filter((r) => r.id !== responseId),
});
+ showToast("Response deleted", "info");
} catch {
setErrorMessage("Failed to delete response. Please try again.");
+ showToast("Failed to delete response. Please try again.", "error");
} finally {
setDeleting(null);
}
@@ -247,9 +329,15 @@ export default function FormResponsesPage() {
})}
{isQuiz &&
(() => {
+ const metaQuizResult = (
+ response.meta as Record
| undefined
+ )?.quiz_result as
+ | { score: number; maxScore: number }
+ | undefined;
const { score, maxScore } = computeQuizScore(
fields,
response.data,
+ response.quiz_result || metaQuizResult,
);
return (
diff --git a/src/pages/PublicFormPage.tsx b/src/pages/PublicFormPage.tsx
index 6a9a8ae..2e3ae3b 100644
--- a/src/pages/PublicFormPage.tsx
+++ b/src/pages/PublicFormPage.tsx
@@ -2,7 +2,10 @@ import { useEffect, useState, useCallback, useMemo } from "react";
import { useParams } from "react-router-dom";
import { submitResponse, getPublicForm } from "../services/api";
import type { FormResponse } from "../services/apiTypes";
-import type { FormFieldConfig, ThankYouScreenConfig } from "../components/BuilderCore/shared/types";
+import type {
+ FormFieldConfig,
+ ThankYouScreenConfig,
+} from "../components/BuilderCore/shared/types";
import FlowPage from "../layouts/FlowPage";
import SinglePage from "../layouts/SinglePage";
import Modal from "../components/common/Modal";
@@ -66,17 +69,22 @@ export default function PublicFormPage() {
{} as Record,
);
- const structuredAnswers = Object.entries(answers).reduce(
- (acc, [fieldId, value]) => {
- const title = fieldIdToTitle[fieldId] || fieldId;
- acc[title] = value;
- return acc;
- },
- {} as Record,
- );
+ const structuredAnswers: Record = {};
+ Object.entries(answers).forEach(([fieldId, value]) => {
+ structuredAnswers[fieldId] = value;
+ const title = fieldIdToTitle[fieldId];
+ if (title && title !== fieldId) {
+ structuredAnswers[title] = value;
+ }
+ });
try {
- await submitResponse(form.id, structuredAnswers as Record, {});
+ const submission = await submitResponse(
+ form.id,
+ structuredAnswers as Record,
+ {},
+ );
+ return submission;
} catch (err) {
setAlertMessage("Failed to submit form. Please try again.");
throw err;
@@ -85,19 +93,32 @@ export default function PublicFormPage() {
[form, formId],
);
- const { fields, type, schema } = useMemo(() => {
+ const { fields, type, schema, isQuiz } = useMemo(() => {
if (!form || !form.schema) {
return {
fields: [],
type: "single" as const,
schema: {} as Record,
+ isQuiz: false,
};
}
const s = form.schema as Record;
+ const formFields = (s.fields as FormFieldConfig[]) || [];
+ const quizFlag = Boolean(
+ s.isQuiz ||
+ s.is_quiz ||
+ formFields.some(
+ (field) =>
+ field.correctAnswer !== undefined &&
+ field.correctAnswer !== null &&
+ field.correctAnswer !== "",
+ ),
+ );
return {
- fields: (s.fields as FormFieldConfig[]) || [],
+ fields: formFields,
type: (s.type as "single" | "flow") || "single",
schema: s,
+ isQuiz: quizFlag,
};
}, [form]);
@@ -160,6 +181,7 @@ export default function PublicFormPage() {
}
onSubmit={handleSubmit}
formId={form.id}
+ isQuiz={isQuiz}
/>
,
meta: Record = {},
-): Promise => {
- await api.post(`/forms/${formId}/responses`, { data: answers, meta });
+): Promise => {
+ const response = await api.post(
+ `/forms/${formId}/responses`,
+ { data: answers, meta },
+ );
+ return response.data;
};
export const uploadFile = async (
diff --git a/src/services/apiTypes.ts b/src/services/apiTypes.ts
index efa6342..331ac5c 100644
--- a/src/services/apiTypes.ts
+++ b/src/services/apiTypes.ts
@@ -1,3 +1,5 @@
+import type { QuizResult } from "../components/BuilderCore/shared/types";
+
export interface CreateFormRequest {
name: string;
description?: string;
@@ -36,6 +38,7 @@ export interface FormSubmission {
data: Record;
meta: Record;
created_at: string;
+ quiz_result?: QuizResult;
}
export interface FormResponsesResult {
diff --git a/vercel.json b/vercel.json
index 1323cda..d0321e7 100644
--- a/vercel.json
+++ b/vercel.json
@@ -4,5 +4,37 @@
"source": "/(.*)",
"destination": "/index.html"
}
+ ],
+ "headers": [
+ {
+ "source": "/(dashboard|builder|forms/.*/responses)(.*)",
+ "headers": [
+ {
+ "key": "X-Frame-Options",
+ "value": "DENY"
+ },
+ {
+ "key": "Content-Security-Policy",
+ "value": "frame-ancestors 'none';"
+ }
+ ]
+ },
+ {
+ "source": "/(.*)",
+ "headers": [
+ {
+ "key": "X-Content-Type-Options",
+ "value": "nosniff"
+ },
+ {
+ "key": "Referrer-Policy",
+ "value": "strict-origin-when-cross-origin"
+ },
+ {
+ "key": "Permissions-Policy",
+ "value": "camera=(), microphone=(), geolocation=()"
+ }
+ ]
+ }
]
}
|