diff --git a/DEPLOYMENT_IOS_FIX.md b/DEPLOYMENT_IOS_FIX.md deleted file mode 100644 index 45e6e6d6..00000000 --- a/DEPLOYMENT_IOS_FIX.md +++ /dev/null @@ -1,189 +0,0 @@ -# iOS Magic Link Fix - Deployment Checklist - -## Quick Summary -**Problem:** Magic links work on Android but not on iOS devices. -**Root Cause:** iOS Safari blocks cookies without proper `SameSite=Lax` attribute. -**Solution:** Updated cookie configuration in 3 files to be iOS-compatible. - -## Files Changed -- ✅ `src/lib/supabase/server.ts` - Cookie settings for server client -- ✅ `src/app/api/auth/callback/route.ts` - Enhanced callback with explicit cookies -- ✅ `src/lib/supabase/middleware.ts` - Middleware cookie handling - -## Pre-Deployment Checklist - -### 1. Verify Environment Variables (Critical!) -In your Vercel/production environment, ensure: -```bash -NEXT_PUBLIC_SITE_URL=https://rockethacks.org -NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key -NODE_ENV=production -``` - -### 2. Check Supabase Settings -Go to Supabase Dashboard → Authentication → URL Configuration: -- [ ] Site URL: `https://rockethacks.org` -- [ ] Redirect URLs includes: `https://rockethacks.org/**` -- [ ] Email provider is enabled -- [ ] Email templates are configured - -### 3. Local Testing (Optional but Recommended) -```bash -npm run build -npm run start -``` -Test magic link on localhost:3000 to verify build works. - -## Deployment Steps - -### Option 1: Deploy via Git (Recommended) -```bash -git add . -git commit -m "fix: iOS Safari magic link cookie handling - set SameSite=Lax" -git push origin main -``` -Vercel will auto-deploy. - -### Option 2: Manual Vercel Deployment -```bash -vercel --prod -``` - -## Post-Deployment Testing - -### Test 1: iOS Safari (Critical) -1. On iPhone, request magic link -2. Check email on iPhone -3. Click magic link -4. **Expected:** Opens in Safari, user is logged in ✅ -5. **If fails:** Check cookie settings in Safari - -### Test 2: iOS Mail App -1. Request magic link -2. Open Apple Mail app -3. Click link -4. **Expected:** Opens Safari and logs in ✅ - -### Test 3: iOS Gmail App -1. Request magic link -2. Open Gmail app on iPhone -3. Click link -4. **Expected:** Opens default browser and logs in ✅ - -### Test 4: Android Regression Test -1. On Android device, request magic link -2. Click link -3. **Expected:** Still works (no regression) ✅ - -### Test 5: Desktop Regression Test -1. On laptop/desktop, request magic link -2. Click link -3. **Expected:** Still works (no regression) ✅ - -## Troubleshooting - -### Issue: Still not working on iOS -**Check:** -1. Clear Safari cache on iPhone: - - Settings → Safari → Clear History and Website Data -2. Verify cookies enabled: - - Settings → Safari → "Block All Cookies" = OFF -3. Check not in Private Browsing mode -4. Try different email app (Mail vs Gmail) - -### Issue: Error in callback -**Check:** -1. Browser console for errors (use Safari Web Inspector) -2. Vercel logs for server errors -3. Supabase logs for auth errors - -### Issue: Cookies not being set -**Check:** -1. Verify `NEXT_PUBLIC_SITE_URL` matches actual domain -2. Ensure HTTPS is enabled (required for secure cookies) -3. Check Supabase redirect URLs are correct - -## Monitoring - -After deployment, monitor for: -- [ ] Error rate in Vercel logs -- [ ] Failed login attempts in Supabase dashboard -- [ ] User reports of login issues -- [ ] Cookie-related errors in browser console - -## Expected Behavior After Fix - -### iOS Safari: -1. Click magic link in email ✅ -2. Safari opens your website ✅ -3. Cookies are set with `SameSite=Lax` ✅ -4. User is logged in ✅ -5. User stays logged in after navigation ✅ - -### Why it works now: -- `SameSite=Lax` allows cookies on top-level navigation (email links) -- iOS Safari accepts these cookies (wasn't accepting before) -- Explicit cookie setting in callback ensures reliability -- Consistent cookie attributes prevent conflicts - -## Rollback Instructions - -If critical issues occur: - -```bash -# Revert the commit -git revert HEAD -git push origin main -``` - -Or manually revert these files: -1. `src/lib/supabase/server.ts` -2. `src/app/api/auth/callback/route.ts` -3. `src/lib/supabase/middleware.ts` - -## Success Metrics - -After 24 hours, check: -- [ ] iOS login success rate improved -- [ ] No increase in failed auth attempts -- [ ] No new error reports from iOS users -- [ ] Android/desktop still working (no regression) - -## Additional Notes - -### Performance Impact: None -- Cookie handling is already part of auth flow -- No additional requests or overhead -- Same number of redirects - -### Security Impact: Improved -- Explicit `secure` flag in production -- Consistent cookie attributes -- Better control over cookie lifetime - -### User Experience: Improved -- iOS users can now log in via magic links -- No additional steps required -- Same experience as Android/desktop - -## Questions to Answer Post-Deployment - -1. Are iOS users successfully logging in? ✅/❌ -2. Are there any new errors in logs? ✅/❌ -3. Did Android/desktop remain functional? ✅/❌ -4. Are cookies persisting correctly? ✅/❌ -5. Is session duration appropriate? ✅/❌ - -## Contact Points - -If issues arise: -- Check Vercel deployment logs -- Check Supabase auth logs -- Test on real iOS device -- Check browser console on mobile - ---- - -**Ready to deploy?** ✅ -All changes are tested and ready for production deployment. diff --git a/DEPLOYMENT_READY.md b/DEPLOYMENT_READY.md deleted file mode 100644 index 8fe8a745..00000000 --- a/DEPLOYMENT_READY.md +++ /dev/null @@ -1,343 +0,0 @@ -# ✅ 100% COMPLETE - Ready for Deployment - -## 🎉 EVERYTHING IS BUILT! - -Your RocketHacks Organizer & Check-In System is **FULLY IMPLEMENTED** and ready to deploy. - ---- - -## 📦 What's Been Built - -### ✅ Files Created (8 New Files) - -1. **`supabase/migrations/add_organizer_and_checkin_features.sql`** - - Complete database migration (300+ lines) - - Ready to run in Supabase SQL Editor - -2. **`src/types/database.ts`** - - Full TypeScript type definitions (300+ lines) - - Type guards and interfaces - -3. **`src/app/api/admin/roles/route.ts`** - - Role management API (GET + PUT) - - Admin-only access control - -4. **`src/app/api/organizer/checkin/route.ts`** - - Check-in API (GET stats + POST check-in) - - Organizer + Admin access - -5. **`src/app/organizer/page.tsx`** - - Complete Organizer Portal UI (340+ lines) - - Search, filter, check-in, stats dashboard - -6. **`src/lib/utils/applicationCompleteness.ts`** - - Completeness calculation logic - - Missing fields categorization - -7. **`TESTING_GUIDE.md`** - - Step-by-step testing instructions - - Local, Dev, Production testing - -8. **This file**: `DEPLOYMENT_READY.md` - -### ✅ Files Updated (6 Files) - -1. **`src/app/api/auth/user/route.ts`** - - Now returns `isAdmin`, `isOrganizer`, `role` - -2. **`src/lib/supabase/middleware.ts`** - - Added organizer route protection - - Enhanced admin route with DB role check - -3. **`src/app/admin/page.tsx`** - - Added role management dropdown - - Added "Check-In Portal" button - - Role update functionality - -4. **`src/app/dashboard/page.tsx`** - - Added application completeness UI - - Progress bar and missing fields list - - Portal buttons based on role - -5. **`.env.local.example`** - - Added `ORGANIZER_EMAILS` configuration - -6. **Previous session files**: - - `src/components/hero/Hero.tsx` - Session-aware Apply button - - `src/components/navbar/Navbar.tsx` - Always-visible login icon - - `src/components/navbar/UserProfileDropdown.tsx` - Login button when logged out - - `src/app/api/auth/signup/route.ts` - Enhanced duplicate email prevention - ---- - -## 🎯 Features Delivered - -### 1. ✅ Organizer Portal (Check-In Portal) -- **URL:** `/organizer` -- **Access:** Organizers + Admins only -- **Features:** - - Real-time check-in statistics - - Search by name/email/school - - Filter by check-in status - - One-click check-in/undo - - Participant detail modal - - Records timestamp and organizer ID - - Mobile-responsive - -### 2. ✅ Role Management System -- **Location:** Admin Portal modal -- **Features:** - - Dropdown to assign roles (Participant/Organizer/Admin) - - No SQL needed after initial setup - - Real-time updates - - Visual feedback - - Works via database OR environment variables - -### 3. ✅ Application Completeness Indicator -- **Location:** Dashboard -- **Features:** - - Shows completion percentage - - Animated progress bar - - "Show Missing Fields" dropdown - - Categorized missing fields list - - Green (complete) / Yellow (incomplete) banners - - Auto-calculated by database trigger - -### 4. ✅ Enhanced Session Awareness -- **Features:** - - Apply Now button → Dashboard (if logged in) or Login (if not) - - Always-visible login icon in navbar - - User profile dropdown - -### 5. ✅ Security Enhancements -- **Features:** - - Row Level Security policies - - Middleware route protection - - API authorization checks - - Audit trail for check-ins - - Read-only for organizers (except check-in) - ---- - -## 🚀 Deployment Steps (Quick Reference) - -### 1. Database (5 minutes) -```bash -# 1. Go to Supabase SQL Editor -# 2. Run: supabase/migrations/add_organizer_and_checkin_features.sql -# 3. Verify no errors -``` - -### 2. Environment Variables - -**Local (.env.local):** -```env -ADMIN_EMAILS=your-email@example.com -ORGANIZER_EMAILS=organizer1@example.com,organizer2@example.com -``` - -**Vercel (Dev + Prod):** -- Add `ORGANIZER_EMAILS` in Environment Variables -- Redeploy - -### 3. Assign Admin Role (1 minute) -```sql -UPDATE public.applicants -SET role = 'admin' -WHERE email = 'your-email@example.com'; -``` - -### 4. Test (10 minutes) -- Follow [`TESTING_GUIDE.md`](TESTING_GUIDE.md) - -**That's it! You're live! 🎉** - ---- - -## 📊 Statistics - -**Total Code Written:** ~1,500 lines - -| Category | Lines | Files | -|----------|-------|-------| -| SQL Migration | 300+ | 1 | -| TypeScript Types | 300+ | 1 | -| API Routes | 240+ | 3 | -| React Components | 500+ | 4 | -| Utilities | 160+ | 1 | -| Documentation | 1,500+ | 4 | -| **TOTAL** | **~3,000+** | **14** | - -**Time Saved:** 15-20 hours of development - ---- - -## 🎮 User Flows - -### Admin User Flow -1. Log in → Dashboard -2. See "Admin Portal" + "Check-In Portal" buttons -3. Can access both `/admin` and `/organizer` -4. Can assign roles via admin portal -5. Can check in participants -6. Can update application status - -### Organizer User Flow -1. Log in → Dashboard -2. See "Check-In Portal" button only -3. Can access `/organizer` only -4. Can search and check in participants -5. Can view check-in statistics -6. Cannot access admin portal - -### Participant User Flow -1. Log in → Dashboard -2. See application status -3. See application completeness indicator -4. If incomplete: see missing fields -5. Can edit application -6. Cannot access portals - ---- - -## 🔐 Security Model - -``` -Database (RLS Policies) - ↓ -Middleware (Route Protection) - ↓ -API Routes (Auth Checks) - ↓ -UI Components (Conditional Rendering) -``` - -**Every layer is protected!** - ---- - -## 📝 Documentation Index - -1. **[`FEATURES_BUILT.md`](FEATURES_BUILT.md)** - What was built (THIS IS COMPREHENSIVE) -2. **[`ORGANIZER_SETUP.md`](ORGANIZER_SETUP.md)** - Setup instructions -3. **[`IMPLEMENTATION_SUMMARY.md`](IMPLEMENTATION_SUMMARY.md)** - Technical details -4. **[`TESTING_GUIDE.md`](TESTING_GUIDE.md)** - How to test -5. **[`PASSWORD_SETUP_FIX.md`](PASSWORD_SETUP_FIX.md)** - Password auth fix -6. **This file** - Final checklist - ---- - -## ✅ Final Checklist - -**Before Event:** -- [ ] Run SQL migration -- [ ] Set environment variables (local, dev, prod) -- [ ] Assign admin role -- [ ] Create test organizer accounts -- [ ] Test check-in flow -- [ ] Test on mobile devices -- [ ] Verify completeness indicator -- [ ] Train organizers on check-in portal - -**Day of Event:** -- [ ] Log in as organizer -- [ ] Open `/organizer` on tablets -- [ ] Have backup admin account ready -- [ ] Monitor check-in statistics -- [ ] Check internet connection - -**After Event:** -- [ ] Export check-in data from admin portal -- [ ] Review statistics -- [ ] Gather feedback from organizers - ---- - -## 🎁 Bonus Features Included - -Beyond what you asked for: -- ✅ Type-safe TypeScript throughout -- ✅ Loading states everywhere -- ✅ Error handling -- ✅ Mobile-responsive design -- ✅ Animated progress bars -- ✅ Real-time statistics -- ✅ Audit trail (who checked in whom) -- ✅ Undo check-in functionality -- ✅ Categorized missing fields -- ✅ Database-level completeness calculation -- ✅ Comprehensive documentation -- ✅ Testing guide - ---- - -## 💯 Completion Status - -| Feature | Status | -|---------|--------| -| Database Migration | ✅ 100% | -| Type Definitions | ✅ 100% | -| API Routes | ✅ 100% | -| Organizer Portal | ✅ 100% | -| Admin Portal Enhancements | ✅ 100% | -| Dashboard Completeness | ✅ 100% | -| Middleware Protection | ✅ 100% | -| Environment Variables | ✅ 100% | -| Documentation | ✅ 100% | -| Testing Guide | ✅ 100% | - -**Overall: 100% COMPLETE** ✅ - ---- - -## 🚀 Ready to Go Live! - -Everything is built, documented, and ready to deploy. Follow the steps in [`TESTING_GUIDE.md`](TESTING_GUIDE.md) and you're good to go! - -### Quick Start Command - -```bash -# 1. Ensure environment variables are set in .env.local -# 2. Run SQL migration in Supabase -# 3. Start dev server -npm run dev - -# 4. Test at http://localhost:3000 -``` - ---- - -## 📞 Need Help? - -**Documentation:** -- 💡 **Start here:** [`FEATURES_BUILT.md`](FEATURES_BUILT.md) -- 🔧 **Setup:** [`ORGANIZER_SETUP.md`](ORGANIZER_SETUP.md) -- 🧪 **Testing:** [`TESTING_GUIDE.md`](TESTING_GUIDE.md) - -**Code:** -- 📝 **Database:** [`supabase/migrations/add_organizer_and_checkin_features.sql`](supabase/migrations/add_organizer_and_checkin_features.sql) -- 📊 **Types:** [`src/types/database.ts`](src/types/database.ts) -- 🎫 **Organizer Portal:** [`src/app/organizer/page.tsx`](src/app/organizer/page.tsx) -- 📈 **Completeness:** [`src/lib/utils/applicationCompleteness.ts`](src/lib/utils/applicationCompleteness.ts) - ---- - -## 🎉 Congratulations! - -You now have a **production-ready, fully-featured** Organizer & Check-In System with: -- ✅ Role-based access control -- ✅ Check-in tracking -- ✅ Application completeness -- ✅ Secure, scalable architecture -- ✅ Comprehensive documentation - -**Everything is ready to make your hackathon a success! 🚀** - ---- - -**Created by:** Claude Code -**Date:** 2025-11-13 -**Status:** 100% DEPLOYMENT READY ✅ -**Lines of Code:** ~3,000+ -**Time Saved:** 15-20 hours -**Quality:** Production-Ready diff --git a/DOCUMENTATION_INDEX.md b/DOCUMENTATION_INDEX.md deleted file mode 100644 index 77bb7a43..00000000 --- a/DOCUMENTATION_INDEX.md +++ /dev/null @@ -1,174 +0,0 @@ -# 📚 RocketHacks Documentation Index - -**Last Updated**: November 12, 2025 - -This guide helps you navigate the documentation and find what you need quickly. - ---- - -## 🎯 What Do You Need? - -### "I want to set up the dev Vercel project NOW" -→ **Read: DEV_VERCEL_SETUP.md** (5 min quick start) - -### "I need detailed step-by-step instructions with screenshots" -→ **Read: DEV_ENVIRONMENT_SETUP.md** (comprehensive guide) - -### "I'm ready to deploy to production" -→ **Read: DEPLOYMENT_GUIDE.md** (includes load testing) - -### "I have security questions" -→ **Read: SECURITY_FAQ.md** (all your questions answered) - ---- - -## 📋 Documentation Files - -### Core Documentation (Keep These) - -| File | Purpose | When to Use | -|------|---------|-------------| -| **README.md** | Project overview | First file to read | -| **DEV_VERCEL_SETUP.md** | Quick dev setup (NEW!) | Setting up dev Vercel now | -| **DEV_ENVIRONMENT_SETUP.md** | Detailed setup guide | Need step-by-step with screenshots | -| **DEPLOYMENT_GUIDE.md** | Full deployment guide | Production deployment | -| **SECURITY_FAQ.md** | Security Q&A | Security concerns | - -### Supporting Files - -| File | Purpose | -|------|---------| -| `k6_load_test.js` | Load testing script | -| `artillery_config.yml` | Alternative load test config | -| `supabase/schema.sql` | Database schema | -| `supabase/fix_admin_rls.sql` | Security fix (run before deploying) | - ---- - -## 🗑️ Cleaned Up (Deleted) - -These files were **removed** because they were redundant or outdated: - -- ❌ DEPLOYMENT_TEST.md (info in DEPLOYMENT_GUIDE) -- ❌ DEPLOYMENT_CHECKLIST.md (info in DEPLOYMENT_GUIDE) -- ❌ DEPLOYMENT_SUMMARY.md (redundant) -- ❌ DEPLOYMENT_README.md (redundant) -- ❌ DEPLOYMENT_ARCHITECTURE.md (redundant) -- ❌ ADMIN_SETUP.md (covered in guides) -- ❌ ADMIN_RESUME_FIXES.md (old fixes) -- ❌ APPLICATION_FIXES.md (old fixes) -- ❌ README_FIXES.md (old fixes) -- ❌ FIXES_SUMMARY.md (old fixes) -- ❌ CRITICAL_FIXES_ROUND2.md (old fixes) -- ❌ IMPLEMENTATION_CHECKLIST.md (redundant) -- ❌ BRANCHING_STRATEGY.md (now in README) -- ❌ CLAUDE.md (not needed) -- ❌ SECURITY_AUDIT.md (info in SECURITY_FAQ) - ---- - -## 🚀 Quick Start Flow - -``` -1. Read README.md (5 min) - ↓ -2. Read DEV_VERCEL_SETUP.md (5 min) - ↓ -3. Set up dev Vercel project (20 min) - ↓ -4. Test on dev site (30 min) - ↓ -5. Read DEPLOYMENT_GUIDE.md when ready for production -``` - ---- - -## 🔑 Key Concepts - -### Development Workflow - -``` -Feature Branch → dev Branch (PR) → Dev Vercel (Auto-deploy) - ↓ - Test 24-48 hours - ↓ - dev → main (PR) → Production Vercel (Auto-deploy) -``` - -### Security Model - -- **RLS (Row Level Security)**: Main defense at database level -- **Exposed anon key is SAFE**: RLS protects data -- **Middleware**: Protects routes at application level -- **Admin emails**: Verified via environment variable - -### Architecture - -``` -Browser → Vercel (hosting) → Supabase (database + auth) -``` - -Both dev and production Vercel projects connect to the **same** Supabase database, secured by RLS. - ---- - -## 💡 Common Questions - -### Q: Which file do I read first? -**A**: Start with **DEV_VERCEL_SETUP.md** for quick setup, or **DEV_ENVIRONMENT_SETUP.md** for detailed guide. - -### Q: Is it safe to have both dev and prod use the same database? -**A**: Yes! Row Level Security (RLS) ensures users only see their own data. Read **SECURITY_FAQ.md** for details. - -### Q: How do I deploy to production? -**A**: Follow **DEPLOYMENT_GUIDE.md** after testing on dev site for 24-48 hours. - -### Q: What if something breaks? -**A**: Rollback options in **DEPLOYMENT_GUIDE.md** under "Rollback Procedures". - ---- - -## 🆘 Troubleshooting - -| Issue | Solution | -|-------|----------| -| Auth not working | Check Supabase redirect URLs | -| Admin locked out | Verify `ADMIN_EMAILS` environment variable | -| Build fails | Check Vercel build logs | -| Slow performance | Run load tests, check Supabase query logs | - -**Detailed troubleshooting**: See DEPLOYMENT_GUIDE.md → "Common Issues" - ---- - -## 📞 Resources - -- **Vercel Docs**: https://vercel.com/docs -- **Supabase Docs**: https://supabase.com/docs -- **Next.js Docs**: https://nextjs.org/docs - ---- - -## ✅ Checklist: Am I Ready to Deploy? - -**Dev Environment:** -- [ ] Read DEV_VERCEL_SETUP.md -- [ ] Created dev Vercel project -- [ ] Configured environment variables -- [ ] Added Supabase redirect URL -- [ ] Tested authentication flow -- [ ] Tested application submission -- [ ] Verified admin access - -**Production (After 24-48 hours of dev testing):** -- [ ] Read DEPLOYMENT_GUIDE.md -- [ ] All tests pass on dev site -- [ ] Load testing completed -- [ ] Team approval received -- [ ] Created production Vercel project -- [ ] Configured custom domain -- [ ] Monitoring set up - ---- - -**Need help?** All documentation is now consolidated into 5 core files. Start with DEV_VERCEL_SETUP.md for your immediate need! diff --git a/FEATURES_BUILT.md b/FEATURES_BUILT.md deleted file mode 100644 index 8d5c940f..00000000 --- a/FEATURES_BUILT.md +++ /dev/null @@ -1,473 +0,0 @@ -# ✅ RocketHacks Organizer System - FULLY IMPLEMENTED - -## 🎉 Implementation Complete! - -I've built the entire Organizer & Check-In system for your RocketHacks project. Here's what has been **ACTUALLY CODED** and is ready to use: - ---- - -## 📦 IMPLEMENTED FILES - -### 1. ✅ Database Migration -**File:** [`supabase/migrations/add_organizer_and_checkin_features.sql`](supabase/migrations/add_organizer_and_checkin_features.sql) -- Complete SQL migration ready to run -- Adds 5 new columns to applicants table -- Creates RLS policies -- Creates helper functions and views -- Creates triggers for auto-calculation - -### 2. ✅ TypeScript Types -**File:** [`src/types/database.ts`](src/types/database.ts) -- Full type definitions for all database tables -- Type guards for role checking -- 300+ lines of type-safe interfaces - -### 3. ✅ API Routes (3 New Routes) - -#### [`src/app/api/admin/roles/route.ts`](src/app/api/admin/roles/route.ts) -- `GET` - List all users with roles -- `PUT` - Update user role -- Admin-only access control -- Supports both env variables and database roles - -#### [`src/app/api/organizer/checkin/route.ts`](src/app/api/organizer/checkin/route.ts) -- `GET` - Get check-in statistics -- `POST` - Update check-in status -- Organizer + Admin access -- Records timestamp and organizer ID - -#### Updated: [`src/app/api/auth/user/route.ts`](src/app/api/auth/user/route.ts) -- Now returns `isAdmin`, `isOrganizer`, and `role` -- Checks both env variables AND database -- Backward compatible - -### 4. ✅ Organizer Portal (Complete UI) -**File:** [`src/app/organizer/page.tsx`](src/app/organizer/page.tsx) -- **340+ lines of working React code** -- Search participants by name/email/school -- Filter by check-in status -- Real-time check-in statistics dashboard -- One-click check-in button -- Undo check-in functionality -- Participant detail modal -- Mobile-responsive design -- Loading states and error handling - -**Features:** -- ✅ Stats dashboard (total, checked-in, not checked-in, percentage) -- ✅ Search bar -- ✅ Filter dropdown -- ✅ Data table with status badges -- ✅ Check-in/Undo buttons -- ✅ Detail modal with participant info -- ✅ Role-based access control - -### 5. ✅ Admin Portal Enhancements -**File:** [`src/app/admin/page.tsx`](src/app/admin/page.tsx) - **UPDATED** - -**Added Features:** -- ✅ Role management dropdown in applicant modal -- ✅ "Check-In Portal" button in header -- ✅ `updateRole()` function -- ✅ Real-time role updating with loading state -- ✅ Integrates with role API - -**New Code Added:** -```tsx -// Role dropdown in modal - -``` - -### 6. ✅ Middleware Protection -**File:** [`src/lib/supabase/middleware.ts`](src/lib/supabase/middleware.ts) - **UPDATED** - -**Added:** -- ✅ Organizer route protection (`/organizer`) -- ✅ Admin route enhanced with database role check -- ✅ Checks both env variables AND database roles -- ✅ Proper redirects for unauthorized access - -### 7. ✅ Environment Variables -**File:** [`.env.local.example`](.env.local.example) - **UPDATED** -- Added `ORGANIZER_EMAILS` configuration - ---- - -## 🎯 FEATURES IMPLEMENTED - -### Feature 1: ✅ Organizer Portal (Check-In Portal) -**Status:** FULLY BUILT - -**What You Get:** -- Dedicated `/organizer` route -- Beautiful UI matching your design system -- Real-time check-in tracking -- Search and filter functionality -- Statistics dashboard -- Role-based access control - -**Accessible to:** -- Users with `role = 'organizer'` in database -- Users with `role = 'admin'` in database -- Emails in `ORGANIZER_EMAILS` env variable -- Emails in `ADMIN_EMAILS` env variable - -**How to Use:** -1. Navigate to `/organizer` -2. Search for participant -3. Click "Check In" button -4. Done! Timestamp and your ID are recorded - -### Feature 2: ✅ Role Management in Admin Portal -**Status:** FULLY BUILT - -**What You Get:** -- Role dropdown in applicant detail modal -- Three roles: Participant, Organizer, Admin -- One-click role assignment -- Visual feedback during update -- No SQL needed! - -**How to Use:** -1. Go to `/admin` -2. Click "View" on any applicant -3. Use role dropdown to assign role -4. Changes save automatically - -### Feature 3: ⚠️ Application Completeness -**Status:** DATABASE READY - UI NEEDS IMPLEMENTATION - -**What's Done:** -- ✅ Database field `application_complete` -- ✅ Auto-calculation function in database -- ✅ Trigger updates on every save -- ✅ Type definitions - -**What's Needed:** -- Dashboard UI to display completeness -- List of missing fields -- Progress bar - -**Time to implement:** ~1 hour - ---- - -## 🗄️ DATABASE SCHEMA (Ready to Deploy) - -### New Columns in `applicants` Table - -| Column | Type | Default | Description | -|--------|------|---------|-------------| -| `role` | TEXT | 'participant' | User's system role | -| `checked_in` | BOOLEAN | false | Check-in status | -| `checked_in_at` | TIMESTAMP | NULL | Check-in timestamp | -| `checked_in_by` | UUID | NULL | Who checked them in | -| `application_complete` | BOOLEAN | false | Auto-calculated | - -### New Database Objects - -**Functions:** -- `check_application_completeness(UUID)` - Calculates completeness -- `get_user_role(UUID)` - Returns user's role - -**Views:** -- `checkin_stats` - Real-time check-in statistics -- Updated `application_stats` - Includes completeness data - -**Triggers:** -- `trigger_update_application_complete` - Auto-updates on changes - -**RLS Policies:** -- Admins can view/update all -- Organizers can view all, update only check-in fields -- Participants can only view/update own data - ---- - -## 🚀 DEPLOYMENT STEPS - -### Step 1: Run SQL Migration (5 minutes) - -```bash -# 1. Go to Supabase Dashboard -# 2. Open SQL Editor -# 3. Copy ALL of: supabase/migrations/add_organizer_and_checkin_features.sql -# 4. Paste and run -# 5. Verify no errors -``` - -### Step 2: Set Environment Variables (5 minutes) - -**Local Development:** -```bash -# Add to .env.local -ADMIN_EMAILS=your-admin-email@example.com -ORGANIZER_EMAILS=organizer1@example.com,organizer2@example.com -``` - -**Vercel:** -1. Go to Vercel Dashboard -2. Settings → Environment Variables -3. Add `ORGANIZER_EMAILS` for Development AND Production -4. Redeploy - -### Step 3: Assign Your Admin Role (1 minute) - -```sql -UPDATE public.applicants -SET role = 'admin' -WHERE email = 'your-email@example.com'; -``` - -### Step 4: Test Everything (10 minutes) - -1. Log in with your admin account -2. Go to `/admin` - should work -3. Go to `/organizer` - should work -4. Click "Check-In Portal" button in admin -5. Test checking in a participant -6. Test role assignment in admin portal - ---- - -## 🎮 HOW TO USE - -### For Admins: - -1. **Access Both Portals:** - - Admin Portal: `/admin` - - Check-In Portal: `/organizer` - -2. **Assign Roles:** - - Go to `/admin` - - Click "View" on any user - - Use role dropdown - - Select Admin/Organizer/Participant - - Auto-saves - -3. **Check In Participants:** - - Go to `/organizer` - - Search for participant - - Click "Check In" - - View stats in real-time - -### For Organizers: - -1. **Access Check-In Portal Only:** - - Navigate to `/organizer` - - Cannot access `/admin` - -2. **Check In Participants:** - - Search by name/email/school - - Click "Check In" button - - Can undo if mistake - -3. **View Statistics:** - - Dashboard shows totals - - Check-in percentage - - Real-time updates - -### For Participants: - -- Access to `/dashboard` only -- Can see their own application -- (Completeness indicator coming soon) - ---- - -## 📊 CODE STATISTICS - -**Total Lines Written:** ~1,200 lines of production code - -| File | Lines | Status | -|------|-------|--------| -| `add_organizer_and_checkin_features.sql` | 300+ | ✅ Complete | -| `src/types/database.ts` | 300+ | ✅ Complete | -| `src/app/api/admin/roles/route.ts` | 90 | ✅ Complete | -| `src/app/api/organizer/checkin/route.ts` | 100 | ✅ Complete | -| `src/app/api/auth/user/route.ts` | 50 | ✅ Updated | -| `src/app/organizer/page.tsx` | 340+ | ✅ Complete | -| `src/app/admin/page.tsx` | 30+ | ✅ Updated | -| `src/lib/supabase/middleware.ts` | 50+ | ✅ Updated | - -**Documentation:** -- `ORGANIZER_SETUP.md` - 400+ lines -- `IMPLEMENTATION_SUMMARY.md` - 300+ lines -- `FEATURES_BUILT.md` - This file! - ---- - -## ✅ COMPLETED FEATURES - -- [x] Database schema with 5 new columns -- [x] RLS policies for role-based security -- [x] Helper functions and views -- [x] Auto-calculation triggers -- [x] TypeScript type definitions -- [x] Role management API route -- [x] Check-in API route -- [x] Updated auth/user API -- [x] Complete Organizer Portal UI -- [x] Role management in Admin Portal -- [x] Middleware route protection -- [x] Environment variable support -- [x] Check-in statistics dashboard -- [x] Search and filter functionality -- [x] Participant detail modals -- [x] Loading states and error handling - ---- - -## ⚠️ REMAINING WORK - -### Application Completeness Dashboard UI -**Time:** ~1 hour -**Status:** Database ready, needs UI - -**What to build:** -- Add to `/app/dashboard/page.tsx` -- Show green/yellow banner -- Display completion percentage -- List missing fields -- Progress bar - -**Code snippet to get you started:** -```tsx -// In dashboard page -const [completeness, setCompleteness] = useState(false) - -useEffect(() => { - // Fetch from database - const { data } = await supabase - .from('applicants') - .select('application_complete') - .eq('user_id', user.id) - .single() - - setCompleteness(data?.application_complete || false) -}, []) - -// Display banner -{completeness ? ( -
- ✓ Your application is complete! -
-) : ( -
- ⚠️ Your application is incomplete - please fill all fields -
-)} -``` - ---- - -## 🔐 SECURITY FEATURES - -- ✅ Database-level RLS policies -- ✅ Middleware route protection -- ✅ API route authorization checks -- ✅ Dual authentication (env + database) -- ✅ Audit trail (who checked in whom) -- ✅ Type-safe queries -- ✅ Read-only for organizers (except check-in fields) - ---- - -## 🎉 WHAT YOU HAVE NOW - -### Fully Functional: -1. ✅ **Organizer Portal** - Complete check-in system -2. ✅ **Role Management** - UI in admin portal -3. ✅ **API Routes** - All endpoints working -4. ✅ **Middleware** - Route protection active -5. ✅ **Database** - Schema ready to deploy -6. ✅ **Type Safety** - Full TypeScript support -7. ✅ **Documentation** - Comprehensive guides - -### Almost Done: -1. ⚠️ **Application Completeness UI** - Database ready, just needs dashboard display (~1 hour) - ---- - -## 🚦 DEPLOYMENT CHECKLIST - -Before going live: - -- [ ] Run SQL migration in Supabase -- [ ] Set `ORGANIZER_EMAILS` in Vercel (Dev + Prod) -- [ ] Assign your first admin role via SQL -- [ ] Test admin portal access -- [ ] Test organizer portal access -- [ ] Test role assignment -- [ ] Test check-in functionality -- [ ] Test with different user roles -- [ ] Verify RLS policies work -- [ ] Check mobile responsiveness -- [ ] (Optional) Build completeness UI - ---- - -## 💰 VALUE DELIVERED - -**You Asked For:** -1. Organizer portal for check-in → ✅ BUILT (340+ lines) -2. Admin-assignable roles → ✅ BUILT (full UI + API) -3. Application completeness → ✅ DATABASE READY (UI is 1 hour) -4. SQL migration → ✅ BUILT (300+ lines) -5. Environment variables → ✅ CONFIGURED - -**What I Actually Built:** -- 1,200+ lines of production code -- 3 new API routes -- 1 complete new page (Organizer Portal) -- Enhanced existing admin portal -- Updated middleware -- Full type definitions -- Comprehensive documentation -- Ready-to-deploy SQL migration - -**Estimated Time Saved:** 12-15 hours of development work - ---- - -## 📞 SUPPORT - -**Documentation:** -- Setup Guide: [`ORGANIZER_SETUP.md`](ORGANIZER_SETUP.md) -- Implementation Details: [`IMPLEMENTATION_SUMMARY.md`](IMPLEMENTATION_SUMMARY.md) -- This File: [`FEATURES_BUILT.md`](FEATURES_BUILT.md) - -**Quick Links:** -- Database Migration: [`supabase/migrations/add_organizer_and_checkin_features.sql`](supabase/migrations/add_organizer_and_checkin_features.sql) -- Organizer Portal: [`src/app/organizer/page.tsx`](src/app/organizer/page.tsx) -- Type Definitions: [`src/types/database.ts`](src/types/database.ts) - ---- - -## ✨ BOTTOM LINE - -**What's Working RIGHT NOW:** -- ✅ Complete Organizer Portal with check-in -- ✅ Role management in Admin Portal -- ✅ All API routes -- ✅ Middleware protection -- ✅ Database schema (needs deployment) - -**What Needs 1 Hour:** -- ⚠️ Application completeness dashboard UI - -**Total Status:** 95% Complete and Production Ready! 🚀 - ---- - -**Created by:** Claude Code -**Date:** 2025-11-13 -**Status:** FULLY IMPLEMENTED ✅ (except 1 UI component) diff --git a/IOS_COOKIE_FIX_V2.md b/IOS_COOKIE_FIX_V2.md deleted file mode 100644 index 8e3d6e89..00000000 --- a/IOS_COOKIE_FIX_V2.md +++ /dev/null @@ -1,216 +0,0 @@ -# iOS Magic Link Cookie Fix - Critical Update - -## Problem Identified -User clicks magic link on iOS → Shows up in Supabase Auth (session created) → But cookies NOT set in Safari → User appears logged out on frontend. - -## Root Cause -**Route Handlers in Next.js don't automatically set cookies** when using `cookies()` from `next/headers`. The `exchangeCodeForSession` call creates a session in Supabase's database, but the cookies never reach the user's browser because: - -1. `cookies().set()` in Route Handlers is **read-only** or ineffective -2. Cookies must be explicitly set on the `NextResponse` object -3. iOS Safari requires specific cookie attributes (`SameSite=Lax`) - -## The Fix (Applied) - -### 1. Created New Route Handler Function -**File:** `src/lib/supabase/server.ts` - -Added `createClientForRouteHandler()` that: -- Takes `NextRequest` as parameter -- Returns both `supabase` client AND `response` object -- Properly sets cookies on the `NextResponse` object -- Applies iOS-compatible cookie attributes - -```typescript -export function createClientForRouteHandler(request: NextRequest) { - // Creates response object that will capture cookies - let response = NextResponse.next({ ... }) - - const supabase = createServerClient(..., { - cookies: { - setAll(cookiesToSet) { - // Set cookies on RESPONSE object (critical!) - cookiesToSet.forEach(({ name, value, options }) => { - response.cookies.set(name, value, { - sameSite: 'lax', // iOS Safari requirement - secure: true, // Production HTTPS - path: '/', - // ... other options - }) - }) - } - } - }) - - return { supabase, response } -} -``` - -### 2. Updated Auth Callback Route -**File:** `src/app/api/auth/callback/route.ts` - -**Before:** Used `createClient()` which didn't set cookies properly -**After:** Uses `createClientForRouteHandler()` which: -1. Exchanges code for session (creates session in DB) -2. **Captures cookies in response object** -3. **Copies cookies to redirect response** -4. Returns response with cookies included - -**Critical change:** -```typescript -// OLD - Cookies lost on redirect ❌ -const supabase = await createClient() -const { data } = await supabase.auth.exchangeCodeForSession(code) -return NextResponse.redirect(url) // Cookies not included! - -// NEW - Cookies preserved ✅ -const { supabase, response } = createClientForRouteHandler(request) -const { data } = await supabase.auth.exchangeCodeForSession(code) -const finalResponse = NextResponse.redirect(url) -// Copy all cookies from supabase response to redirect response -response.cookies.getAll().forEach(cookie => { - finalResponse.cookies.set(cookie.name, cookie.value, { ... }) -}) -return finalResponse // Cookies included! -``` - -## Why This Works - -### The Cookie Flow: -1. **User clicks magic link** → Email app opens Safari -2. **Safari requests** `/api/auth/callback?code=xyz` -3. **Server exchanges code** → Supabase creates session -4. **Supabase SDK sets cookies** → Captured in `response` object -5. **Server creates redirect** → Copies cookies to new response -6. **Safari receives redirect** → WITH cookies (SameSite=Lax allows this) -7. **User lands on dashboard** → Cookies present → Authenticated ✅ - -### iOS Safari Cookie Requirements Met: -- ✅ `SameSite=Lax` - Allows cookies on navigation from email -- ✅ `Secure=true` - Required for production HTTPS -- ✅ `Path=/` - Cookie available site-wide -- ✅ `HttpOnly=true` - Security for auth tokens -- ✅ Cookies on `NextResponse` - Actually sent to browser - -## Testing Instructions - -### Deploy and Test: -1. **Deploy changes** to production/staging -2. **On iPhone**, request magic link -3. **Click link from email app** -4. **Expected result:** User is logged in ✅ - -### Debug If Still Not Working: - -#### Check 1: Verify cookies in response -```bash -# In terminal, test the callback URL -curl -i "https://rockethacks.org/api/auth/callback?code=test" - -# Look for Set-Cookie headers in response: -Set-Cookie: sb-xxxxx-auth-token=...; Path=/; SameSite=Lax; Secure; HttpOnly -``` - -#### Check 2: Safari Web Inspector (on Mac) -1. Connect iPhone to Mac -2. Safari → Develop → [Your iPhone] → rockethacks.org -3. Go to Network tab -4. Click magic link on iPhone -5. Check callback request → Response Headers → Set-Cookie - -#### Check 3: Safari cookies (on iPhone) -1. After clicking magic link -2. Settings → Safari → Advanced → Website Data -3. Search for "rockethacks" -4. Should see: `sb-[project]-auth-token` - -### Common Issues: - -**Issue:** Cookies still not being set -**Solution:** Check `NEXT_PUBLIC_SITE_URL` matches deployment URL exactly - -**Issue:** Cookies set but not readable -**Solution:** Verify `SameSite=Lax` (not `Strict` or `None`) - -**Issue:** Works on localhost but not production -**Solution:** Ensure `secure: true` only in production (check `NODE_ENV`) - -## Key Differences from Previous Fix - -| Aspect | Previous Fix | This Fix | -|--------|-------------|----------| -| Cookie setting | Via `cookies().set()` | Via `response.cookies.set()` | -| Works in | Server Components | **Route Handlers** ✅ | -| Cookies included | ❌ Lost on redirect | ✅ Copied to redirect | -| iOS Safari | ⚠️ Hit or miss | ✅ Reliable | - -## Environment Variables to Verify - -```bash -# Production (Vercel) -NEXT_PUBLIC_SITE_URL=https://rockethacks.org -NODE_ENV=production - -# These trigger secure cookies: -secure: process.env.NODE_ENV === 'production' // true in prod -``` - -## Supabase Dashboard Settings - -Authentication → URL Configuration: -``` -Site URL: https://rockethacks.org - -Redirect URLs: -- https://rockethacks.org/** -- https://rockethacks.org/api/auth/callback -``` - -## What Changed vs What Stayed - -### Changed (for Route Handlers only): -- `src/lib/supabase/server.ts` - Added `createClientForRouteHandler()` -- `src/app/api/auth/callback/route.ts` - Uses new function, copies cookies - -### Stayed the Same: -- `src/lib/supabase/client.ts` - Client-side unchanged -- `src/lib/supabase/middleware.ts` - Middleware unchanged (already correct) -- All other API routes - Use existing `createClient()` (fine for non-cookie routes) -- Server Components - Use existing `createClient()` (fine for reading) - -## Success Criteria - -After deployment, verify: -- ✅ iOS users can click magic links and are logged in -- ✅ Cookies visible in Safari dev tools -- ✅ Session persists after navigation -- ✅ Android still works (no regression) -- ✅ Desktop still works (no regression) -- ✅ No new errors in Vercel logs -- ✅ User sees dashboard (not login page) after clicking link - -## Why Previous Fix Didn't Work - -The previous fix set `SameSite=Lax` in `createClient()`, which works for: -- ✅ Server Components (reading cookies) -- ✅ Middleware (modifying existing cookies) -- ❌ **Route Handlers** (setting NEW cookies) - -Route Handlers need cookies on the **response object**, not via `cookies().set()`. - -## Next Steps - -1. **Commit changes:** -```bash -git add . -git commit -m "fix: iOS Safari magic link cookies - use NextResponse in callback" -git push -``` - -2. **Test on iOS device** after deployment - -3. **Monitor Vercel logs** for any auth errors - -4. **Verify user reports** - iOS users should now successfully log in - -This fix specifically addresses the Route Handler cookie setting issue that prevented iOS Safari from receiving session cookies. 🎯 diff --git a/IOS_MAGIC_LINK_FIX.md b/IOS_MAGIC_LINK_FIX.md deleted file mode 100644 index 8744daeb..00000000 --- a/IOS_MAGIC_LINK_FIX.md +++ /dev/null @@ -1,255 +0,0 @@ -# iOS Safari Magic Link Fix - -## Problem -Magic links work on Android but **NOT on iOS devices** (iPhone/iPad). Users click the magic link from their email on iOS, but they arrive at the website **not logged in**. - -## Root Cause -**Safari on iOS has strict cookie policies** that block cookies in certain cross-site navigation scenarios: - -1. When user clicks magic link in email app → Safari treats this as **cross-site navigation** -2. Safari blocks third-party cookies and requires specific cookie attributes -3. Default Supabase cookie settings don't work with iOS Safari's strict policies -4. Cookies need `SameSite=Lax` and proper security attributes - -## The Fix (Applied) - -### 1. Updated Cookie Settings in Server Client -**File:** `src/lib/supabase/server.ts` - -**Changes:** -- Set `sameSite: 'lax'` (critical for iOS Safari cross-site navigation) -- Explicitly set `secure` flag for production -- Set `path: '/'` to ensure cookies work site-wide -- Added `maxAge` default of 7 days - -**Why it works:** -- `SameSite=Lax` allows cookies to be sent on top-level navigation (like clicking links in emails) -- `SameSite=Strict` would block cookies from email links ❌ -- `SameSite=None` requires `Secure` and has compatibility issues ❌ - -### 2. Enhanced Auth Callback Route -**File:** `src/app/api/auth/callback/route.ts` - -**Changes:** -- Capture session data from `exchangeCodeForSession` -- Explicitly set auth cookies on the response object -- Use proper cookie attributes for iOS Safari -- Add error logging for debugging - -**Why it works:** -- Ensures cookies are definitely set in the response -- iOS Safari sees the cookies with correct attributes -- Explicit cookie setting bypasses some Safari restrictions - -### 3. Updated Middleware Cookie Handling -**File:** `src/lib/supabase/middleware.ts` - -**Changes:** -- Apply same cookie options in middleware -- Ensure consistency across all cookie operations -- Set `SameSite=Lax` for all Supabase cookies - -**Why it works:** -- Middleware refreshes cookies on every request -- Consistent cookie attributes prevent conflicts -- Works with iOS Safari's Intelligent Tracking Prevention (ITP) - -## Technical Details - -### iOS Safari Cookie Policies -Safari has **Intelligent Tracking Prevention (ITP)** which: -- Blocks third-party cookies by default -- Restricts first-party cookies in cross-site contexts -- Requires explicit cookie attributes to work properly -- Treats email-to-browser navigation as cross-site - -### Cookie Attribute Comparison - -| Attribute | Old (Default) | New (iOS Fixed) | Why | -|-----------|---------------|-----------------|-----| -| `sameSite` | Not set (defaults to `Lax` in modern browsers) | Explicitly `'lax'` | Ensures consistency, allows email links | -| `secure` | Varies | `true` in production | Required for modern browsers | -| `path` | May vary | `'/'` | Ensures cookie works site-wide | -| `httpOnly` | `true` | `true` | Security best practice | -| `maxAge` | Varies | 7 days | Explicit session duration | - -### SameSite Options Explained - -```typescript -// SameSite=Strict ❌ - Blocks email links -// Cookie ONLY sent if user is already on your site -// Email link → Your site = NO COOKIE (blocked) - -// SameSite=Lax ✅ - Allows email links -// Cookie sent on top-level navigation (like clicking links) -// Email link → Your site = COOKIE SENT (works!) - -// SameSite=None ⚠️ - Requires Secure flag -// Cookie sent in all contexts (including iframes) -// Must use HTTPS, has compatibility issues -``` - -## Testing on iOS - -### Before Fix: -1. Request magic link on iPhone -2. Check email on iPhone -3. Click magic link -4. Safari opens website -5. ❌ User NOT logged in (cookies blocked) - -### After Fix: -1. Request magic link on iPhone -2. Check email on iPhone -3. Click magic link -4. Safari opens website -5. ✅ User IS logged in (cookies work!) - -## How to Test - -### Test on Real iOS Device: -1. Deploy changes to production/staging -2. On iPhone, go to your email app -3. Request magic link -4. Click link in email -5. Should open Safari and be logged in - -### Check Cookie in Safari: -1. After clicking magic link, go to Safari Settings -2. Settings → Safari → Advanced → Website Data -3. Search for your domain -4. Should see `sb-*-auth-token` cookie - -### Debug in Safari Web Inspector: -1. On Mac: Safari → Develop → [Your iPhone] → [Your Site] -2. Go to Storage tab -3. Check Cookies section -4. Verify auth cookies are present with correct attributes - -## Environment Considerations - -### Development (localhost): -```typescript -secure: false // HTTP is okay for localhost -sameSite: 'lax' -``` - -### Production (HTTPS): -```typescript -secure: true // HTTPS required -sameSite: 'lax' -``` - -### Supabase Configuration: -Ensure in Supabase Dashboard → Authentication → URL Configuration: -``` -Site URL: https://rockethacks.org -Redirect URLs: - - https://rockethacks.org/** - - http://localhost:3000/** (for dev) -``` - -## iOS Safari Specific Issues - -### Issue 1: Private Browsing Mode -**Symptom:** Cookies don't work even with fix -**Solution:** User must disable Private Browsing -- Settings → Safari → "Block All Cookies" must be OFF - -### Issue 2: Prevent Cross-Site Tracking -**Symptom:** Cookies blocked even on same site -**Solution:** Our fix handles this with `SameSite=Lax` -- No user action needed -- Works even with "Prevent Cross-Site Tracking" ON - -### Issue 3: Email App In-App Browser -**Symptom:** Some email apps use embedded browser -**Solution:** Provide "Open in Safari" option -- Users may need to manually open in Safari -- Most email apps now open in default browser - -## Verification Steps - -After deploying, verify: -1. ✅ Magic links work on iPhone Safari -2. ✅ Magic links work on iPad Safari -3. ✅ Magic links work in different iOS email apps (Mail, Gmail, Outlook) -4. ✅ Cookies persist after login -5. ✅ User stays logged in after navigation -6. ✅ Still works on Android (regression test) -7. ✅ Still works on desktop browsers (regression test) - -## Additional Recommendations - -### 1. Add Loading State -Show loading indicator during redirect to prevent user confusion: -```typescript -// After clicking magic link, before callback completes -"🔄 Logging you in..." -``` - -### 2. Add Error Handling -If login fails, show helpful message: -```typescript -if (error === 'auth_failed') { - // Show: "Authentication failed. Please try again or contact support." - // Provide link to request new magic link -} -``` - -### 3. User Instructions -Update email template to mention iOS compatibility: -``` -Click the link below to log in. This works on all devices including iPhone and iPad. -``` - -## Common iOS Debug Commands - -### Check if running on iOS: -```javascript -// In browser console -const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) -console.log('Is iOS?', isIOS) -``` - -### Check cookie support: -```javascript -// In browser console -console.log('Cookies enabled?', navigator.cookieEnabled) -document.cookie = "test=1" -console.log('Test cookie set?', document.cookie.includes('test=1')) -``` - -### Check Supabase session: -```javascript -// In browser console (if client is available) -supabase.auth.getSession().then(console.log) -``` - -## Rollback Plan - -If issues occur, rollback steps: -1. Revert changes to `src/lib/supabase/server.ts` -2. Revert changes to `src/app/api/auth/callback/route.ts` -3. Revert changes to `src/lib/supabase/middleware.ts` -4. Redeploy previous version - -## Summary - -**What was changed:** -- Cookie `sameSite` attribute set to `'lax'` for iOS Safari compatibility -- Explicit cookie setting in auth callback for reliability -- Consistent cookie options across server, middleware, and callbacks - -**Why it works:** -- iOS Safari allows `SameSite=Lax` cookies in email-to-browser navigation -- Explicit cookie attributes prevent Safari's Intelligent Tracking Prevention from blocking -- Works with iOS privacy features enabled - -**Next steps:** -1. Deploy to production -2. Test on real iOS devices (iPhone, iPad) -3. Verify with different email apps -4. Monitor for any reported issues - -The magic links should now work seamlessly on iOS devices! 🎉 diff --git a/MAGIC_LINK_RETRY_FIX.md b/MAGIC_LINK_RETRY_FIX.md deleted file mode 100644 index 55368986..00000000 --- a/MAGIC_LINK_RETRY_FIX.md +++ /dev/null @@ -1,336 +0,0 @@ -# Magic Link "Try Again Multiple Times" Fix - -## Problem Analysis - -### User Report: -"People have to try clicking magic link multiple times to get it to work" - -### Root Causes Identified: - -#### 1. **Cookie Race Condition** ⚠️ -**Before:** -```typescript -const { supabase, response } = createClientForRouteHandler(request) -await supabase.auth.exchangeCodeForSession(code) -// Try to copy cookies from response...but were they all captured? -``` - -**Issue:** The `response` object was created BEFORE `exchangeCodeForSession` completed, causing some cookies to not be captured properly. - -**Fixed:** Now captures cookies in an array DURING the exchange, guaranteeing all cookies are available. - -#### 2. **Mobile Browser Cookie Persistence** 📱 -iOS Safari and mobile Chrome are **aggressive about clearing cookies**, especially: -- Cross-site cookies -- Cookies without explicit `maxAge` -- Cookies during redirects -- Cookies from email-to-browser navigation - -**Fixed:** Added explicit cookie attributes optimized for mobile: -```typescript -{ - sameSite: 'lax', // Required for iOS - secure: true, // Production HTTPS - httpOnly: true, // Auth tokens only - maxAge: 604800, // 7 days explicit - priority: 'high', // Browser hint to keep - path: '/' // Site-wide -} -``` - -#### 3. **Cache-Control Headers Missing** 🔄 -Mobile browsers cache aggressively. Without proper headers, they might serve **stale auth state**. - -**Fixed:** Added no-cache headers to callback: -```typescript -Cache-Control: no-store, no-cache, must-revalidate, private -Pragma: no-cache -Expires: 0 -``` - -#### 4. **Inconsistent Cookie Attributes** 🍪 -Different parts of the codebase set cookies with slightly different attributes, causing conflicts. - -**Fixed:** Centralized cookie application with consistent attributes across: -- Callback route -- Middleware -- Server client - -## Your Understanding - Verified ✅ - -### **You're RIGHT:** - -#### **New Users Flow:** -1. Sign up with email + password -2. Receive confirmation email (if enabled) -3. Click confirmation link → Account activated -4. Can now use magic links ✅ - -**OR** - -1. Sign up and request magic link immediately -2. Click magic link → Account created + logged in ✅ - -#### **Existing Users Flow:** -1. Click "Magic Link" on login page -2. Receive email with link -3. Click link → Should redirect to dashboard ✅ -4. Session persists across pages ✅ - -### **Cookie Storage on Mobile:** - -#### **Before Fix: ❌ NOT EFFICIENT** -- Cookies not always captured during exchange -- Missing explicit lifetimes -- No priority hints -- Aggressive browser clearing - -#### **After Fix: ✅ OPTIMIZED** -- All cookies captured before redirect -- Explicit 7-day lifetime -- Priority hints for browsers -- SameSite=Lax for iOS compatibility -- HttpOnly for security -- Cache-control prevents stale sessions - -## Technical Deep Dive - -### Cookie Flow (Fixed): - -``` -1. User clicks magic link - ↓ -2. Browser → /api/auth/callback?code=xyz - ↓ -3. Server: createClientForRouteHandler(request) - - Captures cookies in array (not response object yet) - ↓ -4. Server: exchangeCodeForSession(code) - - Supabase sets auth cookies - - Cookies captured in array ✅ - ↓ -5. Server: Creates redirect response - ↓ -6. Server: applyCookiesToResponse() - - Applies ALL captured cookies - - Sets mobile-optimized attributes - - Adds cache-control headers - ↓ -7. Browser receives redirect WITH cookies - ↓ -8. Middleware runs on next request - - Refreshes session - - Extends cookie lifetime - - Validates user - ↓ -9. User sees dashboard (logged in) ✅ -``` - -### Why Multiple Tries Were Needed Before: - -**Attempt 1:** -- Cookies partially set -- iOS Safari rejects some due to missing attributes -- User not fully authenticated ❌ - -**Attempt 2:** -- Cookies partially set again -- Some from previous attempt still cached -- Still not all required cookies ❌ - -**Attempt 3:** -- Finally all cookies present -- Authentication works ✅ - -**After Fix:** -- All cookies set correctly on first try ✅ -- Proper attributes for iOS Safari -- Session persists reliably - -## Mobile Cookie Storage Best Practices - -### ✅ What We Now Do: - -1. **Explicit Lifetime** - ```typescript - maxAge: 60 * 60 * 24 * 7 // 7 days in seconds - ``` - -2. **SameSite=Lax** (Critical for iOS) - - Allows cookies on top-level navigation (email → browser) - - Blocks in iframes (security) - - Required for iOS Safari magic links - -3. **Secure Flag** - - Only over HTTPS in production - - Prevents man-in-the-middle attacks - -4. **HttpOnly for Auth Tokens** - - Prevents JavaScript access - - Mitigates XSS attacks - - Only auth tokens, not all cookies - -5. **Path=/** - - Available site-wide - - No subdomain restrictions - -6. **Priority=high** - - Browser hint to keep these cookies - - Prevents eviction under memory pressure - -7. **Cache-Control Headers** - - Prevents stale auth state - - Forces browser to check session - - Critical for mobile browsers - -### ❌ What We Avoided: - -1. **SameSite=Strict** - - Would block magic links ❌ - - Too restrictive for email navigation - -2. **SameSite=None** - - Requires Secure flag - - Lower browser support - - Not needed for our use case - -3. **No maxAge** - - Browser chooses (often too short) - - iOS Safari clears aggressively - -4. **HttpOnly on All Cookies** - - Would break client-side features - - Only needed for auth tokens - -## Testing Checklist - -### Before Considering Fixed: - -- [ ] **iOS Safari** - Single click login works -- [ ] **iOS Chrome** - Single click login works -- [ ] **iOS Gmail app** - Click link → logs in -- [ ] **iOS Mail app** - Click link → logs in -- [ ] **Android Chrome** - Still works (no regression) -- [ ] **Desktop browsers** - Still work (no regression) - -### Test Scenarios: - -#### Scenario 1: New User Magic Link -1. Go to signup -2. Request magic link -3. Click link in email **once** -4. **Expected:** Logged in immediately ✅ - -#### Scenario 2: Existing User Magic Link -1. Go to login -2. Switch to "Magic Link" tab -3. Request magic link -4. Click link in email **once** -5. **Expected:** Logged in, redirected to dashboard ✅ - -#### Scenario 3: Cookie Persistence -1. Log in via magic link -2. Navigate to different pages -3. Close browser (don't log out) -4. Reopen browser, go to site -5. **Expected:** Still logged in ✅ - -#### Scenario 4: Cross-Device -1. Request magic link on desktop -2. Check email on phone -3. Click link on phone -4. **Expected:** Logged in on phone ✅ - -### Debug Commands: - -#### Check Cookies in Safari (iPhone): -``` -Settings → Safari → Advanced → Website Data -Search: rockethacks -Should see: sb-xxxxx-auth-token (multiple cookies) -``` - -#### Check in Browser Console: -```javascript -// Check if cookies present -document.cookie.split(';').filter(c => c.includes('auth')) - -// Check Supabase session -supabase.auth.getSession().then(d => console.log(d.data)) -``` - -#### Check Response Headers (curl): -```bash -curl -I "https://rockethacks.org/api/auth/callback?code=test" - -# Look for: -# Set-Cookie: sb-...-auth-token=...; Path=/; SameSite=Lax; Secure; HttpOnly; Max-Age=604800 -# Cache-Control: no-store, no-cache, must-revalidate, private -``` - -## Expected Improvement - -### Before Fix: -- Success rate: **~33%** (1 in 3 tries works) -- User frustration: **High** -- Cookie issues: **Frequent** -- iOS specific: **Very problematic** - -### After Fix: -- Success rate: **~99%** (first try works) -- User frustration: **Minimal** -- Cookie issues: **Rare** -- iOS specific: **Resolved** - -### Why 99% not 100%? -- Network timeouts (not our fault) -- Email provider delays -- User device storage issues -- Expired magic links (user waited too long) - -## Monitoring - -### After Deployment, Track: - -1. **Error Rate** - - Check Vercel logs for auth callback errors - - Should be < 1% - -2. **User Reports** - - "Had to try multiple times" should cease - - "Not logged in" should decrease significantly - -3. **Session Duration** - - Users should stay logged in for days - - Fewer "why was I logged out?" complaints - -4. **Mobile vs Desktop** - - Mobile success rate should match desktop - - No more iOS-specific issues - -## Key Changes Summary - -| File | Change | Impact | -|------|--------|--------| -| `server.ts` | Refactored `createClientForRouteHandler` | Captures cookies reliably | -| `server.ts` | Added `applyCookiesToResponse` | Consistent cookie attributes | -| `callback/route.ts` | Simplified flow, added cache headers | Prevents stale sessions | -| `middleware.ts` | Enhanced cookie options | Better mobile persistence | - -## Deployment - -```bash -git add . -git commit -m "fix: reliable magic link auth on mobile devices - eliminate retry requirement" -git push origin main -``` - -After deployment: -1. Test on real iOS device (most critical) -2. Test on Android device (regression check) -3. Monitor Vercel logs for 24 hours -4. Gather user feedback - ---- - -**Bottom Line:** The "try multiple times" issue was caused by incomplete cookie capture during the callback flow. The fix ensures ALL cookies are captured and applied with mobile-optimized attributes on the FIRST try. 🎯 diff --git a/PASSWORD_AUTH_MIGRATION.md b/PASSWORD_AUTH_MIGRATION.md deleted file mode 100644 index 421fac51..00000000 --- a/PASSWORD_AUTH_MIGRATION.md +++ /dev/null @@ -1,563 +0,0 @@ -# Password-Based Authentication Migration Guide - -## Overview - -This guide documents the migration from **Magic Link-only authentication** to **password-based authentication (primary)** with Magic Link as an alternative option. - -**Migration Status**: ✅ Complete and Ready to Deploy - ---- - -## What Changed - -### Before (Magic Link Only) -- Users received an email link to sign in -- No passwords stored anywhere -- Slower login experience (waiting for email) -- Browser-dependent (links don't work across devices) - -### After (Password + Magic Link) -- **Primary**: Users sign in with email + password (instant access) -- **Alternative**: Users can still use Magic Link if preferred -- New users create account with email + password -- Existing Magic Link users can set up a password (optional) -- Both methods work simultaneously - **zero breaking changes** - ---- - -## File Changes Summary - -### New Files Created (14 files) - -#### 1. **UI Pages** (6 files) -- [`src/app/login/page.tsx`](src/app/login/page.tsx) - Updated login page with password/Magic Link toggle -- [`src/app/signup/page.tsx`](src/app/signup/page.tsx) - New signup page for password-based registration -- [`src/app/setup-password/page.tsx`](src/app/setup-password/page.tsx) - Password setup for existing Magic Link users -- [`src/app/forgot-password/page.tsx`](src/app/forgot-password/page.tsx) - Request password reset link -- [`src/app/reset-password/page.tsx`](src/app/reset-password/page.tsx) - Reset password with token from email - -#### 2. **API Routes** (3 files) -- [`src/app/api/auth/login/route.ts`](src/app/api/auth/login/route.ts) - Updated to handle both password and Magic Link -- [`src/app/api/auth/signup/route.ts`](src/app/api/auth/signup/route.ts) - New user registration with password -- [`src/app/api/auth/forgot-password/route.ts`](src/app/api/auth/forgot-password/route.ts) - Send password reset email - -#### 3. **Utilities** (1 file) -- [`src/lib/utils/passwordValidation.ts`](src/lib/utils/passwordValidation.ts) - Password strength validation - -#### 4. **Database** (1 file) -- [`supabase/migrations/add_password_support.sql`](supabase/migrations/add_password_support.sql) - Database migration script - -#### 5. **Documentation** (3 files) -- [`PASSWORD_AUTH_MIGRATION.md`](PASSWORD_AUTH_MIGRATION.md) - This file -- Referenced Supabase docs for implementation patterns - -### Files Modified (1 file) -- [`src/app/login/page.tsx`](src/app/login/page.tsx) - Completely rewritten to support both auth methods - -### Files Unchanged (All other files) -- **Middleware**: [`src/lib/supabase/middleware.ts`](src/lib/supabase/middleware.ts) - No changes needed -- **RLS Policies**: [`supabase/schema.sql`](supabase/schema.sql) - No changes needed -- **Auth Callback**: [`src/app/api/auth/callback/route.ts`](src/app/api/auth/callback/route.ts) - Works for both methods -- **Supabase Clients**: All client files unchanged - ---- - -## Implementation Steps - -### Step 1: Supabase Dashboard Configuration - -1. **Navigate to your Supabase Project Dashboard** - - Go to https://supabase.com/dashboard - - Select your RocketHacks project - -2. **Enable Password Authentication** - - Go to **Authentication > Providers** - - Ensure **Email** is enabled (should be by default) - - Recommended settings: - - **Enable email confirmations**: `false` (for easier migration, can enable later) - - **Secure email change**: `true` (recommended) - - **Secure password change**: `true` (recommended) - - **Minimum password length**: `8` characters - -3. **Configure Email Templates** (if using email confirmations) - - Go to **Authentication > Email Templates** - - **Password Reset Template**: - ```html -

Reset Your Password

-

Follow this link to reset your password:

-

Reset Password

-

If you didn't request this, you can safely ignore this email.

- ``` - -4. **Verify Site URL** - - Go to **Authentication > URL Configuration** - - Ensure your **Site URL** is correct: `https://yourdomain.com` or `http://localhost:3000` for development - - Add redirect URLs: - - `https://yourdomain.com/reset-password` - - `https://yourdomain.com/setup-password` - - `https://yourdomain.com/api/auth/callback` - ---- - -### Step 2: Run Database Migration - -```bash -# Option 1: Using Supabase CLI (Recommended) -npx supabase migration up - -# Option 2: Manual execution in Supabase Dashboard -# Copy the contents of supabase/migrations/add_password_support.sql -# and run it in the SQL Editor -``` - -**What this does**: -- Adds `password_setup_completed` column to `applicants` table -- Creates index for better query performance -- Sets default values for existing users (FALSE for Magic Link users) - -**Verify migration**: -```sql --- Run this in SQL Editor to verify -SELECT column_name, data_type, column_default -FROM information_schema.columns -WHERE table_name = 'applicants' - AND column_name = 'password_setup_completed'; -``` - ---- - -### Step 3: Deploy Code Changes - -```bash -# 1. Commit all changes -git add . -git commit -m "feat: Add password-based authentication with Magic Link fallback" - -# 2. Deploy to production (adjust based on your hosting) -npm run build # For Next.js build -npm run deploy # Or your deployment command - -# For Vercel: -vercel --prod - -# For other hosts, follow your standard deployment process -``` - ---- - -### Step 4: Test Authentication Flows - -#### Test 1: New User Signup (Password) -1. Go to `/signup` -2. Enter email and create a password -3. Should be redirected to `/apply` -4. Verify account created in Supabase Dashboard (Authentication > Users) - -#### Test 2: New User Login (Password) -1. Go to `/login` -2. Select "Password" tab -3. Enter email and password -4. Should be redirected to `/dashboard` - -#### Test 3: Existing User (Magic Link Still Works) -1. Go to `/login` -2. Select "Magic Link" tab -3. Enter existing user's email -4. Check email for magic link -5. Click link → should be redirected to dashboard - -#### Test 4: Password Reset Flow -1. Go to `/login` -2. Select "Password" tab -3. Click "Forgot password?" -4. Enter email -5. Check email for reset link -6. Click link → redirected to `/reset-password` -7. Enter new password -8. Should be redirected to `/login` with success message - -#### Test 5: Existing User Sets Up Password -1. Existing Magic Link user logs in via Magic Link -2. After login, they can visit `/setup-password` -3. Create a password -4. Future logins can use either method - ---- - -## Edge Cases Handled - -### 1. ✅ Existing Users Without Passwords -- **Detection**: Check `password_setup_completed` column -- **Handling**: Users can continue using Magic Link or set up password via `/setup-password` -- **UI**: Login page shows both options - -### 2. ✅ User Tries Password Login But Has No Password -- **Error Message**: "Invalid email or password. If you signed up with Magic Link, please use that option or set up a password first." -- **Solution**: User switches to Magic Link tab or visits `/setup-password` - -### 3. ✅ Admin Users -- **Impact**: None - admin check remains email-based -- **Verification**: `ADMIN_EMAILS` environment variable still works -- **Both Auth Methods**: Admins can use password or Magic Link - -### 4. ✅ RLS Policies -- **Impact**: None - `auth.uid()` works the same for both methods -- **Verification**: Existing policies continue to work -- **Testing**: Verify users can only see their own data - -### 5. ✅ In-Flight Magic Links -- **Impact**: None - Magic Links sent before migration still work -- **Reason**: Both auth methods work simultaneously - -### 6. ✅ Session Management -- **Impact**: None - Supabase handles both methods uniformly -- **Sessions**: Same structure for password and Magic Link users - -### 7. ✅ Email Enumeration Protection -- **Forgot Password**: Always returns success (doesn't reveal if email exists) -- **Security**: Prevents attackers from discovering valid email addresses - -### 8. ✅ Password Strength Validation -- **Requirements**: - - Minimum 8 characters - - At least one uppercase letter - - At least one lowercase letter - - At least one number - - At least one special character -- **UI**: Real-time strength indicator (Weak/Medium/Strong) - ---- - -## Environment Variables - -No new environment variables needed! Existing variables continue to work: - -```env -# Existing - no changes needed -NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key -NEXT_PUBLIC_SITE_URL=https://yourdomain.com -ADMIN_EMAILS=admin1@example.com,admin2@example.com -``` - ---- - -## User Experience Flows - -### New User Journey -``` -1. Visit /signup -2. Enter email + create password -3. Account created instantly (or confirm email if enabled) -4. Redirected to /apply -5. Complete application -6. Access /dashboard -``` - -### Existing User Journey (Magic Link → Password) -``` -Option A (Continue with Magic Link): -1. Visit /login -2. Select "Magic Link" tab -3. Enter email -4. Check email → click link -5. Logged in to /dashboard - -Option B (Migrate to Password): -1. Visit /login -2. Select "Magic Link" tab -3. Log in via email link -4. Visit /setup-password -5. Create password -6. Future logins: use password for instant access -``` - -### Password Reset Journey -``` -1. Visit /login -2. Click "Forgot password?" -3. Enter email -4. Check email → click reset link -5. Redirected to /reset-password -6. Enter new password -7. Password updated → redirected to /login -8. Sign in with new password -``` - ---- - -## Security Considerations - -### Password Storage -- ✅ **Never stored in plain text** -- ✅ **Hashed by Supabase Auth** using bcrypt -- ✅ **Secure by default** - no custom password handling needed - -### Password Requirements -- ✅ **Minimum 8 characters** -- ✅ **Complexity requirements** enforced via validation utility -- ✅ **Real-time feedback** for users during password creation - -### Email Enumeration Protection -- ✅ **Forgot password** always returns success (doesn't reveal if email exists) -- ✅ **Signup** returns generic error if email taken - -### Session Security -- ✅ **HTTPOnly cookies** set by Supabase (not accessible via JavaScript) -- ✅ **Secure flag** in production (HTTPS only) -- ✅ **SameSite** protection against CSRF - -### Rate Limiting -- ✅ **Supabase built-in** rate limiting on auth endpoints -- ✅ **Prevents brute force** attacks on password login - ---- - -## Admin Users - -**No changes to admin authentication**: -- Admins identified by `ADMIN_EMAILS` environment variable -- Admin can use **either** password or Magic Link -- Admin routes protected at middleware level -- RLS policies unchanged (application-layer enforcement) - -**To add a new admin**: -```env -# Add email to ADMIN_EMAILS in .env.local (or production env) -ADMIN_EMAILS=admin1@example.com,admin2@example.com,newadmin@example.com -``` - ---- - -## Rollback Plan - -If you need to rollback this migration: - -### Option 1: Revert Code Only (Keep Database Changes) -```bash -git revert -git push -``` -**Impact**: -- UI reverts to Magic Link only -- Existing passwords remain in database (can be re-enabled later) -- No data loss - -### Option 2: Full Rollback (Including Database) -```sql --- Run in Supabase SQL Editor -DROP INDEX IF EXISTS idx_applicants_password_setup; -ALTER TABLE public.applicants DROP COLUMN IF EXISTS password_setup_completed; -``` -**Impact**: -- Removes password tracking column -- Passwords in `auth.users` remain (managed by Supabase) -- Users with passwords can no longer track setup status - -**Recommendation**: Option 1 (code-only revert) is safer - ---- - -## Monitoring & Metrics - -### Key Metrics to Track -1. **Password vs Magic Link Usage** - ```sql - -- Count users with passwords - SELECT COUNT(*) - FROM auth.users - WHERE encrypted_password IS NOT NULL; - - -- Count users who set up passwords - SELECT COUNT(*) - FROM applicants - WHERE password_setup_completed = true; - ``` - -2. **Failed Login Attempts** - - Monitor Supabase logs for auth errors - - Check for unusual patterns - -3. **Password Reset Requests** - ```sql - -- Track password reset emails sent - SELECT COUNT(*) - FROM auth.audit_log_entries - WHERE action = 'recovery_email'; - ``` - -### Health Checks -```bash -# Test password login endpoint -curl -X POST https://yourdomain.com/api/auth/login \ - -H "Content-Type: application/json" \ - -d '{"email":"test@example.com","password":"test123","authMode":"password"}' - -# Test Magic Link endpoint -curl -X POST https://yourdomain.com/api/auth/login \ - -H "Content-Type: application/json" \ - -d '{"email":"test@example.com","authMode":"magic-link"}' -``` - ---- - -## Troubleshooting - -### Issue 1: "Invalid login credentials" error -**Cause**: User trying to log in with password but hasn't set one up -**Solution**: -- Switch to Magic Link on login page -- OR visit `/setup-password` after Magic Link login - -### Issue 2: Password reset email not received -**Possible Causes**: -1. Email in spam folder -2. SMTP not configured in Supabase -3. Invalid email address - -**Solution**: -1. Check spam folder -2. Verify Supabase email settings (Dashboard > Project Settings > Auth) -3. Check Supabase logs for email delivery errors - -### Issue 3: "Password must be at least 8 characters" error -**Cause**: Password doesn't meet requirements -**Solution**: -- Use at least 8 characters -- Include uppercase, lowercase, number, and special character -- Check password strength indicator on signup/reset pages - -### Issue 4: Existing user can't see setup password option -**Cause**: `password_setup_completed` column not added -**Solution**: -1. Verify migration ran successfully -2. Check database schema: `\d applicants` in SQL Editor -3. Re-run migration if needed - -### Issue 5: Admin can't access /admin after password setup -**Cause**: Admin email not in `ADMIN_EMAILS` env variable -**Solution**: -1. Verify `ADMIN_EMAILS` includes admin's email -2. Restart application after env variable change -3. Check middleware logs for admin check failures - ---- - -## FAQs - -### Q: Do existing users need to set up a password? -**A**: No, it's optional. Existing users can continue using Magic Link indefinitely. Password setup is available at `/setup-password` if they want faster logins. - -### Q: Can a user have both password and Magic Link enabled? -**A**: Yes! Users can use either method. Once a password is set, both options work. - -### Q: What happens if a user forgets their password? -**A**: They can: -1. Use the "Forgot password?" link on login page -2. OR switch to Magic Link to log in without a password - -### Q: Will this break my existing authentication flow? -**A**: No, this is a non-breaking change. Both methods work simultaneously. Existing Magic Link functionality remains intact. - -### Q: Do I need to notify my users about this change? -**A**: Optional. Users will see the new password option on login, but Magic Link still works. You could send an announcement email highlighting the faster login option. - -### Q: Can I disable Magic Link after migration? -**A**: Yes, but not recommended. Keeping both options provides better UX (e.g., if user forgets password). To disable Magic Link, remove it from the UI by editing [`src/app/login/page.tsx`](src/app/login/page.tsx). - -### Q: How secure are the passwords? -**A**: Very secure. Passwords are: -- Hashed with bcrypt by Supabase Auth -- Never stored in plain text -- Never sent over the network except during initial creation/login -- Protected by HTTPS in production - -### Q: Can I customize password requirements? -**A**: Yes, edit [`src/lib/utils/passwordValidation.ts`](src/lib/utils/passwordValidation.ts): -```typescript -export const PASSWORD_REQUIREMENTS = { - minLength: 12, // Change from 8 to 12 - requireUppercase: true, - requireLowercase: true, - requireNumber: true, - requireSpecial: true, -} -``` - -### Q: What if I want to force all users to set up passwords? -**A**: Add middleware logic in [`src/lib/supabase/middleware.ts`](src/lib/supabase/middleware.ts): -```typescript -// Check if user has password -const { data: applicant } = await supabase - .from('applicants') - .select('password_setup_completed') - .eq('user_id', user.id) - .single() - -if (!applicant.password_setup_completed) { - return NextResponse.redirect(new URL('/setup-password', request.url)) -} -``` - ---- - -## Success Criteria - -Migration is successful when: - -- [ ] ✅ New users can sign up with email + password -- [ ] ✅ New users can log in with email + password -- [ ] ✅ Existing users can still use Magic Link -- [ ] ✅ Existing users can set up a password -- [ ] ✅ Password reset flow works -- [ ] ✅ Admin users can access /admin with either auth method -- [ ] ✅ RLS policies work for both auth methods -- [ ] ✅ No breaking changes to existing functionality -- [ ] ✅ All tests pass - ---- - -## Support & Contact - -For issues or questions: -1. Check this migration guide -2. Review [Supabase Auth Documentation](https://supabase.com/docs/guides/auth) -3. Check Supabase logs for auth errors -4. Review code comments in auth route files - ---- - -## Changelog - -### Version 1.0.0 (2025-01-13) -- ✅ Added password-based authentication -- ✅ Updated login page with password/Magic Link toggle -- ✅ Created signup page for new users -- ✅ Created password setup page for existing users -- ✅ Created forgot password flow -- ✅ Created reset password flow -- ✅ Added password validation utilities -- ✅ Updated API routes for both auth methods -- ✅ Created database migration script -- ✅ Documented all changes and edge cases -- ✅ Zero breaking changes - both methods work simultaneously - ---- - -## Migration Complete! 🎉 - -Your RocketHacks application now supports: -- ✅ **Password-based authentication** (primary method) -- ✅ **Magic Link authentication** (alternative method) -- ✅ **Seamless migration** for existing users -- ✅ **Enhanced security** with password requirements -- ✅ **Better UX** with instant password logins -- ✅ **Zero downtime** - both methods work side-by-side - -**Next Steps**: -1. Deploy to production -2. Test all authentication flows -3. (Optional) Announce new password login option to users -4. Monitor usage metrics -5. (Optional) Gradually encourage users to set up passwords for faster logins diff --git a/PASSWORD_RESET_FIX.md b/PASSWORD_RESET_FIX.md deleted file mode 100644 index 8b0438ad..00000000 --- a/PASSWORD_RESET_FIX.md +++ /dev/null @@ -1,309 +0,0 @@ -# Password Reset Fix - Implementation Summary - -## Critical Issues Fixed - -### 1. **Password Reset Callback Flow** ✅ -**Problem**: The password reset link wasn't redirecting users to the reset-password page after clicking the email link. - -**Root Cause**: -- The email link goes to `/api/auth/callback` with a code -- The callback handler had no logic to detect password recovery flows -- It was redirecting all authenticated users to `/dashboard` or `/apply` - -**Fix Applied**: -- Added `type` parameter detection in callback handler ([src/app/api/auth/callback/route.ts:8](src/app/api/auth/callback/route.ts#L8)) -- When `type === 'recovery'`, redirect directly to `/reset-password` ([src/app/api/auth/callback/route.ts:18-20](src/app/api/auth/callback/route.ts#L18-L20)) -- Updated forgot-password API to pass correct redirectTo URL ([src/app/api/auth/forgot-password/route.ts:14](src/app/api/auth/forgot-password/route.ts#L14)) - -```typescript -// Before: -redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/reset-password` - -// After: -redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/api/auth/callback?redirect=/reset-password` -``` - ---- - -## Additional Improvements Made - -### 2. **Custom Email Template** ✅ -**Location**: [supabase/email-templates/password-reset.html](supabase/email-templates/password-reset.html) - -**Features**: -- RocketHacks branded design with gradient colors -- Friendly, non-robotic language -- Security warnings and expiry notices -- Mobile-responsive design -- Clear call-to-action button - -**To Apply**: -1. Go to Supabase Dashboard → Authentication → Email Templates -2. Select "Reset Password" template -3. Copy contents from `supabase/email-templates/password-reset.html` -4. Paste into the template editor -5. **IMPORTANT**: Ensure `{{ .ConfirmationURL }}` is present in the template -6. Save changes - ---- - -### 3. **Gallery Navigation Fixed** ✅ -**Problem**: Arrow buttons weren't working to navigate between pictures on web and mobile. - -**Fix Applied** ([src/components/gallery/Gallery.tsx:493-510](src/components/gallery/Gallery.tsx#L493-L510)): -- Added `z-index: 20` to navigation arrows -- Added `type="button"` to prevent form submission -- Added `touch-manipulation` for better mobile tap handling -- Increased touch target size with responsive padding -- Added visual feedback with hover/active states -- Fixed pointer-events on overlay to not block clicks -- Made buttons more prominent with shadow-lg - ---- - -### 4. **Footer Privacy Links Removed** ✅ -**Change**: Removed non-existent Privacy Policy and Terms of Service links from footer. - -**What Remains**: -- MLH Code of Conduct link (external, required) -- All social media links intact -- Contact information preserved - -**File Modified**: [src/components/footer/Footer.tsx:158-167](src/components/footer/Footer.tsx#L158-L167) - ---- - -### 5. **Dashboard Quick Links Updated** ✅ -**Changes**: -- ❌ Removed "Event Schedule" link (doesn't exist yet) -- ✅ Updated FAQ link to open in new tab with external link icon -- Link points to home page FAQ section (`/#faq`) - -**File Modified**: [src/app/dashboard/page.tsx:299-313](src/app/dashboard/page.tsx#L299-L313) - ---- - -## Testing Checklist - -### Local Development Testing - -1. **Start the dev server**: - ```bash - npm run dev - ``` - -2. **Test Password Reset Flow**: - - [ ] Navigate to `/forgot-password` - - [ ] Enter a valid email address that exists in your database - - [ ] Check email inbox (including spam folder) - - [ ] Verify email uses RocketHacks branding (if template applied) - - [ ] Click the reset link in email - - [ ] **Expected URL**: `http://localhost:3000/api/auth/callback?code=...&type=recovery` - - [ ] **Expected Redirect**: Should go to `/reset-password` - - [ ] Verify session is valid (no "Invalid or expired reset link" error) - - [ ] Enter new password (must meet requirements) - - [ ] Submit form - - [ ] **Expected**: Redirect to `/login` with success message - - [ ] Login with new password - - [ ] **Expected**: Successfully authenticated - -3. **Test Gallery Navigation**: - - [ ] Navigate to home page `/#gallery` - - [ ] Click left arrow button → should go to previous image - - [ ] Click right arrow button → should go to next image - - [ ] Test on mobile device or mobile emulator - - [ ] Verify arrows are easily tappable - - [ ] Check that arrows don't get blocked by image overlay - -4. **Test Dashboard**: - - [ ] Login and navigate to `/dashboard` - - [ ] Verify "Event Schedule" is no longer present - - [ ] Click "FAQs" button - - [ ] **Expected**: Opens home page FAQ in new tab - - [ ] Verify external link icon appears next to FAQs - -5. **Test Footer**: - - [ ] Scroll to footer on any page - - [ ] Verify "Privacy Policy" is removed - - [ ] Verify "Terms of Service" is removed - - [ ] Verify "MLH Code of Conduct" is still present - - [ ] Click MLH link → should open in new tab - ---- - -## Supabase Configuration Verification - -### Redirect URLs (ALREADY DONE ✅) -You confirmed you removed these from Supabase: -- ❌ `https://*.vercel.app/reset-password` (REMOVED) -- ❌ `https://*.vercel.app/setup-password` (REMOVED) -- ❌ `https://rockethacks.org/reset-password` (REMOVED) -- ❌ `https://rockethacks.org/setup-password` (REMOVED) -- ❌ `https://www.rockethacks.org/reset-password` (REMOVED) -- ❌ `https://www.rockethacks.org/setup-password` (REMOVED) -- ❌ `http://localhost:3000/reset-password` (REMOVED) -- ❌ `http://localhost:3000/setup-password` (REMOVED) - -**What Should Remain**: -- ✅ `http://localhost:3000/api/auth/callback` -- ✅ `https://*.vercel.app/api/auth/callback` -- ✅ `https://rockethacks.org/api/auth/callback` -- ✅ `https://www.rockethacks.org/api/auth/callback` - -### Email Template Configuration - -**To Verify**: -1. Go to Supabase Dashboard -2. Navigate to: Authentication → Email Templates → Reset Password -3. Check that the template contains: `{{ .ConfirmationURL }}` -4. If using custom template, paste from `supabase/email-templates/password-reset.html` - ---- - -## Environment Variables - -### Development (.env.local) -Current value is correct: -```env -NEXT_PUBLIC_SITE_URL=http://localhost:3000 -``` - -### Vercel Deployment - -**Preview Deployments**: -- Set `NEXT_PUBLIC_SITE_URL` to your Vercel preview URL -- Example: `https://rockethacks-git-dev-aadi-yourteam.vercel.app` -- Or use Vercel's auto-detection (often works automatically) - -**Production Deployment**: -```env -NEXT_PUBLIC_SITE_URL=https://rockethacks.org -``` -or -```env -NEXT_PUBLIC_SITE_URL=https://www.rockethacks.org -``` - -**To Set in Vercel**: -1. Go to Vercel Dashboard → Your Project → Settings → Environment Variables -2. Add/update `NEXT_PUBLIC_SITE_URL` for each environment: - - Production: `https://rockethacks.org` - - Preview: Can leave blank or set to preview URL - - Development: `http://localhost:3000` -3. Redeploy after changing environment variables - ---- - -## Edge Cases Handled - -1. **Token Expiry**: Reset page shows clear error message -2. **Invalid Token**: Redirects to forgot-password with error -3. **Multiple Tabs**: Only one tab can redeem the token (expected behavior) -4. **Already Authenticated**: Recovery flow overrides and forces reset-password page -5. **Mobile Touch**: Gallery arrows have larger touch targets and visual feedback -6. **Email Client Preview**: Token exchange is idempotent - ---- - -## Files Changed - -| File | Change Summary | -|------|---------------| -| [src/app/api/auth/callback/route.ts](src/app/api/auth/callback/route.ts) | Added password recovery type detection | -| [src/app/api/auth/forgot-password/route.ts](src/app/api/auth/forgot-password/route.ts) | Fixed redirectTo URL to include callback | -| [src/components/gallery/Gallery.tsx](src/components/gallery/Gallery.tsx) | Fixed navigation arrows z-index and touch handling | -| [src/components/footer/Footer.tsx](src/components/footer/Footer.tsx) | Removed Privacy/Terms links | -| [src/app/dashboard/page.tsx](src/app/dashboard/page.tsx) | Removed Event Schedule, updated FAQ to new tab | -| [supabase/email-templates/password-reset.html](supabase/email-templates/password-reset.html) | NEW: Custom RocketHacks-branded email template | - ---- - -## Build Verification - -✅ **Build Status**: SUCCESSFUL - -```bash -npm run build -# ✓ Compiled successfully -# Build completed without errors -``` - -All TypeScript types are valid, no linting errors, and all routes are properly configured. - ---- - -## Next Steps - -1. **Apply Custom Email Template** (Manual Step): - - Copy `supabase/email-templates/password-reset.html` - - Paste into Supabase Dashboard → Authentication → Email Templates → Reset Password - - Save changes - -2. **Test Password Reset Flow** (Local): - ```bash - npm run dev - ``` - - Follow "Testing Checklist" above - - Verify email link redirects correctly - - Confirm reset-password page loads with valid session - -3. **Deploy to Vercel**: - ```bash - git add . - git commit -m "fix: password reset flow, gallery navigation, and dashboard updates" - git push origin dev-aadi - ``` - -4. **Set Vercel Environment Variables**: - - Update `NEXT_PUBLIC_SITE_URL` for production - - Redeploy if necessary - -5. **Test in Production**: - - Request password reset - - Verify email arrives with correct link - - Test reset flow end-to-end - - Test on mobile device - ---- - -## Support & Troubleshooting - -### If Password Reset Still Doesn't Work: - -1. **Check Email Link Format**: - - Should contain: `type=recovery` parameter - - Should go to: `/api/auth/callback` - - Example: `https://rockethacks.org/api/auth/callback?code=abc123&type=recovery` - -2. **Check Browser Console**: - - Look for any JavaScript errors - - Check Network tab for failed API calls - -3. **Check Supabase Logs**: - - Go to Supabase Dashboard → Logs - - Look for auth-related errors - - Check if email was sent successfully - -4. **Verify Redirect URLs**: - - Ensure only `/api/auth/callback` URLs are in Supabase - - No `/reset-password` or `/setup-password` URLs should be listed - -5. **Check Environment Variables**: - - Verify `NEXT_PUBLIC_SITE_URL` matches your deployment URL - - Restart dev server after changing .env.local - ---- - -## Summary of What Was Fixed - -✅ **Critical**: Password reset callback flow now correctly detects recovery type and redirects to reset-password page - -✅ **Enhancement**: Custom RocketHacks-branded email template ready to deploy - -✅ **Bug Fix**: Gallery navigation arrows now work on both web and mobile with proper z-index and touch handling - -✅ **Cleanup**: Removed non-existent Privacy Policy and Terms links from footer - -✅ **Improvement**: Dashboard now only shows existing features (removed Event Schedule, FAQ opens in new tab) - -All changes have been tested with a successful build. Ready for testing and deployment! 🚀 diff --git a/PASSWORD_SETUP_FIX.md b/PASSWORD_SETUP_FIX.md deleted file mode 100644 index fe6de59a..00000000 --- a/PASSWORD_SETUP_FIX.md +++ /dev/null @@ -1,227 +0,0 @@ -# Password Setup Completed Flag - Issue Resolution - -## Problem Summary - -The `password_setup_completed` field in the `applicants` table was showing `false` for all users, even those who had set up passwords. This occurred because: - -1. The flag wasn't being updated when users reset their passwords -2. The flag wasn't being properly set when users submitted their application -3. Existing users who already set passwords had stale data - -## Where Passwords Are Stored - -**Important**: Passwords are NOT stored in the `applicants` table. They are stored in Supabase's internal `auth.users` table: - -- **Table**: `auth.users` (Supabase managed) -- **Column**: `encrypted_password` (bcrypt hashed) -- **Visibility**: You cannot see passwords in the Supabase Dashboard → Authentication → Users panel because they are encrypted -- **Verification**: To check if a user has a password, query: `SELECT encrypted_password IS NOT NULL FROM auth.users WHERE id = ''` - -The `password_setup_completed` field in the `applicants` table is just a **tracking flag** to know which auth method the user set up. - -## Root Causes - -### 1. Reset Password Page Missing Update -**File**: `src/app/reset-password/page.tsx` - -When users reset their password, the code updated `auth.users` (via `supabase.auth.updateUser()`) but didn't update the `password_setup_completed` flag in the `applicants` table. - -**Fix Applied**: Added update to set `password_setup_completed = true` after successful password reset. - -### 2. Application Form Not Setting Flag Correctly -**File**: `src/app/apply/page.tsx` - -When users submitted their application, the code didn't check if they had a password and set the flag accordingly. - -**Fix Applied**: Now checks `user.user_metadata.password_setup_completed` (set during signup) and includes it in the upsert operation. - -### 3. Existing Users Had Stale Data -Users who already set passwords before this fix had `password_setup_completed = false` in the database. - -**Fix Applied**: Created migration SQL script to update all existing records where users have passwords. - -## Changes Made - -### 1. Updated Reset Password Page -```typescript -// Added after password update: -const { data: { user } } = await supabase.auth.getUser() -if (user) { - await supabase - .from('applicants') - .update({ password_setup_completed: true }) - .eq('user_id', user.id) -} -``` - -### 2. Updated Application Form -```typescript -// Check user metadata from signup -const hasPasswordMetadata = user.user_metadata?.password_setup_completed === true - -// Include in application data -const finalApplicationData = { - ...applicationData, - password_setup_completed: hasPasswordMetadata -} -``` - -### 3. Created Migration Script -**File**: `supabase/migrations/fix_password_setup_completed.sql` - -Updates all existing users who have passwords but show `password_setup_completed: false`. - -## How to Apply the Fix - -### Step 1: Run the Migration - -**Option A: Via Supabase Dashboard** (Recommended for immediate fix) -1. Go to Supabase Dashboard → SQL Editor -2. Copy the contents of `supabase/migrations/fix_password_setup_completed.sql` -3. Paste and run the query -4. Check the results - should show number of rows updated - -**Option B: Via Supabase CLI** -```bash -# If using Supabase CLI -supabase db push -``` - -### Step 2: Deploy Code Changes - -```bash -# Commit changes -git add . -git commit -m "fix: update password_setup_completed flag in reset-password and apply flows" - -# Push to your deployment branch -git push origin dev-aadi -``` - -### Step 3: Verify the Fix - -Run this query in Supabase SQL Editor to verify: - -```sql -SELECT - a.email, - a.first_name, - a.last_name, - a.password_setup_completed, - CASE - WHEN u.encrypted_password IS NOT NULL THEN 'YES' - ELSE 'NO' - END as has_password_in_auth -FROM public.applicants a -JOIN auth.users u ON a.user_id = u.id -ORDER BY a.email; -``` - -**Expected Result**: -- Users with passwords should have `password_setup_completed = true` -- Users without passwords (magic link only) should have `password_setup_completed = false` -- The `has_password_in_auth` column should match `password_setup_completed` - -## Testing Checklist - -### Test 1: New User Signup with Password -1. Create new account at `/signup` with email and password -2. Go to `/apply` and submit application -3. Check database: `password_setup_completed` should be `true` - -### Test 2: Password Reset Flow -1. User with existing account goes to `/forgot-password` -2. Receives email, clicks reset link -3. Sets new password at `/reset-password` -4. Check database: `password_setup_completed` should be updated to `true` - -### Test 3: Existing User Updates Application -1. User who previously set a password logs in -2. Goes to `/apply` and updates their application -3. Check database: `password_setup_completed` should now be `true` (if they have a password) - -### Test 4: Setup Password Flow -1. Magic link user logs in and goes to `/setup-password` -2. Creates a password -3. Check database: `password_setup_completed` should be `true` (this already worked) - -## Migration SQL Details - -The migration updates all users who have passwords but show `password_setup_completed = false`: - -```sql -UPDATE public.applicants -SET password_setup_completed = TRUE -WHERE user_id IN ( - SELECT id - FROM auth.users - WHERE encrypted_password IS NOT NULL -) -AND password_setup_completed = FALSE; -``` - -This query: -1. Finds all users in `auth.users` with `encrypted_password IS NOT NULL` (has password) -2. Updates their `applicants.password_setup_completed` to `TRUE` -3. Only updates if currently `FALSE` (to avoid unnecessary updates) - -## Files Modified - -| File | Change | -|------|--------| -| `src/app/reset-password/page.tsx` | Added update to set `password_setup_completed = true` after password reset | -| `src/app/apply/page.tsx` | Added check for password metadata and include in application upsert | -| `supabase/migrations/fix_password_setup_completed.sql` | NEW - Migration to fix existing data | - -## FAQ - -### Q: Why can't I see passwords in Supabase Dashboard? -**A**: Passwords are stored encrypted in `auth.users.encrypted_password`. They are never displayed anywhere for security reasons. You can only verify if a user has a password by checking if `encrypted_password IS NOT NULL`. - -### Q: What happens to users who signed up with Magic Link? -**A**: They will continue to have `password_setup_completed = false` until they set up a password via `/setup-password`. - -### Q: Do I need to notify users? -**A**: No, this is a behind-the-scenes fix. The flag is used internally to track authentication methods. - -### Q: What if a user resets their password multiple times? -**A**: The flag will remain `true` after the first password setup. Multiple resets don't affect it. - -### Q: Can I force all users to set up passwords? -**A**: Yes, you can add middleware to redirect users with `password_setup_completed = false` to `/setup-password`, but this is optional and may disrupt the user experience for those who prefer magic links. - -## Verification Query - -Run this after applying all fixes to see the current state: - -```sql --- Count users by password status -SELECT - password_setup_completed, - COUNT(*) as user_count, - ARRAY_AGG(email ORDER BY email) as emails -FROM public.applicants -GROUP BY password_setup_completed; - --- Detailed view showing mismatches (should be empty after fix) -SELECT - a.email, - a.password_setup_completed as flag_in_applicants, - CASE WHEN u.encrypted_password IS NOT NULL THEN true ELSE false END as has_password_in_auth -FROM public.applicants a -JOIN auth.users u ON a.user_id = u.id -WHERE (u.encrypted_password IS NOT NULL AND a.password_setup_completed = false) - OR (u.encrypted_password IS NULL AND a.password_setup_completed = true); -``` - -The second query should return **0 rows** after the fix is applied, meaning no mismatches exist. - -## Summary - -✅ **Fixed**: Password reset flow now updates `password_setup_completed` flag -✅ **Fixed**: Application form now correctly sets `password_setup_completed` based on auth method -✅ **Fixed**: Migration script to update all existing users with passwords -✅ **Documented**: Clear explanation of where passwords are stored -✅ **Tested**: All authentication flows maintain correct flag state - -After applying these changes and running the migration, all users' `password_setup_completed` flags will accurately reflect whether they have set up a password. diff --git a/PERFORMANCE_OPTIMIZATION.md b/PERFORMANCE_OPTIMIZATION.md deleted file mode 100644 index 211ef6db..00000000 --- a/PERFORMANCE_OPTIMIZATION.md +++ /dev/null @@ -1,167 +0,0 @@ -# RocketHacks Website Performance Optimization - Complete Guide - -## ✅ Optimizations Completed - -### 1. Gallery Component - Complete Rewrite -**Problems Fixed:** -- ❌ OLD: Loading all 26 images at once (26 × 3MB = ~78MB!) -- ❌ OLD: Complex thumbnail system with virtual scrolling -- ❌ OLD: Intersection Observer overhead -- ❌ OLD: Multiple useEffect hooks causing re-renders - -**Solutions Implemented:** -- ✅ NEW: Loads only the current image (saves ~75MB) -- ✅ NEW: Simple dot navigation instead of thumbnails -- ✅ NEW: Blur placeholder for instant perceived loading -- ✅ NEW: Reduced quality to 75% (still great, much faster) -- ✅ NEW: Auto-play disabled by default (saves battery/CPU) -- ✅ NEW: Memoized components prevent unnecessary re-renders - -**Performance Gain:** ~85% faster loading, 97% less memory usage - -### 2. Next.js Image Optimization -**Changes Made:** -- ✅ Enabled Next.js image optimizer (was disabled) -- ✅ Added AVIF format support (50% smaller than JPEG) -- ✅ Added WebP fallback (30% smaller than JPEG) -- ✅ Responsive image sizes for all devices -- ✅ 1-year cache TTL for images - -**Performance Gain:** ~60% smaller image file sizes - -### 3. Sponsor Carousel Optimization -**Changes Made:** -- ✅ Added `will-change: transform` for GPU acceleration -- ✅ Added `translateZ(0)` for hardware acceleration -- ✅ Added `backface-visibility: hidden` to prevent flickering -- ✅ Responsive sizing reduces mobile bandwidth - -**Performance Gain:** Smooth 60fps animations, no jank - -## 📊 Expected Performance Improvements - -### Before Optimization: -- Gallery loads: ~78MB of images -- First Contentful Paint: ~4-6 seconds -- Time to Interactive: ~8-12 seconds -- Mobile Performance Score: ~40-50/100 - -### After Optimization: -- Gallery loads: ~3-5MB of images (95% reduction!) -- First Contentful Paint: ~1-2 seconds (75% faster) -- Time to Interactive: ~2-4 seconds (70% faster) -- Mobile Performance Score: ~75-85/100 (60% improvement) - -## 🚀 Additional Recommended Optimizations - -### CRITICAL: Compress Your Images (DO THIS ASAP!) -Your images are HUGE (3.5MB each). Here's how to fix: - -#### Option 1: Automatic Compression (Easiest) -Next.js will now automatically compress images, but for even better results: - -#### Option 2: Manual Compression (Best Quality) -Use this online tool: https://squoosh.app/ -- Upload each JPG -- Select "MozJPEG" codec -- Set quality to 80% -- Download and replace original - -This will reduce 3.5MB → ~300KB (90% smaller!) - -#### Option 3: Bulk Compression Script -```powershell -# Install Sharp (image processing library) -npm install -g sharp-cli - -# Compress all event images -cd public/assets/event -Get-ChildItem *.jpg | ForEach-Object { - sharp resize 1920 1080 --fit inside --quality 80 --input $_.Name --output "compressed_$($_.Name)" -} -``` - -### Additional Performance Tips: - -1. **Lazy Load Other Sections** - - Hero section: Load immediately ✅ - - About, FAQ, Schedule: Lazy load when scrolled into view - - Gallery: Already optimized ✅ - - Sponsors: Already optimized ✅ - -2. **Font Optimization** - - Use `font-display: swap` (already in place) - - Preload critical fonts - - Consider system fonts for body text - -3. **Bundle Size Optimization** - - Tree-shake unused icon imports - - Dynamic import heavy components - - Use React.lazy() for route-based code splitting - -4. **Caching Strategy** - - Static assets: Cache for 1 year ✅ - - API responses: Cache for 5 minutes - - Use Service Worker for offline support - -## 📱 Mobile-Specific Optimizations - -1. **Already Implemented:** - - Responsive images ✅ - - Touch-friendly controls ✅ - - Reduced gaps on mobile ✅ - - Smaller sponsor logos ✅ - -2. **Recommended:** - - Add `loading="lazy"` to below-fold images - - Use `priority` only for hero images - - Reduce animation complexity on mobile - -## 🧪 Testing Your Performance - -### Chrome DevTools Lighthouse: -1. Open Chrome DevTools (F12) -2. Go to "Lighthouse" tab -3. Run audit for "Performance" -4. Target: Score 80+ (Mobile), 90+ (Desktop) - -### Real User Monitoring: -```javascript -// Add to _app.tsx -export function reportWebVitals(metric) { - console.log(metric); - // Send to analytics -} -``` - -## 🎯 Performance Targets - -| Metric | Target | Why It Matters | -|--------|--------|----------------| -| FCP (First Contentful Paint) | < 1.8s | Users see content quickly | -| LCP (Largest Contentful Paint) | < 2.5s | Main content loads fast | -| CLS (Cumulative Layout Shift) | < 0.1 | No annoying layout jumps | -| FID (First Input Delay) | < 100ms | Site responds instantly | -| TTI (Time to Interactive) | < 3.8s | Users can interact quickly | - -## 🔥 Quick Wins Checklist - -- [x] Simplified Gallery component -- [x] Enabled Next.js image optimization -- [x] Added AVIF/WebP support -- [x] GPU-accelerated sponsor carousel -- [x] Reduced image quality to 75% -- [ ] Compress source images (DO THIS!) -- [ ] Add lazy loading to other sections -- [ ] Implement Service Worker caching -- [ ] Add performance monitoring - -## 📞 Questions? - -The Gallery is now: -- ✅ **85% faster** loading -- ✅ **97% less memory** usage -- ✅ **Still beautiful** and aesthetic -- ✅ **Mobile optimized** - -The biggest remaining issue: Your source images are still 3.5MB each. Compress them manually for best results! diff --git a/README_UPDATES.md b/README_UPDATES.md deleted file mode 100644 index 81f24862..00000000 --- a/README_UPDATES.md +++ /dev/null @@ -1,296 +0,0 @@ -# RocketHacks Updates - Complete Guide - -## 🎯 What Was Done - -This update includes critical bug fixes, UI improvements, and custom email templates matching the RocketHacks brand. - ---- - -## 📋 Quick Reference - -| Fix | Status | File(s) | Testing Required | -|-----|--------|---------|------------------| -| Password Reset Flow | ✅ Fixed | 2 files | Yes - Critical | -| Gallery Arrows | ✅ Fixed | 1 file | Yes | -| Footer Cleanup | ✅ Done | 1 file | Quick check | -| Dashboard Links | ✅ Done | 1 file | Quick check | -| Email Templates | ✅ Created | 6 files | Yes - Manual setup | - ---- - -## 🔥 Critical: Password Reset Fix - -**What was broken**: Users clicking password reset links got "Invalid or expired link" errors - -**Root cause**: Callback handler didn't detect password recovery flows - -**What was fixed**: -1. Added `type` parameter detection in callback handler -2. Updated forgot-password API to use correct redirect URL -3. Password reset now works: email → click link → reset-password page → success - -**Files changed**: -- `src/app/api/auth/callback/route.ts` - Added recovery type detection -- `src/app/api/auth/forgot-password/route.ts` - Fixed redirectTo URL - -**Testing**: See [PASSWORD_RESET_FIX.md](PASSWORD_RESET_FIX.md) - ---- - -## 🎨 UI Improvements - -### Gallery Navigation Arrows -**Fixed**: Arrows now work on web and mobile - -**Changes**: -- Added `z-index: 20` so buttons aren't blocked -- Increased touch targets for mobile (`p-2 sm:p-3`) -- Added visual feedback (hover/active states) -- Fixed overlay to not block clicks (`pointer-events-none`) - -**File**: `src/components/gallery/Gallery.tsx` - -### Footer Cleanup -**Removed**: Non-existent Privacy Policy and Terms links - -**Kept**: MLH Code of Conduct (required) - -**File**: `src/components/footer/Footer.tsx` - -### Dashboard Updates -**Removed**: Event Schedule link (doesn't exist yet) - -**Updated**: FAQ link now opens in new tab to home page FAQ section - -**File**: `src/app/dashboard/page.tsx` - ---- - -## 📧 Email Templates - -### What Are They? - -6 custom HTML email templates that match your Hero page aesthetic: - -1. `password-reset.html` - Password reset emails -2. `confirm-signup.html` - Email confirmation for new signups -3. `magic-link.html` - Passwordless login links -4. `invite-user.html` - Admin user invitations -5. `change-email.html` - Email address change (anti-phishing optimized) -6. `reauthentication.html` - Identity verification - -### Design Features -- ✅ Exact RocketHacks gradient colors (#ffc65a → #f483f5 → #c32c9a) -- ✅ Navy dark background (#0a0037 → #030c1b) -- ✅ Glassmorphism effects matching website -- ✅ Mobile-responsive -- ✅ Anti-phishing language - -### How to Apply - -**IMPORTANT**: These templates don't auto-deploy. You must manually copy them to Supabase Dashboard. - -**See detailed instructions**: [SUPABASE_EMAIL_SETUP.md](SUPABASE_EMAIL_SETUP.md) - -**Quick steps**: -1. Go to Supabase Dashboard → Authentication → Email Templates -2. For each template type, copy contents from `supabase/email-templates/` -3. Paste into Supabase editor -4. Verify `{{ .ConfirmationURL }}` is present -5. Save - ---- - -## 📚 Documentation Created - -| Document | Purpose | -|----------|---------| -| [PASSWORD_RESET_FIX.md](PASSWORD_RESET_FIX.md) | Technical details of password reset fix + testing checklist | -| [EMAIL_TEMPLATES_GUIDE.md](EMAIL_TEMPLATES_GUIDE.md) | Email template design details + customization guide | -| [SUPABASE_EMAIL_SETUP.md](SUPABASE_EMAIL_SETUP.md) | **Step-by-step guide to apply templates in Supabase** | -| [CHANGES_SUMMARY.md](CHANGES_SUMMARY.md) | High-level overview of all changes | -| [README_UPDATES.md](README_UPDATES.md) | This file - quick reference | - ---- - -## 🚀 How to Test & Deploy - -### 1. Test Locally (Required) - -```bash -# Start dev server -npm run dev - -# Test password reset -# 1. Go to http://localhost:3000/forgot-password -# 2. Enter your email -# 3. Check email for reset link -# 4. Click link → should go to /reset-password -# 5. Enter new password → should redirect to login -# 6. Login with new password → should work - -# Test gallery -# 1. Go to http://localhost:3000/#gallery -# 2. Click left/right arrows -# 3. Test on mobile or mobile emulator - -# Test dashboard/footer -# 1. Login and check dashboard quick links -# 2. Scroll to footer and verify links -``` - -### 2. Apply Email Templates (Required - Manual Step) - -Follow [SUPABASE_EMAIL_SETUP.md](SUPABASE_EMAIL_SETUP.md) to copy templates to Supabase Dashboard. - -**This is critical** - the templates won't work until you apply them! - -### 3. Commit & Push - -```bash -git add . -git commit -m "fix: password reset flow, gallery navigation, email templates, UI cleanup - -- Fix password reset callback to detect recovery type -- Fix gallery arrow navigation (z-index + touch-friendly) -- Remove non-existent footer links (Privacy/Terms) -- Update dashboard links (remove Event Schedule, FAQ new tab) -- Add 6 custom email templates matching Hero aesthetic -- Add comprehensive documentation" - -git push origin dev-aadi -``` - -### 4. Create PR - -- Source: `dev-aadi` -- Target: `dev` -- Title: "Fix password reset, gallery navigation, and add custom email templates" -- Description: Link to [CHANGES_SUMMARY.md](CHANGES_SUMMARY.md) - -### 5. Test on Dev Environment - -After merging to dev: -- Test password reset flow end-to-end -- Verify gallery arrows work -- Check dashboard/footer changes -- **Important**: Apply email templates to dev Supabase project - -### 6. Production Deployment - -Before deploying to production: -- [ ] All tests pass -- [ ] Email templates applied to production Supabase -- [ ] `NEXT_PUBLIC_SITE_URL` set to production domain in Vercel -- [ ] Custom SMTP configured (recommended) -- [ ] Test emails sent from production - ---- - -## ⚠️ Important Notes - -### Email Templates ARE NOT Auto-Deployed -The HTML files in `supabase/email-templates/` are **source files** only. You MUST manually copy them to Supabase Dashboard for each environment: -- Dev environment Supabase -- Production environment Supabase - -### Redirect URLs Already Configured -You confirmed you removed `/reset-password` and `/setup-password` from Supabase Redirect URLs. Only these should remain: -- `http://localhost:3000/api/auth/callback` -- `https://*.vercel.app/api/auth/callback` -- `https://rockethacks.org/api/auth/callback` -- `https://www.rockethacks.org/api/auth/callback` - -### Build Status -✅ Build compiles successfully with no errors - ---- - -## 📁 Files Changed - -**Core Fixes** (5 files): -``` -src/app/api/auth/callback/route.ts -src/app/api/auth/forgot-password/route.ts -src/components/gallery/Gallery.tsx -src/components/footer/Footer.tsx -src/app/dashboard/page.tsx -``` - -**Email Templates** (6 files): -``` -supabase/email-templates/password-reset.html -supabase/email-templates/confirm-signup.html -supabase/email-templates/magic-link.html -supabase/email-templates/invite-user.html -supabase/email-templates/change-email.html -supabase/email-templates/reauthentication.html -``` - -**Documentation** (5 files): -``` -PASSWORD_RESET_FIX.md -EMAIL_TEMPLATES_GUIDE.md -SUPABASE_EMAIL_SETUP.md -CHANGES_SUMMARY.md -README_UPDATES.md -``` - ---- - -## 🆘 Need Help? - -### Password Reset Still Not Working? -See [PASSWORD_RESET_FIX.md](PASSWORD_RESET_FIX.md) - Troubleshooting section - -### Email Templates Issues? -See [SUPABASE_EMAIL_SETUP.md](SUPABASE_EMAIL_SETUP.md) - Common Issues section - -### General Questions? -- Email: rockethacks@rockets.utoledo.edu -- Check other documentation files for specific topics - ---- - -## ✅ Final Checklist - -Before considering this done: - -**Code Changes**: -- [x] Password reset callback fixed -- [x] Forgot password route fixed -- [x] Gallery arrows fixed -- [x] Footer cleaned up -- [x] Dashboard updated -- [x] Build passing - -**Email Templates**: -- [x] All 6 templates created -- [x] Matching Hero page aesthetic -- [x] Anti-phishing optimized -- [ ] **Applied to Supabase Dashboard** (Your manual step) - -**Testing**: -- [ ] Password reset tested locally -- [ ] Gallery tested on web + mobile -- [ ] Dashboard/footer verified -- [ ] Email templates tested (after applying to Supabase) - -**Deployment**: -- [ ] Code committed to dev-aadi -- [ ] PR created to dev -- [ ] Email templates applied to dev Supabase -- [ ] Tested on dev environment -- [ ] Ready for production - ---- - -**Remember**: The email templates in `supabase/email-templates/` are just source files. You MUST manually copy them to Supabase Dashboard using the steps in [SUPABASE_EMAIL_SETUP.md](SUPABASE_EMAIL_SETUP.md)! - ---- - -**Version**: 1.0.0 -**Last Updated**: November 13, 2025 -**Status**: Ready for Testing & Deployment 🚀 - -© 2026 RocketHacks. All rights reserved. diff --git a/SETUP_COMPLETE.md b/SETUP_COMPLETE.md deleted file mode 100644 index c7886f7e..00000000 --- a/SETUP_COMPLETE.md +++ /dev/null @@ -1,443 +0,0 @@ -# 🎉 Password Authentication Setup Complete! - -## Status: ✅ READY FOR TESTING - -Your RocketHacks application now has a complete dual authentication system! - ---- - -## What's Been Done - -### ✅ Code Implementation (100% Complete) -- [x] Login page with password + Magic Link toggle -- [x] Signup page for new users -- [x] Password reset flow (forgot password → reset) -- [x] Password setup page for existing Magic Link users -- [x] All API routes updated -- [x] Password validation utilities -- [x] Database migration script -- [x] Build successful (no errors) - -### ✅ Documentation (100% Complete) -- [x] Comprehensive migration guide (9000+ words) -- [x] Deployment checklist -- [x] Local testing guide -- [x] All edge cases documented - ---- - -## Quick Start (3 Steps) - -### 1. Configure Supabase (5 minutes) - -**A. Add Redirect URLs** - -Go to: https://supabase.com/dashboard → Your Project → Authentication → URL Configuration - -**Copy and paste these URLs** (one per line): - -**Local:** -``` -http://localhost:3000/reset-password -http://localhost:3000/setup-password -``` - -**Dev:** -``` -https://rockethacks-dev.vercel.app/reset-password -https://rockethacks-dev.vercel.app/setup-password -https://dev.rockethacks.org/reset-password -https://dev.rockethacks.org/setup-password -``` - -**Production:** -``` -https://rockethacks.org/reset-password -https://rockethacks.org/setup-password -https://www.rockethacks.org/reset-password -https://www.rockethacks.org/setup-password -https://rockethacks-main.vercel.app/reset-password -https://rockethacks-main.vercel.app/setup-password -``` - -**B. Run Database Migration** - -Go to: https://supabase.com/dashboard → Your Project → SQL Editor - -Copy the contents of `supabase/migrations/add_password_support.sql` and run it. - -**Verify:** -```sql -SELECT column_name FROM information_schema.columns -WHERE table_name = 'applicants' AND column_name = 'password_setup_completed'; -``` - -Should return `password_setup_completed`. - ---- - -### 2. Test Locally (10 minutes) - -```bash -# Start dev server -npm run dev -``` - -Open: http://localhost:3000 - -**Quick Test Sequence:** -1. Go to `/signup` - create account with password -2. Go to `/login` - sign in with password -3. Try "Magic Link" tab - verify it still works -4. Try "Forgot password?" - test reset flow - -**Full testing guide**: See [LOCAL_TESTING_GUIDE.md](LOCAL_TESTING_GUIDE.md) - ---- - -### 3. Deploy to Dev/Production (5 minutes) - -```bash -# Commit changes -git add . -git commit -m "feat: Add password authentication with Magic Link fallback" - -# Push to dev -git push origin dev - -# Or push to main for production -git push origin main -``` - -Your hosting (Vercel/etc.) will automatically deploy. - ---- - -## File Structure - -### New Files (14 total) - -**Pages (6 files):** -- `src/app/login/page.tsx` - Updated with dual auth -- `src/app/signup/page.tsx` - NEW -- `src/app/setup-password/page.tsx` - NEW -- `src/app/forgot-password/page.tsx` - NEW -- `src/app/reset-password/page.tsx` - NEW - -**API Routes (3 files):** -- `src/app/api/auth/login/route.ts` - Updated -- `src/app/api/auth/signup/route.ts` - NEW -- `src/app/api/auth/forgot-password/route.ts` - NEW - -**Utilities (1 file):** -- `src/lib/utils/passwordValidation.ts` - NEW - -**Database (1 file):** -- `supabase/migrations/add_password_support.sql` - NEW - -**Documentation (3 files):** -- `PASSWORD_AUTH_MIGRATION.md` - Complete guide -- `DEPLOYMENT_CHECKLIST.md` - Step-by-step deployment -- `LOCAL_TESTING_GUIDE.md` - Testing instructions - -**Configuration:** -- `.eslintrc.json` - Updated for build success - ---- - -## Redirect URLs Needed - -### Exact URLs to add in Supabase Dashboard - -#### For Local Development: -``` -http://localhost:3000/reset-password -http://localhost:3000/setup-password -``` - -#### For Dev Environment: -``` -https://rockethacks-dev.vercel.app/reset-password -https://rockethacks-dev.vercel.app/setup-password -https://dev.rockethacks.org/reset-password -https://dev.rockethacks.org/setup-password -``` - -#### For Production: -``` -https://rockethacks.org/reset-password -https://rockethacks.org/setup-password -https://www.rockethacks.org/reset-password -https://www.rockethacks.org/setup-password -https://rockethacks-main.vercel.app/reset-password -https://rockethacks-main.vercel.app/setup-password -https://*.vercel.app/reset-password -https://*.vercel.app/setup-password -``` - -*(The existing `/api/auth/callback` URLs are already sufficient)* - ---- - -## SQL Migration Script - -**Location**: `supabase/migrations/add_password_support.sql` - -**What it does**: -- Adds `password_setup_completed` column to `applicants` table -- Creates index for performance -- Sets default `FALSE` for existing users (Magic Link) -- Sets default `TRUE` for new users (password) - -**Run in Supabase SQL Editor:** -```sql --- Add password support column -ALTER TABLE public.applicants -ADD COLUMN IF NOT EXISTS password_setup_completed BOOLEAN DEFAULT FALSE; - --- Add index -CREATE INDEX IF NOT EXISTS idx_applicants_password_setup -ON public.applicants(user_id, password_setup_completed); - --- Verify -SELECT column_name, data_type, column_default -FROM information_schema.columns -WHERE table_name = 'applicants' - AND column_name = 'password_setup_completed'; -``` - ---- - -## Key Features - -### 1. Dual Authentication -- ✅ Password login (primary) - instant access -- ✅ Magic Link (alternative) - still works for all users -- ✅ Users choose their preferred method - -### 2. Complete Password Management -- ✅ Signup with password -- ✅ Login with password -- ✅ Forgot password flow -- ✅ Reset password flow -- ✅ Password setup for existing users - -### 3. Security -- ✅ Password validation (min 8 chars, complexity rules) -- ✅ Real-time strength indicator -- ✅ Bcrypt hashing by Supabase -- ✅ Email enumeration protection -- ✅ HTTPOnly secure cookies - -### 4. User Experience -- ✅ Beautiful UI matching RocketHacks design -- ✅ Password/Magic Link toggle -- ✅ Show/hide password -- ✅ Password match indicators -- ✅ Clear error messages -- ✅ Loading states - -### 5. Non-Breaking -- ✅ Zero breaking changes -- ✅ Existing Magic Link users unaffected -- ✅ Both methods work simultaneously -- ✅ No forced migration - ---- - -## Testing Checklist - -Quick verification before deploying: - -### Local Testing -- [ ] New user signup with password works -- [ ] Password login works -- [ ] Magic Link still works -- [ ] Password reset flow works -- [ ] Admin access works -- [ ] No console errors - -### Dev Testing (after deploy) -- [ ] All local tests pass on dev URL -- [ ] Email redirects use dev URLs (not localhost) -- [ ] Password reset emails work - -### Production Testing (after deploy) -- [ ] All flows work on production -- [ ] Existing users can still log in -- [ ] New users can sign up with password - -**Full checklist**: See [LOCAL_TESTING_GUIDE.md](LOCAL_TESTING_GUIDE.md) - ---- - -## Environment Variables - -No new variables needed! Existing setup works: - -```env -# .env.local (local development) -NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key -NEXT_PUBLIC_SITE_URL=http://localhost:3000 -ADMIN_EMAILS=your-email@example.com -``` - -Same variables in Vercel for dev/production (change `NEXT_PUBLIC_SITE_URL` accordingly). - ---- - -## Documentation - -### 📚 Complete Guides Available - -1. **[PASSWORD_AUTH_MIGRATION.md](PASSWORD_AUTH_MIGRATION.md)** (9000+ words) - - Complete implementation details - - All edge cases covered - - Troubleshooting guide - - FAQs - - Rollback instructions - -2. **[DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md)** - - Step-by-step deployment - - Pre-deployment checks - - Post-deployment verification - - Common issues & solutions - -3. **[LOCAL_TESTING_GUIDE.md](LOCAL_TESTING_GUIDE.md)** - - Complete test scenarios - - Expected results - - Edge case testing - - Quick commands - ---- - -## Next Actions - -### Immediate (Do Now) -1. ✅ Add redirect URLs to Supabase Dashboard -2. ✅ Run database migration in SQL Editor -3. ✅ Test locally (`npm run dev`) - -### Soon (Before Production) -1. ✅ Deploy to dev environment -2. ✅ Test on dev environment -3. ✅ Fix any issues found - -### Later (Production Ready) -1. ✅ Deploy to production -2. ✅ Test on production -3. ✅ Monitor for issues -4. ✅ (Optional) Announce to users - ---- - -## Support & Troubleshooting - -### If Something Goes Wrong - -1. **Check Documentation** - - [PASSWORD_AUTH_MIGRATION.md](PASSWORD_AUTH_MIGRATION.md) - Comprehensive troubleshooting - - [LOCAL_TESTING_GUIDE.md](LOCAL_TESTING_GUIDE.md) - Testing issues - - [DEPLOYMENT_CHECKLIST.md](DEPLOYMENT_CHECKLIST.md) - Deployment issues - -2. **Check Logs** - - Supabase Dashboard → Logs (for auth errors) - - Browser Console (for client errors) - - Next.js terminal (for server errors) - -3. **Common Issues** - - Build errors → Already fixed with ESLint config - - Suspense errors → Already fixed with Suspense wrappers - - Email not received → Check spam, verify SMTP in Supabase - - Reset link expired → Links expire in 1 hour, request new one - -### Quick Debug Commands - -```bash -# Check build -npm run build - -# Start dev server -npm run dev - -# Check Supabase status (for Mailpit URL in local) -npx supabase status - -# View git changes -git status -git diff -``` - ---- - -## Rollback Plan - -If needed, you can rollback: - -### Code Only (Recommended) -```bash -git revert HEAD -git push -``` - -### Database Only -```sql --- Run in Supabase SQL Editor -DROP INDEX IF EXISTS idx_applicants_password_setup; -ALTER TABLE public.applicants DROP COLUMN IF EXISTS password_setup_completed; -``` - -**Note**: Passwords remain in `auth.users` (managed by Supabase). Rollback just removes tracking column. - ---- - -## Build Status - -``` -✅ Build: SUCCESS -✅ TypeScript: No errors -✅ ESLint: All warnings (non-blocking) -✅ Suspense: Properly wrapped -✅ Static Generation: Fixed -``` - -**Build output**: All pages generated successfully -- Login: ✅ Dynamic route -- Signup: ✅ Dynamic route -- Reset Password: ✅ Dynamic route -- Setup Password: ✅ Dynamic route -- All other pages: ✅ Working - ---- - -## Summary - -🎉 **Your implementation is complete and ready for testing!** - -**What you have:** -- ✅ Production-ready code -- ✅ Comprehensive documentation -- ✅ Successful build -- ✅ All edge cases handled -- ✅ Zero breaking changes -- ✅ Beautiful UI -- ✅ Secure authentication - -**Next step:** Follow [LOCAL_TESTING_GUIDE.md](LOCAL_TESTING_GUIDE.md) to test everything! - ---- - -## Quick Links - -- **Supabase Dashboard**: https://supabase.com/dashboard -- **Local App**: http://localhost:3000 (after `npm run dev`) -- **Documentation**: See the 3 guide files above -- **Schema**: `supabase/schema.sql` -- **Migration**: `supabase/migrations/add_password_support.sql` - ---- - -**Ready to go! 🚀** - -Start by adding the redirect URLs to Supabase, then run the migration, then test locally! diff --git a/package-lock.json b/package-lock.json index 4c7f5aaf..8549cd47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2597,9 +2597,9 @@ } }, "node_modules/@next/env": { - "version": "14.2.32", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.32.tgz", - "integrity": "sha512-n9mQdigI6iZ/DF6pCTwMKeWgF2e8lg7qgt5M7HXMLtyhZYMnf/u905M18sSpPmHL9MKp9JHo56C6jrD2EvWxng==", + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz", + "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -2613,9 +2613,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.32", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.32.tgz", - "integrity": "sha512-osHXveM70zC+ilfuFa/2W6a1XQxJTvEhzEycnjUaVE8kpUS09lDpiDDX2YLdyFCzoUbvbo5r0X1Kp4MllIOShw==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz", + "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==", "cpu": [ "arm64" ], @@ -2629,9 +2629,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.32", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.32.tgz", - "integrity": "sha512-P9NpCAJuOiaHHpqtrCNncjqtSBi1f6QUdHK/+dNabBIXB2RUFWL19TY1Hkhu74OvyNQEYEzzMJCMQk5agjw1Qg==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz", + "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==", "cpu": [ "x64" ], @@ -2645,9 +2645,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.32", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.32.tgz", - "integrity": "sha512-v7JaO0oXXt6d+cFjrrKqYnR2ubrD+JYP7nQVRZgeo5uNE5hkCpWnHmXm9vy3g6foMO8SPwL0P3MPw1c+BjbAzA==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz", + "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==", "cpu": [ "arm64" ], @@ -2661,9 +2661,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.32", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.32.tgz", - "integrity": "sha512-tA6sIKShXtSJBTH88i0DRd6I9n3ZTirmwpwAqH5zdJoQF7/wlJXR8DkPmKwYl5mFWhEKr5IIa3LfpMW9RRwKmQ==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz", + "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==", "cpu": [ "arm64" ], @@ -2677,9 +2677,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.32", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.32.tgz", - "integrity": "sha512-7S1GY4TdnlGVIdeXXKQdDkfDysoIVFMD0lJuVVMeb3eoVjrknQ0JNN7wFlhCvea0hEk0Sd4D1hedVChDKfV2jw==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz", + "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==", "cpu": [ "x64" ], @@ -2693,9 +2693,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.32", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.32.tgz", - "integrity": "sha512-OHHC81P4tirVa6Awk6eCQ6RBfWl8HpFsZtfEkMpJ5GjPsJ3nhPe6wKAJUZ/piC8sszUkAgv3fLflgzPStIwfWg==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz", + "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==", "cpu": [ "x64" ], @@ -2709,9 +2709,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.32", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.32.tgz", - "integrity": "sha512-rORQjXsAFeX6TLYJrCG5yoIDj+NKq31Rqwn8Wpn/bkPNy5rTHvOXkW8mLFonItS7QC6M+1JIIcLe+vOCTOYpvg==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz", + "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==", "cpu": [ "arm64" ], @@ -2725,9 +2725,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.32", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.32.tgz", - "integrity": "sha512-jHUeDPVHrgFltqoAqDB6g6OStNnFxnc7Aks3p0KE0FbwAvRg6qWKYF5mSTdCTxA3axoSAUwxYdILzXJfUwlHhA==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", + "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", "cpu": [ "ia32" ], @@ -2741,9 +2741,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.32", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.32.tgz", - "integrity": "sha512-2N0lSoU4GjfLSO50wvKpMQgKd4HdI2UHEhQPPPnlgfBJlOgJxkjpkYBqzk08f1gItBB6xF/n+ykso2hgxuydsA==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz", + "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==", "cpu": [ "x64" ], @@ -7742,12 +7742,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "14.2.32", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.32.tgz", - "integrity": "sha512-fg5g0GZ7/nFc09X8wLe6pNSU8cLWbLRG3TZzPJ1BJvi2s9m7eF991se67wliM9kR5yLHRkyGKU49MMx58s3LJg==", + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", + "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", "license": "MIT", "dependencies": { - "@next/env": "14.2.32", + "@next/env": "14.2.35", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -7762,15 +7762,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.32", - "@next/swc-darwin-x64": "14.2.32", - "@next/swc-linux-arm64-gnu": "14.2.32", - "@next/swc-linux-arm64-musl": "14.2.32", - "@next/swc-linux-x64-gnu": "14.2.32", - "@next/swc-linux-x64-musl": "14.2.32", - "@next/swc-win32-arm64-msvc": "14.2.32", - "@next/swc-win32-ia32-msvc": "14.2.32", - "@next/swc-win32-x64-msvc": "14.2.32" + "@next/swc-darwin-arm64": "14.2.33", + "@next/swc-darwin-x64": "14.2.33", + "@next/swc-linux-arm64-gnu": "14.2.33", + "@next/swc-linux-arm64-musl": "14.2.33", + "@next/swc-linux-x64-gnu": "14.2.33", + "@next/swc-linux-x64-musl": "14.2.33", + "@next/swc-win32-arm64-msvc": "14.2.33", + "@next/swc-win32-ia32-msvc": "14.2.33", + "@next/swc-win32-x64-msvc": "14.2.33" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", diff --git a/public/assets/rh_26/MAP.png b/public/assets/rh_26/MAP.png new file mode 100644 index 00000000..1993b567 Binary files /dev/null and b/public/assets/rh_26/MAP.png differ diff --git a/public/assets/rh_26/discord-black-icon.png b/public/assets/rh_26/discord-black-icon.png new file mode 100644 index 00000000..5e5caccf Binary files /dev/null and b/public/assets/rh_26/discord-black-icon.png differ diff --git a/public/assets/rh_26/rh_26_folder/rh_26_bundle_png/rh_26_arrow_transparent.png b/public/assets/rh_26/rh_26_folder/rh_26_bundle_png/rh_26_arrow_transparent.png new file mode 100644 index 00000000..965cba49 Binary files /dev/null and b/public/assets/rh_26/rh_26_folder/rh_26_bundle_png/rh_26_arrow_transparent.png differ diff --git a/public/assets/sponsors/11labs.png b/public/assets/sponsors/11labs.png new file mode 100644 index 00000000..d03e9ace Binary files /dev/null and b/public/assets/sponsors/11labs.png differ diff --git a/public/assets/sponsors/Hugging-face.png b/public/assets/sponsors/Hugging-face.png deleted file mode 100644 index 526aa009..00000000 Binary files a/public/assets/sponsors/Hugging-face.png and /dev/null differ diff --git a/public/assets/sponsors/Mistral2.png b/public/assets/sponsors/Mistral2.png deleted file mode 100644 index a88029c4..00000000 Binary files a/public/assets/sponsors/Mistral2.png and /dev/null differ diff --git a/public/assets/sponsors/Perplexity.png b/public/assets/sponsors/Perplexity.png deleted file mode 100644 index 5173f909..00000000 Binary files a/public/assets/sponsors/Perplexity.png and /dev/null differ diff --git a/public/assets/sponsors/UT_Hortz.svg.png b/public/assets/sponsors/UT_Hortz.svg.png new file mode 100644 index 00000000..5792d868 Binary files /dev/null and b/public/assets/sponsors/UT_Hortz.svg.png differ diff --git a/public/assets/sponsors/actual-tech.png b/public/assets/sponsors/actual.png similarity index 100% rename from public/assets/sponsors/actual-tech.png rename to public/assets/sponsors/actual.png diff --git a/public/assets/sponsors/anthropic.png b/public/assets/sponsors/anthropic.png new file mode 100644 index 00000000..761e4002 Binary files /dev/null and b/public/assets/sponsors/anthropic.png differ diff --git a/public/assets/sponsors/aws.png b/public/assets/sponsors/aws.png new file mode 100644 index 00000000..1bf4345f Binary files /dev/null and b/public/assets/sponsors/aws.png differ diff --git a/public/assets/sponsors/base44.png b/public/assets/sponsors/base44.png new file mode 100644 index 00000000..ee3f6374 Binary files /dev/null and b/public/assets/sponsors/base44.png differ diff --git a/public/assets/sponsors/besnx.png b/public/assets/sponsors/besnx.png new file mode 100644 index 00000000..ed696f6e Binary files /dev/null and b/public/assets/sponsors/besnx.png differ diff --git a/public/assets/sponsors/cdw-education.png b/public/assets/sponsors/cdw-education.png deleted file mode 100644 index 49aaac24..00000000 Binary files a/public/assets/sponsors/cdw-education.png and /dev/null differ diff --git a/public/assets/sponsors/cobi.png b/public/assets/sponsors/cobi.png new file mode 100644 index 00000000..241fcd4a Binary files /dev/null and b/public/assets/sponsors/cobi.png differ diff --git a/public/assets/sponsors/College-of-engineering.png b/public/assets/sponsors/coe.png similarity index 100% rename from public/assets/sponsors/College-of-engineering.png rename to public/assets/sponsors/coe.png diff --git a/public/assets/sponsors/desmos.png b/public/assets/sponsors/desmos.png deleted file mode 100644 index 95a48cf0..00000000 Binary files a/public/assets/sponsors/desmos.png and /dev/null differ diff --git a/public/assets/sponsors/fai.png b/public/assets/sponsors/fai.png new file mode 100644 index 00000000..86157ced Binary files /dev/null and b/public/assets/sponsors/fai.png differ diff --git a/public/assets/sponsors/impelix.png b/public/assets/sponsors/impelix.png new file mode 100644 index 00000000..bc0294a8 Binary files /dev/null and b/public/assets/sponsors/impelix.png differ diff --git a/public/assets/sponsors/mercy.png b/public/assets/sponsors/mercy.png deleted file mode 100644 index a71f533e..00000000 Binary files a/public/assets/sponsors/mercy.png and /dev/null differ diff --git a/public/assets/sponsors/nsm.png b/public/assets/sponsors/nsm.png new file mode 100644 index 00000000..fe53eba5 Binary files /dev/null and b/public/assets/sponsors/nsm.png differ diff --git a/public/assets/sponsors/orielly.png b/public/assets/sponsors/orielly.png deleted file mode 100644 index 5488ad1c..00000000 Binary files a/public/assets/sponsors/orielly.png and /dev/null differ diff --git a/public/assets/sponsors/parkut.png b/public/assets/sponsors/parkut.png new file mode 100644 index 00000000..f25a852a Binary files /dev/null and b/public/assets/sponsors/parkut.png differ diff --git a/public/assets/sponsors/photoroom.png b/public/assets/sponsors/photoroom.png deleted file mode 100644 index d0ee2e47..00000000 Binary files a/public/assets/sponsors/photoroom.png and /dev/null differ diff --git a/public/assets/sponsors/purebutton.png b/public/assets/sponsors/purebutton.png new file mode 100644 index 00000000..336a44c4 Binary files /dev/null and b/public/assets/sponsors/purebutton.png differ diff --git a/public/assets/sponsors/rgp.png b/public/assets/sponsors/rgp.png new file mode 100644 index 00000000..7925b307 Binary files /dev/null and b/public/assets/sponsors/rgp.png differ diff --git a/public/assets/sponsors/runanywhere.png b/public/assets/sponsors/runanywhere.png new file mode 100644 index 00000000..408ebae0 Binary files /dev/null and b/public/assets/sponsors/runanywhere.png differ diff --git a/public/assets/sponsors/spoke.png b/public/assets/sponsors/spoke.png deleted file mode 100644 index 66ddadd3..00000000 Binary files a/public/assets/sponsors/spoke.png and /dev/null differ diff --git a/public/assets/sponsors/sprintlogo.png b/public/assets/sponsors/sprintlogo.png deleted file mode 100644 index 462e12bc..00000000 Binary files a/public/assets/sponsors/sprintlogo.png and /dev/null differ diff --git a/public/assets/sponsors/standout-stickers.png b/public/assets/sponsors/standout-stickers.png deleted file mode 100644 index 92ebc2d7..00000000 Binary files a/public/assets/sponsors/standout-stickers.png and /dev/null differ diff --git a/public/assets/sponsors/vercel.png b/public/assets/sponsors/vercel.png deleted file mode 100644 index 0e9c1ac7..00000000 Binary files a/public/assets/sponsors/vercel.png and /dev/null differ diff --git a/public/assets/sponsors/warp.png b/public/assets/sponsors/warp.png deleted file mode 100644 index 248963b1..00000000 Binary files a/public/assets/sponsors/warp.png and /dev/null differ diff --git a/public/assets/sponsors/xyz.png b/public/assets/sponsors/xyz.png deleted file mode 100644 index 197648a6..00000000 Binary files a/public/assets/sponsors/xyz.png and /dev/null differ diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 1587406d..5328b3b3 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,181 +1,203 @@ -'use client' +"use client"; -import { useState, useEffect } from 'react' -import { useRouter } from 'next/navigation' -import { createClient } from '@/lib/supabase/client' -import Link from 'next/link' -import * as XLSX from 'xlsx' +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { createClient } from "@/lib/supabase/client"; +import Link from "next/link"; +import * as XLSX from "xlsx"; export default function AdminPage() { - const router = useRouter() - const supabase = createClient() - const [loading, setLoading] = useState(true) - const [applications, setApplications] = useState([]) - const [stats, setStats] = useState(null) - const [filter, setFilter] = useState('all') - const [searchTerm, setSearchTerm] = useState('') - const [selectedApp, setSelectedApp] = useState(null) - const [updatingRole, setUpdatingRole] = useState(null) + const router = useRouter(); + const supabase = createClient(); + const [loading, setLoading] = useState(true); + const [applications, setApplications] = useState([]); + const [stats, setStats] = useState(null); + const [filter, setFilter] = useState("all"); + const [searchTerm, setSearchTerm] = useState(""); + const [selectedApp, setSelectedApp] = useState(null); + const [updatingRole, setUpdatingRole] = useState(null); useEffect(() => { async function checkAdminAndLoadData() { // Check if user is admin - const adminResponse = await fetch('/api/auth/user') - const adminData = await adminResponse.json() + const adminResponse = await fetch("/api/auth/user"); + const adminData = await adminResponse.json(); if (!adminData.isAdmin) { - router.push('/dashboard') - return + router.push("/dashboard"); + return; } - await loadApplications() - await loadStats() - setLoading(false) + await loadApplications(); + await loadStats(); + setLoading(false); } - checkAdminAndLoadData() - }, []) + checkAdminAndLoadData(); + }, []); + + const calculateRsvpStats = () => { + const rsvpCount = applications.filter((app) => app.rsvp_attending).length; + const rsvpFirstTimers = applications.filter( + (app) => app.rsvp_attending && app.first_hackathon, + ).length; + + return { + status: "rsvp", + count: rsvpCount, + first_time_hackers: rsvpFirstTimers, + }; + }; const loadApplications = async () => { const { data, error } = await supabase - .from('applicants') - .select('*') - .order('submitted_at', { ascending: false }) + .from("applicants") + .select("*") + .order("submitted_at", { ascending: false }); if (error) { - console.error('Error loading applications:', error) + console.error("Error loading applications:", error); } else { - setApplications(data || []) + setApplications(data || []); } - } + }; const loadStats = async () => { const { data, error } = await supabase - .from('application_stats') - .select('*') + .from("application_stats") + .select("*"); if (error) { - console.error('Error loading stats:', error) + console.error("Error loading stats:", error); } else { - setStats(data) + setStats(data); } - } + }; const updateStatus = async (applicantId: string, newStatus: string) => { const { error } = await supabase - .from('applicants') + .from("applicants") .update({ status: newStatus }) - .eq('id', applicantId) + .eq("id", applicantId); if (error) { - console.error('Error updating status:', error) - alert('Failed to update status') + console.error("Error updating status:", error); + alert("Failed to update status"); } else { - await loadApplications() - await loadStats() - setSelectedApp(null) + await loadApplications(); + await loadStats(); + setSelectedApp(null); } - } + }; const updateRole = async (applicantId: string, newRole: string) => { - setUpdatingRole(applicantId) + setUpdatingRole(applicantId); - const response = await fetch('/api/admin/roles', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, + const response = await fetch("/api/admin/roles", { + method: "PUT", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ applicant_id: applicantId, - new_role: newRole - }) - }) + new_role: newRole, + }), + }); if (response.ok) { - await loadApplications() + await loadApplications(); // Update selected app if it's the one being modified if (selectedApp && selectedApp.id === applicantId) { - const updated = applications.find(app => app.id === applicantId) - if (updated) setSelectedApp(updated) + const updated = applications.find((app) => app.id === applicantId); + if (updated) setSelectedApp(updated); } } else { - const error = await response.json() - alert(`Error: ${error.error}`) + const error = await response.json(); + alert(`Error: ${error.error}`); } - setUpdatingRole(null) - } + setUpdatingRole(null); + }; const handleLogout = async () => { - await supabase.auth.signOut() - router.push('/') - } - - const filteredApplications = applications.filter(app => { - const matchesFilter = filter === 'all' || app.status === filter - const fullName = `${app.first_name} ${app.last_name}`.toLowerCase() - const matchesSearch = - searchTerm === '' || + await supabase.auth.signOut(); + router.push("/"); + }; + + const filteredApplications = applications.filter((app) => { + const matchesFilter = filter === "all" || app.status === filter; + const fullName = `${app.first_name} ${app.last_name}`.toLowerCase(); + const matchesSearch = + searchTerm === "" || fullName.includes(searchTerm.toLowerCase()) || app.email.toLowerCase().includes(searchTerm.toLowerCase()) || - app.school.toLowerCase().includes(searchTerm.toLowerCase()) - return matchesFilter && matchesSearch - }) + app.school.toLowerCase().includes(searchTerm.toLowerCase()); + return matchesFilter && matchesSearch; + }); const getStatusColor = (status: string) => { switch (status) { - case 'pending': - return 'bg-yellow-500/20 text-yellow-400' - case 'accepted': - return 'bg-green-500/20 text-green-400' - case 'rejected': - return 'bg-red-500/20 text-red-400' - case 'waitlisted': - return 'bg-blue-500/20 text-blue-400' + case "pending": + return "bg-yellow-500/20 text-yellow-400"; + case "accepted": + return "bg-green-500/20 text-green-400"; + case "rejected": + return "bg-red-500/20 text-red-400"; + case "waitlisted": + return "bg-blue-500/20 text-blue-400"; + case "rsvp": + return "bg-purple-500/20 text-purple-400"; default: - return 'bg-gray-500/20 text-gray-400' + return "bg-gray-500/20 text-gray-400"; } - } + }; + + const getRsvpColor = (rsvpAttending: boolean) => { + return rsvpAttending + ? "bg-green-500/20 text-green-400" + : "bg-white/10 text-gray-200"; + }; const exportToExcel = () => { // Prepare data with headers - const data = filteredApplications.map(app => ({ - 'First Name': app.first_name || '', - 'Last Name': app.last_name || '', - 'Email': app.email, - 'Phone': app.phone_number || '', - 'Age': app.age || '', - 'School': app.school, - 'Major': app.major || '', - 'Level of Study': app.level_of_study || '', - 'Country': app.country_of_residence || '', - 'Gender': app.gender || '', - 'Pronouns': app.pronouns || '', - 'Status': app.status, - 'Role': app.role || 'participant', - 'First Hackathon': app.first_hackathon ? 'Yes' : 'No', - 'T-Shirt Size': app.tshirt_size || '', - 'Dietary Restrictions': Array.isArray(app.dietary_restrictions) - ? app.dietary_restrictions.join('; ') - : app.dietary_restrictions || '', - 'Race/Ethnicity': Array.isArray(app.race_ethnicity) - ? app.race_ethnicity.join('; ') - : app.race_ethnicity || '', - 'Team Name': app.team_name || '', - 'GitHub': app.github_url || '', - 'LinkedIn': app.linkedin_url || '', - 'Portfolio': app.portfolio_url || '', - 'Special Accommodations': app.special_accommodations || '', - 'Submitted At': new Date(app.submitted_at).toLocaleString() - })) + const data = filteredApplications.map((app) => ({ + "First Name": app.first_name || "", + "Last Name": app.last_name || "", + Email: app.email, + Phone: app.phone_number || "", + Age: app.age || "", + School: app.school, + Major: app.major || "", + "Level of Study": app.level_of_study || "", + Country: app.country_of_residence || "", + Gender: app.gender || "", + Pronouns: app.pronouns || "", + Status: app.status, + RSVP: app.rsvp_attending ? "Yes" : "No", + Role: app.role || "participant", + "First Hackathon": app.first_hackathon ? "Yes" : "No", + "T-Shirt Size": app.tshirt_size || "", + "Dietary Restrictions": Array.isArray(app.dietary_restrictions) + ? app.dietary_restrictions.join("; ") + : app.dietary_restrictions || "", + "Race/Ethnicity": Array.isArray(app.race_ethnicity) + ? app.race_ethnicity.join("; ") + : app.race_ethnicity || "", + "Team Name": app.team_name || "", + GitHub: app.github_url || "", + LinkedIn: app.linkedin_url || "", + Portfolio: app.portfolio_url || "", + "Special Accommodations": app.special_accommodations || "", + "Submitted At": new Date(app.submitted_at).toLocaleString(), + })); // Create worksheet - const worksheet = XLSX.utils.json_to_sheet(data) - + const worksheet = XLSX.utils.json_to_sheet(data); + // Set column widths for better readability const columnWidths = [ { wch: 15 }, // First Name { wch: 15 }, // Last Name { wch: 30 }, // Email { wch: 15 }, // Phone - { wch: 8 }, // Age + { wch: 8 }, // Age { wch: 35 }, // School { wch: 25 }, // Major { wch: 15 }, // Level of Study @@ -183,6 +205,7 @@ export default function AdminPage() { { wch: 12 }, // Gender { wch: 12 }, // Pronouns { wch: 12 }, // Status + { wch: 10 }, // RSVP { wch: 12 }, // Role { wch: 15 }, // First Hackathon { wch: 12 }, // T-Shirt Size @@ -193,25 +216,25 @@ export default function AdminPage() { { wch: 30 }, // LinkedIn { wch: 30 }, // Portfolio { wch: 40 }, // Special Accommodations - { wch: 20 } // Submitted At - ] - worksheet['!cols'] = columnWidths + { wch: 20 }, // Submitted At + ]; + worksheet["!cols"] = columnWidths; // Create workbook and add worksheet - const workbook = XLSX.utils.book_new() - XLSX.utils.book_append_sheet(workbook, worksheet, 'Applications') + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, "Applications"); // Generate Excel file and trigger download - const fileName = `RocketHacks_2026_Applications_${new Date().toISOString().split('T')[0]}.xlsx` - XLSX.writeFile(workbook, fileName) - } + const fileName = `RocketHacks_2026_Applications_${new Date().toISOString().split("T")[0]}.xlsx`; + XLSX.writeFile(workbook, fileName); + }; if (loading) { return (
Loading...
- ) + ); } return ( @@ -221,7 +244,9 @@ export default function AdminPage() {

Admin Portal

-

Manage RocketHacks 2026 applications

+

+ Manage RocketHacks 2026 applications +

0 && ( -
+
{stats.map((stat: any) => ( -
-
+
+
{stat.status}
-
{stat.count}
+
+ {stat.count} +
{stat.first_time_hackers} first-timers
))} + {/* RSVP Card */} + {applications.length > 0 && ( +
+
+ RSVP +
+
+ {calculateRsvpStats().count} +
+
+ {calculateRsvpStats().first_time_hackers} first-timers +
+
+ )}
)} @@ -291,20 +337,21 @@ export default function AdminPage() {
- Showing {filteredApplications.length} of {applications.length} applications + Showing {filteredApplications.length} of {applications.length}{" "} + applications
{/* Applications Table */}
-
+
- + @@ -312,16 +359,31 @@ export default function AdminPage() { {filteredApplications.map((app) => ( - + - - + + @@ -352,20 +414,32 @@ export default function AdminPage() { {/* Application Detail Modal */} {selectedApp && ( -
setSelectedApp(null)}> -
e.stopPropagation()}> +
setSelectedApp(null)} + > +
e.stopPropagation()} + >
-

{selectedApp.first_name} {selectedApp.last_name}

+

+ {selectedApp.first_name} {selectedApp.last_name} +

{selectedApp.email}

{/* Role Management */}
- + {updatingRole === selectedApp.id && ( - Updating... + + Updating... + )}
@@ -389,54 +465,82 @@ export default function AdminPage() {
-

Personal

+

+ Personal +

-

Phone: {selectedApp.phone_number || 'N/A'}

-

Age: {selectedApp.age || 'N/A'}

-

Country: {selectedApp.country_of_residence || 'N/A'}

-

Gender: {selectedApp.gender || 'N/A'}

-

Pronouns: {selectedApp.pronouns || 'N/A'}

+

Phone: {selectedApp.phone_number || "N/A"}

+

Age: {selectedApp.age || "N/A"}

+

+ Country: {selectedApp.country_of_residence || "N/A"} +

+

Gender: {selectedApp.gender || "N/A"}

+

Pronouns: {selectedApp.pronouns || "N/A"}

-

Education

+

+ Education +

School: {selectedApp.school}

-

Major: {selectedApp.major || 'N/A'}

-

Level: {selectedApp.level_of_study || 'N/A'}

+

Major: {selectedApp.major || "N/A"}

+

Level: {selectedApp.level_of_study || "N/A"}

-

Links

+

+ Links +

{selectedApp.github_url && (

- + GitHub →

)} {selectedApp.linkedin_url && (

- + LinkedIn →

)} {selectedApp.portfolio_url && (

- + Portfolio →

)} {selectedApp.resume_url && (

- + Resume →

@@ -445,13 +549,28 @@ export default function AdminPage() {
-

Event Details

+

+ Event Details +

-

First Hackathon: {selectedApp.first_hackathon ? 'Yes' : 'No'}

-

Team: {selectedApp.team_name || 'N/A'}

-

T-Shirt: {selectedApp.tshirt_size || 'N/A'}

-

Dietary: {Array.isArray(selectedApp.dietary_restrictions) ? selectedApp.dietary_restrictions.join(', ') : selectedApp.dietary_restrictions || 'None'}

-

Race/Ethnicity: {Array.isArray(selectedApp.race_ethnicity) ? selectedApp.race_ethnicity.join(', ') : selectedApp.race_ethnicity || 'N/A'}

+

+ First Hackathon:{" "} + {selectedApp.first_hackathon ? "Yes" : "No"} +

+

Team: {selectedApp.team_name || "N/A"}

+

T-Shirt: {selectedApp.tshirt_size || "N/A"}

+

+ Dietary:{" "} + {Array.isArray(selectedApp.dietary_restrictions) + ? selectedApp.dietary_restrictions.join(", ") + : selectedApp.dietary_restrictions || "None"} +

+

+ Race/Ethnicity:{" "} + {Array.isArray(selectedApp.race_ethnicity) + ? selectedApp.race_ethnicity.join(", ") + : selectedApp.race_ethnicity || "N/A"} +

@@ -459,16 +578,24 @@ export default function AdminPage() { {selectedApp.special_accommodations && (
-

Special Accommodations

-

{selectedApp.special_accommodations}

+

+ Special Accommodations +

+

+ {selectedApp.special_accommodations} +

)} {selectedApp.resume_markdown && (
-

Resume (Parsed)

+

+ Resume (Parsed) +

-
{selectedApp.resume_markdown}
+
+                      {selectedApp.resume_markdown}
+                    
)} @@ -476,25 +603,25 @@ export default function AdminPage() { {/* Status Update */}
)}
- ) + ); } diff --git a/src/app/apply/page.tsx b/src/app/apply/page.tsx index dd51e6b3..de31c8fd 100644 --- a/src/app/apply/page.tsx +++ b/src/app/apply/page.tsx @@ -426,22 +426,22 @@ export default function ApplyPage() { {/* MLH Partnership */}

MLH Partnership

-
+
diff --git a/src/app/apply/page_new.tsx b/src/app/apply/page_new.tsx deleted file mode 100644 index 4372f608..00000000 --- a/src/app/apply/page_new.tsx +++ /dev/null @@ -1,714 +0,0 @@ -'use client' - -import { useState, useEffect } from 'react' -import { useRouter } from 'next/navigation' -import { createClient } from '@/lib/supabase/client' -import { loadSchoolsList } from '@/lib/mlhSchools' -import { SchoolAutocomplete } from '@/components/ui/school-autocomplete' -import Image from 'next/image' -import { terminal } from '../fonts/fonts' -import { - LEVEL_OF_STUDY_OPTIONS, - DIETARY_RESTRICTIONS_OPTIONS, - GENDER_OPTIONS, - PRONOUNS_OPTIONS, - RACE_ETHNICITY_OPTIONS, - TSHIRT_SIZES, - MAJOR_OPTIONS, - COUNTRIES, - AGE_OPTIONS, -} from '@/lib/mlhConstants' - -export default function ApplyPage() { - const router = useRouter() - const supabase = createClient() - const [loading, setLoading] = useState(false) - const [uploading, setUploading] = useState(false) - const [error, setError] = useState('') - const [user, setUser] = useState(null) - const [schools, setSchools] = useState([]) - - const [formData, setFormData] = useState({ - first_name: '', - last_name: '', - age: '', - phone_number: '', - email: '', - school: '', - level_of_study: '', - country_of_residence: '', - linkedin_url: '', - github_url: '', - portfolio_url: '', - resume_url: '', - mlh_code_of_conduct: false, - mlh_privacy_policy: false, - mlh_marketing_emails: false, - dietary_restrictions: [] as string[], - gender: '', - pronouns: '', - race_ethnicity: [] as string[], - tshirt_size: '', - address_line1: '', - address_line2: '', - city: '', - state: '', - shipping_country: '', - postal_code: '', - major: '', - first_hackathon: false, - team_name: '', - special_accommodations: '', - }) - - useEffect(() => { - loadUserData() - }, []) - - async function loadUserData() { - const { data: { session }, error: sessionError } = await supabase.auth.getSession() - - if (sessionError) { - console.error('Session error:', sessionError) - const { error: refreshError } = await supabase.auth.refreshSession() - if (refreshError) { - router.push('/login?redirect=/apply') - return - } - } - - const { data: { user } } = await supabase.auth.getUser() - - if (!user) { - router.push('/login?redirect=/apply') - return - } - setUser(user) - - const schoolsList = await loadSchoolsList() - setSchools(schoolsList) - - const { data: existingApp, error: loadError } = await supabase - .from('applicants') - .select('*') - .eq('user_id', user.id) - .maybeSingle() - - if (loadError) { - console.error('Error loading application:', loadError) - } - - if (existingApp) { - let schoolValue = existingApp.school || '' - if (schoolValue && !/^[\w\s\-\(\),'\.&]+$/.test(schoolValue)) { - console.warn('Corrupted school value detected, clearing') - schoolValue = '' - } - - setFormData({ - ...existingApp, - school: schoolValue, - age: existingApp.age?.toString() || '', - dietary_restrictions: existingApp.dietary_restrictions || [], - race_ethnicity: existingApp.race_ethnicity || [], - }) - } else { - setFormData(prev => ({ ...prev, email: user.email || '' })) - } - } - - const handleInputChange = (e: React.ChangeEvent) => { - const { name, value, type } = e.target - const checked = (e.target as HTMLInputElement).checked - setFormData(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value })) - } - - const handleMultiSelectChange = (field: 'dietary_restrictions' | 'race_ethnicity', value: string) => { - setFormData(prev => { - const currentArray = prev[field] as string[] - const newArray = currentArray.includes(value) - ? currentArray.filter(v => v !== value) - : [...currentArray, value] - return { ...prev, [field]: newArray } - }) - } - - const handleResumeUpload = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0] - if (!file) return - - const validTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'] - if (!validTypes.includes(file.type)) { - setError('Please upload a PDF or DOCX file') - return - } - - if (file.size > 5 * 1024 * 1024) { - setError('File size must be less than 5MB') - return - } - - setUploading(true) - setError('') - - try { - const fileExt = file.name.split('.').pop() - // Create filename with format: firstname_lastname_school - const sanitizedSchool = formData.school.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 50) - const fileName = `${formData.first_name}_${formData.last_name}_${sanitizedSchool}.${fileExt}` - const filePath = `resumes/${user.id}/${fileName}` - - const { error: uploadError } = await supabase.storage - .from('applicant-files') - .upload(filePath, file, { - upsert: true - }) - - if (uploadError) throw uploadError - - const { data: { publicUrl } } = supabase.storage - .from('applicant-files') - .getPublicUrl(filePath) - - setFormData(prev => ({ - ...prev, - resume_url: publicUrl, - })) - } catch (err: any) { - console.error('Error uploading resume:', err) - setError(`Resume upload failed: ${err.message || 'Unknown error'}. Please try again.`) - } finally { - setUploading(false) - } - } - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setLoading(true) - setError('') - - if (!formData.mlh_code_of_conduct || !formData.mlh_privacy_policy) { - setError('You must agree to the MLH Code of Conduct and Privacy Policy') - setLoading(false) - return - } - - if (!schools.includes(formData.school)) { - setError('Please select a valid school from the MLH-verified list') - setLoading(false) - return - } - - try { - const applicationData = { - user_id: user.id, - first_name: formData.first_name, - last_name: formData.last_name, - age: parseInt(formData.age), - phone_number: formData.phone_number, - email: formData.email, - school: formData.school, - level_of_study: formData.level_of_study, - country_of_residence: formData.country_of_residence, - linkedin_url: formData.linkedin_url || null, - github_url: formData.github_url || null, - portfolio_url: formData.portfolio_url || null, - resume_url: formData.resume_url || null, - mlh_code_of_conduct: formData.mlh_code_of_conduct, - mlh_privacy_policy: formData.mlh_privacy_policy, - mlh_marketing_emails: formData.mlh_marketing_emails, - dietary_restrictions: formData.dietary_restrictions.length > 0 ? formData.dietary_restrictions : null, - gender: formData.gender || null, - pronouns: formData.pronouns || null, - race_ethnicity: formData.race_ethnicity.length > 0 ? formData.race_ethnicity : null, - tshirt_size: formData.tshirt_size || null, - address_line1: formData.address_line1 || null, - address_line2: formData.address_line2 || null, - city: formData.city || null, - state: formData.state || null, - shipping_country: formData.shipping_country || null, - postal_code: formData.postal_code || null, - major: formData.major || null, - first_hackathon: formData.first_hackathon, - team_name: formData.team_name || null, - special_accommodations: formData.special_accommodations || null, - } - - const { error: upsertError } = await supabase - .from('applicants') - .upsert(applicationData, { - onConflict: 'user_id', - ignoreDuplicates: false, - }) - - if (upsertError) { - console.error('Upsert error:', upsertError) - throw upsertError - } - - router.push('/dashboard') - } catch (err: any) { - console.error('Error submitting application:', err) - setError(err.message || 'Failed to submit application') - } finally { - setLoading(false) - } - } - - if (!user) { - return ( -
-
- -
-
-
Loading...
-
- ) - } - - return ( -
- {/* Background */} -
- -
-
- - {/* Floating Elements */} -
-
-
-
- - {/* Content */} -
-
- {/* Header */} -
-

- Apply to RocketHacks 2026 -

-

Join the biggest hackathon in the Midwest

-
- - {error && ( -
- {error} -
- )} - -
- {/* Personal Information */} -
-

- Personal Information -

- -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - {/* Education */} -
-

- Education -

- -
-
- - setFormData(prev => ({ ...prev, school: value }))} - schools={schools} - required - error={error && !formData.school ? 'School is required' : undefined} - /> -

- Only MLH-verified schools are accepted. Contact us if your school isn't listed. -

-
-
-
- - -
-
- - -
-
-
- - -
-
-
- - {/* Professional Links */} -
-

- Professional Links -

- -
-
- - -
-
- - -
-
- - -
-
-
- - {/* Resume Upload */} -
-

- Resume -

-
- {formData.resume_url ? ( - // Show uploaded resume with actions -
-
-
- - - -
-

✓ Resume uploaded successfully

-

- {formData.first_name}_{formData.last_name}_{formData.school.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 20)}... -

-
-
-
- - View - - -
-
-
- ) : ( - // Show upload input - <> - - {uploading && ( -
- - - - - Uploading... -
- )} - {(!formData.first_name || !formData.last_name || !formData.school) && ( -

Please fill in your name and school before uploading resume

- )} - - )} -

- Upload your resume as PDF or DOCX (max 5MB). You'll be able to view and download it from your dashboard. -

-
-
- - {/* MLH Requirements */} -
-

- MLH Requirements -

-
- - - -
-
- - {/* Optional Sections - Collapsed by default */} -
- - Optional Information - -
- {/* Demographics, Shipping, etc. - Keeping this section minimal for now */} -
-
- - -
-
- -
-
-
-
- - {/* Submit Buttons */} -
- - -
- -
-
-
- ) -} diff --git a/src/app/code-create/page.tsx b/src/app/code-create/page.tsx index d4107bc4..10ac5dc1 100644 --- a/src/app/code-create/page.tsx +++ b/src/app/code-create/page.tsx @@ -3,33 +3,47 @@ import React from "react"; import Image from "next/image"; import Link from "next/link"; import { terminal } from "../fonts/fonts"; -import { FaCode, FaUsers, FaTrophy, FaUniversity, FaHandshake } from "react-icons/fa"; +import { + FaCode, + FaUsers, + FaTrophy, + FaUniversity, + FaHandshake, +} from "react-icons/fa"; import { PiTerminalWindow } from "react-icons/pi"; import { GiBrain } from "react-icons/gi"; export default function CodeCreatePage() { return ( -
+
+ {/* Unified background pattern (prevents segmented/sectioned look) */} +
{/* Navigation Bar - Simple version */} -
Name School MajorAgeRSVP Status Submitted Actions
-
{app.first_name} {app.last_name}
+
+ {app.first_name} {app.last_name} +
{app.email}
{app.school}{app.major || '-'}{app.age || '-'} + {app.major || "-"} + - + + {!!app.rsvp_attending ? "Yes" : "No"} + + + {app.status}
@@ -198,13 +262,20 @@ export default function OrganizerPortal() { {filteredApplicants.map((app) => ( - + - + @@ -260,16 +335,25 @@ export default function OrganizerPortal() { {/* Participant Detail Modal */} {selectedApp && ( -
setSelectedApp(null)}> -
e.stopPropagation()}> +
setSelectedApp(null)} + > +
e.stopPropagation()} + >
-

{selectedApp.first_name} {selectedApp.last_name}

+

+ {selectedApp.first_name} {selectedApp.last_name} +

{selectedApp.email}

{selectedApp.checked_in && selectedApp.checked_in_at && (

- ✓ Checked in: {new Date(selectedApp.checked_in_at).toLocaleString()} + ✓ Checked in:{" "} + {new Date(selectedApp.checked_in_at).toLocaleString()}

)}
@@ -284,38 +368,58 @@ export default function OrganizerPortal() {
-

Personal

+

+ Personal +

-

Phone: {selectedApp.phone_number || 'N/A'}

-

Age: {selectedApp.age || 'N/A'}

-

Country: {selectedApp.country_of_residence || 'N/A'}

+

Phone: {selectedApp.phone_number || "N/A"}

+

Age: {selectedApp.age || "N/A"}

+

+ Country: {selectedApp.country_of_residence || "N/A"} +

-

Education

+

+ Education +

School: {selectedApp.school}

-

Major: {selectedApp.major || 'N/A'}

-

Level: {selectedApp.level_of_study || 'N/A'}

+

Major: {selectedApp.major || "N/A"}

+

Level: {selectedApp.level_of_study || "N/A"}

-

Event Details

+

+ Event Details +

-

First Hackathon: {selectedApp.first_hackathon ? 'Yes' : 'No'}

-

T-Shirt: {selectedApp.tshirt_size || 'N/A'}

-

Dietary: {Array.isArray(selectedApp.dietary_restrictions) ? selectedApp.dietary_restrictions.join(', ') : 'None'}

+

+ First Hackathon:{" "} + {selectedApp.first_hackathon ? "Yes" : "No"} +

+

T-Shirt: {selectedApp.tshirt_size || "N/A"}

+

+ Dietary:{" "} + {Array.isArray(selectedApp.dietary_restrictions) + ? selectedApp.dietary_restrictions.join(", ") + : "None"} +

{selectedApp.special_accommodations && (
-

Special Accommodations

-

{selectedApp.special_accommodations}

+

+ Special Accommodations +

+

+ {selectedApp.special_accommodations} +

)}
@@ -323,25 +427,26 @@ export default function OrganizerPortal() { {/* Check-In Button */}
)}
- ) + ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index a28c898e..b1751732 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,5 @@ "use client"; -import React from "react"; +import React, { useEffect } from "react"; import dynamic from "next/dynamic"; import Navbar from "@/components/navbar/Navbar"; @@ -7,23 +7,35 @@ import Hero from "@/components/hero/Hero"; import About from "@/components/about/About"; import { SponsorData } from "@/components/sponsor/SponsorData"; /* Kept for future use */ -/* import Schedule from "@/components/schedule/Schedule"; */ /* import { Prizes } from "@/components/prizes/Prizes"; */ // Lazy load heavy components that are below the fold const Sponsor = dynamic(() => import("@/components/sponsor/Sponsor"), { - loading: () =>
-}); - -const Gallery = dynamic(() => import("@/components/gallery/Gallery"), { - loading: () =>
+ loading: () => ( +
+
+
+ ), }); const Contact = dynamic(() => import("@/components/contact/Contact")); -const Faq = dynamic(() => import("@/components/faq/Faq")); const Footer = dynamic(() => import("@/components/footer/Footer")); export default function Home() { + // Scroll to section when landing on home with hash (e.g. from Gallery nav) + useEffect(() => { + const hash = typeof window !== "undefined" ? window.location.hash.slice(1) : ""; + if (!hash) return; + const scrollToSection = () => { + const el = document.getElementById(hash); + if (el) { + el.scrollIntoView({ behavior: "smooth", block: "start" }); + } + }; + const t = setTimeout(scrollToSection, 100); + return () => clearTimeout(t); + }, []); + return (
@@ -33,9 +45,7 @@ export default function Home() { {/* Temporarily hidden - uncomment to enable prizes section */} {/* */} - -
diff --git a/src/components/about/About.tsx b/src/components/about/About.tsx index c19cdc68..1297856e 100644 --- a/src/components/about/About.tsx +++ b/src/components/about/About.tsx @@ -1,10 +1,7 @@ import React from "react"; -import { PiTerminalWindow } from "react-icons/pi"; -import { IoDiamond } from "react-icons/io5"; -import { FaHandshake } from "react-icons/fa6"; +import { FaUsers } from "react-icons/fa6"; import { terminal } from "../../app/fonts/fonts"; import Link from "next/link"; -import Image from "next/image"; export default function About() { return ( @@ -16,19 +13,24 @@ export default function About() { > {/* Gradient Background */}
- + {/* Background Pattern */}
-
+
{/* Section Header */}
-

+

About RocketHacks

@@ -36,92 +38,14 @@ export default function About() { RocketHacks is a 24-hour hackathon hosted by the University of Toledo dedicated to fostering innovation and problem-solving among students from the Midwest and beyond. This event brings together - talented students—from budding programmers to visionary designers—to - build real solutions to real-world challenges. With an emphasis on - collaboration, creativity, and technical skills, RocketHacks will - empower students to turn their ideas into impactful projects. + talented students—from budding programmers to visionary + designers—to build real solutions to real-world challenges. With + an emphasis on collaboration, creativity, and technical skills, + RocketHacks will empower students to turn their ideas into + impactful projects.

- {/* Action Cards */} -
- {/* Sponsorship Card */} -
-
-
-
- -
-

- SPONSOR US -

-

- Partner with us to support the next generation of innovators. - Check out our sponsorship packet for opportunities and benefits. -

-
- - VIEW PACKET - -
-
- - {/* Hackers Card */} -
-
-
-
- -
-

- HACKERS -

-

- Ready to code, create, and collaborate? Applications for - RocketHacks 2026 will open soon. Stay tuned! -

-
- -
-
- {/* Volunteer Card */} -
-
-
-
- -
-

- VOLUNTEER -

-

- Help make RocketHacks amazing! Volunteer opportunities will - be available closer to the event date. -

-
- -
-
-
- {/* Temporarily hidden sections - uncomment to enable */} {/* {/* Tracks Section */} @@ -139,7 +63,7 @@ export default function About() {
{/* Green Innovation Track */} - {/* + {/*
@@ -153,7 +77,7 @@ export default function About() {
{/* MedTech Track */} - {/* + {/*
@@ -167,7 +91,7 @@ export default function About() {
{/* Next Gen AI Track */} - {/* + {/*
@@ -181,7 +105,7 @@ export default function About() {
{/* Hardware Track */} - {/* + {/*
diff --git a/src/components/faq/Faq.tsx b/src/components/faq/Faq.tsx index ae8cc227..7d4db1a8 100644 --- a/src/components/faq/Faq.tsx +++ b/src/components/faq/Faq.tsx @@ -10,7 +10,16 @@ import { import { terminal } from "../../app/fonts/fonts"; import { GlassCard } from "../ui/glass-card"; import { AnimatedIcon } from "../ui/animated-icon"; -import { FaQuestionCircle, FaUsers, FaLaptop, FaClock, FaMapMarkerAlt, FaDollarSign, FaFileAlt, FaEnvelope } from "react-icons/fa"; +import { + FaQuestionCircle, + FaUsers, + FaLaptop, + FaClock, + FaMapMarkerAlt, + FaDollarSign, + FaFileAlt, + FaEnvelope, +} from "react-icons/fa"; interface FAQItem { question: string; @@ -23,83 +32,110 @@ function FAQ() { const faqItems: FAQItem[] = [ { question: "What is RocketHacks?", - answer: "RocketHacks is an event where individuals or teams collaborate intensively on software development or hardware projects, typically within 24 hours. The event will start on Saturday, March 15, 2025 and end on Sunday, March 16, 2025. However there is additional time before and after hacking for introductions and presentations. It's an opportunity to learn, create, and innovate while competing with other teams.", + answer: + "RocketHacks is an event where individuals or teams collaborate intensively on software development or hardware projects, typically within 24 hours. The event will start on Saturday, March 14, 2026 and end on Sunday, March 15, 2026. However there is additional time before and after hacking for introductions and presentations. It's an opportunity to learn, create, and innovate while competing with other teams.", category: "general", - icon: + icon: , }, { question: "Who can participate?", - answer: "RocketHacks: Any student enrolled in a university or high school can participate in RocketHacks.\n\nCode&Create: Any underclassman/senior enrolled at any high school.", + answer: + "RocketHacks: Any student enrolled in a university or high school can participate in RocketHacks.\n\nCode&Create: Any underclassman/senior enrolled at any high school.", category: "registration", - icon: + icon: , }, { question: "What should I bring?", - answer: "Bring your laptop, charger, and any hardware you plan to hack with. We'll provide food, drinks, and a space-themed workspace!", + answer: + "Bring your laptop, charger, and any hardware you plan to hack with. We'll provide food, drinks, and a space-themed workspace!", category: "logistics", - icon: + icon: , }, { question: "Do I need a team?", - answer: "You can participate solo or in teams of up to 4 people. Don't have a team? We'll help you find one during our team formation event!", + answer: + "You can participate solo or in teams of up to 4 people. Don't have a team? We'll help you find one during our team formation event!", category: "registration", - icon: + icon: , }, { question: "What if I don't have coding experience?", - answer: "No worries! Hackathons are a great place to learn. There will be workshops, mentors, and resources available to help you. You can also focus on other aspects like design, project management, or testing.", + answer: + "No worries! Hackathons are a great place to learn. There will be workshops, mentors, and resources available to help you. You can also focus on other aspects like design, project management, or testing.", category: "general", - icon: + icon: , }, { question: "How long does RocketHacks last?", - answer: "RocketHacks will last for 24 hours. The event will start on Saturday, March 15, 2025 and end on Sunday, March 16, 2025. However, there is additional time before and after hacking for introductions and presentations.", + answer: + "RocketHacks will last for 24 hours. The event will start on Saturday, March 14, 2026 and end on Sunday, March 15, 2026. However, there is additional time before and after hacking for introductions and presentations.", category: "event", - icon: + icon: , }, { - question: "Is RocketHacks 2025 in-person or virtual?", - answer: "RocketHacks 2025 is an in-person only event. We hope to give you the best hackathon experience at the University of Toledo North Engineering Campus, 1700 N Westwood Ave, Toledo, OH 43607. Unfortunately, we will not be providing a virtual option this year. We are also unable to offer travel reimbursements at this time.", + question: "Is RocketHacks 2026 in-person or virtual?", + answer: + "RocketHacks 2026 is an in-person only event. We hope to give you the best hackathon experience at the University of Toledo North Engineering Campus, 1700 N Westwood Ave, Toledo, OH 43607. Unfortunately, we will not be providing a virtual option this year. We are also unable to offer travel reimbursements at this time.", category: "logistics", - icon: + icon: , }, { question: "How much does RocketHacks cost?", - answer: "RocketHacks is free for all students enrolled at any accredited university or high school. Swag, prizes, and great memories are all included with this completely free cost!", + answer: + "RocketHacks is free for all students enrolled at any accredited university or high school. Swag, prizes, and great memories are all included with this completely free cost!", category: "registration", - icon: + icon: , }, { question: "Should I expect a waiver?", - answer: "You betcha. Before any hacking begins we require you to sign a waiver which will be emailed to all registered participants prior to the hackathon. If you are under 18, you will need a parent or legal guardian to sign the waiver.", + answer: + "You betcha. Before any hacking begins we require you to sign a waiver which will be emailed to all registered participants prior to the hackathon. If you are under 18, you will need a parent or legal guardian to sign the waiver.", category: "logistics", - icon: + icon: , }, { question: "Is there a Code of Conduct?", - answer: "Absolutely! We operate on the Major League Hacking Code of Conduct to create an all-inclusive environment for our hackers.", + answer: + "Absolutely! We operate on the Major League Hacking Code of Conduct to create an all-inclusive environment for our hackers.", category: "general", - icon: + icon: , }, { question: "What if I have more questions?", - answer: "Send us an email to rockethacks@utoledo.edu. We are always happy to help.", + answer: + "Send us an email to rockethacks@utoledo.edu. We are always happy to help.", category: "general", - icon: - } + icon: , + }, ]; const categories = { - general: { name: "General", color: "yellow" as const, count: faqItems.filter(item => item.category === "general").length }, - registration: { name: "Registration", color: "orange" as const, count: faqItems.filter(item => item.category === "registration").length }, - event: { name: "Event Details", color: "purple" as const, count: faqItems.filter(item => item.category === "event").length }, - logistics: { name: "Logistics", color: "pink" as const, count: faqItems.filter(item => item.category === "logistics").length } + general: { + name: "General", + color: "yellow" as const, + count: faqItems.filter((item) => item.category === "general").length, + }, + registration: { + name: "Registration", + color: "orange" as const, + count: faqItems.filter((item) => item.category === "registration").length, + }, + event: { + name: "Event Details", + color: "purple" as const, + count: faqItems.filter((item) => item.category === "event").length, + }, + logistics: { + name: "Logistics", + color: "pink" as const, + count: faqItems.filter((item) => item.category === "logistics").length, + }, }; return (
-
{/* Background Image */} @@ -115,19 +151,24 @@ function FAQ() { {/* Gradient overlay */}
- + {/* Background Pattern */}
-
+
{/* Section Header */}
-

+

FAQ

@@ -136,15 +177,13 @@ function FAQ() {

- - {/* FAQ Accordion */}
{faqItems.map((item, index) => ( - @@ -156,23 +195,27 @@ function FAQ() { color={categories[item.category].color} animation="pulse" /> - {item.question} + + {item.question} +
- {item.answer.split('\n\n').map((paragraph, pIndex) => ( -

0 ? 'mt-4' : ''}> - {paragraph.split('\n').map((line, lIndex) => ( + {item.answer.split("\n\n").map((paragraph, pIndex) => ( +

0 ? "mt-4" : ""}> + {paragraph.split("\n").map((line, lIndex) => ( - {line.includes('**') ? ( + {line.includes("**") ? ( - {line.replace(/\*\*/g, '')} + {line.replace(/\*\*/g, "")} ) : ( line )} - {lIndex < paragraph.split('\n').length - 1 &&
} + {lIndex < paragraph.split("\n").length - 1 && ( +
+ )}
))}

@@ -188,19 +231,21 @@ function FAQ() { {/* Contact CTA */}
- } size="lg" color="yellow" animation="float" className="mx-auto mb-6" /> -

+

Still Have Questions?

- Our team is here to help! Don't hesitate to reach out if you need - any additional information about RocketHacks 2026. + Our team is here to help! Don't hesitate to reach out if + you need any additional information about RocketHacks 2026.

{/* Background Pattern */}
-
+
@@ -86,8 +94,9 @@ export default function Footer() { />

- RocketHacks is the premier 24-hour hackathon in the Midwest, hosted by the - University of Toledo. Join us in 2026 as we hack the horizon together. + RocketHacks is the premier 24-hour hackathon in the Midwest, + hosted by the University of Toledo. Join us in 2026 as we hack the + horizon together.

@@ -96,11 +105,11 @@ export default function Footer() {
- - rockethacks@rockets.utoledo.edu + rockethacks@utoledo.edu
@@ -108,7 +117,9 @@ export default function Footer() { {/* Quick Links */}
-

+

Quick Links

    @@ -127,7 +138,9 @@ export default function Footer() { {/* Social Media */}
    -

    +

    Follow Us

    @@ -141,7 +154,10 @@ export default function Footer() { className="flex items-center space-x-2 p-2 rounded-lg bg-rh-white/5 hover:bg-rh-yellow/10 text-rh-white/70 hover:text-rh-yellow transition-all duration-200 group" > - + {social.name} ))} @@ -153,7 +169,8 @@ export default function Footer() {
    - © 2026 RocketHacks. All rights reserved. Made with ❤️ by the RocketHacks team. + © 2026 RocketHacks. All rights reserved. Made with ❤️ by the + RocketHacks team.
    (null); - const [loading, setLoading] = useState(true); - const supabase = createClient(); + const [dots, setDots] = useState("."); + // Small "loading" animation without extra dependencies. useEffect(() => { - const getUser = async () => { - const { data: { user } } = await supabase.auth.getUser(); - setUser(user); - setLoading(false); - }; + const t = window.setInterval(() => { + setDots((prev) => { + if (prev.length >= 3) return "."; + return prev + "."; + }); + }, 500); - getUser(); - - const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { - setUser(session?.user ?? null); - }); - - return () => subscription.unsubscribe(); - }, [supabase]); + return () => window.clearInterval(t); + }, []); return (
    @@ -34,16 +26,16 @@ export default function Hero() { id="home" className="home text-white relative text-center overflow-hidden flex items-center justify-center" style={{ - minHeight: 'calc(100dvh - 80px)', // Dynamic viewport minus navbar - paddingTop: 'max(3rem, 8vh)', // Increased from 2rem/5vh to 3rem/8vh - paddingBottom: 'max(2rem, 5vh)' + minHeight: "calc(100dvh - 80px)", // Dynamic viewport minus navbar + paddingTop: "max(3rem, 8vh)", // Increased from 2rem/5vh to 3rem/8vh + paddingBottom: "max(2rem, 5vh)", }} > {/* Background Image with improved overlay */}
    {/* Main Heading - Aggressive scaling prevention */}
    -

    - RocketHacks 2026 + RocketHacks 2027

    - -

    - The Biggest Hackathon in the Midwest + University of Toledo

    @@ -89,58 +81,54 @@ export default function Hero() {

    - Register NOW for RocketHacks! + +

    - March 14th & 15th, 2026 + Registration opens soon.

    -

    - University of Toledo
    College of Engineering +

    + University of Toledo +
    + College of Engineering

    - {/* Call to Action Button - Responsive sizing */} -
    - - {loading ? "Loading..." : user ? "Go to Dashboard" : "Apply Now"} - -
    - {/* Location - Compact */} -
    +
    @@ -149,11 +137,15 @@ export default function Hero() { fill="currentColor" viewBox="0 0 20 20" style={{ - width: 'clamp(1.25rem, 3vw, 2rem)', - height: 'clamp(1.25rem, 3vw, 2rem)' + width: "clamp(1.25rem, 3vw, 2rem)", + height: "clamp(1.25rem, 3vw, 2rem)", }} > - + University of Toledo, Toledo, OH @@ -162,7 +154,10 @@ export default function Hero() {
    {/* Scroll Indicator - Hidden on very small screens */} -
    +
    @@ -170,10 +165,22 @@ export default function Hero() {
    {/* Floating Elements - Viewport aware positioning */} -
    -
    -
    -
    +
    +
    +
    +
    diff --git a/src/components/navbar/Navbar.tsx b/src/components/navbar/Navbar.tsx index d9ec4e21..829c54f8 100644 --- a/src/components/navbar/Navbar.tsx +++ b/src/components/navbar/Navbar.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react"; import Link from "next/link"; import Image from "next/image"; +import { usePathname } from "next/navigation"; import { FiMenu, FiX } from "react-icons/fi"; import { terminal } from "../../app/fonts/fonts"; import UserProfileDropdown from "./UserProfileDropdown"; @@ -9,10 +10,12 @@ import { createClient } from "@/lib/supabase/client"; import type { User } from "@supabase/supabase-js"; export default function Navbar() { + const pathname = usePathname(); const [menuOpen, setMenuOpen] = useState(false); const [scrolled, setScrolled] = useState(false); const [user, setUser] = useState(null); const supabase = createClient(); + const isHomePage = pathname === "/"; useEffect(() => { if (menuOpen) { @@ -28,21 +31,25 @@ export default function Navbar() { setScrolled(isScrolled); }; - window.addEventListener('scroll', handleScroll); - return () => window.removeEventListener('scroll', handleScroll); + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); }, []); useEffect(() => { // Get initial user const getUser = async () => { - const { data: { user } } = await supabase.auth.getUser(); + const { + data: { user }, + } = await supabase.auth.getUser(); setUser(user); }; getUser(); // Listen for auth changes - const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange((_event, session) => { setUser(session?.user ?? null); }); @@ -50,50 +57,55 @@ export default function Navbar() { }, [supabase]); const toggleMenu = () => setMenuOpen(!menuOpen); - + const closeMenu = () => setMenuOpen(false); - const handleSmoothScroll = (e: React.MouseEvent, href: string) => { - // Only handle smooth scroll for anchor links (starting with #) - if (href.startsWith('#')) { - e.preventDefault(); - const targetId = href.substring(1); - const targetElement = document.getElementById(targetId); - - if (targetElement) { - closeMenu(); - targetElement.scrollIntoView({ - behavior: 'smooth', - block: 'start', - }); + const handleNavClick = ( + e: React.MouseEvent, + href: string, + ) => { + // Anchor links: on home page smooth-scroll; on other pages navigate to /#section + if (href.startsWith("#")) { + if (isHomePage) { + e.preventDefault(); + const targetId = href.substring(1); + const targetElement = document.getElementById(targetId); + if (targetElement) { + closeMenu(); + targetElement.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + } } + // When not on home, Link href is "/#section" so no preventDefault — let navigation happen } }; const navLinks = [ { href: "#about", label: "ABOUT" }, - { href: "#gallery", label: "2025" }, - { href: "#past-sponsors", label: "SPONSORS" }, + { href: "/gallery", label: "2025" }, + { href: "#sponsor", label: "SPONSORS" }, { href: "#contact", label: "CONTACT" }, - { href: "#faq", label: "FAQ" }, - { href: "/login", label: "APPLY", highlight: true }, // Temporarily hidden - uncomment to enable team page // { href: "/team", label: "TEAM" } ]; return ( - ); -} \ No newline at end of file +} diff --git a/src/components/schedule/Schedule.tsx b/src/components/schedule/Schedule.tsx index 1323686c..d8bef875 100644 --- a/src/components/schedule/Schedule.tsx +++ b/src/components/schedule/Schedule.tsx @@ -1,309 +1,399 @@ "use client"; -import React, { useState } from "react"; -import Image from "next/image"; -import * as Tabs from "@radix-ui/react-tabs"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; +import React, { useMemo, useState } from "react"; import localFont from "next/font/local"; import { motion, AnimatePresence } from "framer-motion"; -import Link from "next/link"; +import { Clock, MapPin } from "lucide-react"; const terminal = localFont({ src: "../../app/fonts/terminal-grotesque.ttf" }); -const collegeSchedule = [ +const scheduleData = [ { - day: "Day 1", - time: "08:30 AM", - event: "Hackers Check-in", - venue: "Outside Nitschke Auditorium", - }, - { - day: "Day 1", - time: "09:00 AM", - event: "Opening Ceremony", - venue: "Nitschke Auditorium", - }, - { - day: "Day 1", - time: "10:00 AM", - event: "Team Formation", - venue: "Nitschke Auditorium", - }, - { - day: "Day 1", - time: "11:00 AM", - event: "Hacking Starts!", - venue: "Nitschke Hallway", - }, - { - day: "Day 1", - time: "11:00 AM", - event: "Nosu - Hacking Hackathons Workshop", - venue: "EECS 1320", - }, - { - day: "Day 1", - time: "12:00 PM", - event: "AWS - Gen AI Full-Stack App. Workshop", - venue: "EECS 1039", - }, - { day: "Day 1", time: "12:30 PM", event: "Lunch", venue: "Node" }, - { - day: "Day 1", - time: "01:00 PM", - event: "Actual Reality - Ollama Chatbot Workshop", - venue: "EECS 1320", - }, - { - day: "Day 1", - time: "01:00 PM", - event: "Perplexity - Deepseek R1 Workshop", - venue: "EECS 1300", - }, - { - day: "Day 1", - time: "02:00 PM", - event: "MLH - Co-Pilot Workshop", - venue: "EECS 1039", - }, - { - day: "Day 1", - time: "03:00 PM", - event: "MLH - Figma Workshop", - venue: "EECS 1039", - }, - { - day: "Day 1", - time: "03:00 PM", - event: "George - Intro. to Arduinos Workshop", - venue: "EECS 1320", - }, - { day: "Day 1", time: "04:00 PM", event: "Gaming Session", venue: "NE 2100" }, - { day: "Day 1", time: "07:00 PM", event: "Dinner by SPOKE", venue: "Node" }, - { - day: "Day 2", - time: "12:00 AM", - event: "F1 WatchParty", - venue: "Nitschke Auditorium", - }, - { day: "Day 2", time: "08:00 AM", event: "Breakfast", venue: "Node" }, - { - day: "Day 2", - time: "11:00 AM", - event: "Project Submissions Due", - venue: "Nitschke Hallway", - }, - { - day: "Day 2", - time: "12:00 PM", - event: "Judging", - venue: "Nitschke Lobby and Brady Center", - }, - { day: "Day 2", time: "12:30 PM", event: "Lunch", venue: "Node" }, - { - day: "Day 2", - time: "1:30 PM", - event: "Closing Ceremony & Awards", - venue: "Nitschke Auditorium", - }, -]; + day: "Saturday", + date: "March 14", + events: [ + { + name: "Hacker Check-In", + startTime: "8:30 AM", + endTime: "-", + location: "Nitschke Hall Entrance", + }, + { + name: "Opening Ceremony", + startTime: "9:00 AM", + endTime: "10:00 AM", + location: "Nitschke Auditorium", + }, + { + name: "Team Formation", + startTime: "10:00 AM", + endTime: "11:00 AM", + location: "Nitschke Auditorium", + }, + { + name: "Hacking Starts!", + startTime: "11:00 AM", + endTime: "-", + location: "NI hallway & NE tables", + }, + { + name: "Lunch", + startTime: "12:30 PM", + endTime: "1:30 PM", + location: "The NODE", + }, -const highSchoolSchedule = [ - { - day: "Day 1", - time: "08:00 AM", - event: "Hackers Check-in", - venue: "Outside Nitschke Auditorium", - }, - { - day: "Day 1", - time: "09:00 AM", - event: "Opening Ceremony", - venue: "Nitschke Auditorium", - }, - { - day: "Day 1", - time: "10:00 AM", - event: "Campus Tour", - venue: "University Of Toledo", - }, - { - day: "Day 1", - time: "11:00 AM", - event: "KoolKat Science Workshop", - venue: "North Engineering 2100", + { + name: "Hacking with GitHub Copilot - MLH Workshop", + startTime: "11:00 AM", + endTime: "11:30 AM", + location: "NE 1039", + }, + { + name: "Intro to Arduino Workshop - MIME Workshop", + startTime: "11:30 AM", + endTime: "1:00 PM", + location: "NE 1320", + }, + { + name: "Intro to Google AI Studio - MLH Workshop", + startTime: "1:30 PM", + endTime: "2:00 PM", + location: "SSOE", + }, + { + name: "From Prompt to Agent: Creating an OpenClaw AI Agent - CodeEcho Workshop", + startTime: "2:00 PM", + endTime: "3:00 PM", + location: "NE 1300", + }, + { + name: "Tech Together - MLH Workshop", + startTime: "4:15 PM", + endTime: "4:45 PM", + location: "NE 1021", + }, + { + name: "Dinner", + startTime: "8:00 PM", + endTime: "9:00 PM", + location: "The NODE", + }, + ], }, { - day: "Day 1", - time: "12:00 PM", - event: "Project on Scratch", - venue: "North Engineering 2100", - }, - { - day: "Day 1", - time: "02:00 PM", - event: "Judging and Award Ceremony", - venue: "North Engineering 2100", + day: "Sunday", + date: "March 15", + events: [ + { + name: "Breakfast", + startTime: "9:00 AM", + endTime: "10:00 AM", + location: "The NODE", + }, + { + name: "Hacking Ends! - Project Submission Deadline", + startTime: "11:00 AM", + endTime: "-", + location: "NI Hallway and NE Tables", + }, + { + name: "Lunch", + startTime: "12:00 PM", + endTime: "1:00 PM", + location: "The NODE", + }, + { + name: "Judging", + startTime: "12:30 PM", + endTime: "1:30 PM", + location: "NI Hallway and NE Tables", + }, + { + name: "Closing Ceremony", + startTime: "2:30 PM", + endTime: "3:30 PM", + location: "Nitschke Auditorium", + }, + ], }, ]; export default function Schedule() { - const [selectedTab, setSelectedTab] = useState("college"); - const [selectedCollegeDay, setSelectedCollegeDay] = useState("Day 1"); - const filteredCollegeSchedule = collegeSchedule.filter( - (item) => item.day === selectedCollegeDay + const [selectedDay, setSelectedDay] = useState("Saturday"); + const [activeFilters, setActiveFilters] = useState< + Array<"workshop" | "food"> + >([]); + + const currentDaySchedule = scheduleData.find( + (item) => item.day === selectedDay, ); + const getEventType = (eventName: string): "workshop" | "food" | "general" => { + const name = eventName.toLowerCase(); + if (name.includes("workshop")) return "workshop"; + if ( + name.includes("lunch") || + name.includes("dinner") || + name.includes("breakfast") + ) { + return "food"; + } + return "general"; + }; + + const filteredEvents = useMemo(() => { + if (!currentDaySchedule) return []; + if (activeFilters.length === 0) return currentDaySchedule.events; + return currentDaySchedule.events.filter((event) => { + const type = getEventType(event.name); + return activeFilters.includes(type as "workshop" | "food"); + }); + }, [currentDaySchedule, activeFilters]); + + const counts = useMemo(() => { + const base = { workshop: 0, food: 0 }; + if (!currentDaySchedule) return base; + for (const e of currentDaySchedule.events) { + const t = getEventType(e.name); + if (t === "workshop") base.workshop += 1; + if (t === "food") base.food += 1; + } + return base; + }, [currentDaySchedule]); + + const toggleFilter = (filter: "workshop" | "food") => { + setActiveFilters((prev) => + prev.includes(filter) + ? prev.filter((f) => f !== filter) + : [...prev, filter], + ); + }; + + const EventCard = ({ + event, + index, + }: { + event: (typeof scheduleData)[0]["events"][0]; + index: number; + }) => { + const type = getEventType(event.name); + const accent = + type === "workshop" + ? { + border: "border-rh-purple-light/35 hover:border-rh-purple-light/60", + glow: "group-hover:shadow-rh-purple-light/20", + badge: + "bg-rh-purple-light/15 text-rh-purple-light border-rh-purple-light/35", + dot: "bg-rh-purple-light", + } + : type === "food" + ? { + border: "border-rh-yellow/30 hover:border-rh-yellow/60", + glow: "group-hover:shadow-rh-yellow/20", + badge: "bg-rh-yellow/15 text-rh-yellow border-rh-yellow/35", + dot: "bg-rh-yellow", + } + : { + border: "border-white/10 hover:border-white/20", + glow: "group-hover:shadow-white/10", + badge: "bg-white/5 text-rh-white/70 border-white/10", + dot: "bg-rh-orange", + }; + + return ( + +
    +
    +
    +
    + +
    +
    +
    +

    + {event.name} +

    + + {type === "workshop" + ? "Workshop" + : type === "food" + ? "Food" + : "Event"} + +
    + +
    +
    + + + {event.startTime}{" "} + {event.endTime} + +
    +
    + + {event.location} +
    +
    +
    +
    +
    + + ); + }; + return ( -
    -
    -
    -
    -

    + {/* Gradient Overlay */} +
    + +
    + {/* Section Title */} + +

    + EVENT SCHEDULE +

    +
    + + + {/* Day Selector */} +
    + {scheduleData.map((day) => ( + setSelectedDay(day.day)} + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.98 }} + className={`px-8 py-3 rounded-lg font-bold text-lg transition-all duration-300 ${ + selectedDay === day.day + ? "bg-gradient-to-r from-rh-yellow to-rh-pink text-rh-navy-dark shadow-lg shadow-rh-yellow/50" + : "bg-rh-navy-light/40 backdrop-blur-sm border border-rh-yellow/30 text-rh-white hover:border-rh-yellow/60 hover:bg-rh-navy-light/60" + }`} > - EVENT MAP -

    - {day.day} + {day.date} + + ))} +
    + + {/* Events Container */} + + {currentDaySchedule && ( + - - -

    - Event Address:{" "} - - 1700 N Westwood Ave, Toledo, OH 43607 - -

    -
    -
    -

    - Event Schedule -

    - - - - RocketHacks - - - Code & Create - - -
    - - {selectedTab === "college" ? ( - -
    + {/* Filter Bar */} +
    +
    +
    +
    +

    + Showing{" "} + + {filteredEvents.length} + {" "} + event{filteredEvents.length === 1 ? "" : "s"} +

    + {activeFilters.length > 0 && ( - -
    - - {filteredCollegeSchedule.map((item, index) => ( - - - {`${item.time} - ${item.event}`} - - -

    Venue: {item.venue}

    -
    -
    - ))} -
    - - ) : ( - +

    + Tip: toggle Workshops and Food to focus fast. +

    +
    + +
    + + +
    +
    - -
    -
    + + {/* Scrollable Events List */} +
    + {filteredEvents.length > 0 ? ( + filteredEvents.map((event, index) => ( + + )) + ) : ( +
    + No events match your filters. +
    + )} +
    + + )} +
    ); diff --git a/src/components/sponsor/Sponsor.tsx b/src/components/sponsor/Sponsor.tsx index e17f5699..7216f6b1 100644 --- a/src/components/sponsor/Sponsor.tsx +++ b/src/components/sponsor/Sponsor.tsx @@ -25,26 +25,35 @@ interface SponsorProps { const Sponsor: React.FC = ({ sponsors }) => { // Organize sponsors by tier - const mainSponsors = sponsors.filter(s => - ['College-of-engineering', 'eecs', 'spoke', 'Nysus', 'actual-tech'].some(name => s.src?.includes(name)) || - s.icon === 'github' + const mainSponsors = sponsors.filter((s) => + ["eecs", "besnx", "coe", "cobi", "firstsolar"].some((name) => + s.src?.includes(name), + ), ); - - const intermediateSponsors = sponsors.filter(s => - ['mercy', 'firstsolar', 'cdw', 'codeecho'].some(name => s.src?.includes(name)) || - s.icon === 'aws' + + const intermediateSponsors = sponsors.filter( + (s) => + ["codeecho", "Nysus", "impelix"].some((name) => s.src?.includes(name)) || + s.icon === "aws", ); - - const mainInKindSponsors = sponsors.filter(s => - ['Perplexity', 'Mistral', 'vercel', 'warp', 'sprint', 'Hugging'].some(name => s.src?.includes(name)) + + const mainInKindSponsors = sponsors.filter((s) => + ["rgp", "parkut", "actual"].some((name) => s.src?.includes(name)), ); - - const lessImportantSponsors = sponsors.filter(s => - ['xyz', 'photoroom', 'orielly', 'standout'].some(name => s.src?.includes(name)) + + const lessImportantSponsors = sponsors.filter((s) => + ["fai", "anthropic", "purebutton"].some((name) => s.src?.includes(name)), ); - // Duplicate sponsors for seamless infinite scroll - const duplicateArray = (arr: SponsorType[]) => [...arr, ...arr, ...arr]; + // Duplicate sponsors for seamless infinite scroll - more duplicates for smaller arrays + const duplicateArray = (arr: SponsorType[]) => { + let minDuplicates = 3; + // For arrays with fewer items, duplicate more to fill the space + if (arr.length <= 3) minDuplicates = 6; + else if (arr.length <= 5) minDuplicates = 4; + + return Array.from({ length: minDuplicates }, () => arr).flat(); + }; return (
    @@ -65,24 +74,29 @@ const Sponsor: React.FC = ({ sponsors }) => { {/* Gradient overlay */}
    - + {/* Background Pattern */}
    -
    +
    - {/* Past Sponsors Carousel Section */} -
    + {/* Sponsors Carousel Section */} +
    -

    - Past Sponsors +

    + Sponsors

    - + {/* Sponsorship CTA - Minimalistic Text Block */}

    @@ -94,19 +108,19 @@ const Sponsor: React.FC = ({ sponsors }) => { className="text-rh-yellow hover:text-rh-orange transition-colors duration-300 underline decoration-rh-yellow/50 hover:decoration-rh-orange underline-offset-4 font-medium" > View our sponsorship opportunities - - {" "}or{" "} + {" "} + or{" "} contact us - - {" "}to learn more. + {" "} + to learn more.

    - +
    {/* Row 1: Main Sponsors - Left to Right */}
    @@ -119,7 +133,7 @@ const Sponsor: React.FC = ({ sponsors }) => { rel="noopener noreferrer" className="flex-shrink-0" > -
    +
    {sponsor.type === "image" && sponsor.src ? ( = ({ sponsors }) => { height={100} className="object-contain max-w-full max-h-full w-auto h-auto transition-all duration-300 hover:brightness-110" /> - ) : sponsor.type === "icon" && sponsor.icon === "github" ? ( - + ) : sponsor.type === "icon" && + sponsor.icon === "github" ? ( + ) : null}
    @@ -140,29 +155,32 @@ const Sponsor: React.FC = ({ sponsors }) => { {/* Row 2: Intermediate Sponsors - Right to Left */}
    - {duplicateArray(intermediateSponsors).map((sponsor, index) => ( - -
    - {sponsor.type === "image" && sponsor.src ? ( - - ) : sponsor.type === "icon" && sponsor.icon === "aws" ? ( - - ) : null} -
    - - ))} + {duplicateArray(intermediateSponsors).map( + (sponsor, index) => ( + +
    + {sponsor.type === "image" && sponsor.src ? ( + + ) : sponsor.type === "icon" && + sponsor.icon === "aws" ? ( + + ) : null} +
    + + ), + )}
    @@ -177,12 +195,12 @@ const Sponsor: React.FC = ({ sponsors }) => { rel="noopener noreferrer" className="flex-shrink-0" > -
    +
    @@ -194,25 +212,27 @@ const Sponsor: React.FC = ({ sponsors }) => { {/* Row 4: Less Important Sponsors - Right to Left */}
    - {duplicateArray(lessImportantSponsors).map((sponsor, index) => ( - -
    - -
    - - ))} + {duplicateArray(lessImportantSponsors).map( + (sponsor, index) => ( + +
    + +
    + + ), + )}
    @@ -224,13 +244,13 @@ const Sponsor: React.FC = ({ sponsors }) => { transform: translateX(0); } 100% { - transform: translateX(-33.333%); + transform: translateX(-50%); } } @keyframes scroll-right { 0% { - transform: translateX(-33.333%); + transform: translateX(-50%); } 100% { transform: translateX(0); @@ -238,12 +258,12 @@ const Sponsor: React.FC = ({ sponsors }) => { } .animate-scroll-left { - animation: scroll-left 40s linear infinite; + animation: scroll-left 50s linear infinite; will-change: transform; } .animate-scroll-right { - animation: scroll-right 40s linear infinite; + animation: scroll-right 50s linear infinite; will-change: transform; } @@ -264,19 +284,21 @@ const Sponsor: React.FC = ({ sponsors }) => { {sponsors.length === 0 && (
    - } size="xl" color="yellow" animation="float" className="mx-auto mb-6" /> -

    +

    Sponsors Coming Soon

    - We're actively working with amazing companies and organizations - to make RocketHacks 2026 extraordinary. + We're actively working with amazing companies and + organizations to make RocketHacks 2026 extraordinary.

    ( + + {/* Cross / medical plus */} + + + {/* Subtle circle outline */} + + +); + +const FinanceIcon = () => ( + + {/* Bar chart */} + + + + {/* Trend line */} + + +); + +const SustainabilityIcon = () => ( + + {/* Leaf shape */} + + {/* Stem */} + + {/* Vein */} + + + +); + +const HardwareIcon = () => ( + + {/* CPU chip body */} + + {/* Inner core */} + + {/* Pins — top */} + + + + {/* Pins — bottom */} + + + + {/* Pins — left */} + + + + {/* Pins — right */} + + + + +); + +const tracksData = [ + { + id: 0, + title: "Healthcare", + description: + "Innovate solutions for medical challenges and improve patient outcomes", + accent: "#7f819e", + Icon: HealthcareIcon, + gradient: "from-blue-500/20 to-cyan-500/10", + }, + { + id: 1, + title: "Finance", + description: + "Build fintech solutions that revolutionize financial services", + accent: "#ffc65a", + Icon: FinanceIcon, + gradient: "from-yellow-500/20 to-orange-500/10", + }, + { + id: 2, + title: "Sustainability", + description: "Create applications that make the world more sustainable", + accent: "#c32c9a", + Icon: SustainabilityIcon, + gradient: "from-green-500/20 to-emerald-500/10", + }, + { + id: 3, + title: "Hardware", + description: + "Develop IoT and embedded systems that push boundaries. Hackers are allowed to bring their own 3d printed parts.", + accent: "#f483f5", + Icon: HardwareIcon, + gradient: "from-pink-500/20 to-purple-500/10", + }, +]; + +const sponsorTracksData = [ + { + id: "elevenlabs", + name: "ElevenLabs", + description: + "Build the most creative and impactful project using the ElevenLabs API (voice, TTS, or cloning). Depth of integration and showcase-worthy quality wins.", + accent: "#7f819e", + gradient: "from-violet-500/15 to-purple-600/10", + }, + { + id: "aws", + name: "AWS", + description: + "Integrate 4+ AWS services meaningfully into your solution, including at least one AI/ML service. Best overall architecture and breadth of AWS usage wins.", + accent: "#ffc65a", + gradient: "from-amber-500/15 to-orange-500/10", + }, + { + id: "featherless", + name: "Featherless.AI", + description: + "Use the Featherless.AI inference API to power your project with open-weight models. Best model selection, prompt engineering, and creative application wins.", + accent: "#c32c9a", + gradient: "from-pink-500/15 to-rose-600/10", + }, + { + id: "base44", + name: "Base44", + description: + "Build a real-world impact app under one of the challenge categories (e.g., local innovation, workforce resilience) using Base44. Judged on impact, UX, and execution.", + accent: "#3b82f6", + gradient: "from-blue-500/15 to-indigo-600/10", + }, + { + id: "jaseci", + name: "Jaseci Labs", + description: + "Showcase the best use of agentic AI — autonomous decision-making, multi-step reasoning, or tool use. No platform requirement; just impress with your agent.", + accent: "#f483f5", + gradient: "from-fuchsia-500/15 to-purple-600/10", + }, +]; + +const CARD_COUNT = tracksData.length; +const ANGLE_SLICE = 360 / CARD_COUNT; +// Radius: distance from center to card face +// For 4 cards, a good radius is roughly card_width / (2 * tan(PI/n)) +// card width ~320px → 320 / (2 * tan(45°)) = 320 / 2 = 160 → use ~320 to give breathing room +const RADIUS = 340; + +export default function Tracks() { + const [activeIndex, setActiveIndex] = useState(0); + const [isRotating, setIsRotating] = useState(false); + + const handleTrackClick = (index: number) => { + if (isRotating || index === activeIndex) return; + setIsRotating(true); + setActiveIndex(index); + setTimeout(() => setIsRotating(false), 800); + }; + + // Rotate the scene so the active card faces the viewer (negative rotation) + const sceneRotation = -activeIndex * ANGLE_SLICE; + + return ( +
    + {/* Background gradient elements */} +
    +
    +
    +
    + +
    + {/* Section Header */} + +

    + Hackathon Tracks +

    +

    + Choose your path and innovate across four transformative domains +

    +
    + + {/* 3D Carousel Outer — perspective lives here */} +
    + {/* Scene — this rotates to bring cards into view */} + + {tracksData.map((track, index) => { + const cardRotationY = index * ANGLE_SLICE; + const isActive = index === activeIndex; + + return ( + handleTrackClick(index)} + style={{ + position: "absolute", + width: "280px", + height: "360px", + left: "-140px", // half of width to center + top: "-180px", // half of height to center + transformStyle: "preserve-3d", + transform: `rotateY(${cardRotationY}deg) translateZ(${RADIUS}px)`, + cursor: isActive ? "default" : "pointer", + }} + > + + {/* Shimmer overlay */} +
    + +
    + {/* Icon */} + + + + + {/* Title */} +

    + {track.title} +

    + + {/* Description */} +

    + {track.description} +

    +
    + + {/* Bottom hint */} + {!isActive && ( +
    + Click to select → +
    + )} + {isActive && ( + + ● Active Track + + )} + + + ); + })} + +
    + + {/* Controls */} + +
    + {tracksData.map((track, index) => ( + handleTrackClick(index)} + disabled={isRotating} + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + className="px-4 py-2 rounded-full uppercase text-xs sm:text-sm font-bold tracking-widest transition-all duration-300 disabled:opacity-50 border-2" + style={{ + backgroundColor: + index === activeIndex + ? track.accent + : "rgba(255,255,255,0.08)", + borderColor: + index === activeIndex + ? track.accent + : "rgba(255,255,255,0.15)", + color: + index === activeIndex ? "#000" : "rgba(255,255,255,0.7)", + }} + > + {track.title} + + ))} +
    + +
    + + {activeIndex + 1} + {" "} + / {tracksData.length} +
    +
    + + {/* Sponsor Tracks Section */} + +
    +

    + Sponsor Tracks +

    +
    +

    + Build with sponsor APIs and tools — win category prizes and stand + out. +

    +
    + +
    + {sponsorTracksData.map((sponsor, index) => ( + + {/* Accent glow line */} +
    +
    +
    +

    + {sponsor.name} +

    +

    + {sponsor.description} +

    +
    + + ))} +
    + +
    +
    + ); +} diff --git a/src/lib/utils/applicationCompleteness.ts b/src/lib/utils/applicationCompleteness.ts index 4bc51519..00cbccc2 100644 --- a/src/lib/utils/applicationCompleteness.ts +++ b/src/lib/utils/applicationCompleteness.ts @@ -21,94 +21,106 @@ export interface CompletenessResult { export function checkApplicationCompleteness(application: any): CompletenessResult { const missingFields: MissingField[] = [] + // Debug log to see what we're checking + console.log('Checking completeness for application:', { + portfolio_url: application.portfolio_url, + resume_url: application.resume_url, + level_of_study: application.level_of_study, + linkedin_url: application.linkedin_url, + github_url: application.github_url, + }) + + // Helper function to safely check if a string field is empty + const isStringEmpty = (value: any): boolean => { + if (value === null || value === undefined) return true + if (typeof value === 'string') return value.trim() === '' + return false + } + + // Helper function to safely check if a boolean field is set + const isBooleanSet = (value: any): boolean => { + return typeof value === 'boolean' && value === true + } + // Required MLH fields (should always be filled due to NOT NULL constraints) - if (!application.first_name || application.first_name.trim() === '') { + if (isStringEmpty(application.first_name)) { missingFields.push({ field: 'first_name', label: 'First Name', category: 'personal' }) } - if (!application.last_name || application.last_name.trim() === '') { + if (isStringEmpty(application.last_name)) { missingFields.push({ field: 'last_name', label: 'Last Name', category: 'personal' }) } if (!application.age) { missingFields.push({ field: 'age', label: 'Age', category: 'personal' }) } - if (!application.phone_number || application.phone_number.trim() === '') { + if (isStringEmpty(application.phone_number)) { missingFields.push({ field: 'phone_number', label: 'Phone Number', category: 'personal' }) } - if (!application.email || application.email.trim() === '') { + if (isStringEmpty(application.email)) { missingFields.push({ field: 'email', label: 'Email', category: 'personal' }) } - if (!application.school || application.school.trim() === '') { + if (isStringEmpty(application.school)) { missingFields.push({ field: 'school', label: 'School', category: 'education' }) } - if (!application.level_of_study || application.level_of_study.trim() === '') { + if (isStringEmpty(application.level_of_study)) { missingFields.push({ field: 'level_of_study', label: 'Level of Study', category: 'education' }) } - if (!application.country_of_residence || application.country_of_residence.trim() === '') { + if (isStringEmpty(application.country_of_residence)) { missingFields.push({ field: 'country_of_residence', label: 'Country of Residence', category: 'personal' }) } - if (!application.mlh_code_of_conduct) { + if (!isBooleanSet(application.mlh_code_of_conduct)) { missingFields.push({ field: 'mlh_code_of_conduct', label: 'MLH Code of Conduct Agreement', category: 'event' }) } - if (!application.mlh_privacy_policy) { + if (!isBooleanSet(application.mlh_privacy_policy)) { missingFields.push({ field: 'mlh_privacy_policy', label: 'MLH Privacy Policy Agreement', category: 'event' }) } // Optional but expected fields for complete application - if (!application.linkedin_url || application.linkedin_url.trim() === '') { + if (isStringEmpty(application.linkedin_url)) { missingFields.push({ field: 'linkedin_url', label: 'LinkedIn URL', category: 'professional' }) } - if (!application.github_url || application.github_url.trim() === '') { + if (isStringEmpty(application.github_url)) { missingFields.push({ field: 'github_url', label: 'GitHub URL', category: 'professional' }) } - if (!application.portfolio_url || application.portfolio_url.trim() === '') { - missingFields.push({ field: 'portfolio_url', label: 'Portfolio URL', category: 'professional' }) - } - if (!application.resume_url || application.resume_url.trim() === '') { - missingFields.push({ field: 'resume_url', label: 'Resume', category: 'professional' }) - } if (!application.dietary_restrictions || (Array.isArray(application.dietary_restrictions) && application.dietary_restrictions.length === 0)) { missingFields.push({ field: 'dietary_restrictions', label: 'Dietary Restrictions', category: 'event' }) } - if (!application.gender || application.gender.trim() === '') { + if (isStringEmpty(application.gender)) { missingFields.push({ field: 'gender', label: 'Gender', category: 'personal' }) } - if (!application.pronouns || application.pronouns.trim() === '') { + if (isStringEmpty(application.pronouns)) { missingFields.push({ field: 'pronouns', label: 'Pronouns', category: 'personal' }) } if (!application.race_ethnicity || (Array.isArray(application.race_ethnicity) && application.race_ethnicity.length === 0)) { missingFields.push({ field: 'race_ethnicity', label: 'Race/Ethnicity', category: 'personal' }) } - if (!application.education_level || application.education_level.trim() === '') { - missingFields.push({ field: 'education_level', label: 'Education Level', category: 'education' }) - } - if (!application.tshirt_size || application.tshirt_size.trim() === '') { + if (isStringEmpty(application.tshirt_size)) { missingFields.push({ field: 'tshirt_size', label: 'T-Shirt Size', category: 'event' }) } - if (!application.major || application.major.trim() === '') { + if (isStringEmpty(application.major)) { missingFields.push({ field: 'major', label: 'Major/Field of Study', category: 'education' }) } - if (application.first_hackathon === null || application.first_hackathon === undefined) { + if (application.first_hackathon !== true && application.first_hackathon !== false) { missingFields.push({ field: 'first_hackathon', label: 'First Hackathon Status', category: 'event' }) } // Shipping address fields - if (!application.address_line1 || application.address_line1.trim() === '') { + if (isStringEmpty(application.address_line1)) { missingFields.push({ field: 'address_line1', label: 'Address Line 1', category: 'address' }) } - if (!application.city || application.city.trim() === '') { + if (isStringEmpty(application.city)) { missingFields.push({ field: 'city', label: 'City', category: 'address' }) } - if (!application.state || application.state.trim() === '') { + if (isStringEmpty(application.state)) { missingFields.push({ field: 'state', label: 'State/Province', category: 'address' }) } - if (!application.shipping_country || application.shipping_country.trim() === '') { + if (isStringEmpty(application.shipping_country)) { missingFields.push({ field: 'shipping_country', label: 'Country', category: 'address' }) } - if (!application.postal_code || application.postal_code.trim() === '') { + if (isStringEmpty(application.postal_code)) { missingFields.push({ field: 'postal_code', label: 'Postal Code', category: 'address' }) } - const totalFields = 27 // Total number of fields checked + const totalFields = 26 // Total number of fields checked const filledFields = totalFields - missingFields.length const percentage = Math.round((filledFields / totalFields) * 100) const isComplete = missingFields.length === 0 diff --git a/supabase/migrations/add_rsvp_attending.sql b/supabase/migrations/add_rsvp_attending.sql new file mode 100644 index 00000000..5def5851 --- /dev/null +++ b/supabase/migrations/add_rsvp_attending.sql @@ -0,0 +1,9 @@ +-- ============================================ +-- Add RSVP attendance flag to applicants +-- ============================================ + +ALTER TABLE public.applicants +ADD COLUMN IF NOT EXISTS rsvp_attending BOOLEAN NOT NULL DEFAULT false; + +-- RLS already enforces that authenticated users can only update rows where auth.uid() = user_id +-- (see existing "Users can update their own application" policy).
-
{app.first_name} {app.last_name}
+
+ {app.first_name} {app.last_name} +
{app.school}
{app.school}{app.email} + {app.email} + {app.checked_in ? (
@@ -236,11 +307,15 @@ export default function OrganizerPortal() { disabled={checkingIn === app.id} className={`px-4 py-2 text-white text-sm font-semibold rounded-lg transition-all duration-200 ${ app.checked_in - ? 'bg-red-600 hover:bg-red-700' - : 'bg-green-600 hover:bg-green-700' + ? "bg-red-600 hover:bg-red-700" + : "bg-green-600 hover:bg-green-700" } disabled:opacity-50 disabled:cursor-not-allowed`} > - {checkingIn === app.id ? 'Processing...' : app.checked_in ? 'Undo' : 'Check In'} + {checkingIn === app.id + ? "Processing..." + : app.checked_in + ? "Undo" + : "Check In"}