Skip to content

Commit d68c9f3

Browse files
committed
feat: update GrantTranche model to use aiReceipts as JSON and adjust related logic
1 parent d5ef0d1 commit d68c9f3

5 files changed

Lines changed: 48 additions & 42 deletions

File tree

prisma/schema.prisma

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ model GrantTranche {
340340
socialPost String? @db.VarChar(500)
341341
colosseumLink String? @db.VarChar(500)
342342
githubRepo String? @db.VarChar(500)
343-
aiReceipt String? @db.VarChar(500)
343+
aiReceipts Json?
344344
345345
@@index([applicationId])
346346
@@index([grantId])

src/app/api/grant-application/request-tranche/route.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export async function POST(request: Request) {
3333
colosseumLink,
3434
githubRepo,
3535
aiReceipt,
36+
aiReceipts,
3637
} = body;
3738

3839
logger.debug(`Request body: ${safeStringify(body)}`);
@@ -67,7 +68,12 @@ export async function POST(request: Request) {
6768
socialPost,
6869
colosseumLink,
6970
githubRepo,
70-
aiReceipt,
71+
aiReceipts:
72+
Array.isArray(aiReceipts) && aiReceipts.length > 0
73+
? aiReceipts
74+
: typeof aiReceipt === 'string' && aiReceipt.trim()
75+
? [aiReceipt]
76+
: undefined,
7177
});
7278
},
7379
{ ttlSeconds: 300 },

src/features/grants/components/Modals/TrancheFormModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -405,9 +405,9 @@ export const TrancheFormModal = ({ grant, applicationId, onClose }: Props) => {
405405
const path = extractArenaColosseumPath(value);
406406
field.onChange(
407407
path
408-
? `${COLOSSEUM_ARENA_PREFIX}/${path}`
408+
? `${COLOSSEUM_ARENA_PREFIX}${path}`
409409
: value
410-
? `${COLOSSEUM_ARENA_PREFIX}/${value.replace(/^\/+/, '')}`
410+
? `${COLOSSEUM_ARENA_PREFIX}${value.replace(/^\/+/, '')}`
411411
: '',
412412
);
413413
}}

src/features/grants/utils/createTranche.ts

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { isAgenticEngineeringGrant } from './stGrant';
88
const CLOUDINARY_HOST = 'res.cloudinary.com';
99
const MAX_EVENT_PICTURES = 5;
1010
const MAX_EVENT_RECEIPTS = 10;
11+
const MAX_AGENTIC_RECEIPTS = 3;
1112

