GuidesApplication & Review System

Application & Review System

Complete guide to the CxC application submission and admin review system.

Overview

The CxC application system includes:

  • Multi-step application form with validation
  • Team member selection
  • Resume upload with secure storage
  • Form state persistence across sessions
  • Admin review interface with scoring system

Application Flow

Form Structure

The application is divided into 6 main steps (desktop view) and 11 pages (mobile view):

Step 1: Your Info

  • Contact Info - Phone number and Discord username
  • About You - T-shirt size, dietary restrictions, age, country
  • Optional Demographics - Gender and ethnicity (optional)

Step 2: Your Experience

  • Education - University, program, year of study
  • Hackathon Experience - Prior experience level, number of hackathons attended
  • Links & Documents - GitHub, LinkedIn, website, resume upload

Step 3: Application Questions

  • Question 1 - Technical project description (challenges, learnings)
  • Question 2 - Creative haiku submission

Step 4: Team Members (Optional)

  • Search and select up to 3 team members
  • Only users with existing accounts appear in search

Step 5: MLH Requirements

  • MLH Code of Conduct agreement (required)
  • Information sharing authorization (required)
  • Marketing email opt-in (optional)

Step 6: Review & Submit

  • Review all submitted information
  • View selected team members with names
  • Preview uploaded resume
  • Submit final application

Form State Persistence

Automatic Progress Saving

  • Current step/page saved to localStorage
  • Returns to last position on page reload
  • Prevents losing progress from browser crashes

Field-Level Autosave

  • All form fields automatically save to localStorage as you type
  • Data persists across page refreshes
  • Restored when returning to the application
  • Cleared upon successful submission

Supported Fields

  • Text inputs (name, email, phone, Discord)
  • Select dropdowns (university, program, year, etc.)
  • Text areas (application questions)
  • Checkboxes (MLH agreements)
  • File uploads (resume filename tracked)
  • Team member selections

Team Member Selection

How It Works

  1. Click the search input in the “Team Members” step
  2. Search by name or email address
  3. Select users from the dropdown (max 3)
  4. Team members displayed as removable tags
  5. Names shown in review section if available

API Integration

// Fetch all registered users
const { emails } = await getUserEmails();
 
// emails structure:
// [
//   { id: "uuid", email: "user@example.com", display_name: "John Doe" },
//   ...
// ]

Team Member Display

  • Shows both name and email if name is available
  • Falls back to email only if name not set
  • Team members shown in application review
  • Visible to reviewers during admin review

Resume Upload

Upload Process

  1. Select PDF, DOC, or DOCX file (max 10 MB)
  2. File uploaded immediately upon selection
  3. Validation runs before upload:
    • File size check (max 10 MB)
    • File type validation (PDF, DOC, DOCX)
  4. Previous resume automatically replaced
  5. Filename displayed in form after upload

Storage & Security

  • Resumes stored in private Supabase bucket
  • Access requires signed URLs (1-hour expiration)
  • Ownership verified before generating URLs
  • Files organized by user ID: {userId}/{filename}

API Usage

// Upload resume
await uploadResume(file);
 
// Get current resume
const { resume, url, key } = await getResume();
// resume: { name: "resume.pdf", ... }
// url: "https://...signed-url..."
// key: "userId/resume.pdf"

Application Submission

Submission Process

  1. Complete all required fields in steps 1-5
  2. Review information in step 6
  3. Click “Submit Application” button
  4. Backend validates all data
  5. Application status changed to “submitted”
  6. LocalStorage cleared automatically
  7. Redirected to “Submitted” confirmation page

Submission Protection

  • Cannot edit application after submission
  • Attempting to access /apply redirects to “Submitted” page
  • Application data locked in database

Admin Review System

Review Assignment

Fair Assignment Algorithm

  • Returns application with least number of reviews
  • Excludes applications already reviewed by requesting admin
  • Ties broken lexicographically by application ID
  • Ensures even distribution of reviews across applications

API Endpoint

GET /api/applications/random
 
