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
14 changes: 14 additions & 0 deletions packages/api/src/routers/hackathon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,13 @@ export const hackathonRouter = createTRPCRouter({

const participants = await ctx.db!.query.hackathonParticipants.findMany({
where: eq(hackathonParticipants.hackathonId, input.hackathonId),
columns: {
id: true,
hackathonId: true,
userId: true,
teamId: true,
registrationStatus: true,
},
with: {
user: {
columns: {
Expand Down Expand Up @@ -357,6 +364,13 @@ export const hackathonRouter = createTRPCRouter({
team: {
with: {
participants: {
columns: {
id: true,
hackathonId: true,
userId: true,
teamId: true,
registrationStatus: true,
},
with: {
user: {
columns: {
Expand Down
47 changes: 43 additions & 4 deletions packages/api/src/routers/judge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,9 @@ export const judgeRouter = createTRPCRouter({
tableNumber: project.tableNumber,
category: project.category,
teamMembers: project.teamMembers,
tracks: project.tracks,
challenges: project.challenges,
isCreateX: project.isCreateX,
},
totalScore,
voteCount,
Expand Down Expand Up @@ -554,6 +557,7 @@ export const judgeRouter = createTRPCRouter({
judgeId: z.string().uuid(),
hackathonId: z.string().uuid(),
isLead: z.boolean().optional(),
track: z.string().optional(),
})
)
.mutation(async ({ ctx, input }) => {
Expand All @@ -577,17 +581,28 @@ export const judgeRouter = createTRPCRouter({
judgeId: input.judgeId,
hackathonId: input.hackathonId,
isLead: input.isLead || false,
track: input.track,
})
.returning();

const projects = await ctx.db!.query.judgingProjects.findMany({
// Fetch all projects for this hackathon
const allProjects = await ctx.db!.query.judgingProjects.findMany({
where: eq(judgingProjects.hackathonId, input.hackathonId),
orderBy: [asc(judgingProjects.tableNumber)],
});

if (projects.length > 0) {
// Filter based on track if assigned
const assignedProjects = input.track
? allProjects.filter((p) => {
const inTracks = p.tracks?.includes(input.track!) ?? false;
const inChallenges = p.challenges?.includes(input.track!) ?? false;
return inTracks || inChallenges;
})
: allProjects;

if (assignedProjects.length > 0) {
await ctx.db!.insert(judgeQueue).values(
projects.map((p, idx) => ({
assignedProjects.map((p, idx) => ({
judgeId: input.judgeId,
hackathonId: input.hackathonId,
projectId: p.id,
Expand All @@ -609,6 +624,9 @@ export const judgeRouter = createTRPCRouter({
teamMembers: z.string().max(500).optional(),
projectUrl: z.string().url().optional(),
repoUrl: z.string().url().optional(),
tracks: z.array(z.string()).optional(),
challenges: z.array(z.string()).optional(),
isCreateX: z.boolean().default(false),
})
)
.mutation(async ({ ctx, input }) => {
Expand All @@ -631,6 +649,9 @@ export const judgeRouter = createTRPCRouter({
tableNumber: z.number().min(1),
category: z.string().max(100).optional(),
teamMembers: z.string().max(500).optional(),
tracks: z.array(z.string()).optional(),
challenges: z.array(z.string()).optional(),
isCreateX: z.boolean().default(false),
})
),
})
Expand Down Expand Up @@ -685,11 +706,29 @@ export const judgeRouter = createTRPCRouter({
)
);

let projects = await ctx.db!.query.judgingProjects.findMany({
// Get judge assignment to check for track restriction
const assignment = await ctx.db!.query.judgeAssignments.findFirst({
where: and(
eq(judgeAssignments.judgeId, input.judgeId),
eq(judgeAssignments.hackathonId, input.hackathonId)
),
});

// Fetch all projects (or filter in query if possible, but JS filter matches assignToHackathon logic)
const allProjects = await ctx.db!.query.judgingProjects.findMany({
where: eq(judgingProjects.hackathonId, input.hackathonId),
orderBy: [asc(judgingProjects.tableNumber)],
});

// Filter based on track if assigned
let projects = (assignment?.track)
? allProjects.filter((p) => {
const inTracks = p.tracks?.includes(assignment.track!) ?? false;
const inChallenges = p.challenges?.includes(assignment.track!) ?? false;
return inTracks || inChallenges;
})
: allProjects;

if (input.shuffle) {
projects = shuffleArray(projects);
}
Expand Down
4 changes: 4 additions & 0 deletions packages/db/src/schemas/hackathons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ export const hackathonProjects = pgTable("hackathon_project", {
name: text("name").notNull(),
description: text("description").notNull(),
technologies: text("technologies").array(),
tracks: text("tracks").array(), // Enum: GEN-AI, SPORTS, FINANCE, HEALTH, CYBER, NONE
challenges: text("challenges").array(), // Enum: AGG, ASSURANT, AWS, CAPONE, GROWTH, MLH_MONGODB, MLH_STREAMLIT, MLH_TECH, MLH_CLOUDFLARE, MLH_REACH_CAPITAL
isCreateX: boolean("is_create_x").default(false),
teamMembers: text("team_members").array(), // Store names/emails if not fully linked
githubUrl: text("github_url"),
demoUrl: text("demo_url"),
videoUrl: text("video_url"),
Expand Down
4 changes: 4 additions & 0 deletions packages/db/src/schemas/judge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const judgeAssignments = pgTable("judge_assignment", {
.references(() => hackathons.id, { onDelete: "cascade" }),
assignedAt: timestamp("assigned_at").defaultNow().notNull(),
isLead: boolean("is_lead").notNull().default(false),
track: text("track"), // Optional: if set, judge only sees projects in this track/challenge
}, (table) => [
index("assignment_judge_id_idx").on(table.judgeId),
index("assignment_hackathon_id_idx").on(table.hackathonId),
Expand All @@ -44,6 +45,9 @@ export const judgingProjects = pgTable("judging_project", {
description: text("description"),
tableNumber: integer("table_number").notNull(),
category: text("category"), // e.g., "AI", "Web3", "Health", "Sustainability"
tracks: text("tracks").array(),
challenges: text("challenges").array(),
isCreateX: boolean("is_create_x").default(false),
teamMembers: text("team_members"), // comma-separated or JSON string
projectUrl: text("project_url"),
repoUrl: text("repo_url"),
Expand Down
110 changes: 110 additions & 0 deletions sites/mainweb/app/(portal)/admin-judging/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,116 @@ export default function AdminResultsPage() {
</LiquidGlass>
)}

{/* Projected Winners Section */}
{rankings && rankings.rankings.length > 0 && (
<div className="mb-12">
<h2 className="text-3xl font-black text-white italic uppercase tracking-tighter mb-6">
Projected <span className="text-[#00A8A8]">Winners</span>
</h2>

{/* Logic Calculation */}
{(() => {
// 1. Identify Overall Winners (Top 3)
// Sort by total score descending
const sortedByScore = [...rankings.rankings].sort((a, b) => b.totalScore - a.totalScore);
const overallWinners = sortedByScore.slice(0, 3);
const overallWinnerIds = new Set(overallWinners.map(r => r.project.id));

// 2. Identify Track Winners (Top 1 per Track, excluding Overall)
// Get all unique tracks
const allTracks = Array.from(new Set(rankings.rankings.flatMap(r => r.project.tracks || [])));
const trackWinners: Record<string, typeof rankings.rankings[0]> = {};
const usedWinnerIds = new Set(overallWinnerIds);

allTracks.forEach(track => {
// Find highest scoring project in this track that hasn't won yet
const candidate = sortedByScore.find(r =>
r.project.tracks?.includes(track) && !usedWinnerIds.has(r.project.id)
);

if (candidate) {
trackWinners[track] = candidate;
usedWinnerIds.add(candidate.project.id);
}
});

// 3. Identify Challenge (Sponsor) Winners (Top 1 per Challenge, NO exclusions)
const allChallenges = Array.from(new Set(rankings.rankings.flatMap(r => r.project.challenges || [])));
const challengeWinners: Record<string, typeof rankings.rankings[0]> = {};

allChallenges.forEach(challenge => {
const candidate = sortedByScore.find(r => r.project.challenges?.includes(challenge));
if (candidate) {
challengeWinners[challenge] = candidate;
}
});

return (
<div className="space-y-8">
{/* Overall Winners */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{overallWinners.map((w, i) => (
<LiquidGlass key={w.project.id} className={`p-6 rounded-xl border-t-4 ${i === 0 ? 'border-yellow-500 shadow-[0_0_30px_rgba(234,179,8,0.2)]' :
i === 1 ? 'border-gray-400' :
'border-orange-700'
}`}>
<p className="text-[10px] text-gray-500 uppercase tracking-widest mb-2 font-bold">
{i === 0 ? 'Grand Prize' : i === 1 ? '2nd Place' : '3rd Place'}
</p>
<h3 className="text-xl font-black text-white uppercase mb-1">{w.project.name}</h3>
<p className="text-3xl font-black text-[#00A8A8] tabular-nums mb-2">{w.totalScore}</p>
<p className="text-xs text-gray-500 font-mono">ID: {w.project.id.slice(-6).toUpperCase()}</p>
</LiquidGlass>
))}
</div>

{/* Track Winners */}
{Object.keys(trackWinners).length > 0 && (
<div>
<h3 className="text-sm text-gray-400 uppercase tracking-widest mb-4 font-mono font-bold pl-2 border-l-2 border-[#00A8A8]">
Track Winners (Excl. Overall)
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
{Object.entries(trackWinners).map(([track, w]) => (
<div key={track} className="bg-white/5 border border-white/5 rounded-lg p-5 hover:bg-white/10 transition-colors">
<p className="text-[10px] text-blue-400 uppercase tracking-widest mb-2 font-bold">{track}</p>
<h4 className="text-lg font-bold text-white mb-1 truncate" title={w.project.name}>{w.project.name}</h4>
<p className="text-xl font-bold text-gray-400 tabular-nums">{w.totalScore}</p>
</div>
))}
</div>
</div>
)}

{/* Challenge Winners */}
{Object.keys(challengeWinners).length > 0 && (
<div>
<h3 className="text-sm text-gray-400 uppercase tracking-widest mb-4 font-mono font-bold pl-2 border-l-2 border-purple-500">
Challenge Winners (Sponsors)
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
{Object.entries(challengeWinners).map(([challenge, w]) => (
<div key={challenge} className="bg-white/5 border border-white/5 rounded-lg p-5 hover:bg-white/10 transition-colors">
<p className="text-[10px] text-purple-400 uppercase tracking-widest mb-2 font-bold">{challenge}</p>
<h4 className="text-lg font-bold text-white mb-1 truncate" title={w.project.name}>{w.project.name}</h4>
<div className="flex justify-between items-end">
<p className="text-xl font-bold text-gray-400 tabular-nums">{w.totalScore}</p>
{overallWinnerIds.has(w.project.id) && (
<span className="text-[8px] bg-yellow-500/20 text-yellow-500 px-1.5 py-0.5 rounded uppercase font-bold">Also Overall</span>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
);
})()}
</div>
)}

{/* Rankings Table Logic... */}
{/* Rankings Table */}
<div className="space-y-6">
<div className="flex justify-between items-end mb-4">
Expand Down
13 changes: 11 additions & 2 deletions sites/mainweb/app/(portal)/judge/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -327,8 +327,15 @@ export default function JudgePage() {
</svg>
</div>
<div>
{assignments?.[0]?.track && (
<div className="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold bg-[#00A8A8]/20 text-[#00A8A8] border border-[#00A8A8]/30 mb-2 uppercase tracking-wide">
Track: {assignments[0].track}
</div>
)}
<h2 className="text-lg font-bold text-white">{project.name}</h2>
{project.teamMembers && <p className="text-gray-500 text-sm">{project.teamMembers}</p>}
{project.teamMembers && <p className="text-gray-500 text-sm mb-2">{project.teamMembers}</p>}


</div>
</div>
</LiquidGlass>
Expand Down Expand Up @@ -398,7 +405,9 @@ export default function JudgePage() {
</div>
<div>
<h1 className="text-lg font-bold text-white">{project.name}</h1>
{project.teamMembers && <p className="text-gray-500 text-sm">{project.teamMembers}</p>}
{project.teamMembers && <p className="text-gray-500 text-sm mb-2">{project.teamMembers}</p>}


</div>
</LiquidGlass>

Expand Down
Loading