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
- Click the search input in the “Team Members” step
- Search by name or email address
- Select users from the dropdown (max 3)
- Team members displayed as removable tags
- 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
- Select PDF, DOC, or DOCX file (max 10 MB)
- File uploaded immediately upon selection
- Validation runs before upload:
- File size check (max 10 MB)
- File type validation (PDF, DOC, DOCX)
- Previous resume automatically replaced
- 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
- Complete all required fields in steps 1-5
- Review information in step 6
- Click “Submit Application” button
- Backend validates all data
- Application status changed to “submitted”
- LocalStorage cleared automatically
- Redirected to “Submitted” confirmation page
Submission Protection
- Cannot edit application after submission
- Attempting to access
/applyredirects 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:
-
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
-
Links & Portfolio Card
- GitHub profile link
- LinkedIn profile link
- Personal website/portfolio
- Score: 0-2 for link quality and professional presence
-
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
-
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 descendinggetStatistics(): Calculates aggregate statistics across all applications and reviewsgetReviewerAverageScores(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
- Monitor Review Progress: Track how many applications have been reviewed
- Identify Top Contributors: Recognize most active reviewers
- Quality Assurance: Analyze average scores to identify scoring patterns
- Load Balancing: See which reviewers are most active to distribute future review assignments
- 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, ... } | nullUpdate 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
- Check browser localStorage is enabled
- Verify
useFormFieldPersistencehook is called - Check for JavaScript errors in console
- Clear localStorage and try again:
localStorage.clear()
Resume Upload Fails
- Check file size (max 10 MB)
- Verify file type (PDF, DOC, DOCX)
- Check network connection
- Verify Supabase storage configuration
- Check bucket permissions
Review Page Access Denied
- Verify user has admin role in database
- Check profile table:
SELECT role FROM profiles WHERE id = 'user-id' - Verify middleware is running correctly
- Check authentication status
Team Member Not Appearing in Search
- Verify the user has created an account
- Check the user’s email is confirmed
- Verify the user exists in auth.users table
- Check for typos in search query
Next Steps
- Creating API Endpoints - Learn to build new endpoints
- Database Setup - Configure and manage the database
- Development Tips - Improve your workflow