// Returns:
{
  application: {
    id: "app-uuid",
    profile_id: "user-uuid",
    email: "applicant@example.com",
    resume_url: "https://...signed-url...",
    team_members_with_names: [
      { email: "teammate@example.com", display_name: "Jane Smith" }
    ],
    // ... all application fields ...
  }
}

Review Interface

Accessing the Review Page

  • Navigate to /review (admin users only)
  • Non-admin users automatically redirected to home
  • Role verification happens in middleware

Review Components

The review interface displays complete application information across organized cards:

  1. Resume Review Card

    • Links to uploaded resume with signed URL
    • Resume preview and download
    • File information display
    • Score: 0-3 for resume quality and relevance
  2. Links & Portfolio Card

    • GitHub profile link
    • LinkedIn profile link
    • Personal website/portfolio
    • Score: 0-2 for link quality and professional presence
  3. Question 1 Card (Technical Project)

    • Full technical project description
    • Challenges faced and solutions
    • Learning outcomes
    • Score: 0-7 for technical depth, problem-solving, and learning
  4. Question 2 Card (Creative Haiku)

    • Haiku submission display
    • Creative expression evaluation
    • Score: 0-3 for creativity and following format

Additional Information Displayed

  • Contact details (email, phone, Discord)
  • Personal info (t-shirt size, dietary restrictions, age, country)
  • Demographics (gender, ethnicity - if provided)
  • Education (university, program, year of study)
  • Hackathon experience level
  • Team members with names and emails (if applicable)

Scoring Guidelines

  • Resume Score: 0-3 scale - Quality, relevance, and professionalism
  • Links Score: 0-2 scale - Portfolio quality and online presence
  • Q1 Score: 0-7 scale - Technical depth, problem-solving, and learning
  • Q2 Score: 0-3 scale - Creativity and format adherence
  • All scores required before submission
  • Validation prevents duplicate reviews
  • Real-time error feedback

Review Submission

Submit Review Process

// Client-side submission
await submitReview({
  application_id: "app-uuid",
  resume_score: 2,    // 0-3
  links_score: 1,     // 0-2
  q1_score: 5,        // 0-7
  q2_score: 2         // 0-3
});
 
// Response:
{
  success: true,
  message: "Review saved successfully",
  review: { id: "review-uuid", ... },
  total_reviews: 42  // Total reviews by this reviewer
}

Server-Side Validation

  • Resume Score: Must be 0-3
  • Links Score: Must be 0-2
  • Q1 Score: Must be 0-7
  • Q2 Score: Must be 0-3
  • All four scores required
  • Duplicate review check (same reviewer + application)
  • Admin role verification
  • Database transaction ensures consistency

After Submission

  • Success message displayed
  • Option to review another application
  • “Get New Application” button fetches next review

Review Storage

Database Schema

CREATE TABLE reviews (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  application_id UUID NOT NULL REFERENCES applications(id),
  reviewer_id UUID NOT NULL REFERENCES profiles(id),
  resume_score INTEGER NOT NULL CHECK (resume_score >= 0 AND resume_score <= 3),
  links_score INTEGER NOT NULL CHECK (links_score >= 0 AND links_score <= 2),
  q1_score INTEGER NOT NULL CHECK (q1_score >= 0 AND q1_score <= 7),
  q2_score INTEGER NOT NULL CHECK (q2_score >= 0 AND q2_score <= 3),
  reviewed_at TIMESTAMP NOT NULL DEFAULT NOW(),
  CONSTRAINT reviews_application_id_reviewer_id_key UNIQUE(application_id, reviewer_id)
);

Key Features

  • Each review linked to specific application and reviewer
  • Timestamp records when review submitted
  • Unique compound constraint on (application_id, reviewer_id) prevents duplicate reviews per reviewer
  • Allows multiple reviewers per application and multiple applications per reviewer
  • Score validation at database level with appropriate CHECK constraints

Admin Leaderboard

The leaderboard system provides comprehensive analytics and gamification for the review process, helping superadmins track reviewer performance and overall application review progress.

Accessing the Leaderboard

Navigation

  • Navigate to /admin/leaderboard (superadmin users only)
  • Non-superadmin users receive 403 Forbidden error
  • Accessible from Admin dropdown in navbar

