- {formData.questions.map((question) => {
- // render pie chart for MULTIPLE_CHOICE or DROPDOWN questions
- if (
- question.type === "MULTIPLE_CHOICE" ||
- question.type === "DROPDOWN"
- ) {
- return (
-
- );
- }
-
- // render bar chart for LINEAR_SCALE or NUMBER questions
- if (
- question.type === "LINEAR_SCALE" ||
- question.type === "NUMBER"
- ) {
- return (
-
- );
- }
-
- // render horizontal bar chart for CHECKBOXES questions
- if (question.type === "CHECKBOXES") {
- return (
-
- );
- }
-
- return null;
- })}
-
-
- {/* text responses section - for SHORT_ANSWER and PARAGRAPH questions */}
- {/* renders a separate table for each text-based question */}
-
- {formData.questions.map((question) => {
- // render table for SHORT_ANSWER or PARAGRAPH questions
- if (
- question.type === "SHORT_ANSWER" ||
- question.type === "PARAGRAPH"
- ) {
- return (
-
- );
- }
-
- return null;
- })}
-
- {/* date and time responses section - for DATE and TIME questions */}
- {/* renders a separate table for each date/time question */}
-
- {formData.questions.map((question) => {
- // render table for DATE or TIME questions
- if (question.type === "DATE" || question.type === "TIME") {
- return (
-
- );
- }
-
- return null;
- })}
+
+
+
+ All Responses
+ Per User
+
- >
+
+
+
+
+
+
+
)}
diff --git a/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx b/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx
index 2a1a2b360..3db8951b0 100644
--- a/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx
+++ b/apps/blade/src/app/forms/[formName]/_components/form-responder-client.tsx
@@ -3,6 +3,7 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { CheckCircle2, Loader2, XCircle } from "lucide-react";
+import { z } from "zod";
import { Button } from "@forge/ui/button";
import { Card } from "@forge/ui/card";
@@ -11,6 +12,9 @@ import { InstructionResponseCard } from "~/app/forms/[formName]/_components/inst
import { QuestionResponseCard } from "~/app/forms/[formName]/_components/question-response-card";
import { api } from "~/trpc/react";
+const emailSchema = z.string().email("Invalid email address");
+const phoneSchema = z.string().regex(/^\+?\d{7,15}$/, "Invalid phone number");
+
interface FormResponderClientProps {
formName: string;
userName: string;
@@ -26,6 +30,7 @@ export function FormResponderClient({
const [responses, setResponses] = useState<
Record
>({});
+ const [touchedFields, setTouchedFields] = useState>(new Set());
const [isSubmitted, setIsSubmitted] = useState(false);
const [showCheckmark, setShowCheckmark] = useState(false);
const [showText, setShowText] = useState(false);
@@ -173,6 +178,10 @@ export function FormResponderClient({
}));
};
+ const handleFieldBlur = (questionText: string) => {
+ setTouchedFields((prev) => new Set(prev).add(questionText));
+ };
+
const handleSubmit = () => {
// Build response data object
const responseData: Record = {};
@@ -210,15 +219,78 @@ export function FormResponderClient({
handleCallbacks(responseData);
};
+ const getValidationError = (question: (typeof form.questions)[number]) => {
+ if (!touchedFields.has(question.question)) {
+ return null;
+ }
+
+ const response = responses[question.question];
+
+ if (question.optional) {
+ if (
+ !response ||
+ response === "" ||
+ (Array.isArray(response) && response.length === 0)
+ ) {
+ return null;
+ }
+ } else {
+ if (response === null || response === undefined || response === "") {
+ return null;
+ }
+ if (Array.isArray(response) && response.length === 0) return null;
+ }
+
+ if (question.type === "EMAIL" && typeof response === "string") {
+ const result = emailSchema.safeParse(response);
+ if (!result.success) {
+ return "Please enter a valid email address";
+ }
+ }
+ if (question.type === "PHONE" && typeof response === "string") {
+ const result = phoneSchema.safeParse(response);
+ if (!result.success) {
+ return "Please enter a valid phone number (7-15 digits, optional + prefix)";
+ }
+ }
+
+ return null;
+ };
+
const isFormValid = () => {
// Check if all required questions have responses
return form.questions.every((question) => {
- if (question.optional) return true; // Optional questions don't need validation
+ if (question.optional) {
+ const response = responses[question.question];
+ if (
+ !response ||
+ response === "" ||
+ (Array.isArray(response) && response.length === 0)
+ ) {
+ return true;
+ }
+
+ if (question.type === "EMAIL" && typeof response === "string") {
+ return emailSchema.safeParse(response).success;
+ }
+ if (question.type === "PHONE" && typeof response === "string") {
+ return phoneSchema.safeParse(response).success;
+ }
+ return true;
+ }
const response = responses[question.question];
if (response === null || response === undefined || response === "")
return false;
if (Array.isArray(response) && response.length === 0) return false;
+
+ if (question.type === "EMAIL" && typeof response === "string") {
+ return emailSchema.safeParse(response).success;
+ }
+ if (question.type === "PHONE" && typeof response === "string") {
+ return phoneSchema.safeParse(response).success;
+ }
+
return true;
});
};
@@ -299,6 +371,9 @@ export function FormResponderClient({
) => {
handleResponseChange(item.question, value);
}}
+ onBlur={() => handleFieldBlur(item.question)}
+ formId={formQuery.data.id}
+ error={getValidationError(item)}
/>
)}
diff --git a/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx b/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx
index d4d121516..bca4ab517 100644
--- a/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx
+++ b/apps/blade/src/app/forms/[formName]/_components/question-response-card.tsx
@@ -2,9 +2,12 @@
import type { z } from "zod";
import * as React from "react";
+import { useRef, useState } from "react";
import Image from "next/image";
+import { FileUp, Loader2, X } from "lucide-react";
import type { QuestionValidator } from "@forge/consts/knight-hacks";
+import { Button } from "@forge/ui/button";
import { Card } from "@forge/ui/card";
import { Checkbox } from "@forge/ui/checkbox";
import { DatePicker } from "@forge/ui/date-picker";
@@ -18,7 +21,11 @@ import {
SelectTrigger,
SelectValue,
} from "@forge/ui/select";
+import { Slider } from "@forge/ui/slider";
import { TimePicker } from "@forge/ui/time-picker";
+import { toast } from "@forge/ui/toast";
+
+import { api } from "~/trpc/react";
type FormQuestion = z.infer