Skip to content

Commit 8a9b4ad

Browse files
0xdevcollinsclaude
andcommitted
fix(submissions): tighten Zod schema + surface backend debug info
- Add max constraints that previously only existed on the backend DTO so validation fires inline (projectName 100, description 5000, URL 500). - ApiErrorField gains an optional `debug` field that the backend Prisma filter populates outside production. - useSubmission's error formatter prefers `debug` over the generic field message when present, so toasts show the real Prisma reason behind "Data validation failed" instead of a blank "validation: …" line. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bf06ced commit 8a9b4ad

3 files changed

Lines changed: 27 additions & 9 deletions

File tree

components/hackathons/submissions/SubmissionForm.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,15 @@ const LICENSE_OPTIONS = [
9595
type License = (typeof LICENSE_OPTIONS)[number];
9696

9797
const baseSubmissionSchema = z.object({
98-
projectName: z.string().min(3, 'Project name must be at least 3 characters'),
98+
projectName: z
99+
.string()
100+
.min(3, 'Project name must be at least 3 characters')
101+
.max(100, 'Project name cannot exceed 100 characters'),
99102
category: z.string().min(1, 'Please select a category'),
100-
description: z.string().min(50, 'Description must be at least 50 characters'),
103+
description: z
104+
.string()
105+
.min(50, 'Description must be at least 50 characters')
106+
.max(5000, 'Description cannot exceed 5000 characters'),
101107
logo: z.string().optional(),
102108
banner: z.string().optional(),
103109
videoUrl: z
@@ -110,7 +116,13 @@ const baseSubmissionSchema = z.object({
110116
links: z.array(
111117
z.object({
112118
type: z.string(),
113-
url: z.union([z.string().url('Please enter a valid URL'), z.literal('')]),
119+
url: z.union([
120+
z
121+
.string()
122+
.url('Please enter a valid URL')
123+
.max(500, 'URL cannot exceed 500 characters'),
124+
z.literal(''),
125+
]),
114126
})
115127
),
116128
participationType: z.enum(['INDIVIDUAL', 'TEAM']),

hooks/hackathon/use-submission.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,16 @@ import { reportError } from '@/lib/error-reporting';
1717
function getApiErrorMessage(err: unknown, fallback: string): string {
1818
const apiErr = err as ApiError | undefined;
1919
if (apiErr && typeof apiErr.message === 'string' && apiErr.message) {
20-
const firstField =
21-
Array.isArray(apiErr.errors) && apiErr.errors.length > 0
22-
? apiErr.errors[0].message
23-
: null;
24-
if (firstField && firstField !== apiErr.message) {
25-
return `${apiErr.message}: ${firstField}`;
20+
const first = Array.isArray(apiErr.errors) ? apiErr.errors[0] : undefined;
21+
const fieldMsg = first?.message;
22+
// `debug` is only present outside production; surfaces the real Prisma
23+
// reason when the generic "Data validation failed" fires.
24+
const debug = first?.debug;
25+
if (debug && debug !== apiErr.message) {
26+
return `${apiErr.message}: ${debug}`;
27+
}
28+
if (fieldMsg && fieldMsg !== apiErr.message) {
29+
return `${apiErr.message}: ${fieldMsg}`;
2630
}
2731
return apiErr.message;
2832
}

lib/api/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export interface ApiResponse<T = unknown> {
2626
export interface ApiErrorField {
2727
field?: string;
2828
message: string;
29+
/** Populated by the backend Prisma filter outside production. */
30+
debug?: string;
2931
}
3032

3133
export interface ApiError {

0 commit comments

Comments
 (0)