Dashboard Overview

The leaderboard page displays comprehensive review statistics and rankings:

Statistics Cards (Top Row)

General Statistics

  • Total Applications: Count of all submitted applications
  • Total Reviews: Aggregate number of reviews completed
  • Total Reviewers: Number of unique reviewers who have submitted at least one review
  • Avg Reviews/App: Average number of reviews per application

Average Score Metrics

  • Avg Resume Score: Mean resume score across all reviews (out of 3)
  • Avg Links Score: Mean links score across all reviews (out of 2)
  • Avg Q1 Score: Mean Question 1 score across all reviews (out of 7)
  • Avg Q2 Score: Mean Question 2 score across all reviews (out of 3)

Top 3 Podium Display

Visual Ranking

  • Animated podium showing top 3 reviewers
  • Medal icons: 🏆 Gold (1st), 🥈 Silver (2nd), 🥉 Bronze (3rd)
  • Different podium heights for visual hierarchy
  • Color-coded borders: Yellow (1st), Silver (2nd), Bronze (3rd)
  • Displays reviewer name, email, and total review count
  • Click to view detailed reviewer statistics

Full Leaderboard Table

Features

  • Complete ranked list of all reviewers
  • Shows rank position, reviewer name, email, and review count
  • Animated entry transitions
  • Click any entry to view detailed statistics
  • Responsive design for mobile and desktop

Reviewer Detail Modal

Detailed Statistics per Reviewer

When clicking on a reviewer, a modal displays their individual performance metrics:

  • Reviewer Information: Name and email
  • Average Scores:
    • Resume Score (out of 3)
    • Links Score (out of 2)
    • Q1 Score (out of 7)
    • Q2 Score (out of 3)
  • Total Reviews: Number of reviews completed by this reviewer

Modal Features

  • Loading state with spinner during data fetch
  • Organized grid layout with score cards
  • Score context indicators (“Out of X”)
  • Review count summary at bottom

Leaderboard API

Get Leaderboard Data and Statistics

GET /api/admin/leaderboard
 
// Response:
{
  leaderboard: [
    {
      reviewer_id: "uuid",
      review_count: 42,
      name: "John Doe",
      email: "john@example.com"
    },
    ...
  ],
  statistics: {
    total_applications: 150,
    total_reviews: 300,
    total_reviewers: 15,
    avg_reviews_per_application: 2.0,
    avg_resume_score: 2.1,
    avg_links_score: 1.5,
    avg_q1_score: 5.2,
    avg_q2_score: 2.3
  }
}

Get Reviewer-Specific Scores

GET /api/admin/leaderboard?reviewerId={uuid}
 
// Response:
{
  reviewerScores: {
    avg_resume_score: 2.3,
    avg_links_score: 1.8,
    avg_q1_score: 5.5,
    avg_q2_score: 2.5,
    total_reviews: 42
  }
}

Implementation Details

Backend Services

AdminReviewService methods:

  • getLeaderboard(): Returns all reviewers with review counts, sorted by count descending
  • getStatistics(): Calculates aggregate statistics across all applications and reviews
  • getReviewerAverageScores(reviewerId): Computes average scores for a specific reviewer

Security

  • Superadmin-only access via ProfileService role check
  • Graceful error handling with fallback values
  • Request timeout protection (30 seconds)
  • Promise.allSettled for partial failure handling

Performance Optimizations

  • Parallel data fetching for leaderboard and statistics
  • Optimized SQL queries with proper aggregations
  • Client-side caching with state management
  • Animated transitions with Framer Motion

Use Cases

For Superadmins

  1. Monitor Review Progress: Track how many applications have been reviewed
  2. Identify Top Contributors: Recognize most active reviewers
  3. Quality Assurance: Analyze average scores to identify scoring patterns
  4. Load Balancing: See which reviewers are most active to distribute future review assignments
  5. Performance Analytics: Compare individual reviewer scoring patterns against overall averages

Gamification Benefits

  • Encourages healthy competition among reviewers
  • Provides recognition for top contributors
  • Motivates consistent review participation
  • Transparent progress tracking