1213
const parseHttpUrl = (value: string): URL | null => {
1314
try {
@@ -30,25 +31,39 @@ const isCloudinaryUrl = (value: string) => {
3031

3132
const normalizeImageUrls = (
3233
value: unknown,
33-
{ label, required, max }: { label: string; required: boolean; max: number },
34+
{
35+
label,
36+
required,
37+
max,
38+
itemType = 'files',
39+
requiredMessage,
40+
}: {
41+
label: string;
42+
required: boolean;
43+
max: number;
44+
itemType?: 'files' | 'images';
45+
requiredMessage?: string;
46+
},
3447
): string[] | undefined => {
3548
if (value === undefined || value === null) {
3649
if (required) {
37-
throw new Error(`${label} are required.`);
50+
throw new Error(requiredMessage ?? `${label} are required.`);
3851
}
3952
return undefined;
4053
}
4154
if (!Array.isArray(value)) {
42-
throw new Error(`${label} must be a list of image URLs.`);
55+
throw new Error(`${label} must be a list of ${itemType} URLs.`);
4356
}
4457
if (value.length === 0) {
4558
if (required) {
46-
throw new Error(`At least one ${label.toLowerCase()} is required.`);
59+
throw new Error(
60+
requiredMessage ?? `At least one ${label.toLowerCase()} is required.`,
61+
);
4762
}
4863
return undefined;
4964
}
5065
if (value.length > max) {
51-
throw new Error(`${label} must be ${max} or fewer images.`);
66+
throw new Error(`${label} must be ${max} or fewer ${itemType}.`);
5267
}
5368
return value.map((entry) => {
5469
if (typeof entry !== 'string') {
@@ -104,29 +119,6 @@ const normalizeAttendeeCount = (
104119
return parsed;
105120
};
106121

107-
const normalizeSingleCloudinaryFile = (
108-
value: unknown,
109-
{ label, required }: { label: string; required: boolean },
110-
): string | undefined => {
111-
if (value === undefined || value === null || value === '') {
112-
if (required) {
113-
throw new Error(`${label} is required.`);
114-
}
115-
return undefined;
116-
}
117-
118-
if (typeof value !== 'string') {
119-
throw new Error(`${label} must be a valid file URL.`);
120-
}
121-
122-
const trimmed = value.trim();
123-
if (!isCloudinaryUrl(trimmed)) {
124-
throw new Error(`${label} must be a Cloudinary URL.`);
125-
}
126-
127-
return trimmed;
128-
};
129-
130122
const normalizeSpecificHostUrl = (
131123
value: unknown,
132124
{
@@ -170,7 +162,10 @@ const normalizeSpecificHostUrl = (
170162
throw new Error(`${label} must be a valid ${host} URL.`);
171163
}
172164

173-
return parsed.toString();
165+
return new URL(
166+
`${pathSegments.join('/')}${parsed.search}${parsed.hash}`,
167+
`https://${host}/`,
168+
).toString();
174169
};
175170

176171
type CreateTrancheProps = {
@@ -185,7 +180,7 @@ type CreateTrancheProps = {
185180
socialPost?: string;
186181
colosseumLink?: string;
187182
githubRepo?: string;
188-
aiReceipt?: string;
183+
aiReceipts?: string[];
189184
};
190185

191186
export async function createTranche({
@@ -200,7 +195,7 @@ export async function createTranche({
200195
socialPost,
201196
colosseumLink,
202197
githubRepo,
203-
aiReceipt,
198+
aiReceipts,
204199
}: CreateTrancheProps) {
205200
const application = await prisma.grantApplication.findUniqueOrThrow({
206201
where: { id: applicationId },
@@ -277,11 +272,14 @@ export async function createTranche({
277272
label: 'Event pictures',
278273
required: requiresEventProof,
279274
max: MAX_EVENT_PICTURES,
275+
itemType: 'images',
276+
requiredMessage: 'At least one event picture is required.',
280277
});
281278
const normalizedEventReceipts = normalizeImageUrls(eventReceipts, {
282279
label: 'Event receipts',
283280
required: requiresEventProof,
284281
max: MAX_EVENT_RECEIPTS,
282+
requiredMessage: 'At least one event receipt is required.',
285283
});
286284
const normalizedAttendeeCount = normalizeAttendeeCount(
287285
attendeeCount,
@@ -303,9 +301,11 @@ export async function createTranche({
303301
host: 'github.com',
304302
minPathSegments: 2,
305303
});
306-
const normalizedAiReceipt = normalizeSingleCloudinaryFile(aiReceipt, {
307-
label: 'AI subscription receipt',
304+
const normalizedAiReceipts = normalizeImageUrls(aiReceipts, {
305+
label: 'AI subscription receipts',
308306
required: requiresAgenticFinalProof,
307+
max: MAX_AGENTIC_RECEIPTS,
308+
requiredMessage: 'AI subscription receipt is required.',
309309
});
310310

311311
let trancheAmount = 0;
@@ -388,7 +388,7 @@ export async function createTranche({
388388
colosseumLink: normalizedColosseumLink,
389389
}),
390390
...(normalizedGithubRepo && { githubRepo: normalizedGithubRepo }),
391-
...(normalizedAiReceipt && { aiReceipt: normalizedAiReceipt }),
391+
...(normalizedAiReceipts && { aiReceipts: normalizedAiReceipts }),
392392
},
393393
include: {
394394
GrantApplication: {

src/features/grants/utils/stGrant.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -475,20 +475,20 @@ export const AGENTIC_ENGINEERING_GRANT_COPY: {
475475
description:
476476
'Share your Colosseum project link, GitHub repository, AI subscription receipt, payout wallet, and anything else that will help the sponsor review the final tranche.',
477477
colosseumLink: {
478-
label: "Link to your project's Colosseum link",
478+
label: "Link to your project's Colosseum profile",
479479
description: 'Paste the path after the Colosseum Arena host.',
480-
placeholder: 'projects/your-project',
480+
placeholder: 'projects/explore/your-project',
481481
},
482482
githubRepo: {
483483
label: 'Link to the Github Repo',
484484
description:
485-
'If the repo is private, share access with <>@superteam.fun.',
485+
'If the repo is private, share access with abhwshek@gmail.com.',
486486
placeholder: 'owner/repo',
487487
},
488488
aiReceipt: {
489489
label: 'Upload your AI Subscription Receipt',
490490
description:
491-
'The receipt should mention your name or entity. Upload a PDF or PNG.',
491+
'The receipt should mention your name or entity. Upload up to 3 PDFs or PNGs.',
492492
},
493493
walletAddress: {
494494
label: 'Solana Wallet Address',

0 commit comments

Comments
 (0)