Skip to content

Commit a5db3c4

Browse files
authored
fix(submissions): unbreak submission detail page + publish hackathon recap (#586)
* fix(submissions): wire submission entity type through votes, comments, and rank chip The submission detail page (/projects/[slug]?type=submission) was treating submissions as crowdfunding projects, which broke every interactive surface for hackathon entries: - Sidebar getVoteCounts and the voters tab's getProjectVotes were hardcoded to CROWDFUNDING_CAMPAIGN, so the lookup failed with "Failed to load voting data" and disabled the buttons. - createVote on the voters tab was also hardcoded, so even a click through hit Prisma's "Invalid reference to related record" (P2025) for non-existent campaign rows. - ProjectComments used CommentEntityType.PROJECT, so comments and replies posted against a non-existent project record. - The Follow button hardcoded entityType='PROJECT' with the submission's id, leading to "Failed to update follow status". The follow EntityType enum has no HACKATHON_SUBMISSION value, so the button is hidden on submission pages. - The status badge displayed raw enum values like "SHORTLISTED" and there was no signal that a submission was a winner. All vote/comment surfaces now derive the entity type from the URL, matching the backend's HACKATHON_SUBMISSION enum on both votes and comments. The page mapper surfaces submissionRank and a friendly submissionStatus on the project, and the sidebar header renders a "Winner" / "Rank #N" chip next to the existing status badge. * fix(blog): use object-cover so card and detail covers fill the frame The earlier switch back to object-contain left letterbox bands around banners that aren't an exact 2:1 ratio on both the BlogCard grid and the post-details hero. With properly-sized cover banners, object-cover fills the frame without visible cropping. Authors should size cover images for a 2:1 ratio on the card and the responsive heights on the details page. * feat(blog): publish Boundless x Trustless Work hackathon winners recap Adds the post-hackathon recap announcing the three main winners (Conductor, Crypt, GoPadi), the five honorable mentions, and the Showcase recognitions from the May 16 finale. Featured on the blog index. * chore(deps): npm audit fix to clear js-cookie high-severity advisory GHSA-qjx8-664m-686j (js-cookie per-instance prototype hijack in assign()) was newly flagged as high severity. The pre-push hook runs `npm audit --audit-level=high` and refused to push any branch until this was patched. Lockfile-only update, no package.json changes, semver-compatible. Bundled here because the gate was blocking the parent PR; reviewers can treat this as an unrelated chore commit.
1 parent 3fb4121 commit a5db3c4

12 files changed

Lines changed: 962 additions & 873 deletions

File tree

app/(landing)/projects/[slug]/page.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,29 @@ function mapSubmissionToCrowdfunding(
273273

274274
const projectId = subData.id || subData._id || '';
275275

276+
// Map raw submission status to a friendly display label so the sidebar
277+
// badge reads "Shortlisted" instead of "SHORTLISTED". The raw value is
278+
// also preserved on submissionStatus so anything that needs the original
279+
// enum value can still get it.
280+
const submissionRank: number | null =
281+
typeof subData.rank === 'number' ? subData.rank : null;
282+
const rawSubmissionStatus: string =
283+
typeof subData.status === 'string' ? subData.status : '';
284+
const friendlySubmissionStatus = (() => {
285+
switch (rawSubmissionStatus.toUpperCase()) {
286+
case 'SUBMITTED':
287+
return 'Submitted';
288+
case 'SHORTLISTED':
289+
return 'Shortlisted';
290+
case 'DISQUALIFIED':
291+
return 'Disqualified';
292+
case 'WITHDRAWN':
293+
return 'Withdrawn';
294+
default:
295+
return rawSubmissionStatus || 'pending';
296+
}
297+
})();
298+
276299
// Find demo video in links if not provided directly
277300
let demoVideoUrl = subData.videoUrl || '';
278301
if (!demoVideoUrl && socialLinks.length > 0) {
@@ -320,7 +343,9 @@ function mapSubmissionToCrowdfunding(
320343
vision: null,
321344
details: null,
322345
category: subData.category || 'General',
323-
status: subData.status || 'pending',
346+
status: friendlySubmissionStatus,
347+
submissionRank,
348+
submissionStatus: rawSubmissionStatus || null,
324349
creatorId: subData.participantId || subData.userId || '',
325350
organizationId: subData.organizationId || null,
326351
teamMembers: teamMembers,

components/landing-page/blog/BlogCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const BlogCard = ({ post, onCardClick }: BlogCardProps) => {
2424
src={post.coverImage}
2525
alt={post.title}
2626
fill
27-
className='object-contain transition-transform duration-500 group-hover:scale-105'
27+
className='object-cover transition-transform duration-500 group-hover:scale-105'
2828
sizes='(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw'
2929
/>
3030
{/* Gradient Overlay */}

components/landing-page/blog/BlogPostDetails.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ const BlogPostDetails: React.FC<BlogPostDetailsProps> = ({
140140
src={post.coverImage}
141141
alt={post.title}
142142
fill
143-
className='rounded-lg object-contain'
143+
className='rounded-lg object-cover'
144144
priority
145145
sizes='(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 70vw'
146146
/>

components/project-details/comment-section/project-comments.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22

33
import { useState } from 'react';
4+
import { useSearchParams } from 'next/navigation';
45
import { useOptionalAuth } from '@/hooks/use-auth';
56
import { useCommentSystem } from '@/hooks/use-comment-system';
67
import { useCommentRealtime } from '@/hooks/use-comment-realtime';
@@ -17,6 +18,11 @@ interface ProjectCommentsProps {
1718

1819
export function ProjectComments({ projectId }: ProjectCommentsProps) {
1920
const { user } = useOptionalAuth();
21+
const searchParams = useSearchParams();
22+
const entityType =
23+
searchParams.get('type') === 'submission'
24+
? CommentEntityType.HACKATHON_SUBMISSION
25+
: CommentEntityType.PROJECT;
2026
const [sortBy, setSortBy] = useState<
2127
'createdAt' | 'updatedAt' | 'totalReactions'
2228
>('createdAt');
@@ -28,7 +34,7 @@ export function ProjectComments({ projectId }: ProjectCommentsProps) {
2834

2935
// Initialize the comment system for this project
3036
const commentSystem = useCommentSystem({
31-
entityType: CommentEntityType.PROJECT,
37+
entityType,
3238
entityId: projectId,
3339
page: 1,
3440
limit: 20,
@@ -38,7 +44,7 @@ export function ProjectComments({ projectId }: ProjectCommentsProps) {
3844
// Real-time updates
3945
useCommentRealtime(
4046
{
41-
entityType: CommentEntityType.PROJECT,
47+
entityType,
4248
entityId: projectId,
4349
userId: currentUserId,
4450
enabled: true,
@@ -71,7 +77,7 @@ export function ProjectComments({ projectId }: ProjectCommentsProps) {
7177
try {
7278
await commentSystem.createComment.createComment({
7379
content,
74-
entityType: CommentEntityType.PROJECT,
80+
entityType,
7581
entityId: projectId,
7682
});
7783
} catch (error) {
@@ -84,7 +90,7 @@ export function ProjectComments({ projectId }: ProjectCommentsProps) {
8490
await commentSystem.createComment.createComment({
8591
content,
8692
parentId: parentCommentId,
87-
entityType: CommentEntityType.PROJECT,
93+
entityType,
8894
entityId: projectId,
8995
} as any);
9096
} catch (error) {

components/project-details/project-sidebar/ProjectSidebarActions.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22

33
import { useState } from 'react';
4+
import { useSearchParams } from 'next/navigation';
45
import {
56
ArrowUp,
67
DollarSign,
@@ -24,6 +25,8 @@ export function ProjectSidebarActions({
2425
crowdfund,
2526
}: ProjectSidebarActionsProps) {
2627
const [isSharePopupOpen, setIsSharePopupOpen] = useState(false);
28+
const searchParams = useSearchParams();
29+
const isSubmission = searchParams.get('type') === 'submission';
2730

2831
const handleShareClick = () => {
2932
setIsSharePopupOpen(true);
@@ -109,13 +112,15 @@ export function ProjectSidebarActions({
109112
</BoundlessButton>
110113
)}
111114

112-
<div className='flex-1'>
113-
<FollowButton
114-
entityType='PROJECT'
115-
entityId={project.id}
116-
className='h-12 w-full'
117-
/>
118-
</div>
115+
{!isSubmission && (
116+
<div className='flex-1'>
117+
<FollowButton
118+
entityType='PROJECT'
119+
entityId={project.id}
120+
className='h-12 w-full'
121+
/>
122+
</div>
123+
)}
119124

120125
<div className='relative'>
121126
<BoundlessButton

components/project-details/project-sidebar/ProjectSidebarHeader.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
'use client';
22

3-
import { Calendar } from 'lucide-react';
3+
import { Calendar, Trophy } from 'lucide-react';
44
import Image from 'next/image';
55
import { ProjectSidebarHeaderProps } from './types';
66

77
export function ProjectSidebarHeader({
88
project,
99
projectStatus,
1010
}: ProjectSidebarHeaderProps) {
11+
const submissionRank = project.submissionRank ?? null;
1112
const getStatusStyles = () => {
1213
switch (projectStatus) {
1314
case 'CAMPAIGNING':
@@ -21,11 +22,16 @@ export function ProjectSidebarHeader({
2122
case 'idea':
2223
return 'bg-warning-75 border-warning-600 text-warning-600';
2324
case 'Validated':
25+
case 'Shortlisted':
2426
return 'bg-success-75 border-success-600 text-success-600';
2527
case 'Rejected':
28+
case 'Disqualified':
2629
return 'bg-red-900/30 border-red-600 text-red-400';
30+
case 'Withdrawn':
31+
return 'bg-gray-800/60 border-gray-700 text-gray-300';
2732
case 'pending':
2833
case 'SUBMITTED':
34+
case 'Submitted':
2935
return 'bg-gray-800 border-gray-700 text-white';
3036
default:
3137
return 'text-white border-gray-700';
@@ -60,6 +66,14 @@ export function ProjectSidebarHeader({
6066
>
6167
{projectStatus}
6268
</div>
69+
{submissionRank != null && (
70+
<div className='flex items-center gap-1 rounded-lg border border-yellow-500/40 bg-yellow-500/10 px-2.5 py-1 text-xs font-semibold text-yellow-400'>
71+
<Trophy className='h-3.5 w-3.5' />
72+
<span>
73+
{submissionRank === 1 ? 'Winner' : `Rank #${submissionRank}`}
74+
</span>
75+
</div>
76+
)}
6377
</div>
6478
</div>
6579
</div>

components/project-details/project-sidebar/index.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function ProjectSidebar({
4545
// Real-time vote updates
4646
useVoteRealtime(
4747
{
48-
entityType: VoteEntityType.CROWDFUNDING_CAMPAIGN,
48+
entityType,
4949
entityId: projectId || '',
5050
enabled: !!projectId,
5151
},
@@ -82,10 +82,7 @@ export function ProjectSidebar({
8282

8383
const fetchVoteCounts = async () => {
8484
try {
85-
const response: any = await getVoteCounts(
86-
projectId,
87-
VoteEntityType.CROWDFUNDING_CAMPAIGN
88-
);
85+
const response: any = await getVoteCounts(projectId, entityType);
8986
// The API returns { success: true, message: '...', data: { ... } }
9087
setVoteCounts(response.data || response);
9188
} catch {
@@ -94,7 +91,7 @@ export function ProjectSidebar({
9491
};
9592

9693
fetchVoteCounts();
97-
}, [projectId]);
94+
}, [projectId, entityType]);
9895

9996
const handleVote = async (value: 1 | -1) => {
10097
if (isVoting) return;

components/project-details/project-sidebar/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,10 @@ export type ProjectStatus =
6060
| 'Funding'
6161
| 'idea'
6262
| 'pending'
63-
| 'SUBMITTED';
63+
| 'SUBMITTED'
64+
// Hackathon-submission display labels emitted by the submission mapper
65+
// in app/(landing)/projects/[slug]/page.tsx.
66+
| 'Submitted'
67+
| 'Shortlisted'
68+
| 'Disqualified'
69+
| 'Withdrawn';

components/project-details/project-voters/index.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useState, useEffect } from 'react';
2+
import { useSearchParams } from 'next/navigation';
23
import Empty from './Empty';
34
import Image from 'next/image';
45
import { CrowdfundingProject, Crowdfunding } from '@/features/projects/types';
@@ -40,6 +41,11 @@ interface ProjectVotersProps {
4041

4142
const ProjectVoters = ({ project, crowdfund }: ProjectVotersProps) => {
4243
const { user } = useOptionalAuth();
44+
const searchParams = useSearchParams();
45+
const isSubmission = searchParams.get('type') === 'submission';
46+
const entityType = isSubmission
47+
? VoteEntityType.HACKATHON_SUBMISSION
48+
: VoteEntityType.CROWDFUNDING_CAMPAIGN;
4349
const [voteStats, setVoteStats] = useState<VoteStats>({
4450
upvotes: 0,
4551
downvotes: 0,
@@ -63,7 +69,7 @@ const ProjectVoters = ({ project, crowdfund }: ProjectVotersProps) => {
6369

6470
useVoteRealtime(
6571
{
66-
entityType: VoteEntityType.CROWDFUNDING_CAMPAIGN,
72+
entityType,
6773
entityId: projectId || '',
6874
enabled: !!projectId,
6975
},
@@ -116,7 +122,7 @@ const ProjectVoters = ({ project, crowdfund }: ProjectVotersProps) => {
116122
const response = await getProjectVotes(projectId, {
117123
limit: 20,
118124
offset: 0,
119-
entityType: VoteEntityType.CROWDFUNDING_CAMPAIGN,
125+
entityType,
120126
includeVoters: true,
121127
});
122128

@@ -137,10 +143,7 @@ const ProjectVoters = ({ project, crowdfund }: ProjectVotersProps) => {
137143
});
138144
setVoters(response.data.voters);
139145
} else {
140-
const countsResponse = await apiGetVoteCounts(
141-
projectId,
142-
VoteEntityType.CROWDFUNDING_CAMPAIGN
143-
);
146+
const countsResponse = await apiGetVoteCounts(projectId, entityType);
144147
setVoteStats({
145148
upvotes: countsResponse.upvotes,
146149
downvotes: countsResponse.downvotes,
@@ -160,7 +163,7 @@ const ProjectVoters = ({ project, crowdfund }: ProjectVotersProps) => {
160163
};
161164

162165
fetchInitialVoteData();
163-
}, [projectId]);
166+
}, [projectId, entityType]);
164167

165168
const handleVote = async (voteType: VoteType) => {
166169
if (!projectId || voting) return;
@@ -171,7 +174,7 @@ const ProjectVoters = ({ project, crowdfund }: ProjectVotersProps) => {
171174
try {
172175
await createVote({
173176
projectId,
174-
entityType: VoteEntityType.CROWDFUNDING_CAMPAIGN,
177+
entityType,
175178
voteType,
176179
});
177180

0 commit comments

Comments
 (0)