API Reference

Application Endpoints

Create Application

POST /api/applications
Body: { profile_id: string }
Response: { id: string, profile_id: string, status: "draft", ... }

Get User’s Application

GET /api/applications
Response: { id: string, profile_id: string, status: string, ... } | null

Update Application

PATCH /api/applications
Body: Partial<ApplicationData>
Response: { success: boolean }

Resume Endpoints

Upload Resume

POST /api/applications/resumes
Body: FormData with file
Response: { 
  message: "Resume uploaded successfully",
  key: "userId/filename.pdf"
}

Get Resume

GET /api/applications/resumes
Response: {
  resume: { name: string } | null,
  url: string | null,  // Signed URL, 1-hour expiration
  key: string | null
}

Review Endpoints (Admin Only)

Get Random Application

GET /api/applications/random
Response: {
  application: {
    id: string,
    email: string,
    resume_url: string,
    team_members_with_names: Array<{email: string, display_name: string}>,
    ...allApplicationFields
  }
}

Submit Review

POST /api/review/submit
Body: {
  application_id: string,
  resume_score: number,  // 0-3
  links_score: number,   // 0-2
  q1_score: number,      // 0-7
  q2_score: number       // 0-3
}
Response: {
  success: true,
  message: string,
  review: { id: string, ... },
  total_reviews: number
}

Get Leaderboard (Superadmin Only)

GET /api/admin/leaderboard
Response: {
  leaderboard: Array<{
    reviewer_id: string,
    review_count: number,
    name: string | null,
    email: string
  }>,
  statistics: {
    total_applications: number,
    total_reviews: number,
    total_reviewers: number,
    avg_reviews_per_application: number,
    avg_resume_score: number,
    avg_links_score: number,
    avg_q1_score: number,
    avg_q2_score: number
  }
}

Get Reviewer Statistics (Superadmin Only)

GET /api/admin/leaderboard?reviewerId={uuid}
Response: {
  reviewerScores: {
    avg_resume_score: number,
    avg_links_score: number,
    avg_q1_score: number,
    avg_q2_score: number,
    total_reviews: number
  }
}

User Endpoints

Get All User Emails

GET /api/users/emails
Response: {
  emails: Array<{
    id: string,
    email: string,
    display_name: string | null
  }>
}

Client API Functions

Application Functions

import { 
  fetchApplication, 
  createApplication, 
  updateApplication 
} from "@/lib/api";
 
// Get user's application
const app = await fetchApplication(userId);
 
// Create new application
const newApp = await createApplication(userId);
 
// Update application
await updateApplication(data);

Resume Functions

import { uploadResume, getResume } from "@/lib/api";
 
// Upload resume
await uploadResume(fileObject);
 
// Get current resume
const { resume, url, key } = await getResume();

Review Functions

import { getRandomApplication, submitReview } from "@/lib/api";
 
// Get next application to review
const { application } = await getRandomApplication();
 
// Submit review scores
await submitReview({
  application_id: app.id,
  resume_score: 2,    // 0-3
  links_score: 1,     // 0-2
  q1_score: 5,        // 0-7
  q2_score: 2         // 0-3
});
});

User Functions

import { getUserEmails } from "@/lib/api";
 
// Get all registered users for team selection
const { emails } = await getUserEmails();

Backend Services

ApplicationService

import { ApplicationService } from "@uwdsc/server/cxc/services/applicationService";
 
const service = new ApplicationService();
 
// Get application by user ID
const app = await service.getApplicationByProfileId(userId);
 
// Create new application
const newApp = await service.createApplication(userId);
 
// Update application
await service.updateApplication(userId, data);
 
// Get all user emails (for team selection)
const users = await service.getAllUserEmails();

AdminReviewService

import { AdminReviewService } from "@uwdsc/server/cxc/services/adminReviewService";
 
const service = new AdminReviewService();
 
// Get next application for review
const app = await service.getNextApplicationForReview(
  reviewerId,
  getResumeUrlFunction
);
 
