Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ const ActionButtons = () => {
const isParticipant = user
? !!hackathon?.isParticipant ||
(hackathon?.participants || []).some(
(p: Participant) => p.userId === user.id
(p: Participant) =>
(p as any).userId === user.id || p.user?.id === user.id
Comment on lines +41 to +42
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify there are no `any` escapes left in this component.
rg -nP --type=tsx '\bas\s+any\b|:\s*any\b' 'app/(landing)/hackathons/[slug]/components/header/ActionButtons.tsx'

Repository: boundlessfi/boundless

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Read the file around the flagged lines
cat -n 'app/(landing)/hackathons/[slug]/components/header/ActionButtons.tsx' | head -60 | tail -30

echo "---"

# Search for 'any' in the file without type restrictions
rg -n 'as any|: any' 'app/(landing)/hackathons/[slug]/components/header/ActionButtons.tsx'

echo "---"

# Find the Participant type definition
rg -n 'type Participant|interface Participant' --no-heading

Repository: boundlessfi/boundless

Length of output: 4586


🏁 Script executed:

#!/bin/bash
# Read the Participant interface definition
cat -n 'types/hackathon/participant.ts' | sed -n '99,128p'

Repository: boundlessfi/boundless

Length of output: 879


Remove any cast in participant check.

Line 42 uses (p as any).userId, which drops type safety and is unnecessary since the Participant interface already defines userId as a direct property.

Suggested fix
-      (hackathon?.participants || []).some(
-        (p: Participant) =>
-          (p as any).userId === user.id || p.user?.id === user.id
-      )
+      (hackathon?.participants || []).some(
+        (p: Participant) => p.userId === user.id || p.user?.id === user.id
+      )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
(p: Participant) =>
(p as any).userId === user.id || p.user?.id === user.id
(hackathon?.participants || []).some(
(p: Participant) => p.userId === user.id || p.user?.id === user.id
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(landing)/hackathons/[slug]/components/header/ActionButtons.tsx around
lines 41 - 42, Replace the unsafe cast in the participant identity check: in the
arrow predicate that currently uses "(p as any).userId === user.id || p.user?.id
=== user.id", remove the any cast and use the typed property "p.userId" instead
(i.e., "p.userId === user.id || p.user?.id === user.id") so the Participant type
is respected; ensure the predicate parameter remains typed as Participant and
that optional chaining on p.user?.id is preserved.

)
: false;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
useHackathon,
useHackathonParticipants,
} from '@/hooks/hackathon/use-hackathon-queries';
import { useDebounce } from '@/hooks/use-debounce';
import { TabsContent } from '@/components/ui/tabs';
import { ChevronDown, Search, Filter, Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
Expand Down Expand Up @@ -53,6 +54,12 @@ const Participants = () => {
'all' | 'submitted' | 'in_progress'
>('all');
const [skillFilter, setSkillFilter] = useState<string>('all');
const [participationType, setParticipationType] = useState<
'all' | 'individual' | 'team'
>('all');
const [searchQuery, setSearchQuery] = useState('');
const debouncedSearch = useDebounce(searchQuery, 300);

const [page, setPage] = useState(1);
const [accumulatedParticipants, setAccumulatedParticipants] = useState<
Participant[]
Expand All @@ -68,13 +75,15 @@ const Participants = () => {
limit,
status: statusFilter === 'all' ? undefined : statusFilter,
skill: skillFilter === 'all' ? undefined : skillFilter,
type: participationType === 'all' ? undefined : participationType,
search: debouncedSearch || undefined,
});

// Reset page and list when filters change
React.useEffect(() => {
setPage(1);
setAccumulatedParticipants([]);
}, [statusFilter, skillFilter]);
}, [statusFilter, skillFilter, participationType, debouncedSearch]);

