diff --git a/backend/src/routes/projects.ts b/backend/src/routes/projects.ts index 0465805..197829b 100644 --- a/backend/src/routes/projects.ts +++ b/backend/src/routes/projects.ts @@ -491,6 +491,30 @@ projects.get("/:id", async ({ params, headers }) => { hasSubmittedFeedback = feedbackCheck.length > 0; } + // Check if project can be resubmitted (first submission must be on or before March 17th, 2026) + let canResubmit = true; + if (isOwner) { + const firstSubmission = await db + .select({ createdAt: projectActivityTable.createdAt }) + .from(projectActivityTable) + .where( + and( + eq(projectActivityTable.projectId, parseInt(params.id)), + eq(projectActivityTable.action, "project_submitted"), + ), + ) + .orderBy(projectActivityTable.createdAt) + .limit(1); + + if (firstSubmission.length > 0) { + const cutoffDate = new Date("2026-03-18T00:00:00Z"); + canResubmit = firstSubmission[0].createdAt < cutoffDate; + } else { + // Never submitted before - cannot submit new projects + canResubmit = false; + } + } + // Calculate effective hours (subtract overlapping shipped project hours) // Uses activity-derived shipped dates for ordering (consistent with Airtable sync and admin review) const projectHours = project[0].hoursOverride ?? project[0].hours ?? 0; @@ -533,6 +557,7 @@ projects.get("/:id", async ({ params, headers }) => { owner: projectOwner[0] || null, isOwner, hasSubmittedFeedback: isOwner ? hasSubmittedFeedback : undefined, + canResubmit: isOwner ? canResubmit : undefined, activity, }; }); @@ -840,6 +865,27 @@ projects.post("/:id/submit", async ({ params, headers, body }) => { return { error: "Project cannot be submitted in current status" }; } + // Check if this is a resubmission - if so, verify first submission was on or before March 17th + const firstSubmission = await db + .select({ createdAt: projectActivityTable.createdAt }) + .from(projectActivityTable) + .where( + and( + eq(projectActivityTable.projectId, parseInt(params.id)), + eq(projectActivityTable.action, "project_submitted"), + ), + ) + .orderBy(projectActivityTable.createdAt) + .limit(1); + + if (firstSubmission.length > 0) { + // This is a resubmission - check if first submission was on or before March 17th, 2026 + const cutoffDate = new Date("2026-03-18T00:00:00Z"); // Midnight March 18th = end of March 17th + if (firstSubmission[0].createdAt >= cutoffDate) { + return { error: "Resubmissions are only allowed for projects first submitted on or before March 17th" }; + } + } + // Sync hours from Hackatime before submitting if (project[0].hackatimeProject) { await syncSingleProject(parseInt(params.id)); diff --git a/frontend/src/routes/projects/[id]/+page.svelte b/frontend/src/routes/projects/[id]/+page.svelte index 62374e5..4ae0374 100644 --- a/frontend/src/routes/projects/[id]/+page.svelte +++ b/frontend/src/routes/projects/[id]/+page.svelte @@ -74,6 +74,7 @@ let owner = $state(null); let isOwner = $state(false); let isAdmin = $state(false); + let canResubmit = $state(true); let activity = $state([]); let loading = $state(true); let error = $state(null); @@ -104,6 +105,7 @@ project = result.project; owner = result.owner; isOwner = result.isOwner; + canResubmit = result.canResubmit ?? true; activity = result.activity || []; } catch (e) { error = e instanceof Error ? e.message : 'Failed to load project'; @@ -454,7 +456,7 @@ {$t.project.awaitingReview} - {:else if project.status === 'shipped'} + {:else if project.status === 'shipped' && canResubmit} ship update + {:else if project.status === 'shipped' && !canResubmit} + + + shipped + {:else if project.status === 'permanently_rejected'} {$t.project.permanentlyRejected} + {:else if !canResubmit} + + + submissions closed + {:else if $tutorialActiveStore}