// Submit review
const result = await service.submitReview(
  {
    application_id: "app-id",
    resume_score: 2,    // 0-3
    links_score: 1,     // 0-2
    q1_score: 5,        // 0-7
    q2_score: 2         // 0-3
  },
  reviewerId
);
 
// Get leaderboard (superadmin only)
const leaderboard = await service.getLeaderboard();
// Returns: Array<{ reviewer_id, review_count, name, email }>
 
// Get overall statistics (superadmin only)
const statistics = await service.getStatistics();
// Returns: { total_applications, total_reviews, total_reviewers, avg_reviews_per_application, avg_resume_score, avg_links_score, avg_q1_score, avg_q2_score }
 
// Get reviewer-specific average scores (superadmin only)
const reviewerScores = await service.getReviewerAverageScores(reviewerId);
// Returns: { avg_resume_score, avg_links_score, avg_q1_score, avg_q2_score, total_reviews }

ResumeService

import { ResumeService } from "@uwdsc/server/core/services/resumeService";
 
const service = new ResumeService();
 
// Upload resume (replaces existing)
await service.uploadResume({ file, userId });
 
// Get user's resume
const result = await service.getUserResume(userId);
 
// Get signed URL (1-hour expiration)
const { url } = await service.getSignedResumeUrl(userId, 3600);
 
// Delete resume
await service.deleteResume(`${userId}/filename.pdf`);

Form Persistence Hook

useFormFieldPersistence

Custom hook for automatic form field persistence to localStorage.

import { useFormFieldPersistence } from "@/hooks/useFormFieldPersistence";
 
// In your form component:
function MyFormSection({ form }: { form: UseFormReturn<AppFormValues> }) {
  // Persist individual fields
  useFormFieldPersistence(form, "phone");
  useFormFieldPersistence(form, "discord");
  useFormFieldPersistence(form, "university_name");
  
  // Custom storage key (optional)
  useFormFieldPersistence(form, "cxc_q1", "q1_save");
  
  // ... render form
}

Features

  • Automatically saves on field change
  • Restores from localStorage on mount
  • Handles form.reset() correctly
  • Works with text inputs, selects, checkboxes, arrays
  • Special handling for undefined/empty values
  • Cleared on application submission

Best Practices

Application Form

Do:

  • Validate all inputs before allowing step progression
  • Save progress frequently to localStorage
  • Provide clear error messages
  • Allow users to go back and edit previous steps
  • Show progress indicator (step numbers)

Don’t:

  • Submit incomplete applications
  • Allow editing after submission
  • Store sensitive data in localStorage indefinitely
  • Skip validation on form submission

Resume Handling

Do:

  • Validate file size and type client-side
  • Show upload progress/feedback
  • Handle network errors gracefully
  • Use signed URLs for private access
  • Set appropriate URL expiration times

Don’t:

  • Store resumes in public buckets
  • Allow arbitrary file types
  • Keep expired signed URLs
  • Skip virus scanning (in production)

Review System

Do:

  • Verify admin role before showing review UI
  • Prevent duplicate reviews at database level
  • Track review counts for analytics
  • Provide clear scoring criteria
  • Show all relevant application information

Don’t:

  • Allow non-admins to access review endpoints
  • Skip validation of review scores
  • Allow reviews of already-reviewed applications
  • Expose applicant personally identifiable information unnecessarily

Troubleshooting

Form State Not Persisting

  1. Check browser localStorage is enabled
  2. Verify useFormFieldPersistence hook is called
  3. Check for JavaScript errors in console
  4. Clear localStorage and try again: localStorage.clear()

Resume Upload Fails

  1. Check file size (max 10 MB)
  2. Verify file type (PDF, DOC, DOCX)
  3. Check network connection
  4. Verify Supabase storage configuration
  5. Check bucket permissions

Review Page Access Denied

  1. Verify user has admin role in database
  2. Check profile table: SELECT role FROM profiles WHERE id = 'user-id'
  3. Verify middleware is running correctly
  4. Check authentication status
  1. Verify the user has created an account
  2. Check the user’s email is confirmed
  3. Verify the user exists in auth.users table
  4. Check for typos in search query

Next Steps