// Accumulate participants as they are fetched
React.useEffect(() => {
Expand Down Expand Up @@ -133,14 +142,28 @@ const Participants = () => {
>
{/* Header with Count and Filters */}
<div className='flex flex-col gap-6 md:flex-row md:items-end md:justify-between'>
<div>
<h2 className='text-2xl font-bold text-white'>Participants</h2>
<p className='mt-1 text-gray-500'>
<span className='font-bold text-gray-300'>
{totalBuilders.toLocaleString()}
</span>{' '}
builders competing in {hackathon.name}
</p>
<div className='flex flex-1 flex-col gap-4'>
<div>
<h2 className='text-2xl font-bold text-white'>Participants</h2>
<p className='mt-1 text-gray-500'>
<span className='font-bold text-gray-300'>
{totalBuilders.toLocaleString()}
</span>{' '}
builders competing in {hackathon.name}
</p>
</div>

{/* Search Bar */}
<div className='relative w-full max-w-md'>
<input
type='text'
placeholder='Search by name, username, or project...'
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
className='hover:border-primary/20 focus:border-primary/30 h-10 w-full rounded-xl border border-white/5 bg-[#141517] pr-4 pl-10 text-sm text-white transition-all outline-none placeholder:text-gray-500'
/>
<Search className='absolute top-1/2 left-3.5 h-4 w-4 -translate-y-1/2 text-gray-500' />
</div>
</div>

<div className='flex items-center gap-3'>
Expand Down Expand Up @@ -175,6 +198,38 @@ const Participants = () => {
</DropdownMenuContent>
</DropdownMenu>

{/* Type Filter */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className='hover:border-primary/20 flex min-w-[140px] items-center justify-between gap-2 rounded-xl border border-white/5 bg-[#141517] px-4 py-2.5 text-sm font-medium text-gray-400 transition-all hover:text-white'>
<span>
{participationType === 'all'
? 'Type: All'
: participationType === 'individual'
? 'Type: Individual'
: 'Type: Team'}
</span>
<ChevronDown className='h-4 w-4 opacity-50' />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align='end'
className='w-48 border-white/10 bg-[#141517] text-gray-300'
>
<DropdownMenuItem onClick={() => setParticipationType('all')}>
All Types
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setParticipationType('individual')}
>
Individual
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setParticipationType('team')}>
Team
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

{/* Status Filter */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
Expand Down Expand Up @@ -222,7 +277,7 @@ const Participants = () => {
key={p.id}
name={p.user.profile.name}
username={p.user.profile.username}
image={p.user.profile.image}
image={p.user.profile.image || undefined}
submitted={!!p.submittedAt}
skills={p.user.profile.skills}
userId={p.userId ?? p.user?.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,45 +228,11 @@ const ParticipantsPage: React.FC = () => {
}
};

// Frontend-side filtering as per requirement
const filteredParticipants = useMemo(() => {
return participants.filter(participant => {
// Search: filter by name or username
const search = filters.search.toLowerCase();
const matchesSearch = search
? (participant.user?.profile?.name || '')
.toLowerCase()
.includes(search) ||
(participant.user?.profile?.username || '')
.toLowerCase()
.includes(search)
: true;

// Status: filter by participant.submission.status
// Filter values are 'submitted', 'not_submitted', etc.
// ParticipantSubmission.status values are 'submitted', 'shortlisted', etc.
const matchesStatus =
filters.status === 'all'
? true
: filters.status === 'not_submitted'
? !participant.submission
: participant.submission?.status?.toLowerCase() ===
filters.status.toLowerCase();

// Type: filter by participant.participationType
const matchesType =
filters.type === 'all'
? true
: participant.participationType?.toLowerCase() ===
filters.type.toLowerCase();

return matchesSearch && matchesStatus && matchesType;
});
}, [participants, filters.search, filters.status, filters.type]);
// Removed redundant frontend-side filtering as backend now handles it.

// Mock table instance for DataTablePagination
const table = useReactTable({
data: filteredParticipants,
data: participants,
columns: [], // Not used for rendering here
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
Expand Down Expand Up @@ -352,15 +318,15 @@ const ParticipantsPage: React.FC = () => {
<div className='space-y-6'>
{view === 'table' ? (
<ParticipantsTable
data={filteredParticipants}
data={participants}
loading={participantsLoading}
onReview={handleReview}
onViewTeam={handleViewTeam}
onGrade={handleGrade}
/>
) : (
<ParticipantsGrid
data={filteredParticipants}
data={participants}
loading={participantsLoading}
onReview={handleReview}
onViewTeam={handleViewTeam}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,27 +72,8 @@ export default function SubmissionsPage() {
fetchSession();
}, []);

// Frontend-side filtering
const filteredSubmissions = useMemo(() => {
return allSubmissions.filter(sub => {
const search = filters.search?.toLowerCase() || '';
const matchesSearch = search
? (sub.projectName || '').toLowerCase().includes(search) ||
(sub.participant?.name || '').toLowerCase().includes(search) ||
(sub.participant?.username || '').toLowerCase().includes(search)
: true;

const matchesStatus = !filters.status || sub.status === filters.status;

const matchesType =
!filters.type || sub.participationType === filters.type;

return matchesSearch && matchesStatus && matchesType;
});
}, [allSubmissions, filters.search, filters.status, filters.type]);

const table = useReactTable({
data: filteredSubmissions,
data: allSubmissions,
columns: [],
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
Expand Down Expand Up @@ -162,7 +143,7 @@ export default function SubmissionsPage() {
) : (
<div className='space-y-6'>
<SubmissionsManagement
submissions={filteredSubmissions}
submissions={allSubmissions}
pagination={pagination}
filters={filters}
loading={loading}
Expand Down
2 changes: 1 addition & 1 deletion components/hackathons/submissions/submissionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ interface SubmissionCardProps {
title: string;
description: string;
submitterName: string;
submitterAvatar?: string;
submitterAvatar?: string | null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a null-avatar fallback in the header avatar UI.

Line 31 now allows null, but the current header avatar still builds url(${submitterAvatar}) unconditionally, which can render a broken avatar for missing images.

Suggested fix
-          <div
-            style={{ backgroundImage: `url(${submitterAvatar})` }}
-            className={`size-8 rounded-full border-2 bg-white bg-cover bg-center ${isPinned ? 'border-primary' : 'border-[`#2B2B2B`]'}`}
-          ></div>
+          {submitterAvatar ? (
+            <div
+              style={{ backgroundImage: `url(${submitterAvatar})` }}
+              className={`size-8 rounded-full border-2 bg-white bg-cover bg-center ${isPinned ? 'border-primary' : 'border-[`#2B2B2B`]'}`}
+            />
+          ) : (
+            <div
+              className={`flex size-8 items-center justify-center rounded-full border-2 bg-zinc-800 text-xs font-bold text-zinc-300 ${isPinned ? 'border-primary' : 'border-[`#2B2B2B`]'}`}
+            >
+              {submitterName.charAt(0).toUpperCase()}
+            </div>
+          )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
submitterAvatar?: string | null;
{submitterAvatar ? (
<div
style={{ backgroundImage: `url(${submitterAvatar})` }}
className={`size-8 rounded-full border-2 bg-white bg-cover bg-center ${isPinned ? 'border-primary' : 'border-[`#2B2B2B`]'}`}
/>
) : (
<div
className={`flex size-8 items-center justify-center rounded-full border-2 bg-zinc-800 text-xs font-bold text-zinc-300 ${isPinned ? 'border-primary' : 'border-[`#2B2B2B`]'}`}
>
{submitterName.charAt(0).toUpperCase()}
</div>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/hackathons/submissions/submissionCard.tsx` at line 31, The header
avatar currently constructs `url(${submitterAvatar})` even when
`submitterAvatar` can be null; update the component (submissionCard / the header
avatar rendering) to use a safe fallback image or initials when
`submitterAvatar` is null or empty—e.g., compute a resolvedAvatar =
submitterAvatar ?? '<fallback-url-or-empty>' (or conditionally render a
placeholder/avatar-initials) and use that value wherever
`url(${submitterAvatar})` is used so the UI never attempts to load a null URL.

category?: string;
categories?: string[];
status?: 'Pending' | 'Approved' | 'Rejected';
Expand Down
2 changes: 1 addition & 1 deletion components/hackathons/submissions/submissionCard2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { cn } from '@/lib/utils';
interface SubmissionCard2Props {
title: string;
submitterName: string;
submitterAvatar?: string;
submitterAvatar?: string | null;
image?: string;
upvotes?: number;
comments?: number;
Expand Down
2 changes: 1 addition & 1 deletion components/organization/cards/ParticipantInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const ParticipantInfo: React.FC<ParticipantInfoProps> = ({
<div className='flex flex-row items-center justify-start p-5'>
<div className='flex-1'>
<Avatar className='h-10.5 w-10.5'>
<AvatarImage src={userAvatar} />
<AvatarImage src={userAvatar || undefined} />
<AvatarFallback>{userName.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar>
<h4 className='text-sm text-white'>{userName}</h4>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface Voter {
id: string;
name: string;
username: string;
avatar?: string;
avatar?: string | null;
votedAt?: string;
voteType?: 'positive' | 'negative';
}
Expand Down Expand Up @@ -42,7 +42,10 @@ export const SubmissionVotesTab: React.FC<SubmissionVotesTabProps> = ({
<div className='flex min-w-0 items-center gap-4'>
<div className='relative'>
<Avatar className='h-12 w-12 shrink-0 border border-gray-800'>
<AvatarImage src={voter.avatar} alt={voter.name} />
<AvatarImage
src={voter.avatar || undefined}
alt={voter.name}
/>
<AvatarFallback className='bg-background-card font-bold text-gray-400'>
{voter.name.charAt(0).toUpperCase()}
</AvatarFallback>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface TeamMember {
id: string;
name: string;
role: string;
avatar?: string;
avatar?: string | null;
username?: string;
}

Expand All @@ -37,7 +37,10 @@ export const TeamSection: React.FC<TeamSectionProps> = ({ teamMembers }) => {
className='group bg-background-card/20 hover:bg-background-card/40 flex items-center gap-4 rounded-xl border border-gray-900/60 p-4 transition-all hover:border-gray-800'
>
<Avatar className='h-12 w-12 border border-gray-800 transition-transform group-hover:scale-105'>
<AvatarImage src={member.avatar} alt={member.name} />
<AvatarImage
src={member.avatar || undefined}
alt={member.name}
/>
<AvatarFallback className='bg-background-card font-bold text-gray-400'>
{member.name.charAt(0).toUpperCase()}
</AvatarFallback>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ export interface TeamMember {
id: string;
name: string;
role: string;
avatar?: string;
avatar?: string | null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check nullable avatar usage patterns in ReviewSubmissionModal components.
rg -nP --type=tsx 'src=\{comment\.author\.avatar\}' components/organization/cards/ReviewSubmissionModal
rg -nP --type=tsx 'src=\{(member|voter)\.avatar\s*(\|\||\?\?)\s*undefined\}' components/organization/cards/ReviewSubmissionModal

Repository: boundlessfi/boundless

Length of output: 120


🏁 Script executed:

#!/bin/bash
# First, explore the ReviewSubmissionModal directory structure
echo "=== Directory structure ==="
find components/organization/cards/ReviewSubmissionModal -type f | head -20

echo ""
echo "=== Search for avatar usage without type restriction ==="
rg -n 'src.*avatar' components/organization/cards/ReviewSubmissionModal

echo ""
echo "=== Search for comment.author.avatar specifically ==="
rg -n 'comment\.author\.avatar' components/organization/cards/ReviewSubmissionModal

echo ""
echo "=== Check types.ts file content ==="
cat -n components/organization/cards/ReviewSubmissionModal/types.ts

Repository: boundlessfi/boundless

Length of output: 3076


Fix avatar null-safety in SubmissionCommentsTab after type widening.

After widening the avatar fields to string | null (lines 5, 13, 24 in types.ts), SubmissionCommentsTab.tsx still passes comment.author.avatar directly to AvatarImage without normalization. Update line 41 to use comment.author.avatar || undefined to match the null-safe pattern already applied in TeamSection.tsx (line 41) and SubmissionVotesTab.tsx (line 46).

Also applies to: 13-13, 24-24

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/cards/ReviewSubmissionModal/types.ts` at line 5,
SubmissionCommentsTab passes comment.author.avatar (now typed string | null)
directly to AvatarImage; make it null-safe by normalizing to undefined like the
other tabs—replace the direct prop with comment.author.avatar || undefined in
SubmissionCommentsTab (the prop passed to AvatarImage around the current line
41) so it matches the pattern used in TeamSection.tsx and
SubmissionVotesTab.tsx.

username?: string;
}

export interface Voter {
id: string;
name: string;
username: string;
avatar?: string;
avatar?: string | null;
votedAt?: string;
voteType?: 'positive' | 'negative';
}
Expand All @@ -21,7 +21,7 @@ export interface Comment {
author: {
name: string;
username: string;
avatar?: string;
avatar?: string | null;
};
createdAt: string;
reactions?: {
Expand Down
7 changes: 5 additions & 2 deletions components/organization/cards/TeamModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface TeamMember {
id: string;
name: string;
role: string;
avatar?: string;
avatar?: string | null;
}

type ParticipationType = 'team' | 'individual' | 'no-submission';
Expand Down Expand Up @@ -168,7 +168,10 @@ export default function TeamModal({
className='group flex cursor-pointer items-center gap-3 rounded-lg p-3 transition-colors hover:bg-gray-900/50'
>
<Avatar className='h-10 w-10 flex-shrink-0'>
<AvatarImage src={member.avatar} alt={member.name} />
<AvatarImage
src={member.avatar || undefined}
alt={member.name}
/>
<AvatarFallback>
{member.name.charAt(0).toUpperCase()}
</AvatarFallback>
Expand Down
5 changes: 4 additions & 1 deletion components/organization/hackathons/ParticipantsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ export function ParticipantsTable({
return (
<div className='flex items-center gap-3'>
<Avatar className='h-9 w-9 border border-gray-800'>
<AvatarImage src={user.profile.image} alt={user.profile.name} />
<AvatarImage
src={user.profile.image || undefined}
alt={user.profile.name}
/>
<AvatarFallback className='bg-background-card text-xs'>
{user.profile.name?.substring(0, 2).toUpperCase()}
</AvatarFallback>
Expand Down
3 changes: 3 additions & 0 deletions hooks/hackathon/use-hackathon-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,16 @@ export interface ParticipantsQueryParams {
page?: number;
limit?: number;
status?: string;
search?: string;
type?: string;
skill?: string;
}

export interface SubmissionsQueryParams {
page?: number;
limit?: number;
status?: string;
search?: string;
sort?: string;
}

Expand Down
2 changes: 1 addition & 1 deletion hooks/hackathon/use-organizer-submissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function useOrganizerSubmissions(
const currentPage = rawPag?.page || page;
const limit = rawPag?.limit || pagination.limit || initialLimit;
const total = rawPag?.total || 0;
const totalPages = rawPag?.totalPages || Math.ceil(total / limit) || 1;
const totalPages = Math.ceil(total / limit) || 1;

setSubmissions(list);
setPagination({
Expand Down
Loading
Loading